--- name: standard-web-crawler description: > 网站爬虫。抓取响应里已有正文的 HTML 并入库,或给抓取加 URL 去重、范围边界和增量判断时使用。 --- # 网站爬虫 适用:HTTP(S) 请求加 HTML 解析,正文在响应里,并且要按域名、深度、页数收住范围。 URL 列表已知、范围固定的一次性脚本,直接请求这些 URL。正文要执行 JS 才出现时,用无头浏览器;参数和失败处理跟下面不同。 ## URL 标准化 同一个页面常有多种写法。进入去重集合(`set` 或已抓取记录表)之前先标准化,再比较: ``` https://Example.com/a/b/ (末尾斜杠 + 域名大写) https://example.com/a/b#section2 (fragment) https://example.com/a/b?utm_source=x&gclid=y (追踪参数) ``` 规则: - scheme、netloc(域名)转成小写。 - 去掉 fragment(`#` 及之后)。锚点不是新页面。 - 去掉尾部斜杠。根路径 `/` 保留。 - 去掉追踪参数(`utm_*`、`fbclid`、`gclid` 等)。这些参数不改变页面内容。 上面三个字符串标准化后是同一个 URL。用原始字符串比较会抓三次,浪费配额,也可能因为请求量异常触发目标站的限流或封禁。 ## 抓取边界 | 边界 | 做法 | |---|---| | 同域检查 | 只抓与起始 URL 同域名的链接 | | 最大深度 | 限制从起始页出发的跳转层数,设硬顶(如 10) | | 最大页数 | 限制单次任务的总页面数,设硬顶 | | include/exclude 正则 | 控制路径,例如只抓某个子目录 | 过滤顺序:协议(只允许 http/https,排除 `javascript:`、`mailto:`、`tel:`、`data:`)→ 同域 → 排除静态资源(`.css`、`.js`、`.png`、`.pdf`、`.mp3` 等,这些不是内容页)→ include 正则 → exclude 正则。先做协议和域名比较,再做正则。 ## robots.txt 与限速 - 抓取前按 `User-agent: *` 检查 robots.txt。不允许的路径跳过。 - 按域名缓存解析结果。每个页面都重新请求一次,会多打目标站,也拖慢自己。 - robots.txt 本身 404 或超时:允许抓取。拿不到文件就放弃整次任务会漏掉多数没有 robots.txt 的站点。 - 请求间隔可配置,内部或自己有权限的站点可以调到接近 0。默认值保守。无间隔的高频抓取会压到小站点,也是最容易被 WAF 封禁 IP 的特征。 ## 失败分类 - 临时失败(网络超时、5xx、429):放进重试队列,指数退避,并且有次数上限。没有上限时,目标站持续故障会把抓取任务一起拖死。 - 永久失败(404、403、DNS 解析失败、robots.txt 明确禁止):标记跳过,记下原因,不重试。404 不会因为多请求几次变成 200。 原因要分开记,后面才能查「这个页面为什么没抓到」。所有失败走同一套重试,会把配额花在 404 上。任何失败都直接放弃,会把网络抖动算成抓取失败,完整率偏低。 ## 正文抽取 先去掉非正文,再抽出正文: ```python excluded_tags = ["script", "style", "nav", "footer", "header", "aside", "form", "noscript", "iframe", "svg", "canvas"] excluded_selectors = ["ads", "advertisement", "tracker", "cookie-banner", "popup"] ``` 输出 Markdown,保留标题层级。排除外部链接和社交媒体分享链接,去掉 overlay / 弹窗。 长度阈值按字符数算,或者设为 0,把「是不是噪音」交给上面的标签和选择器。按空格分词的字数阈值会把正常长度的中文正文判成过短而丢掉。 ## 增量变更检测 同一批 URL 定期重抓时,先判断正文变没变,再决定要不要重新入库、重新触发下游。 - 对抽出的正文算哈希(如 `sha256`),与上次相同就跳过后续处理。 - 更省流量:先发 `HEAD`,或带 `If-Modified-Since` / `If-None-Match`。服务端支持时直接拿 304,不下载正文。 - 算哈希之前去掉时间戳、随机广告位、访问计数器。这些片段设计上每次渲染都不同,混进哈希会让每次都被当成变更。排除口径和「正文抽取」一致;两处不一致时,比较的是两份不同的抽取结果。