1. 为什么需要文档时光机与截图归档器?
在开源社区和文档维护工作中,我们经常遇到一个棘手问题:官方文档会随着版本迭代不断更新,但某些关键功能的旧版文档可能突然消失。上个月还能正常运行的代码示例,这个月可能就因为文档改版而失效。更糟的是,当你在技术社区看到有人引用某个文档片段时,很可能点击链接后发现"404 Page Not Found"。
我最近就踩过这样的坑。在为开源项目提交PR时,引用了官方文档中的某个API说明,结果两周后维护者回复说文档已经更新,我引用的内容不复存在。这种经历促使我开发了这个工具,它能自动抓取文档网站的快照,并建立可追溯的存档系统。
Playwright作为现代浏览器自动化工具,相比传统爬虫方案有三大优势:
- 能完美处理SPA(单页应用)等动态渲染内容
- 内置智能等待机制,避免因网络延迟导致的截图不全
- 支持PDF生成和全页面截图,保留完整的文档样式
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与Playwright基础配置
2.1 安装Python环境
推荐使用Python 3.8+版本,通过pyenv或conda管理多版本环境。安装Playwright时需要注意:
bash复制# 创建虚拟环境
python -m venv doc_archiver
source doc_archiver/bin/activate # Linux/Mac
doc_archiver\Scripts\activate # Windows
# 安装核心依赖
pip install playwright
playwright install # 这会下载Chromium、Firefox和WebKit浏览器
注意:Playwright默认会安装三个浏览器引擎,如果只需要Chromium,可以使用
playwright install chromium节省磁盘空间。
2.2 初始化Playwright项目
创建基础项目结构:
code复制/doc_archiver
├── configs/ # 配置文件
├── archives/ # 存档文件
│ ├── screenshots/ # 截图
│ └── pdfs/ # PDF版本
├── utils/ # 工具函数
└── main.py # 主程序
在configs/urls.json中配置需要监控的文档URL列表:
json复制{
"targets": [
{
"name": "React Docs",
"url": "https://reactjs.org/docs/getting-started.html",
"schedule": "0 0 * * *", // 每天执行
"depth": 2 // 爬取深度
}
]
}
3. 核心爬取逻辑实现
3.1 页面导航与内容提取
使用Playwright的异步API实现智能爬取:
python复制async def crawl_page(url, depth=1):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
try:
await page.goto(url, wait_until="networkidle")
# 提取页面文本内容
content = await page.content()
title = await page.title()
# 全页面截图
screenshot_path = f"archives/screenshots/{slugify(title)}.png"
await page.screenshot(path=screenshot_path, full_page=True)
# 生成PDF
pdf_path = f"archives/pdfs/{slugify(title)}.pdf"
await page.pdf(path=pdf_path)
# 提取所有链接用于深度爬取
if depth > 0:
links = await page.eval_on_selector_all(
"a[href]", "elements => elements.map(el => el.href)"
)
for link in filter(is_valid_url, links):
await crawl_page(link, depth-1)
finally:
await browser.close()
3.2 处理动态加载内容
现代文档网站常用懒加载技术,需要特殊处理:
python复制# 滚动到页面底部确保所有内容加载
async def auto_scroll(page):
await page.evaluate(
"""async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if(totalHeight >= scrollHeight){
clearInterval(timer);
resolve();
}
}, 100);
});
}"""
)
4. 高级功能实现
4.1 差异对比与变更检测
通过文本哈希和像素对比识别文档变更:
python复制def detect_changes(new_content, archived_content):
# 文本内容对比
text_hash = hashlib.md5(new_content.encode()).hexdigest()
if text_hash != archived_content['text_hash']:
return True
# 截图像素对比
new_img = Image.open(new_content['screenshot_path'])
old_img = Image.open(archived_content['screenshot_path'])
if new_img.size != old_img.size:
return True
diff = ImageChops.difference(new_img, old_img)
if diff.getbbox():
return True
return False
4.2 自动生成变更报告
使用Jinja2模板生成HTML报告:
python复制from jinja2 import Environment, FileSystemLoader
def generate_report(changes):
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('report.html')
report_html = template.render(
date=datetime.now().strftime("%Y-%m-%d"),
changes=changes
)
with open(f"reports/{datetime.now().strftime('%Y%m%d')}.html", "w") as f:
f.write(report_html)
5. 部署与自动化
5.1 配置定时任务
使用APScheduler实现定时爬取:
python复制from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
@scheduler.scheduled_job('cron', hour=3)
def scheduled_crawl():
with open('configs/urls.json') as f:
targets = json.load(f)['targets']
for target in targets:
asyncio.run(crawl_page(
target['url'],
depth=target.get('depth', 1)
))
scheduler.start()
5.2 Docker化部署
创建Dockerfile实现一键部署:
dockerfile复制FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc python3-dev && \
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt && \
playwright install && \
playwright install-deps
CMD ["python", "main.py"]
6. 实战技巧与避坑指南
-
处理登录墙文档:
对于需要登录的文档网站,可以使用Playwright的存储状态功能:python复制context = await browser.new_context(storage_state="auth.json")首次手动登录后,通过
await context.storage_state(path="auth.json")保存认证状态。 -
绕过反爬机制:
- 设置合理的请求间隔:
await page.wait_for_timeout(2000) - 随机化User-Agent:
python复制await context.set_extra_http_headers({ "User-Agent": random.choice(USER_AGENTS) })
- 设置合理的请求间隔:
-
性能优化技巧:
- 启用请求拦截避免加载不必要资源:
python复制await page.route("**/*.{png,jpg,jpeg}", lambda route: route.abort()) - 复用浏览器实例减少启动开销
- 并行处理多个页面:
asyncio.gather()
- 启用请求拦截避免加载不必要资源:
-
常见问题排查:
- 截图不全:确保等待足够时间并启用
full_page=True - 元素找不到:尝试多种选择器策略,如
page.wait_for_selector() - 内存泄漏:定期重启浏览器实例
- 截图不全:确保等待足够时间并启用
这个工具在我日常工作中已经节省了数十小时的文档查找时间。最实用的功能是它能自动检测文档变更并发出提醒,让团队始终掌握关键文档的变动情况。你可以根据实际需求扩展更多功能,比如集成到CI/CD流程中,或在文档变更时自动创建GitHub Issue。
