1. 项目背景与核心价值
最近在整理技术文档时发现一个痛点:很多开源项目的文档更新频繁,但缺乏历史版本存档。当我们需要回溯某个旧版本的功能说明时,往往只能依赖Wayback Machine等第三方服务,而这些服务对中文文档的覆盖率有限。于是萌生了自己搭建一个轻量级文档存档系统的想法。
Playwright作为新一代浏览器自动化工具,相比传统的Selenium在速度和稳定性上有明显优势,特别适合这种需要高频截图的场景。这个项目将利用Playwright实现三个核心功能:
- 定时抓取指定文档页面
- 自动生成带时间戳的完整页面截图
- 构建可按时间轴浏览的文档存档库
2. 技术选型与环境搭建
2.1 Playwright优势解析
选择Playwright主要基于以下考量:
- 多语言支持:原生支持Python API,无需额外封装
- 无头模式性能:比Selenium节省约40%的内存占用
- 自动等待机制:内置智能等待,避免手动设置sleep
- 全浏览器支持:Chromium/Firefox/WebKit统一API
# 安装Playwright及浏览器二进制 pip install playwright playwright install2.2 配套工具链
- FastAPI:提供简单的管理界面和API
- SQLite:存储文档元数据
- Pillow:图片处理(添加水印等)
- APScheduler:定时任务管理
注意:建议使用Python 3.8+环境,某些异步特性在旧版本可能不兼容
3. 核心功能实现
3.1 页面捕获模块
async def capture_page(url: str, save_path: str): async with async_playwright() as p: browser = await p.chromium.launch() context = await browser.new_context( viewport={'width': 1920, 'height': 1080}, device_scale_factor=2 # 高清截图 ) page = await context.new_page() try: await page.goto(url, timeout=60000) await page.evaluate('window.scrollTo(0, document.body.scrollHeight)') await page.screenshot( path=save_path, full_page=True, type='jpeg', quality=90 ) finally: await browser.close()关键参数说明:
device_scale_factor: 设置2倍DPI保证截图清晰度full_page: 自动滚动截取完整页面- 超时设置为60秒适应大型文档
3.2 存档管理系统设计
数据库表结构设计:
CREATE TABLE documents ( id INTEGER PRIMARY KEY, url TEXT NOT NULL, title TEXT, capture_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, file_path TEXT UNIQUE, hash_value TEXT # 用于检测内容变更 );去重逻辑实现:
def needs_update(url: str, new_hash: str) -> bool: # 获取最近一次记录的hash last_record = db.execute( "SELECT hash_value FROM documents WHERE url=? ORDER BY capture_time DESC LIMIT 1", (url,) ).fetchone() return not last_record or last_record[0] != new_hash4. 高级功能扩展
4.1 智能变更检测
通过组合以下策略提升变更识别准确率:
- 文本哈希:对正文内容生成SHA256
- 视觉差分:使用OpenCV比较连续截图
- DOM结构比对:分析关键节点变化
def compute_visual_diff(img1_path, img2_path): img1 = cv2.imread(img1_path, cv2.IMREAD_GRAYSCALE) img2 = cv2.imread(img2_path, cv2.IMREAD_GRAYSCALE) # 结构相似性比较 score, _ = ssim(img1, img2, full=True) return score < 0.95 # 相似度低于95%视为有变更4.2 自动化部署方案
使用Docker Compose编排服务:
version: '3' services: archiver: build: . volumes: - ./data:/app/data environment: - SCHEDULE=0 3 * * * # 每天凌晨3点执行 restart: unless-stopped5. 实战问题排查手册
5.1 常见问题解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 截图出现空白区域 | 页面懒加载 | 添加滚动延迟:await page.wait_for_timeout(2000) |
| 中文显示为方框 | 字体缺失 | 启动时加载中文字体:context.add_init_script() |
| 页面加载超时 | 资源阻塞 | 设置拦截规则:await page.route('**/*.{png,jpg}', lambda route: route.abort()) |
5.2 性能优化技巧
- 浏览器实例复用:改为全局单例模式减少启动开销
- 并行处理:使用asyncio.gather同时抓取多个页面
- 缓存策略:对静态资源启用本地缓存
# 浏览器实例池实现 class BrowserPool: def __init__(self, max_instances=3): self.semaphore = asyncio.Semaphore(max_instances) self.browsers = [] async def get_browser(self): async with self.semaphore: if not self.browsers: return await p.chromium.launch() return self.browsers.pop()6. 项目演进方向
- OCR增强:对截图进行文字识别实现全文搜索
- 差异报告:自动生成版本间变更摘要
- 通知系统:当检测到关键内容变更时发送警报
实际使用中发现,对于React/Vue等SPA应用,需要额外处理客户端路由。我的做法是在捕获前先获取所有文档链接:
links = await page.evaluate('''() => { return [...document.querySelectorAll('a[href^="/docs/"]')] .map(a => a.href); }''')这个方案目前已经稳定运行半年,累计归档了超过1200个技术文档版本。最意外的是发现有些项目的文档甚至会在发布后临时修改内容,有了这个存档系统就能准确追踪这些细微变更