聊聊数据的"冷热分离":如何对历史爬虫数据进行合理的归档与压缩存储?

简介: 本文是面向爬虫工程师的冷热分离实战指南:直击数据堆积痛点,详解如何用隧道代理高效采集,并通过SQLite(热层)+Parquet/zstd(冷层)实现低成本、高效率的数据分层存储与归档,附完整可运行代码及中文注释。(239字)

作者按:这是一篇写给爬虫工程师和数据处理同学的实战笔记。我们不谈虚的,直接从"数据是怎么堆成山的"讲起,再给一套能直接抄走的代码。文中的代理部分基于隧道代理产品,代码全部带中文注释。

tu-clean.jpg

一、为什么爬虫团队迟早都要面对"数据堆成山"

每个爬虫项目开始时都很轻巧:写个脚本,跑通几个页面,把结果塞进一张表。前两周一切正常,查询飞快,磁盘空闲。

问题出在第三周以后。

一旦你接上了正规代理、把采集做成"长期任务",数据量会超出大多数人的直觉。以我做财经舆情采集的经验为例,单是一个标的每天的新增新闻、公告、研报就能有几百条,正文 + 元数据存下来,一个月就是几十 GB。半年之后,那张主表轻轻松松破亿行。

这时候会出现三个典型症状:

  1. 热查询变慢:你只想查"今天抓到的数据",数据库却要扫整张大表。
  2. 存储账单变贵:热存储(高 IOPS 的关系库)按容量收费,存三年历史数据等于一直为"几乎不读"的数据付溢价。
  3. 备份和恢复变重:每次备份都要带着那 90% 几乎不访问的历史数据一起走。

这三个症状,本质上说的是同一件事——你的数据不是均匀的,它们天然分成了"热"和"冷"。把它们一视同仁地放在一起,是工程上的偷懒,迟早要还。

二、什么是冷热分离

一句话:按"被访问的频率"把数据分层存放,热数据追求快,冷数据追求省。

维度 热数据(Hot) 冷数据(Cold)
时间范围 最近 N 天(如 30 天) N 天之前的所有历史
访问频率 高,日常查询/可视化都打它 极低,仅在回溯、复盘、训练时偶发访问
存储介质 关系库 / 高速 NoSQL 列式文件(Parquet)+ 对象存储 / NAS
核心诉求 低延迟读写 高压缩比、低成本长期留存
可接受延迟 毫秒级 秒级甚至分钟级都行

关键认知:冷数据不是"垃圾数据",它往往比热数据更有长期价值(训练语料、合规留存、历史回溯),只是"现在用不到"。所以我们的目标不是删掉它,而是用更便宜的方式把它"妥善冻存"。

三、先看看数据是怎么被"生产"出来的

要谈归档,得先谈采集。这里必须点名一个现实:很多时候数据堆得快,是因为代理太好用了。

我长期用爬虫代理(隧道版)。它的工作方式是——你只需要在请求里配置一次代理地址,之后每一次请求的出口 IP 都在后台自动轮换,业务侧完全不用自己维护 IP 池、不用写换 IP 逻辑、不用处理 IP 失效:

# 亿牛云隧道代理:一次配置,出口 IP 自动轮换
PROXY_HOST = "proxy.16yun.cn"   # 理服务器域名
PROXY_PORT = "3100"             # 隧道端口(以控制台实际分配为准)
PROXY_USER = "16YUNxxxx"        # 账号用户名
PROXY_PASS = "xxxxxxxx"         # 账号密码

# requests 标准代理字典格式:http://用户名:密码@域名:端口
PROXIES = {
   
    "http":  f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
    "https": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
}

这种"无感采集"恰恰是冷热分离最好的理由:隧道代理把采集成本压到极低,于是你天然会去跑长期、大规模的采集任务,数据自然分层。 换言之,代理解决了"能不能持续抓"的问题,而冷热分离解决的是"抓了一年后怎么办"的问题——它们是同一套工程的两端。

