蜘蛛爬虫分析中如何发现抓取死循环?
用日志聚合、URL归一化和链接图分析量化URL/内容比、新增速率比与抓取深度,定位制造抓取死循环的URL模板,并通过归一化去重和护栏配置防止爬虫失控。
学完这篇,你能用日志聚合、URL 归一化和链接图分析三步,在爬虫跑飞的当天就定位出是哪个 URL 模板在制造抓取死循环,并加上能兜住它的护栏配置。
抓取死循环(也叫爬虫陷阱)的典型表现是:URL 数量指数级增长,但有效内容几乎不增加。下面按可执行顺序拆开讲。
第一步:先把"死循环"变成三个可量化指标
这一步要做什么:把模糊的"好像抓不完"变成能看数字报警的三个指标,做完你就能判断当前任务是否已经失控。
三个指标分别是:
- URL/内容比 = 抓取 URL 总数 ÷ 去重后内容指纹数。正常站点这个值在 1~5,超过 20 基本就是陷阱。
- 新增 URL 速率 ÷ 新增内容速率。前者持续大于后者 3 倍以上,说明在空转。
- 平均抓取深度。深度持续上抬且没有收敛趋势,说明存在纵向陷阱。
注意:不要只看"队列里还有多少条"。队列大是正常的,只有"URL 涨、内容不涨"才是死循环。
第二步:从抓取日志里聚合出可疑 URL 模板
这一步要做什么:把每条 URL 里的数字、ID、时间戳替换成占位符,得到"模板",然后按模板统计,做完你就能一眼看出是哪个模板在爆量。
假设你的日志是 Nginx 风格或 Scrapy 输出的 TSV,第 7 列是 URL:
awk '{print $7}' crawl.log \
| sed -E 's#/[0-9]{2,}#/<NUM>#g; s#[?&][a-z_]+=[^&]*#\&<KV>#g' \
| sort | uniq -c | sort -rn | head -20
输出会像这样:
482913 /calendar/<NUM>/<KV>
31044 /list/<KV>
1207 /article/<NUM>
第一行就是典型日历陷阱:从某个月份一路"下个月"点下去,永远不会到底。
如果你把抓取结果落在数据库或 ES 里,用 SQL 更直观:
SELECT url_template,
COUNT(*) AS urls,
COUNT(DISTINCT content_hash) AS uniq_content,
COUNT(*) * 1.0 / NULLIF(COUNT(DISTINCT content_hash), 0) AS url_per_content
FROM crawl_pages
WHERE fetched_at > NOW() - INTERVAL '1 hour'
GROUP BY url_template
HAVING COUNT(*) > 500
ORDER BY url_per_content DESC;
url_per_content > 20 的模板,直接拉出来人工看两条样本。
第三步:做 URL 归一化后重新去重,看真实重复率
这一步要做什么:把等价 URL 折叠成同一条,做完你能知道有多少"量"其实是同一页面的参数排列组合。
坑在于:?a=1&b=2 和 ?b=2&a=1 是同一个页面,但去重器眼里是两个 URL;带 sid、utm_source、_t 的更是每条都不同。
from urllib.parse import urlsplit, parse_qsl, urlencode
DROP = {"sid", "sessionid", "phpsessid", "_t", "timestamp", "utm_source", "utm_medium"}
def canon(u: str) -> str:
s = urlsplit(u)
q = sorted((k, v) for k, v in parse_qsl(s.query) if k.lower() not in DROP)
path = s.path.rstrip("/") or "/"
return f"{s.scheme}://{s.netloc}{path}?{urlencode(q)}"
把最近一万条 URL 跑一遍 canon(),再去重计数。如果去重后只剩几十条,那这个模板就是纯参数爆炸,直接进黑名单。
注意:归一化时不要把真正的分页参数(如
page)也丢掉,否则会把正常翻页误判成重复而漏抓。
第四步:构建链接图,用强连通分量定位回路
这一步要做什么:把"页面 A 链到页面 B"存成有向边,找出互相可达的簇,做完你能看到回路具体长什么样,而不只是看到一个爆量模板。
import networkx as nx
G = nx.DiGraph()
for src, dst in edges: # edges 来自解析出的出链
G.add_edge(src, dst)
for comp in nx.strongly_connected_components(G):
if len(comp) > 1:
print("回路簇大小:", len(comp), list(comp)[:5])
注意:不要直接上
nx.simple_cycles(G)。在稠密链接图上环的数量是指数级的,会直接把内存吃满,必须先做 SCC 收缩再在簇内找环。
第五步:加护栏,让死循环下一次自己停住
这一步要做什么:把上面找到的模板和深度特征写成规则,做完同类陷阱会被自动拦截。
Scrapy 项目在 settings.py 里加:
DEPTH_LIMIT = 8 # 纵向陷阱:挡住"下个月"式无限深入
URLLENGTH_LIMIT = 200 # 超长参数串直接丢
AUTOTHROTTLE_ENABLED = True
宽向陷阱(同层无限翻页)DEPTH_LIMIT 挡不住,要在 Spider 里按模板限流:
MAX_URLS_PER_TEMPLATE = 200
def _template(self, url):
return re.sub(r"\d+", "<NUM>", url)
if self.counter[self._template(url)] > MAX_URLS_PER_TEMPLATE:
return None # 不再跟进该模板
再叠加一层内容指纹去重(对正文算 simhash,海明距离 ≤ 3 视为同页),参数排列组合就彻底失效了。
第六步:把指标接到监控上
这一步要做什么:让异常在发生 10 分钟内自动告警,而不是等你发现磁盘满了。
把第一步的三个指标每 5 分钟打点到 Prometheus,配两条规则:url_per_content > 20 持续 10 分钟,或 新增URL速率 / 新增内容速率 > 3 持续 10 分钟,触发告警。
注意:告警阈值要按站点分别设。电商列表页本身 URL 就多,全站统一阈值会天天误报。
小结
- 死循环的本质是"URL 涨、内容不涨",先量化成 URL/内容比、速率比、平均深度三个指标。
- 用
sed或 SQL 把 URL 里的数字参数替换成占位符做模板聚合,爆量模板会自己浮出来。 - 去重前必须做 URL 归一化(排序参数、剔除
sid/utm_*),否则参数排列组合会骗过你去重器。 - 链接图分析先用强连通分量收缩,别硬跑
simple_cycles。 - 护栏要分两层:
DEPTH_LIMIT挡纵向陷阱,按模板限流 + 内容指纹去重挡横向陷阱。 - 最后把指标接进监控并分站点设阈值,否则下次还会再踩一遍。
原文链接:https://www.gj0.com/thread-1807.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。