网页快照这件事,比“更新”复杂得多

——谈谈增量抓取、时间意识,以及我们踩过的坑


01|事情是这样开始的:凌晨,我被电话吵醒了

有些项目真的是越做越清醒,尤其是那种能把人从睡梦里叫醒的。

几个月前,我们负责的某个政府采购网站上线了新版页面结构。按理说那天只是例行增量抓取,但过了一阵,数据仓库里突然出现了断层现象:

  • 某些字段消失了
  • 某些字段变为空值
  • 还有几条数据看起来像被人手动改过

运营同事第一句话就是:

“程序是不是抓错了?”

我盯着日志翻了十几分钟,越看越确定:

不是抓取抓错,是我们之前的抓取逻辑太天真了。

我们一直以为自己在做“增量抓取”,实际上是:

把最新数据覆盖老版本,然后把历史抹掉。

那一瞬间我意识到一个事实:

不是我们没抓到数据,而是我们从未认真保存过它的历史形态。

02|开始追问题:真正的坑不在“抓取”,而在“存怎么存”

过去我们非常习惯这种逻辑:

新内容 != 旧内容 ——> 更新

看起来很合理,但互联网内容变化的方式远没有这么直接。

后来复盘,我们总结了几个核心误区:

误区一:页面变了 ≠ 数据变了

DOM 结构改动很常见,但页面结构变化并不意味着数据意义变化。

有的网站今天 divdiv,明天 <p><span>,但内容本身根本没变。

这种情况保存快照没有意义。

误区二:字段值变化 ≠ 版本升级

例如:

  • 报名人数
  • 收藏量
  • 点赞数

这些字段随时间变化属正常,保存每次变化会制造大量噪音。

误区三:字段消失往往才是最重要事件

字段消失通常意味着:

  • 条件变更
  • 政策调整
  • 或者,有东西不想让你看到

这种变化最值得保存,可惜却经常被忽略。

直到那一刻我才真正意识到:

抓取做的不是“抓网页”,是在记录网页内容的演变历史。

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": "<html>...</html>"
}

一句话概括:

不仅保存内容,还保存变化发生的上下文意义。

04|关键代码示例

"""
网页快照抓取示例
带差异判断 + 时间意识 + 亿牛云代理示例
"""

import asyncio
import hashlib
import json
from datetime import datetime
from pathlib import Path
from playwright.async_api import async_playwright

# === 亿牛云代理配置(www.16yun.cn)===
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|最后一点反思:抓取不是“抓最新”,而是“记录过程”

回头看,这件事让我彻底改观。

以前我们想的是:

“我只需要最新内容。”

现在变成:

“我要知道这一条数据从出现到现在经历了什么。”

这两句话差别不大,但结果天差地别。

当你能处理变化时间事件含义,它就不仅仅是抓取器,而是一个“内容记忆系统”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值