补充:还有"优质代理"(短效 IP 列表)产品线,适合需要自己精细控制 IP 切换节奏的场景。本文的主线用隧道版即可,因为它最省心,也最能说明"数据为何会快速堆积"。

四、分层架构设计

一套最小可用的冷热分离架构长这样:

采集层 ──(隧道代理)──▶ 热层 (SQLite/PostgreSQL, 仅最近 N 天)
                                      │
                         定时归档任务 (每天凌晨)
                                      ▼
                              冷层 (Parquet + zstd, 对象存储/NAS)
                                      ▲
                          查询网关:热查热层,冷查冷层,必要时"回暖"

三个组件职责清晰:

  • 热层:承接实时写入和高频查询,只留最近窗口的数据,保持"小而快"。
  • 归档任务:独立于采集,按天跑,把超出窗口的数据搬去冷层并压缩,再从热层清理。
  • 冷层:列式存储 + 高压缩,几乎不花钱,但随时能读回来。

五、实战代码(可直接运行)

下面给一套完整的可运行示例。依赖只有两个:requests(采集)、pyarrow(写 Parquet)。

pip install requests pyarrow
# 定时任务用到 schedule(可选):pip install schedule

5.1 接入隧道代理,抓取并封装记录

"""基于隧道代理的爬虫数据冷热分离示例。"""
from __future__ import annotations

import sqlite3
import time
from dataclasses import dataclass, asdict
from datetime import datetime, timedelta

import requests

# ============================================================
# 1) 亿牛云隧道代理配置
#    隧道代理特点:一次配置,出口 IP 自动轮换,
#    业务侧无需自己维护 IP 池,适合长期大规模采集。
# ============================================================
PROXY_HOST = "proxy.16yun.cn"   # 代理服务器域名
PROXY_PORT = "3100"             # 隧道端口(以控制台实际分配为准)
PROXY_USER = "16YUNxxxx"        # 账号用户名
PROXY_PASS = "xxxxxxxx"         # 账号密码

# requests 标准代理字典格式:http://用户名:密码@域名:端口
PROXIES: dict[str, str] = {
   
    "http":  f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
    "https": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
}


@dataclass
class CrawledRecord:
    """一条爬虫落地数据。"""
    url: str
    title: str
    content: str
    crawled_at: str  # ISO 格式时间字符串


def fetch_page(target_url: str) -> CrawledRecord:
    """通过隧道代理抓取页面,并封装为热层记录。

    隧道代理会自动切换出口 IP,这里只需把 PROXIES 透传给 requests,
    不需要任何手动换 IP 的逻辑,也不用担心单个 IP 被目标站封禁。
    """
    resp = requests.get(
        target_url,
        proxies=PROXIES,          # 关键:所有请求经由亿牛云隧道转发
        timeout=10,
        headers={
   "User-Agent": "Mozilla/5.0"},
    )
    resp.raise_for_status()
    # 简化演示:真实场景请用 readability / parsel 抽取正文
    return CrawledRecord(
        url=target_url,
        title=resp.url,
        content=resp.text[:5000],                 # 截断,避免单条过大
        crawled_at=datetime.now().isoformat(timespec="seconds"),
    )

5.2 热层写入:只保留最近窗口的数据

# ============================================================
# 2) 热层:最近数据落在 SQLite(生产可平滑换成 PostgreSQL)
#    热层追求写入/查询的低延迟,只保留最近 N 天。
# ============================================================
HOT_DB = "hot_crawl.db"
HOT_RETENTION_DAYS = 30  # 热数据保留窗口(天)


def init_hot_db(conn: sqlite3.Connection) -> None:
    """建表:热层只存最近窗口内的数据。"""
    conn.execute(
        """CREATE TABLE IF NOT EXISTS hot_records (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            url         TEXT UNIQUE,   -- 用 URL 去重,避免重复抓取污染热数据
            title       TEXT,
            content     TEXT,
            crawled_at  TEXT
        )"""
    )
    conn.commit()


