KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 渲染:爬虫看到的是不是你看到的 — keel 龙骨
本章目标:讲清「渲染(Rendering)」环节——从拿到的 HTML 到最终参与索引的文本,中间发生了什么。 验收标准:关掉 JavaScript 后,页面正文仍然完整可见。
本章目标:讲清「渲染(Rendering)」环节——从拿到的 HTML 到最终参与索引的文本,中间发生了什么。
验收标准:关掉 JavaScript 后,页面正文仍然完整可见。
为什么渲染是一个独立环节:抓取只保证"HTML 到手"。如果正文是由 JavaScript 在浏览器里生成的,那么爬虫拿到的很可能只是一个空壳。抓取成功 ≠ 拿到内容,两者之间隔着的就是渲染。
一、四种渲染方式
| 方式 | 谁执行渲染 | 爬虫拿到 | 适用 |
|---|---|---|---|
| HTML 直出 / SSG / 预渲染 | 构建时生成静态 HTML | 完整正文 | 内容型站点(首选) |
| SSR | 服务端每请求渲染 | 完整正文 | 动态/个性化 |
| ISR | 服务端增量再生成 | 完整正文(可能有延迟) | 大量内容的混合站 |
| CSR(客户端渲染) | 浏览器里的 JS | 空壳(除非再渲染) | 登录后的应用 |
一个典型的 CSR 页面源码:
<div id="root"></div>
<script src="/assets/index-abc123.js"></script>
爬虫第一次拿到的就是这个。正文在 index-abc123.js 运行之后才出现。
二、两波索引:Google 的二次渲染
Google 具备"执行 JavaScript 再渲染一次"的能力,这个过程叫渲染队列。因此 Google 的索引实际分两波:
- 第一波(立刻):索引原始 HTML 里能直接读到的内容。
- 第二波(延迟):把页面排进渲染队列,用无头浏览器执行 JS,拿到最终 DOM 再更新索引。
关键事实:
- 第二波是排队+延迟的,可能是数天甚至更久,且不保证执行。
- 其他所有爬虫基本只有第一波。百度、Bing、以及几乎全部 AI 爬虫(GPTBot、ClaudeBot 等)不执行 JS。
- 因此 "Google 能渲染"不能作为依赖——你在为其他所有引擎做优化。
依赖 JS 渲染 = 把收录机会交给一个延迟的、不保证的队列。
三、JS 站点的真实风险
| 风险 | 说明 | 后果 |
|---|---|---|
| 内容在 JS 里 | 正文由 fetch 回来再渲染 | 除 Google 外全部抓不到 |
| 链接是 JS 事件 | onclick 而非 <a href> |
链接图断裂,发现失败 |
| 分页/无限滚动 | 靠滚动触发加载 | 深层内容不可达 |
| 动态路由 | History API 切换,无真实 URL | 每个状态无独立 URL |
| 渲染前后不一致 | JS 改了标题/加了内容 | 两波索引内容打架 |
| 渲染超时 | JS 太重,渲染队列放弃 | 第二波永远不完成 |
判定一个站点是否有此风险,只看一件事:关掉 JS 后,正文在不在。
四、自证内容可见(最直接的判据)
# 1) 用 curl 拿源码,检查是否包含正文关键词
curl -s https://www.xxxxxx.cn/learning/courses/tools/lessons/1/ | grep -c '本章目标'
# 返回 0 → 正文不在 HTML 里,爬虫抓到的是空壳
# 2) 对比:带 JS 与不带 JS 的可见文本差异(需 headless 环境)
# 关 JS 的渲染结果 ≈ 爬虫第一波拿到的内容
# 3) 检查链接是不是真的 <a href>
curl -s https://www.xxxxxx.cn/learning/ \
| grep -oE '<a [^>]*href="[^"]+"' | wc -l
# 数量远少于你在浏览器里看到的导航项 → 链接由 JS 生成
三条判据:
| 检查项 | 通过标准 |
|---|---|
| 正文在初始 HTML | curl 源码中含正文文本 |
链接是 <a href> |
curl 能提取出站内链接 |
| 关键元数据直出 | title/description/canonical 在源码中 |
五、解决方案与代价
| 方案 | 做法 | 代价 |
|---|---|---|
| 预渲染 / SSG(推荐) | 构建时为每个路由生成含正文的 HTML | 路由需可枚举;构建变慢 |
| SSR | 服务端渲染 | 需要常驻服务;成本高于静态 |
| 同构框架 | Next/Nuxt 等 | 架构改造 |
| 动态渲染(Dynamic Rendering) | 给爬虫返回预渲染版本 | 已被 Google 标记为过渡方案,维护两套 |
| 混合渲染 | 关键内容直出,交互部分水合 | 复杂度上升,但最实用 |
本站采用route shell 预渲染:构建脚本按内容清单为每个课程/章节生成 HTML 外壳,正文直接内联。收益是三重的——爬虫拿到完整文本、首屏直出、LCP 同时受益。
实现要点:预渲染产物的 URL 必须与前端路由逐字符一致,否则会出现两份内容相同、URL 不同的页面,直接制造重复内容(回到第 02 章的 canonical)。
六、混合渲染的实用边界
完全静态有时做不到(比如需要登录态、个性化推荐)。可用的折中:
- 正文直出,交互后挂:首屏 HTML 含完整正文,JS 只负责点赞、评论等增量交互。
- 关键内容内联:把标题、摘要、结构化数据写进 HTML。
- 链接必须真实:导航、分页、相关推荐一律用
<a href>,不用onclick。 - 渲染前后一致性:JS 不要大幅改写
<title>、h1与正文——两波索引会对不上,让搜索引擎难以判断该采信哪一版。
# 渲染前后一致性自检:源码中的 title 与渲染后的 title 是否一致
curl -s https://www.xxxxxx.cn/learning/ | grep -oiE '<title>[^<]*</title>'
七、故障速查
| 现象 | 渲染层原因 | 处理 |
|---|---|---|
| curl 源码里没有正文 | CSR,内容靠 JS | 改预渲染/SSR |
| Google 收录了但百度没有 | JS 渲染,百度不执行 | 直出正文 |
| 首页能抓、内页抓不到 | 内页链接是 JS 生成 | 改 <a href> |
| 收录内容与页面不符 | 两波索引内容不一致 | 保证渲染前后一致 |
| 内容全丢 | 渲染队列超时 | 减小 JS 体积,关键内容直出 |
| 分页第 N 页不收录 | 无限滚动无独立 URL | 给出可抓取的分页 URL |
八、本章验收
-
curl页面源码能看到文章正文(不依赖 JS) - 站内导航与分页链接都是
<a href>,curl可提取 - title / description / canonical 在初始 HTML 中直出
- 已确认 JS 渲染不会大幅改写正文与标题
- 明白"Google 能渲染"不能作为依赖,其他爬虫只有第一波
# 一条命令的渲染总检
curl -s https://www.xxxxxx.cn/learning/courses/tools/lessons/1/ \
| grep -c -e '本章目标' -e '<title>'
拿到正文之后,它能不能进索引,还取决于下一章索引环节的准入判据。