文档站点快照与长图归档:从爬虫抓取到无头浏览器渲染实战

📅 发布时间:2026/10/13 4:29:13
文档站点快照与长图归档:从爬虫抓取到无头浏览器渲染实战
1. 为什么文档站点快照比想象中更依赖长图这条出口1.1 文档站点的消亡从不打招呼文档站点这东西平时不觉得金贵一旦改版或者下线满群找缓存的人能排到明年。我做过不少Python爬虫项目说实话最能让我觉得有复用价值的反而不是那些花里胡哨的数据采集而是一个不太起眼的文档快照归档器——把站点原样抓下来再顺手渲染成长图给团队留底存档。说个实际场景某项目的内部技术文档站页面里写满了接口变更记录、数据库字段说明、上线排查手册。某天运维升级站点目录结构全变了几百个技术文档的旧链接直接打不开。如果没有事前快照很多一次性记录的细节就彻底丢了。归档的意义不是预防什么大事而是让查旧资料这件事永远有退路。你可能会问直接点击浏览器另存为不行吗当然行但只能一页一页存。面对一个几十上百页的文档站手工操作效率太低而且保存下来的HTML文件散落在一堆命名混乱的目录里后续根本没法快速找回。我做这个工具的思路很明确输入一个站点入口链接程序自己遍历、抓取、落盘输出一套结构清晰的本地快照再把每个页面渲染成长图归档方便在聊天工具和文档群里直接转发查看。1.2 快照方案对比HTML、PDF、长图各管一段动手前先想清楚一点快照为什么最终要落到长图很多人第一反应是存HTML就够了但实测下来不同格式对不同场景各有优势。归档格式优点缺点适合场景原始HTML文字可检索、体积小、保留原站结构依赖本地资源文件稍有不慎就面目全非深度归档、后续全文搜索单页截图直观、接近真实渲染效果页面高一点就要滚动N次一屏放不下快速分享单页内容长图能完整呈现整篇页面、适合聊天工具里翻阅生成拼接需要处理滚动截屏、合图成本较高团队日常查阅、离线备份、对外展示PDF打印排版规范、可加书签目录中文字体嵌入麻烦、重构网页布局需要时间正式交付、打印场景我的归档器最终采用HTML快照 长图双轨制HTML负责深度保存和检索长图负责快速阅读和分享。这个组合在实际使用中基本覆盖了所有需求而且每种格式的生产成本都不高。1.3 典型用法与项目目录设计工具拿到一个入口URL会主动发现站内所有相对路径的页面生成如下目录archive_root/ mirror_site/ docs/ intro.html api/ user.html order.html assets/ css/ js/ images/ screenshots/ docs/ intro.html.png api/ user.html.png index.html manifest.json镜像目录按域名下的路径结构一对一还原截图目录跟随镜像目录平行生成。这样后续想查某个页面的文字内容就进mirror_site想在聊天群里甩图就直接找screenshots各取所需。2. 抓取层的正确姿势不写通用爬虫只写会过日子的爬虫2.1 先摸清站点结构再动手拿到目标站点后我不会直接开写爬虫而是先做一次手动侦察。打开站点观察导航栏、目录页、分页结构再看URL规律。绝大多数文档站的链接结构都很规整要么是/docs/xxx.html这样的静态路径要么是/article?id123的动态参数。这个规律决定了我后续的遍历策略。以最常见的静态站点为例整个过程分三步先用一行代码拉取入口页用解析库提取页面上所有同域链接再用宽度优先遍历逐层往里走。这个过程的示意图大概是从入口页提取所有a标签的href。过滤掉外部域名、mailto:、javascript:等无效链接。把链接标准化成绝对路径放进待抓取队列。依次抓取每个待抓取页面重复第1步提取新链接。设定最大深度防止递归抓出整个外网。关键参数我一般这样设置最大深度设3到5层同域限制必须打开抓取间隔控制在0.5到1秒。有人可能觉得间隔无所谓但真遇到对端有频率限制时你哭都来不及。慢一点稳一点归档工具不是竞速比赛。2.2 递归遍历、URL去重与同域限制爬虫的第一大敌不是反爬而是自己的失控。URL去重做得不好很容易把同一个页面抓几十次。我用一个seen集合存已经访问过的URL标准化结果标准化的过程包括去掉#锚点、默认端口号、大小写归一化。对动态参数则按是否影响页面内容来决定要不要保留。同域限制要单独强调。我们用urllib.parse.urljoin把相对链接拼成绝对链接然后判断目标URL的netloc是否和入口一致。如果入口是文档站的主域名比如docs.example.com那么所有指向cdn.example.com的静态资源链接不能作为页面抓取入口但可以作为资源下载地址单独处理。实际在项目里我会把URL分类存放需要抓取的文档页链接、需要下载的资源链接、不需要跟进的外链。这种分类比统一放进一个队列更安全后面做资源落盘时也更顺手。2.3 页面保存与资源落盘抓下来的HTML不能只存一个html字符串要把页面的CSS、JS、图片一并下载到本地否则断网后打开快照就是一堆没有样式的纯文本归档效果大打折扣。具体做法是先解析HTML里所有link、script、img、source标签的地址然后逐个下载到assets目录。下载时注意资源路径的映射关系原站路径是/assets/css/theme.css本地就保存成assets/css/theme.css。这样HTML里只要把绝对路径改成本地相对路径就能在无网状态下完整渲染。不少页面还带字体文件、图标文件这些容易漏。我的策略是不管类型只要在assets目录下且属于资源标签一律下载。文件量大时控制并发数我常用线程池限制5个并发下载任务避免把对方站点资源服务器打穿。3. 静态重放与本地化改造让快照不依赖任何在线资源3.1 HTML里的CSS/JS必须跟着落地快照能不能活关键看CSS和JS有没有跟着到位。很多爬虫工具只抓HTML不抓资源结果离线打开时页面布局全乱这不算真正的快照。要称得上快照必须保证把HTML文件拷到任何一台没有网络的电脑上用浏览器打开效果和线上一致。资源落地的方式我分三步实现第一步提取所有资源的URL第二步下载到和原站路径一致的本地位置第三步把HTML里的src和href改写成相对路径。改写相对路径有个坑就是文档里使用了/开头的绝对路径时需要根据当前页面在本地目录中的深度计算出一串../前缀。例如页面保存在docs/api/user.html资源地址是/assets/css/theme.css那么本地改写后应该是../../assets/css/theme.css。不少工具就挂在这个计算上我会写一个独立的路径转换函数并给每个生成的HTML做一遍自检确保所有引用的本地资源都能通过相对路径找到。3.2 链接改写与编码处理页面里除了资源引用还有大量站内链接这些链接如果不改用户点击快照里的链接时会跳到线上违背快照的初衷。我的做法是把所有站内链接统一改写成指向本地镜像内对应HTML文件的相对路径。如果链接指向的页面没有抓到就先留一个占位标记同时生成一个missing列表归档完成后统一检查。这样即使有漏网之页也能在检查时发现。编码问题同样容易忽略。老一点的站点还在用gbk编码新站点基本是utf-8。我的做法是抓取响应时优先看Content-Type头里的charset参数没有的话再通过页面meta标签判断最后才用字节流做嗅探。保存文件时统一转成utf-8并同步修改meta charset这样后续不管在什么操作系统上打开都稳定。3.3 遇到前端渲染站点怎么办现在不少新文档站改用前端框架页面内容全靠JS动态渲染。如果直接抓取原始HTML拿到的只是一个空骨架真正的文档正文根本不在里面。这时候我的策略是先渲染再抓取也是后面的长图环节一定会用到的手段。我用了无头浏览器来渲染页面等待页面网络空闲、关键节点出现后再获取渲染后的HTML。和直接抓取相比这个步骤慢一些内存占用也高一些但面对现代前端站点是必须的。考虑到快照本就是对当前站点状态留存渲染后的HTML比原始HTML更有保存价值。4. 长图生成从能截屏到截得干净4.1 选择无头浏览器而不是requests直接截如果你以为长图就是拿抓下来的HTML拼一拼那就低估这事了。长图的本质是把浏览器视口里渲染出的页面完整截出来按原逻辑拼在一起。想想也知道requests能拿到HTML文本但拿不到渲染结果。真正做这件事的是无头浏览器。我选择无头浏览器的主要原因有三个一是能稳定等待懒加载图片完成二是能设置一致的viewport宽度避免不同屏幕下截图比例失控三是能获取完整的页面渲染高度为分段截屏提供坐标依据。跑文档站快照这个场景headless模式足够了不用弹真实窗口。初始化和浏览器窗口设置的要点如下打开本地HTML文件而不是线上URL这样可以完全排除网络因素的影响。viewport宽度根据目标站点设计稿设定我常用1440像素这个宽度必须固定因为后续拼接长图时所有截图尺寸要一致。4.2 分段滚动截屏的合图策略页面高度动辄三四千像素远超单屏高度直接截一张大图在很多操作系统中会被裁剪或超限。我的做法是分段截取按视口高度把整页切分成N段每段滚动到对应位置后截一张图最后用图像库按顺序垂直拼接。用Python描述核心逻辑就是# 先获取整页高度和视口高度 page_height browser.evaluate(document.body.scrollHeight) viewport_height browser.evaluate(window.innerHeight) # 分段截图 screenshots [] for y in range(0, page_height, viewport_height): browser.screenshot(clip{x: 0, y: y, width: viewport_width, height: viewport_height}) screenshots.append(part_image) # 拼接 final_width screenshots[0].width final_height sum(img.height for img in screenshots) canvas Image.new(RGB, (final_width, final_height), (255, 255, 255)) current_y 0 for img in screenshots: canvas.paste(img, (0, current_y)) current_y img.height这个方案简单直接但有一个隐藏陷阱浏览器滚动时如果页面内容有动画或动态加载两次截图之间元素位置可能漂移。我用了一个小技巧——每次滚动后固定等待几百毫秒确保渲染稳定再截图。4.3 六个容易翻车的细节分段截图看着简单实际操作中我从踩坑里总结出了六个细节第一懒加载图片。页面里很多图片是滚动到附近才开始加载的。如果第一段第一次滚动时下面的图片还没触发加载后续截取到那段时可能已经加载了也可能没有。我的解决方式是在截屏前先模拟一次慢速滚动到底触发所有懒加载后再回到顶部开始正式分段截图。第二固定页头。现在很多文档站有position: sticky的顶部导航滚动时它一直悬浮在视口顶部。分段截图如果直接各截各的拼接出来的长图上会有多个重复的导航栏。解决办法是截屏前临时在页面上注入一段样式把sticky元素改成position: static等截图完成再还原。这个处理非常关键不做的话长图根本没法看。第三WebGL和canvas动态内容。这类元素经常渲染不出来或者截出来是空白。如果归档的文档站点包含图表类页面我会额外等待一小段时间再截屏但不是所有动态内容都能救回来。遇到这种情况我的经验是在索引里打个标记提示该页截图质量受影响。第四长图尺寸上限。对非常长的页面比如完整打印版的API文档分段数量多到几百张时内存占用巨大拼接也可能超时。我限定单文件最长不超过两万像素超过就生成分段长图并在文件后缀里加序号例如user.html.part1.png、user.html.part2.png。第五背景透明。某些页面的body背景是透明的拼接时如果底色不一致长图上会出现一道明显的分界线。我会在拼接前把画布底色统一设置成白色同时把每段截图的透明区域先合成为白色背景。第六中文字体渲染。Linux服务器上如果不装中文字体截图里的中文全是方块。部署前先用fc-list检查有没有中文字体没有就安装一套开源中文字体。这一步虽然不算代码问题但栽过跟头的人都知道它的重要性。5. 归档索引与增量差异快照工具的使用感和自动化的关键5.1 自动生成索引页快照和长图都有了如果连入口都找不到归档就变成了一堆没有目录的仓库。我会给每次归档生成一个index.html索引页按站点的原始目录结构展示所有抓取页面每一行显示页面标题、链接、截图状态以及抓取时间。索引页本质上是个小的静态HTML生成逻辑不复杂遍历镜像目录提取每个HTML页面的title标签再结合截图目录做状态标注。抓取失败的页面会在索引里被标成红色方便一眼看出哪些链接有问题。这里分享一个小经验索引页的生成时间要用本地服务器时区不要用UTC否则团队成员看时间还得换算。另外索引页里可以加一个最基础的站内搜索框用JavaScript做标题过滤。这个小功能大幅提升日常查找效率实现成本只有二三十行代码。5.2 用Git做版本对比文档站是活的今天记录的接口明天可能就变了。如果每次归档都覆盖旧文件那么曾经的版本就丢了。我的做法是把归档目录放进Git仓库每次归档结束后提交一次这样历史版本全部都有据可查。Git在这里还承担一个重要作用差异提醒。通过对比当前版本和上一版本的变动文件列表可以快速知道哪些文档页发生了修改。我的自动化流程里归档完成后会执行一次git diff --stat然后把变动的文件清单单独输出到一个变更日志里。有人会觉得文档站点改动频繁Git仓库会越来越臃肿。实际上纯文档快照的变动量不大多数页面常年不变Git仓库的增长非常可控。如果归档频率高、页面体量大也可以考虑只对文本型HTML做版本控制把截图目录排除掉截图留最新一份即可。5.3 定时任务与站点变更警报至此手动执行归档已经没有问题了但既然是归档器就应该让它自动跑起来。我用系统的定时任务比如Linux的crontab设定每周日凌晨执行一次完整归档工作日每天早上执行一次增量检查。增量检查的模式是这样的先去抓目标站点的首页或某个固定入口做一个轻量对比判断是否有明显的结构变动。如果有变动就触发一次完整归档并把变更提醒发到通知渠道。值得注意的是提醒里不要附带上敏感或内部信息只发送某文档站结构有变化快照已更新这类提示即可。归档工具是内部系统保持低调更重要。6. 稳定性设计与常见问题排查6.1 拒绝被反爬节奏和特征有人可能会担心做了爬虫会不会被目标站点封。文档站一般不是高价值反爬目标但存在基础防护的也不少。我从一开始就坚持三条原则控制抓取频率、设置合理的请求头、遵守站点的robots协议。频率控制是最重要的所有请求之间至少要间隔几百毫秒并发不要开太大。请求头方面我会设置一个常规浏览器的User-Agent不要用默认的Python UA。这里多说一句很多爬虫被抓不是UA问题而是请求间隔太规律机器行为太明显。我实际测试时会把间隔做一点随机抖动比如0.6秒到0.9秒之间随机效果比固定间隔好得多。6.2 乱码、404、动态加载的兜底策略实际运行中我遇到过三类高频故障。第一类是乱码。表现是归档后的HTML打开全是乱码多半是编码判断失误。兜底方案是在抓取时把原始字节流先存一份解析阶段既用Content-Type头判断也做字节序列检测两边都判断不了时再按照utf-8处理。如果取回后HTML里出现大量替换字符就直接报告编码识别失败。第二类是404和链接失效。文档站经常有旧链接指向已删除的页面。我的策略是抓取过程中遇到404时记录状态不中断整体流程归档完成后输出一个失效链接清单方便人工判断是站点迁移了还是确实删除了。第三类是动态加载内容截不全。表现是长图里某个区块空白或者元素错位。这通常和前端渲染时序有关。兜底处理是在截屏前等待页面networkidle状态额外等待秒数同时在长图底部生成一个带时间的归档生成说明小尾巴标明该图的生成环境。6.3 我保留的两个增强项这个归档器搭好后我保留了两个增强功能没用默认开启但随时可以启用。第一个是全文检索。镜像目录里的HTML文件都是纯文本用搜索引擎思路做全文索引并不难。把每个页面的正文提取出来存入索引库就可以实现毫秒级的关键词检索。这个功能适合归档量特别大的场景普通几十上百页的文档站暂时用不上。第二个是数据库版归档清单。目前索引信息存在manifest.json里页面量增加后可以迁移到数据库。迁移的好处是可以做更灵活的状态查询比如哪些页面连续三次没有变化、哪些页面的长图生成失败。我个人的习惯是小站点用文件版大站点用数据库版需要切换时成本也就一次序列化导入导出。最后再分享一个实际运行中的心得这个归档器跑顺之后最大的价值不在于爬虫本身而在于定时、可靠、不打扰。我把它部署在一台低配置服务器上每周静默执行工作群里偶尔收到一条变更提醒。大家真正需要的时候它把历史文档完整地摆在手边——这才是归档工具该有的样子。