def save_hot(conn: sqlite3.Connection, rec: CrawledRecord) -> None:
    """写入热层;URL 冲突则忽略(INSERT OR IGNORE)。"""
    conn.execute(
        "INSERT OR IGNORE INTO hot_records(url, title, content, crawled_at) "
        "VALUES (:url, :title, :content, :crawled_at)",
        asdict(rec),
    )
    conn.commit()

5.3 归档任务:迁移 + 压缩为 Parquet(zstd)

# ============================================================
# 3) 冷层归档:把超过保留窗口的数据导出为压缩 Parquet,并从热层删除。
#    冷层追求存储成本与压缩比,查询频率极低。
# ============================================================
import os
import pyarrow as pa
import pyarrow.parquet as pq

COLD_DIR = "cold_archive"  # 冷层目录(生产可换成对象存储挂载路径)


def archive_cold(conn: sqlite3.Connection, batch_date: str) -> str:
    """将热层中早于保留窗口的记录导出为压缩 Parquet,再从热层清理。

    Args:
        conn: 热层数据库连接。
        batch_date: 归档批次日期,用作文件名,如 2026-08-17。
    Returns:
        生成的冷层文件路径;若没有需要归档的数据返回空字符串。

    安全性:先写冷层并落盘,确认成功后再删热层,避免丢数据。
    """
    cutoff = (datetime.now() - timedelta(days=HOT_RETENTION_DAYS)).isoformat(timespec="seconds")
    rows = conn.execute(
        "SELECT url, title, content, crawled_at FROM hot_records WHERE crawled_at < ?",
        (cutoff,),
    ).fetchall()

    if not rows:
        return ""  # 没有需要归档的数据,直接返回

    # 转成列式结构,Parquet 对列式数据压缩效率最高
    table = pa.table(
        {
   
            "url":        [r[0] for r in rows],
            "title":      [r[1] for r in rows],
            "content":    [r[2] for r in rows],
            "crawled_at": [r[3] for r in rows],
        }
    )

    os.makedirs(COLD_DIR, exist_ok=True)
    out_path = os.path.join(COLD_DIR, f"crawl_{batch_date}.parquet")
    # zstd:压缩比高、解压极快,且级别可调,非常适合冷数据长期留存
    pq.write_table(table, out_path, compression="zstd", compression_level=3)

    # 归档文件已落盘,再从热层清理(先写后删,杜绝数据丢失)
    conn.execute("DELETE FROM hot_records WHERE crawled_at < ?", (cutoff,))
    conn.commit()
    return out_path

5.4 冷数据按需"回暖"

# ============================================================
# 4) 冷数据按需回暖:按时间范围扫描归档文件,供临时分析/回溯。
# ============================================================
def warm_up(start_date: str, end_date: str) -> list[dict]:
    """扫描冷层 Parquet,按 crawled_at 范围过滤并返回记录。

    冷层查询频率低,逐文件扫描即可;数据量大时可加日期分区或索引 manifest。
    """
    result: list[dict] = []
    if not os.path.isdir(COLD_DIR):
        return result
    for fname in os.listdir(COLD_DIR):
        if not fname.endswith(".parquet"):
            continue
        table = pq.read_table(os.path.join(COLD_DIR, fname))
        df = table.to_pandas()
        masked = df[(df["crawled_at"] >= start_date) & (df["crawled_at"] <= end_date)]
        result.extend(masked.to_dict("records"))
    return result

5.5 串起来:抓取 → 存热层 → 定时归档

# ============================================================
# 5) 串联示例:先抓一条存热层,再手动触发一次归档。
#    生产环境用 schedule / crontab / Airflow 把 daily_job 定时跑起来。
# ============================================================
def daily_job() -> None:
    """每天凌晨执行的归档任务。"""
    today = datetime.now().strftime("%Y-%m-%d")
    with sqlite3.connect(HOT_DB) as conn:
        init_hot_db(conn)
        path = archive_cold(conn, today)
        print(f"[{today}] 归档完成:{path or '无新增冷数据'}")


