Key Takeaways
一篇面向工程实践的 SPA 抓取决策指南:从 HTML、Hydration、XHR/GraphQL 到 Playwright 浏览器回退,并覆盖无限滚动、就绪判断、证据留存与成本控制。
SPA 只是前端交互模型,不等于“HTML 一定没有数据”。React、Vue、Next.js 等应用可能采用 CSR、SSR、SSG、流式渲染或混合模式。真正决定采集方案的是:目标数据在哪一层出现,以及必须执行哪些动作才能得到它。
先用一个决策树判断
这个顺序的目标不是“绕过浏览器”,而是减少不必要的 CPU、内存、带宽和失败面。
第一步:检查初始 HTML
先用普通 HTTP 客户端获取页面:
检查:
- 商品名、价格或正文是否已经存在
<script type="application/ld+json">- 页面中的 JSON 状态
- canonical、语言、市场信息
- 是否只是一个空壳
<div id="root"></div>
如果业务数据已经在 HTML 中,就没有必要为了同一份数据启动 Chromium。
SSR、CSR 与 Hydration 的区别
SSR/SSG:服务器先生成可见 HTML,浏览器收到后可以直接看到主要内容。
CSR:初始 HTML 可能只有壳,JavaScript 运行后再请求数据并生成 DOM。
Hydration:服务器已经生成 HTML,客户端 JavaScript 再把交互逻辑“接上去”。React 官方 hydrateRoot 文档明确描述了这一过程:React hydrateRoot↗。
因此看到 React 并不意味着必须浏览器抓取;很多 React 页面在 JavaScript 执行前已经拥有足够数据。
第二步:寻找 Hydration 或内嵌状态
常见位置包括:
__NEXT_DATA__window.__INITIAL_STATE__- JSON script block
- RSC/框架序列化数据
- JSON-LD
不要只按变量名硬编码。先保存原始 HTML,再定位包含目标业务字段的数据块,并验证它与页面显示一致。
一个简单的 Python 示例:
不要假设所有 Next.js 页面都有相同结构;框架版本和路由模式会变化。
第三步:识别 XHR、fetch 和 GraphQL
如果数据不在初始 HTML,浏览器通常会通过网络请求加载它。
重点观察:
- XHR/fetch URL
- GraphQL endpoint 和 operation name
- query 参数
- request body
- 必须的公开 cookie / locale 参数
- 分页 cursor
- 响应 Schema
如果接口是公开且自动访问得到授权,直接请求通常比重放完整浏览器更稳定。
但不要把浏览器里看到的接口自动视为“允许无限调用”。仍应检查授权、服务条款、速率和数据使用边界。
API-first 的验证标准
从接口获取数据后,至少验证:
不要只因为 JSON 能解析就认为结果正确。需要检查业务主键、市场、币种、时间和关键字段。
什么时候必须使用 Playwright
浏览器更适合这些情况:
- 数据必须执行 JavaScript 才产生
- 需要点击、选择、滚动或切换 tab
- 内容依赖浏览器状态
- 需要获取渲染后布局或可见文本
- API 调用依赖复杂前端状态且没有稳定、授权的直接接口
- 需要保存 trace、截图或 DOM 作为 QA 证据
Playwright 最小渲染模板
Playwright 官方文档把 networkidle 标记为 DISCOURAGED,建议使用能够代表页面业务就绪的断言或元素信号,而不是等待网络完全安静:Playwright Page↗。
不要把 networkidle 当成 SPA 的完成条件
现代 SPA 常常持续存在:
- analytics
- polling
- WebSocket
- 推荐流
- 广告
- telemetry
页面可能永远达不到你想象中的“网络静止”。更可靠的是等待:
- 产品卡出现
- 表格行数达到预期
- loading skeleton 消失
- 某个 API response 返回
- 页面状态文字变为 Ready
等待特定接口
这种方法适合把“DOM 抓取”转成“浏览器负责建立状态,代码读取结构化响应”。
无限滚动不能靠固定 sleep
固定 sleep(3000) 会在快页面浪费时间,在慢页面丢数据。
更好的循环条件:
- 记录当前业务记录数。
- 滚动或点击“加载更多”。
- 等待记录数增加、cursor 变化或接口响应。
- 连续多轮没有新增则停止。
- 设置最大页数、最大时间和最大字节数。
伪代码:
真实项目还应结合网络响应或业务游标,而不是只看 DOM 数量。
Lazy loading 与虚拟列表
有些列表只在 viewport 中保留少量 DOM 节点。此时 locator.count() 可能不会随总数据量增加。
识别信号:
- DOM 数量稳定但内容不断替换
- 滚动条高度变化
- API cursor 持续推进
- React/Vue virtualized list
这种页面应优先从网络数据层获取完整记录,并把 DOM 用作抽样验证。
代理应该在哪一层加入
如果 SPA 数据通过 HTTP API 即可获取,可以在 HTTP 客户端配置代理;如果必须浏览器渲染,再在 Browser 或 BrowserContext 层配置代理。
Playwright 官方支持 HTTP(S) 和 SOCKS5,并支持 browser/context 两级配置:Playwright Network↗。
不要为了“使用代理”而强制把本来可以 HTTP 完成的任务升级到浏览器。
资源拦截要谨慎
图片、字体和媒体通常可以降低带宽,但不要无脑阻止所有 script、XHR 或 CSS。SPA 的核心数据和渲染逻辑可能依赖这些资源。
建议先记录资源类型和业务影响,再决定是否拦截。
页面分类比 HTTP 200 更重要
SPA 常见假成功:
- HTTP 200 + 登录页
- HTTP 200 + CAPTCHA/挑战页
- HTTP 200 + consent page
- HTTP 200 + skeleton 永不消失
- HTTP 200 + 错地区页面
定义业务分类:
只有 expected 才应该计入有效结果。
证据与回归
每次解析器发布建议保存:
- 原始 URL
- 最终 URL
- 抓取时间
- HTTP 状态
- 数据来源层:HTML / hydration / API / browser
- 关键响应样本
- 内容 hash
- parser version
- 必要时的 screenshot/trace
当站点前端改版时,你可以判断到底是数据源变了、选择器变了,还是路由被拒绝。
成本模型
这不是绝对性能排名,但通常是一个合理的工程起点。浏览器增加 CPU、内存、启动时间和资源请求,因此应该服务于“必须执行 JavaScript/交互”的需求。
最终指标建议使用:
上线检查清单
- 是否先检查了 HTML?
- 是否检查了 hydration/JSON-LD?
- 是否确认了接口访问授权?
- 浏览器是否真的是必要条件?
- 是否等待业务信号而非固定 sleep?
- 是否避免依赖
networkidle? - 无限滚动是否有确定停止条件?
- 是否区分 HTTP 成功与业务成功?
- 是否保存足够证据用于 parser 回归?
- 是否设置最大时间、重试和流量预算?
相关内容
Alex Vance
核心基础设施架构团队
本文由 BytesFlows 工程团队审查。代码示例适用于合规的公开网页数据采集、QA 自动化、SEO 监测与市场研究工作流。实际基准表现可能因目标站点防爬策略、地理位置、客户端运行环境及请求频率而异。