⭐ 推荐:社区规则条款 V1.0

蜘蛛爬虫分析中如何发现抓取死循环?

liulian
liulian 归队站长新人站长
发布于 2026-10-13 03:00 ·2 浏览 ·0 回复
内容摘要

用日志聚合、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 挡纵向陷阱,按模板限流 + 内容指纹去重挡横向陷阱。
  • 最后把指标接进监控并分站点设阈值,否则下次还会再踩一遍。
版权声明:本文来自 GJ站长论坛《蜘蛛爬虫分析中如何发现抓取死循环?》
原文链接:https://www.gj0.com/thread-1807.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~