if __name__ == "__main__":
    # 演示:抓一条 → 存热层 → 触发归档
    with sqlite3.connect(HOT_DB) as conn:
        init_hot_db(conn)
        rec = fetch_page("https://example.com")
        save_hot(conn, rec)
    daily_job()

    # 常驻定时模式(需 pip install schedule):
    # import schedule
    # schedule.every().day.at("03:00").do(daily_job)
    # while True:
    #     schedule.run_pending()
    #     time.sleep(60)

六、压缩选型:为什么选 zstd

冷层的核心指标是压缩比 × 解压速度。常见列式压缩对文本型爬虫数据的表现大致如下(定性,具体看数据):

算法 压缩比 压缩速度 解压速度 适用场景
gzip 高(~3–4x) 通用,但不如 zstd 灵活
snappy 中(~2x) 纯追求速度、不在意体积
zstd 高(~3–5x,可调) 中–快 极快 冷数据长期留存首选

我选 zstd 的理由:爬虫正文是高度重复的文本,zstd 的压缩比很能打;而当你某天要"回暖"一批历史数据做复盘时,zstd 的解压速度几乎是即时的,不会拖垮分析任务。级别设 3 是性价比甜点——再往上压,CPU 成本上升但体积收益递减。

七、踩坑与最佳实践

  1. 归档是破坏性操作,先写后删。永远先把冷层文件落盘并校验,再从热层 DELETE。不要反过来,也不要在没备份时直接 rm 冷文件。
  2. 给冷层建一份 manifest。每个 Parquet 文件名带上时间范围(如 crawl_2026-08-17.parquet),再维护一个索引文件记录"哪个时间段在哪个文件",回暖时就能先定位、再读取,不必全扫。
  3. 保留窗口要和业务对齐。热窗口不是越短越好——如果你每天要看"近 45 天趋势",那就把 HOT_RETENTION_DAYS 设成 45,而不是拍脑袋定 30。
  4. 代理层要做超时与重试封装。隧道代理虽然自动换 IP,但单请求仍可能超时;务必给 requeststimeout 和退避重试,避免把"半截脏数据"写进热层。
  5. 合规留存期优先于一切优化。金融、舆情类数据常有合规留存要求,冷热分离是"挪地方存",不是"少存",别为了省空间把该留的也清了。

八、总结

冷热分离不是什么高深架构,它只是承认了一个朴素事实:数据被访问的频率从来都不均匀。把"天天用"的和"三年用一次"的放在同一种存储里,既浪费钱又拖慢系统。

而它和代理的关系很微妙:像隧道代理这种"一次配置、自动换 IP"的产品,把采集门槛降到了极低,于是你天然会去跑长年累月的采集——数据堆得快,恰恰说明你更需要一套像样的冷热分层来接住它。

