区分服务器返回的原始 HTML、浏览器执行 JS 后的渲染 DOM 以及爬虫实际抓取三者之间的差异,并说明不同爬虫对 JS 渲染的不同处理方式。
同一个页面在你的浏览器里看起来完整,但在初始响应中却几乎不包含任何有用的内容。
这句话听起来自相矛盾,直到你把开发者经常混为一谈的两件事区分开来:服务器返回的 HTML,和浏览器执行页面 JavaScript 后生成的文档。
对于使用现代浏览器的用户来说,这种区别很容易忽略。浏览器运行应用程序、获取数据、更新 DOM,然后展示最终页面。
爬虫不一定遵循同样的路径。有的爬虫会渲染 JavaScript,有的稍后渲染,有的使用受限的执行环境,有的主要检查收到的响应。爬虫也可能在脚本失败、请求被阻止或渲染超出资源预算时停止。
因此,有用的问题不只是"这个页面加载了吗?",而是"尝试读取它的系统能获得哪个版本的页面?"
同一页面的三个版本
区分三种表示形式会很有帮助。
初始 HTML 或原始 HTML,是客户端 JavaScript 运行前服务器返回的响应体。你可以用 HTTP 客户端或浏览器的"查看源代码"功能来检查它。
浏览器渲染后的 DOM,是浏览器解析响应并执行相关 JavaScript 后的文档。这是你通常在开发者工具的 Elements 面板中检查的内容。
爬虫响应,是服务器针对特定爬虫请求返回的内容。它可能与发送给普通浏览器的初始 HTML 一致,但也可能不一致。网站可能会将已知爬虫路由到服务端渲染或预渲染的 HTML,而将客户端应用发送给正常访客。
考虑一个简化的客户端渲染应用:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Acme Analytics</title>
<script type="module" src="/assets/app.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
JavaScript 运行后,DOM 可能包含产品标题、多个段落、定价链接、文档导航、结构化数据,以及客户问题。但上述响应中并不存在任何这些信息。
浏览器有足够的信息来构建页面。而只读取响应的系统则没有。
CSR 和注水(hydration)是相关的,但并不相同
客户端渲染(CSR)通常意味着 JavaScript 在浏览器中创建大部分有意义的文档。服务器发送一个壳,客户端获取或计算内容。
注水从服务器已经渲染好的 HTML 开始。JavaScript 将行为附加到现有标记上。因此,一个注水后的页面可以在 JavaScript 运行前就暴露大量内容——前提是服务器响应真的包含那些内容。
仅凭框架选择无法告诉你爬虫收到的是哪种结果。React、Vue、Svelte、Angular 及类似工具都可以参与客户端渲染、服务端渲染、静态生成,或它们的组合。关键的是特定 URL 的实际部署响应。
这就是为什么"React 网站对爬虫不可见"这种说法毫无用处。有些 React 网站返回完整文档,有些返回薄壳,还有很多介于两者之间。
原始响应中会丢失什么?
当初始 HTML 很薄时,丢失的内容不限于正文。常见的缺失包括:
主要标题和描述性文本
通过 API 加载的产品或文章内容
内部导航和上下文链接
由客户端代码插入的 canonical、description 或社交元数据
挂载后生成的结构化数据
分页或相关内容链接
标签页、手风琴或客户端路由内的文本
请求失败时替换预期内容的错误和空状态
每种缺失的后果各不相同。缺失正文会减少非渲染阅读器能理解的内容。缺失内部链接会影响发现。缺失结构化数据会移除明确的机器可读描述,即使可见文案仍然存在。
单纯的字符数差异无法告诉你页面是否存在严重问题。Cookie 横幅可以增加数千个不重要的字符,而较小的差异可能包含了唯一的产品描述或更深层页面的链接。对比需要数字和检查两者兼顾。
搜索引擎爬虫和 AI 爬虫不是同一受众
把世界分成"Googlebot"和"AI 爬虫"很诱人,但这两个群体都包含目的和能力各异的系统。
搜索引擎可能会获取文档、单独安排渲染、用另一个服务重新访问它,并结合其他信号进行整合。AI 公司可能运营不同的爬虫用于搜索索引、模型训练、用户请求的检索,以及链接预览。回答实时问题的助手也可能使用与其公司通用网页爬虫行为不同的检索系统。
不要假设每个爬虫都执行相同的 JavaScript——或者一次成功的渲染就证明了它对所有其他爬虫都可用。把重要的公开信息放在有用的 HTML 中,然后用证据来验证特定爬虫的行为。
这在传统搜索之外也很重要,因为 AI 可见性至少有两个独立的层次:
系统能否访问和理解页面?
对于特定问题,它是否选择检索、引用、提及或推荐该信息?
改善第一层不能保证第二层。但一个不暴露有意义内容的页面会制造一个可以避免的技术障碍。
如何比较各个版本
从确切的公开 URL 开始,而不是开发路由或孤立的组件。
使用 HTTP 客户端并有意地跟随重定向:
curl -L https://example.com/product
保存响应体并将其作为文档检查。查找页面的主要标题、一句有特色的句子、重要链接、canonical 元数据以及结构化数据。
还要记录最终 URL、状态码、内容类型、重定向链和响应大小。200 响应只证明服务器成功返回了某些内容,不能证明响应中包含页面。
"查看源代码"是服务器响应的便捷表示。不要将其与 Elements 面板混淆,后者显示浏览器处理后的实时 DOM。
在源代码中搜索页面上明显出现的一句话。如果它不存在,判断浏览器是否在渲染期间添加了它。
不加载 JavaScript 来打开页面是一个有用的诊断手段,尽管它不是完美的爬虫模拟。它能快速揭示应用程序启动前是否存在有意义的 HTML。
预期结果取决于产品。复杂的编辑器可能合理地需要 JavaScript 才能运行。它的公开着陆页仍然应该在不等待应用程序 API 的情况下可理解。
使用 Playwright、Puppeteer 或其他浏览器自动化工具来捕获生成的 DOM。定义"完成"的含义:load 可能太早,而 network idle 在有分析或实时连接的页面上可能永远不会到达。
比较有意义的信号,而不是逐字节差分:
规范化后的可见文本
链接和目的地
标题、canonical 和 description
结构化数据块
最终 URL 和 HTTP 行为
User-Agent 测试可以揭示路由差异,但必须仔细解读。不同的响应并不自动意味着爬虫会执行或接受它。相反,相同的响应也不能说明爬虫稍后会渲染什么。
检查你自己的基础设施做了什么。不要仅根据一个 user-agent 请求就声称了解爬虫的内部渲染过程。
预渲染何时有用
当公开的、内容导向的路由发送薄的应用壳,而立即更改应用的渲染架构不实际时,预渲染会很有用。
渲染服务可以执行页面、捕获有用的 HTML、缓存它,然后将这个表示形式服务于符合条件的爬虫。这对于已建立的 SPA 特别实用,因为框架迁移与问题本身不成比例。
输出应该保持最新、保留正确的状态和重定向,并代表访客收到的相同公开内容。过期或实质性不同的内容会制造一个新问题。
预渲染何时不必要
不要仅仅因为网站使用了 JavaScript 就添加渲染层。
当你满足以下条件时,可能不需要它:
服务端渲染或静态生成已经返回了重要内容
原始版本和渲染版本只在交互行为上有差异
该路由是私有的或仅限应用的,不打算用于发现
缺失的内容可以通过更小的服务端更改直接修复
页面的公开元数据、文本、链接和结构化数据已经存在
最好的结果不是"预渲染一切",而是"以最少不必要的复杂性可靠地使每个公开页面的重要内容可访问"。
测试响应,而不是框架标签
这种原始版本与渲染版本的差距,是促使我构建 Prerender Buddy 的问题之一。它的免费检查器比较初始响应、爬虫面向的 HTML 和浏览器渲染的页面,因此诊断基于 URL 的实际行为而非对其技术栈的假设。
执行基本调查你不需要任何产品。curl、浏览器源代码、开发者工具和一个受控的浏览器脚本会让你走得很远。
重要的习惯是不要把"它在我的浏览器里能加载"当作测试的终点。一个现代页面是一系列表示形式的序列。如果发现很重要,请在界面可见之前检查到达的表示形式。