Python爬虫零基础入门【第六章:增量、去重、断点续爬·第1节】增量采集:只抓新增/更新(新手也能做)!

🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [📚 上期回顾](#📚 上期回顾)
      • [🎯 本篇目标](#🎯 本篇目标)
      • [💡 增量采集是什么?](#💡 增量采集是什么?)
      • [🔑 两种常见增量策略](#🔑 两种常见增量策略)
        • [策略 1:基于时间戳(last_time)](#策略 1:基于时间戳(last_time))
        • [策略 2:基于 ID(last_id)](#策略 2:基于 ID(last_id))
      • [🛠️ 代码实战:基于时间戳的增量采集](#🛠️ 代码实战:基于时间戳的增量采集)
      • [⚠️ 新手常见坑](#⚠️ 新手常见坑)
        • [坑 1:时区问题](#坑 1:时区问题)
        • [坑 2:边界数据重复](#坑 2:边界数据重复)
        • [坑 3:时间格式不一致](#坑 3:时间格式不一致)
      • [📊 增量效果验收](#📊 增量效果验收)
      • [🚀 进阶优化](#🚀 进阶优化)
        • [优化 1:增量 + 去重双保险](#优化 1:增量 + 去重双保险)
        • [优化 2:增量失败回退](#优化 2:增量失败回退)
      • [📝 小结](#📝 小结)
      • [🎯 下期预告](#🎯 下期预告)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》

订阅后更新会优先推送,按目录学习更高效~

📚 上期回顾

上一期《Python爬虫零基础入门【第五章:数据保存与入库·第4节】原始数据留存:HTML/截图/源文件如何管理(可追溯)!》内容中,我们搞定了原始数据留存,学会了用规范的目录结构管理 HTML、截图和源文件。现在你的爬虫不仅能采集数据,还能把"犯罪现场"完整保存下来------这在排查问题时简直就是救命稻草。

但新问题来了:每次运行都把整站从头爬一遍,太浪费了吧?网站可能一天只更新十几条内容,你却要重复抓取几千条旧数据。这不仅耗时耗流量,还容易被服务器盯上。

今天,我们就来解决这个痛点------只抓新的,跳过旧的。😎

🎯 本篇目标

看完这篇,你能做到:

  1. 理解增量采集的核心思想(用时间戳或 ID 做边界)
  2. 设计一个实用的增量策略(last_time 或 last_id)
  3. 处理边界问题(漏采、重复、时区)
  4. 写出可验收的增量代码(跑两次,第二次只抓新增)

验收标准很简单:第一次跑全量,第二次只抓新增的 3 条数据

💡 增量采集是什么?

举个生活例子:

你每天早上刷新闻 App,它不会把昨天、前天的新闻再推一遍,而是只显示今天的新内容。这就是增量思维。

爬虫也一样:

  • 全量采集:从第 1 页爬到第 100 页,不管是不是已经抓过
  • 增量采集:记住上次爬到哪里,下次从那个位置开始,遇到旧数据就停

核心问题是:怎么知道哪些是"旧数据"?

🔑 两种常见增量策略

策略 1:基于时间戳(last_time)

适用场景:数据有明确的发布时间或更新时间。

原理:

json 复制代码
如果 文章发布时间 <= 上次采集的最大时间:
    说明这条数据已经采集过,跳过

优点:

  • 逻辑直观,好理解
  • 不依赖数据库 ID

缺点:

  • 依赖数据源的时间字段(有些网站不给时间)
  • 时区问题要小心处理
策略 2:基于 ID(last_id)

适用场景:数据有递增的唯一 ID(如文章 ID、订单号)。

原理:

json 复制代码
如果 当前ID <= 上次采集的最大ID:
    说明这条数据已经采集过,跳过

优点:

  • 稳定可靠,不受时间影响
  • 适合大部分数据库场景

缺点:

  • ID 必须严格递增(有些系统 ID 是随机的)
  • 需要数据源提供 ID 字段

🛠️ 代码实战:基于时间戳的增量采集

第一步:记录上次采集的时间

我们用一个简单的文本文件存储上次的最大时间:

python 复制代码
import json
from datetime import datetime
from pathlib import Path

class IncrementalState:
    """增量状态管理器"""
    
    def __init__(self, state_file="state.json"):
        self.state_file = Path(state_file)
        self.state = self._load_state()
    
    def _load_state(self):
        """加载上次的状态"""
        if self.state_file.exists():
            with open(self.state_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        return {"last_time": None, "last_id": None}
    
    def get_last_time(self):
        """获取上次采集的最大时间"""
        return self.state.get("last_time")
    
    def update_last_time(self, new_time):
        """更新最大时间"""
        # 确保是 ISO 格式字符串
        if isinstance(new_time, datetime):
            new_time = new_time.isoformat()
        
        self.state["last_time"] = new_time
        self._save_state()
    
    def _save_state(self):
        """保存状态到文件"""
        with open(self.state_file, 'w', encoding='utf-8') as f:
            json.dump(self.state, f, ensure_ascii=False, indent=2)
第二步:判断是否需要跳过
python 复制代码
from datetime import datetime

def should_skip(item_time, last_time):
    """
    判断这条数据是否应该跳过
    
    Args:
        item_time: 当前数据的时间(字符串或 datetime)
        last_time: 上次采集的最大时间(字符串)
    
    Returns:
        True 表示跳过,False 表示继续采集
    """
    if last_time is None:
        # 第一次运行,全量采集
        return False
    
    # 统一转成 datetime 对象比较
    if isinstance(item_time, str):
        item_time = datetime.fromisoformat(item_time)
    if isinstance(last_time, str):
        last_time = datetime.fromisoformat(last_time)
    
    # 如果当前数据的时间 <= 上次最大时间,说明已采集过
    return item_time <= last_time
第三步:完整的增量爬虫示例
python 复制代码
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import time

class IncrementalSpider:
    """支持增量采集的爬虫"""
    
    def __init__(self, base_url):
        self.base_url = base_url
        self.state = IncrementalState()
        self.new_items = []  # 本次新采集的数据
    
    def parse_item_time(self, time_str):
        """解析时间字符串(根据实际格式调整)"""
        # 示例:'2026-01-23 10:30:00'
        return datetime.strptime(time_str.strip(), '%Y-%m-%d %H:%M:%S')
    
    def crawl_list_page(self, page=1):
        """采集列表页"""
        url = f"{self.base_url}/list?page={page}"
        resp = requests.get(url, timeout=10)
        resp.raise_for_status()
        
        soup = BeautifulSoup(resp.text, 'html.parser')
        items_time = self.state.get_last_time()
        max_time = None  # 记录本次采集的最大时间
        
        for item in items:
            title = item.select_one('.title').text.strip()
            time_str = item.select_one('.publish-time').text
            item_time = self.parse_item_time(time_str)
            
            # 核心判断:是否跳过
            if should_skip(item_time, last_time):
                print(f"⏭️  跳过旧数据:{title} ({item_time})")
                return False  # 遇到旧数据,停止翻页
            
            # 新数据,采集并记录
            print(f"✅ 采集新数据:{title} ({item_time})")
            self.new_items.append({
                'title': title,
                'time': item_time.isoformat()
            })
            
            # 更新最大时间
            if max_time is None or item_time > max_time:
                max_time = item_time
        
        # 更新状态
        if max_time:
            self.state.update_last_time(max_time)
        
        return True  # 继续翻页
    
    def run(self):
        """运行增量采集"""
        print(f"📌 上次采集时间:{self.state.get_last_time()}")
        
        page = 1
        while True:
            print(f"\n🔍 采集第 {page} 页...")
            should_continue = self.crawl_list_page(page)
            
            if not should_continue:
                print("🛑 遇到旧数据,停止采集")
                break
            
            page += 1
            time.sleep(1)  # 礼貌延迟
            
            if page > 10:  # 安全边界
                print("⚠️ 达到最大页数限制")
                break
        
        print(f"\n📊 本次新增数据:{len(self.new_items)} 条")
        return self.new_items

# 使用示例
if __name__ == '__main__':
    spider = IncrementalSpider('https://example.com')
    items = spider.run()

⚠️ 新手常见坑

坑 1:时区问题

现象:明明是新数据,却被误判为旧数据跳过。

原因:服务器时间是 UTC,你本地是北京时间(+8),直接比较会出错。

解决:

python 复制代码
from datetime import timezone, timedelta

# 统一转成 UTC
def to_utc(dt):
    if dt.tzinfo is None:
        # 假设无时区信息的是本地时间
        dt = dt.replace(tzinfo=timezone(timedelta(hours=8)))
    return dt.astimezone(timezone.utc)

# 比较时先转换
item_time_utc = to_utc(item_time)
last_time_utc = to_utc(last_time)
return item_time_utc <= last_time_utc
坑 2:边界数据重复

场景:上次采集到 2026-01-23 10:00:00,这次有新数据也是这个时间。

问题:时间相同,会被跳过(漏采)。

解决方案 A(推荐):

python 复制代码
# 改成 < 而不是 <=
return item_time < last_time

解决方案 B(更严谨):

python 复制代码
# 记录 last_time 和 last_id 双重边界
if item_time == last_time and item_id <= last_id:
    return True  # 跳过
坑 3:时间格式不一致

现象:有的页面是 2026-01-23,有的是 Jan 23, 2026

解决:

python 复制代码
from dateutil import parser

def safe_parse_time(time_str):
    """容错的时间解析"""
    try:
        return parser.parse(time_str)
    except Exception as e:
        print(f"⚠️ 时间解析失败:{time_str},错误:{e}")
        return None

📊 增量效果验收

第一次运行(全量):
json 复制代码
📌 上次采集时间:None

🔍 采集第 1 页...
✅ 采集新数据:文章A (2026-01-23 10:00:00)
✅ 采集新数据:文章B (2026-01-23 09:30:00)
✅ 采集新数据:文章C (2026-01-23 09:00:00)

📊 本次新增数据:3 条

state.json 内容:

json 复制代码
{
  "last_time": "2026-01-23T10:00:00"
}
第二次运行(增量):

假设网站新增了 2 条数据。

json 复制代码
📌 上次采集时间:2026-01-23T10:00:00

🔍 采集第 1 页...
✅ 采集新数据:文章D (2026-01-23 11:00:00)
✅ 采集新数据:文章E (2026-01-23 10:30:00)
⏭️  跳过旧数据:文章A (2026-01-23 10:00:00)
🛑 遇到旧数据,停止采集

📊 本次新增数据:2 条

state.json 更新:

json 复制代码
{
  "last_time": "2026-01-23T11:00:00"
}

🚀 进阶优化

优化 1:增量 + 去重双保险

虽然有增量判断,但某些场景下(如数据更新)可能需要双重校验:

python 复制代码
from hashlib import md5

def get_dedup_key(item):
    """生成去重键"""
    raw = f"{item['title']}_{item['time']}"
    return md5(raw.encode()).hexdigest()

# 配合集合去重
seen_keys = set()

if get_dedup_key(item) in seen_keys:
    print("⚠️ 重复数据,跳过")
    continue

seen_keys.add(get_dedup_key(item))
优化 2:增量失败回退

如果本次采集中途失败,不应该更新 last_time(否则会漏数据)。

python 复制代码
def run(self):
    temp_max_time = None
    
    try:
        # 采集逻辑...
        temp_max_time = max([item['time'] for item in self.new_items])
    except Exception as e:
        print(f"❌ 采集失败:{e}")
        print("🔄 不更新 last_time,下次重新采集")
        return
    
    # 只有成功时才更新
    if temp_max_time:
        self.state.update_last_time(temp_max_time)

📝 小结

今天我们学会了增量采集的核心思想

  1. 用时间或 ID 做边界,跳过旧数据
  2. 状态持久化,记住上次采集到哪里
  3. 处理边界问题(时区、重复、失败回退)

增量采集不仅能提升效率,还能降低被封的风险。但记住:宁可多抓几条,也不要漏掉新数据 。遇到边界情况时,让 < 替代 <= 就能减少很多麻烦。

🎯 下期预告

增量解决了"只抓新的",但还有个问题没解决:万一中途断电怎么办?

下一篇《断点续爬:失败队列、重放、任务状态》,我们会设计一个任务状态表,让爬虫具备"断点续传"能力------不管在第几页挂掉,重启后都能无缝继续。💪

记得动手验收:让你的爬虫跑两次,第二次只抓 3 条新增数据!加油!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
万粉变现经纪人8 小时前
如何解决 pip install pyodbc 报错 缺少 ‘cl.exe’ 或 ‘sql.h’(ODBC 头文件)问题
数据库·python·sql·网络协议·bug·ssl·pip
dazzle9 小时前
Python数据结构(五):队列详解
数据结构·python
翱翔的苍鹰9 小时前
完整的“RNN + jieba 中文情感分析”项目之一:需要添加添加 JWT 认证
人工智能·python·rnn
0思必得09 小时前
[Web自动化] 爬虫URL去重
运维·爬虫·python·selenium·自动化
Cherry的跨界思维9 小时前
【AI测试全栈:质量】40、数据平权之路:Python+Java+Vue全栈实战偏见检测与公平性测试
java·人工智能·python·机器学习·ai测试·ai全栈·ai测试全栈
wheelmouse77889 小时前
Python 装饰器函数(decoratots) 学习笔记
笔记·python·学习
老歌老听老掉牙9 小时前
差分进化算法深度解码:Scipy高效全局优化实战秘籍
python·算法·scipy
工程师老罗9 小时前
Pycharm下新建一个conda环境后,如何在该环境下安装包?
人工智能·python
dazzle9 小时前
Python数据结构(四):栈详解
开发语言·数据结构·python