一次网页抓取流量异常的排查记录
3029 字约 11 分钟
摘要从 Auto Email Sender 的一次流量异常出发,记录一次批量抓取中 10 GB 的下载量如何被追查到浏览器使用、资源加载和重复抓取。
Auto Email Sender 是一个本地运行的导师联系工具。用户可以在里面整理个人材料,从高校官网抓取导师资料,判断研究方向是否匹配,然后生成并审核邮件草稿。真正发给谁、什么时候发,始终由用户确认。邮件发出后,系统还会继续跟踪回复和后续任务。
其中,“智能抓取”负责流程的最前端:用户提供某个学院的教师列表页,系统自动找到导师主页,再整理出姓名、职称、研究方向和邮箱。如果正文中没有邮箱,还会继续检查页面中的 PDF、图片文字和少量相关链接。
从功能上看,这一步只是为后续的匹配和写信准备公开文本。但在一次批量抓取期间,我在 FlowWatch 中看到,负责渲染页面的 chrome-headless-shell 在过去 12 小时里下载了 10.202 GiB,其中约 4.8 GiB 集中在两个小时内。
我的第一反应是统计可能有误。
抓取的只是导师姓名、研究方向和邮箱,并不是在播放视频。几段网页文字,怎么会用掉 10 GB?
流量从哪里来
我先把 FlowWatch 的进程记录和应用自己的页面抓取账本对照起来。流量确实来自项目通过 Playwright 启动的 Chromium,而不是系统中的其他浏览器,也不是监控把流量归到了错误的进程。
接着,抓取账本显示了一组不太正常的比例:12 小时内共有 6,353 次页面访问,其中 5,502 次使用了浏览器,约占 86.6%;另有 1,726 次访问指向已经访问过的页面,重复比例约为 27.2%。
具体的任务更直观。福州大学化学学院的一次抓取产生了 602 次页面访问,而且全部使用 Chromium。另一个任务只涉及 104 个不同的导师主页,却产生了 202 次导师主页访问记录。某个学院的公共目录在同一次任务中被加载了 68 次:第一次来自正常入口,其余 67 次都发生在邮箱补全阶段。
此时最明显的怀疑对象是并发。多个任务同时运行,确实可能让浏览器在短时间内集中下载大量内容。
但并发只能解释“峰值为什么这么高”,无法解释“总量为什么这么大”。即使把重复下载放慢执行,带宽曲线会变平,但这十次重复下载的内容仍然要全部下载一遍。
并发放大了异常,但异常并不是由并发产生的。
于是我沿着一次抓取实际经过的路径,逐步往下查。
被放大的访问策略
抓取器同时支持普通 HTTP 请求和 Chromium。HTTP 适合直接返回完整 HTML 的页面,速度快,传输的数据也少;Chromium 可以执行 JavaScript,用于处理动态渲染、交互式分页和加密字段。
两种方式各有用途。问题出在为了提高成功率而设置的一条规则上:只要某个学院的目录页曾经需要浏览器,系统就会记住这个域名适合使用 Chromium。之后,同一域名下的导师主页也都直接交给浏览器处理。
最初,我把这当作对站点特性的合理判断。但实际查看几所高校的网站后发现,目录页和导师主页的技术方案往往并不相同:
- 教师列表可能依赖 JavaScript 分页;
- 导师主页却可能是服务端直接返回的静态 HTML;
- 同一个域名下,甚至同时存在旧版和新版的网站。
“这个目录页需要浏览器”被扩大成了“这个网站的所有页面都需要浏览器”。一次目录页的判断,最终让数百个原本可以用普通请求获取的导师主页都启动了 Chromium。
于是,第一项修改确定下来:目录页和导师主页分别记录访问策略。导师主页先尝试普通 HTTP 请求,只有在返回内容为空、被拦截或确实依赖脚本时,才改用浏览器。
为什么不直接停用 Chromium?有些教师目录确实依赖动态渲染、交互式分页,或者需要 JavaScript 处理后的字段。问题不在于使用浏览器本身,而在于把某个页面的判断扩大到了整个域名。
这次修改减少了浏览器的启动次数,但仍然无法解释一个页面为什么会产生十几 MB 的流量。
浏览器多下载了什么
我继续检查传输的内容,发现 Chromium 并没有做错什么。它像正常浏览器一样,下载头像、背景图、轮播图、字体、样式表和媒体资源,力求把页面完整还原出来。
而爬虫最终只需要读取正文和链接。
某个学院页面的 HTML 本身并不大,但其中一组字体资源让单次访问产生了约 11 MiB 的下载量。对浏览网页的人来说,字体决定页面显示是否正确;但对只想找出研究方向和邮箱的抓取器来说,这些字体没有提供任何新的信息。
于是我开始按资源类型限制浏览器的请求:字体和音视频不再下载,图片请求返回一个 1×1 的透明占位图;HTML、CSS、JavaScript、接口请求和 iframe 仍然正常加载。
修改图片拦截时,我马上遇到了一个矛盾:抓取流程本身支持图片 OCR。如果图片都被替换了,邮箱图片还怎么识别?
答案在两个流程的分界处。浏览器加载图片是为了渲染整个页面,而 OCR 只需要少数可能包含联系方式的原图。页面 HTML 中的 src、data-src 等地址依然存在,因此系统可以在真正需要 OCR 时,再通过独立请求下载候选原图。
图片并没有消失浏览器渲染阶段使用极小的透明占位图,OCR 阶段再按需下载候选原图。这样既不会让每个页面自动下载全部装饰图片,也保留了识别图片中邮箱的能力。
这个调整解决了“每次访问太重”的问题,但抓取账本中 27.2% 的重复访问仍然存在。
被重复打开的页面
继续检查邮箱补全流程后,我发现:页面第一次抓取完成后,后续步骤拿到的只有一段整理过的文本,原始 HTML、页面链接、图片地址和最终 URL 都没有一起传下去。
大多数时候,文本已经够用。但如果文本中没有邮箱,补全流程就需要检查 PDF、图片和相关链接。为了拿回这些信息,它只能重新打开导师主页。
更麻烦的是,模型有时会把学院公共目录或“联系方式”页面选作寻找邮箱的入口。几十位导师可能都指向同一个公共页面,如果当前任务没有缓存,这个页面就会在每位导师的处理中被重新下载一次。前面提到的 68 次目录加载,正是这样产生的。
至此,流量异常的因果链终于完整了:
- 域名级别的判断让大量静态导师页面使用了浏览器;
- 浏览器为每个页面下载完整的视觉资源;
- 第一次抓取只保留了文本,后续补全只能重新打开页面;
- 多位导师引用同一个公共页面时,相同请求被重复发出;
- 网络失败和多轮重试又放大了前面四项的消耗。
真正需要保存的,不是一个更长的文本字段,而是第一次访问得到的完整页面记录:最终 URL、访问状态、正文、原始 HTML、链接,以及实际采用的抓取方式。后续的 PDF、OCR 和相关页面筛选都基于这份记录继续进行,不再为了“再看一眼 HTML”而重新打开导师主页。
对于同一任务中反复出现的 URL,系统会复用已有结果。如果多个步骤同时请求同一个页面,只会发起一次实际访问,其余步骤等待并共享结果。已经确认是学院目录的页面,也不会再被当作某位导师的个人补充页面处理。
结果复用的原则一次页面访问得到的信息,应该足以支撑后续所有步骤。
流量下降,内容不变
修改完成后,最直观的是一张更好看的流量图。但流量下降并不能证明抓取器变好了,也可能只是正文、链接或动态内容被一并拦截了。
我选取了六个结构不同的高校公开页面,分别用优化前后的逻辑访问,同时比较正文长度、链接数量、规范化后的文本和页面中的关键内容。结果可以归纳为三组:
| 对照范围 | 优化前 | 优化后 | 流量减少 |
|---|---|---|---|
| 六个页面合计 | 约 16.84 MB | 约 1.56 MB | 90.8% |
| 原本最重的页面 | 约 11.60 MB | 约 0.16 MB | 98.6% |
| 一个原本很轻的页面 | 约 0.12 MB | 约 0.12 MB | 基本不变 |
六个页面的正文、链接和关键内容保持一致,总传输量下降约 90.8%。其中,原本最重的页面减少了 98.6%,原本就很轻的页面则几乎没有变化。
第三行同样重要。它说明这些修改不会让所有网站都出现漂亮的数字,收益来自页面原本确实加载了大量无关资源。如果页面本身没有冗余,也就没有太多可以削减的内容。
六个页面只能说明六个页面这组对照无法覆盖整个互联网。如果正文只存在于图片中,或者依赖图片触发脚本、使用 Canvas、Blob URL 或 CSS 背景生成内容,这些页面的情况仍需继续观察。
没有继续收紧的限制
排查期间,还有几种看起来能进一步降低流量的做法:大幅减少重试次数、限制最大分页数量,或者让多个任务共享同一个浏览器上下文。
这些做法并非没有价值,只是会改变抓取能力本身。弱网环境下可能确实需要多次重试,大型教师目录也可能真的超过几十页,而共享上下文会让 Cookie、缓存和页面状态相互影响。
因此,第一轮只保留了那些能通过前后对照解释清楚的修改:能直接请求的页面不启动浏览器;必须使用浏览器时不下载无关资源;第一次访问得到的完整结果交给后续步骤复用。
最初看到 10 GB 时,我以为是 Chromium 出了问题,随后又怀疑并发太高。查到最后,浏览器和并发都只是放大器。流量来自几个单独看都很合理的决定:记住站点的访问方式、完整渲染页面、按需补全邮箱、失败后重试。这些决定在一条很长的流程中叠加在一起,才产生了这么大的流量。
这次排查最有价值的地方,不只是 90.8% 的下降。它让我重新理解了“抓取成功”的含义:既要拿到正确的内容,也要避免已经拿到的内容被后续步骤反复下载。