——谈谈增量抓取、时间意识,以及我们踩过的坑
01|事情是这样开始的:凌晨,我被电话吵醒了
有些项目真的是越做越清醒,尤其是那种能把人从睡梦里叫醒的。
几个月前,我们负责的某个政府采购网站上线了新版页面结构。按理说那天只是例行增量抓取,但过了一阵,数据仓库里突然出现了断层现象:
- 某些字段消失了
- 某些字段变为空值
- 还有几条数据看起来像被人手动改过
运营同事第一句话就是:
“程序是不是抓错了?”
我盯着日志翻了十几分钟,越看越确定:
不是抓取抓错,是我们之前的抓取逻辑太天真了。
我们一直以为自己在做“增量抓取”,实际上是:
把最新数据覆盖老版本,然后把历史抹掉。
那一瞬间我意识到一个事实:
不是我们没抓到数据,而是我们从未认真保存过它的历史形态。
02|开始追问题:真正的坑不在“抓取”,而在“存怎么存”
过去我们非常习惯这种逻辑:
新内容 != 旧内容 ——> 更新
看起来很合理,但互联网内容变化的方式远没有这么直接。
后来复盘,我们总结了几个核心误区:
误区一:页面变了 ≠ 数据变了
DOM 结构改动很常见,但页面结构变化并不意味着数据意义变化。
有的网站今天 div 套 div,明天
改 ,但内容本身根本没变。
这种情况保存快照没有意义。
误区二:字段值变化 ≠ 版本升级
例如:
- 报名人数
- 收藏量
- 点赞数
这些字段随时间变化属正常,保存每次变化会制造大量噪音。
误区三:字段消失往往才是最重要事件
字段消失通常意味着: - 条件变更
- 政策调整
- 或者,有东西不想让你看到
这种变化最值得保存,可惜却经常被忽略。
直到那一刻我才真正意识到:
抓取做的不是“抓网页”,是在记录网页内容的演变历史。
03|重新设计:给系统“时间意识”和“事件含义”
我们最终把抓取逻辑调整为三个关键策略:
1)时间窗口(Time Window)
不同字段变化频率不同,保存策略也要跟着调整。
例如: - 文案类字段:只在内容变化时保存
- 状态类字段:按周期采样或满足阈值后保存
- 永久字段:存一次即可
这样比“定时保存”更智能。
2)事件驱动(Event Driven)
我们不再简单判断“变了没变”,而是判断“变化属于哪一类”。
变化类型
含义
新增字段
schema_change
内容发生变化
content_update
字段被删除
removal_event
页面结构变化但内容没变化
ignore
这让抓取行为更接近真实观察,而不是无脑比对字符。
3)结构化快照,而不是纯 HTML 存档
最终快照不只是原始 HTML,它应该携带元信息,例如:
{
"snapshot_time": "2025-11-24 13:22:11",
"event_type": "content_update",
"diff_summary": "新增要求:注册资本需≥500万",
"content_hash": "b24793aed…",
"parsed_data": {...},
"raw_html": "
}
一句话概括:
不仅保存内容,还保存变化发生的上下文意义。
04|关键代码示例
"""
网页快照抓取示例
带差异判断 + 时间意识 + 亿牛云代理示例
"""
import asyncio
import hashlib
import json
from datetime import datetime
from pathlib import Path
from playwright.async_api import async_playwright
=== 16YUN代理配置===
PROXY_HOST = "proxy.16yun.cn"
PROXY_PORT = "3100"
PROXY_USER = "your_username"
PROXY_PASS = "your_password"
=== 快照存储路径 ===
SNAPSHOT_DIR = Path("./snapshots")
SNAPSHOT_DIR.mkdir(exist_ok=True)
def hash_text(text: str) -> str:
"""生成文本hash,用于判断内容是否变化"""
return hashlib.sha256(text.encode("utf-8")).hexdigest()
async def capture(url: str):
"""抓取页面并存储快照(带事件判断)"""
async with async_playwright() as p:
browser = await p.chromium.launch(proxy={
"server": f"http://{PROXY_HOST}:{PROXY_PORT}",
"username": PROXY_USER,
"password": PROXY_PASS
})
page = await browser.new_page()
await page.goto(url, timeout=60000)
html = await page.content()
text = await page.inner_text("body")
await browser.close()
new_hash = hash_text(text)
file = SNAPSHOT_DIR / f"{url.replace('https://','').replace('/', '_')}.json"
# == 判断是否需要存新快照 ==
if file.exists():
old = json.loads(file.read_text())
if old.get("content_hash") == new_hash:
print("没有变化,跳过。")
return
event = "content_update"
else:
event = "first_capture"
snapshot = {
"url": url,
"snapshot_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"event_type": event,
"content_hash": new_hash,
"text": text,
"raw_html": html
}
file.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2))
print(f"检测到变化 → 已保存快照:{file.name}")
if name == "main":
asyncio.run(capture("https://www.example.com"))
05|最后一点反思:抓取不是“抓最新”,而是“记录过程”
回头看,这件事让我彻底改观。
以前我们想的是:
“我只需要最新内容。”
现在变成:
“我要知道这一条数据从出现到现在经历了什么。”
这两句话差别不大,但结果天差地别。
当你能处理变化、时间、事件含义,它就不仅仅是抓取器,而是一个“内容记忆系统”。