一个抓取任务开始时,本能反应是去抓一个无头浏览器。Playwright、Puppeteer 或 Selenium 能加载真实浏览器所能加载的任何东西,所以它感觉像是那个安全的默认选项。但无头浏览器是取回一个页面最贵的方式。它烧 CPU 与内存、把整个页面以及附着其上的每一份资产都下下来,而且它慢,这就给你每个 worker 能采集多少页面设了上限。很多时候,你付出这一切,只是为了抽出几个字段,而它们从头到尾都以纯文本躺在那里。真正的问题从来不是把”用不用浏览器”当作一种习惯,而是这个具体页面到底要求什么。这里就讲如何决定。
一个无头浏览器真正给你的是什么
一个无头浏览器,是一个没有可见窗口的真实浏览器引擎。它运行页面的 JavaScript、构建 DOM、执行现代页面所发起的后台 fetch 与 XHR 调用、渲染客户端内容,并让你通过点击、滚动与输入去与结果交互。它还呈现一份完整、逼真的浏览器指纹与 TLS 握手,因为它确实就是一个浏览器。那是很多能力。问题在于,它的每一部分都要花点代价,而大多数页面并不需要其中的大部分。
什么时候纯 HTTP 就够了——而这比你以为的更常见
在自动化一个浏览器之前,先看看这个页面到底是由什么构成的。在很大一部分情形里,一个像 Python 的 requests 或 httpx 那样的纯 HTTP 客户端,就是你所需要的全部。
第一种情形是服务器端渲染的 HTML。如果你想要的数据出现在最初那份 HTML 响应里,去看页面源代码、或对那个 URL 做一次 curl,你就会看见它正躺在那里,那么一个浏览器除了额外开销之外什么也不添。第二种,也是人们最常错过的,是一个底层的 JSON API。现代页面极常从一个由页面自己调用的后端 endpoint 去渲染,而如果你打开网络面板、盯住那些 XHR 请求,你会经常发现一份干净的 JSON 响应,里面正是你想要的数据,无需解析 HTML。直接用 HTTP 去调那个 endpoint,比去渲染那个消费它的页面更快、更稳、也更好解析。第三种情形是静态或轻度动态的页面,那里没有任何重要之物依赖客户端脚本。
这条路不只是更简单,它还便宜得惊人。一个 HTTP 请求拉的是你所要的那些字节、别无其他,所以它快、并行得好,而且经由你的代理只搬动其中一小部分数据。最后这一点直接关乎成本:通用的客户端模式在用 Python 使用住宅代理那篇指南里,而守在 HTTP 这条路上,是削减代理带宽与把延迟压低的最大杠杆之一。
什么时候你才真的需要一个浏览器
有些页面确实要求一个,而对它们硬上 HTTP,是它自己那份浪费时间。当数据只有在 JavaScript 跑过之后才存在时——一个把内容在客户端渲染、背后没有可访问 API 的单页应用——去抓一个无头浏览器。当数据是通过交互被拼装出来时——那种只在你滚动或点击时才去取更多的无限滚动与懒加载分页——去抓一个。当一个登录或多步流程依赖客户端脚本去建立一个可用会话时,去抓一个。以及,当一个站点主动检查一个真实浏览器上下文——执行 JavaScript 质询、或探查一个光秃秃的 HTTP 客户端无法满足的浏览器专属信号时——去抓一个。在这些情形里,浏览器不是额外开销,它是唯一能拿到数据的工具。
浏览器这条路的代价,直说
当你确实用一个浏览器时,要清楚你在花什么。最大的隐性代价是带宽。一个浏览器会像一个人的浏览器那样把整个页面加载下来——HTML,加上每一张图片、每一份样式表、每一种字体、每一个跟踪器、以及每一个第三方脚本——这可能是你真正想要的那一份 HTML 文档或 JSON 数据块尺寸的许多倍。那些字节的每一个都要经由你的代理旅行。解法是把你不需要的资源类型拦掉——图片、媒体、字体与分析——好让浏览器渲染到足以产出你的数据、却不去下载整个页面,这能把浏览器这条路上的代理带宽大幅削减。第二个代价是速度:渲染很慢,所以一个浏览器 worker 每分钟采集的页面,远少于一个 HTTP worker,而浏览器又吃内存,所以要扩起一支它们的舰队,是比开火 HTTP 请求更重的基础设施。第三个代价对许多团队是个意外,那就是无头浏览器并不自动更隐蔽。开箱即用地,它带着它自己那些可被检测的破绽,所以一套幼稚的无头搭建,可能比一个良构的 HTTP 请求更容易被标记,而不是更难。
大多数人跳过的那条中间路
极大量”我需要一个浏览器”的情形,其实是”我需要一个看起来真实的请求”。在升级到完整渲染之前,先试试带一份正确、一致的指纹的纯 HTTP:完整而连贯的请求头、妥当的 cookie 处理,以及一份相匹配的 TLS 与 HTTP/2 指纹,因为一个带着浏览器形状的一组指纹的请求,往往能在一个光秃秃的客户端被拦之处通过。把这个与避免被封的那套通用纪律配起来,许多看似要求一个浏览器的页面,结果会以一小部分的代价向一个良构的 HTTP 请求让步。只有当数据确实无论如何都不肯从别的方式来时,才升级到一个浏览器。
无论哪条路,代理都是需要的
选 HTTP 而非浏览器,并不改变你对一个干净网络身份的需要——两条路都经由住宅 IP 去看起来像寻常访客。改变的是你为此付多少。一个浏览器每页经由代理推送多出许多倍的数据,所以在计量的住宅带宽上,你所选的工具对成本有着直接的影响。住宅代理及其按 GB 计价,在两种做法之下都是同一套,而这恰恰是为什么,在更轻的工具行得通时就去抓它,值得那份纪律。
一套简短的决策流程
在写下任何一行自动化之前,把每一个目标都过一遍同一道快速检查。
- 数据在最初的 HTML 里吗?用 HTTP。
- 页面是否调用一个返回该数据的 JSON 或 XHR endpoint?用 HTTP 去调那个 endpoint。
- 内容是否只在 JavaScript 跑过之后才出现、且没有可访问的 API?用一个无头浏览器。
- 数据是否只通过滚动或点击才加载?用一个无头浏览器。
- 你拥有正确的数据路径却仍被封吗?先修指纹与 IP,再把浏览器当作最后手段去考虑。
在实践中,对 view-source 与网络面板的一瞥,会在你对任何事下承诺之前就回答掉前两问,而前两问所覆盖的站点,比大多数人所设想的更多。当你必须渲染时,让它保持精简:
# HTTP first: hit the underlying JSON endpoint the page already callsimport requests
PROXY = "http://customer-USERNAME-country-us:PASSWORD@p.shifter.io:443"proxies = {"http": PROXY, "https": PROXY}
r = requests.get("https://shop.example.com/api/products?page=1", proxies=proxies, timeout=15)r.raise_for_status()data = r.json() # structured data, no rendering, minimal bytes
# Only if rendering is truly required, run a browser through the same# proxy and block heavy resource types so it does not pull the whole page:# route.abort() on image / media / font / stylesheet requests# before reading the rendered content.底线
无头浏览器是用于客户端渲染与真正交互的对的工具,也是用于其余一切——也就是网络的大部分——的错的工具。在自动化任何东西之前,先查数据是在 HTML 里、还是在一个 JSON endpoint 之后;比起去起一个浏览器,优先选一个带真实指纹的良构 HTTP 请求;而当你确实要渲染时,把你不需要的拦掉,好让你不至于为了一小把字段而付钱去下载一整个页面。两条路都需要住宅 IP,但只有其中一条会为每一条记录都把一整个页面经由代理送出。把浏览器留到最后、而非最先去抓,而那个能可靠拿到数据的最便宜的工具,就是该用的那一个。
那一层网络层,正是住宅代理所提供的——一个由真实的、家庭级 IP 组成的大池子,无论你是用纯 HTTP 取回、还是驱动一个完整浏览器,它都一样地运作。按 GB 计价正是为什么更轻的那条路会回本:你只搬动自己真正需要的数据,横跨这份工作所要求的那么多目标。