相关文章
|
20天前
|
人工智能 API 开发工具
阿里云百炼Token Plan全功能详解:订阅规则、支持模型与API实操教程
在大模型应用快速普及的当下,开发者与企业团队经常会遇到一个现实难题:项目会同时用到文本推理、视觉理解、图片生成、AI视频生成等多种能力,不同模型分属不同服务,需要分别开通权限、管理多套密钥、分别结算账单,不仅管理成本高,预算也很难提前把控。很多开发人员一边使用代码智能体工具做程序开发,一边调用图像视频模型做素材生成,来回切换多个平台,账号、密钥、账单分散,一旦业务量上涨,实际开销很容易超出预期。阿里云百炼推出的Token Plan,就是面向这类场景打造的一站式大模型订阅服务,通过统一Credits额度,实现多款主流大模型共享一套订阅权益,降低多模型场景下的管理复杂度,适配个人开发者、独立工作室
141 2
|
19天前
|
Ubuntu 测试技术 网络安全
【Docker项目实战篇】Docker部署PDD查看器PdfDing
【Docker项目实战篇】Docker部署PDD查看器PdfDing
151 2
【Docker项目实战篇】Docker部署PDD查看器PdfDing
|
4天前
|
存储 运维 安全
医疗患者门户钓鱼攻击风险与闭环防御研究 —— 基于 MyChart 诈骗事件实证分析
本文以宾州MyChart钓鱼事件为案例,剖析医疗门户钓鱼攻击的社会工程学本质:不依赖漏洞,而利用公众对医疗服务的信任实施仿冒欺诈。研究揭示其危害遍及患者隐私、机构声誉与行业生态,并提出技术防护、制度建设、用户教育、情报协同、应急处置五位一体的闭环防御路径。(239字)
31 4
|
19天前
|
人工智能 开发框架 Java
如何入门学习 Agent 开发?
本文分享Agent开发实战经验:强调甄别一手资讯、聚焦Context本质而非框架、坚持实操落地、重视效果评测与自我迭代,助新手避开玄学误区,从真实场景出发高效入门。(238字)
81 5
|
18天前
|
人工智能 自然语言处理 算法
RAG 检索工程:经典检索、向量代理与 topk 的取舍
本文厘清RAG检索中经典单次与代理多路的本质差异,指出分水岭在于查询由程序写死还是模型规划;强调BM25保精准、向量管语义、top-k须固定以保障GEO采样可比性,并约束名单类答案的多样性与引用可溯性。(239字)
|
18天前
|
数据采集 监控 供应链
1688 商品详情驱动的选品、竞品分析与采购实战指南
1688是“中国制造”的数字入口,汇聚60万源头工厂。本文详解如何通过API接口实现数据化选品:解析批发价阶梯、库存、供应商资质等核心字段;构建四层选品漏斗;以图搜款溯源跨境爆款;建立采购评分卡与动态监控模型,助力高效决策。(239字)
|
18天前
|
缓存 前端开发 测试技术
通义千问Qwen3.7 Plus与Max实测对比:多模态能力、推理表现与性价比深度解析
在大模型应用落地的过程中,很多开发者会陷入选型困境,同样属于Qwen3.7系列的两款主力基座Qwen3.7‑Max与Qwen3.7‑Plus,都具备百万级超长上下文窗口,支持长周期Agent智能体运行,但二者在模态支持、推理侧重、计费成本、实际业务表现上存在明显分化。不少开发者只看到参数规格相近,直接盲目选用高价Max,造成业务调用成本成倍上涨;也有部分业务场景对纯文本硬核推理要求极高,选用Plus之后遇到复杂逻辑任务出现能力瓶颈。本文将从底层架构、多模态能力、基准实测数据、代码Agent表现、计费性价比、真实业务场景、API实操调用、选型避坑多个维度,完整拆解两款模型的差异,帮助个人开发者、
213 1
|
20天前
|
自然语言处理 供应链 新能源
供应链金融智能风控:5方案引擎架构实战
供应链金融的风控核心不是单一模型,而是多方案引擎的协同决策。本文从源码层面解析5种风控方案引擎的架构设计、规则配置和决策流转,覆盖应收账款融资、订单融资、存货融资等场景。
|
19天前
|
Java Linux 开发工具
IDEA官网下载2026|IDEA社区版安装+使用图文步骤
IntelliJ IDEA(简称IDEA)是JetBrains推出的主流Java集成开发环境,集代码编写、编译、调试、版本控制等功能于一体。提供免费社区版(支持Java/Kotlin/Maven/Gradle等)和付费旗舰版(增强Spring、数据库、前端等支持),开箱即用、智能提示强,适合学习与日常开发。
|
1月前
|
弹性计算 测试技术
阿里云服务器ECS按量付费怎么样?按量付费可以转包年包月吗?
阿里云ECS按量付费为后付费模式,适用于测试、抢购等临时场景,按秒计费、随时释放;支持转包年包月(需满足运行中、无未付订单等条件),操作便捷,可选周/月/年周期,兼顾灵活与成本优化。阿里云服务器ECS官网:https://t.aliyun.com/U/AZBUsA