爬虫爬到一半网络断了、电脑重启了、IP 被封了——如果从头再来,前面的进度就白费了。断点续爬就是让爬虫记住爬到了哪里,中断后自动从断点继续。
一、为什么需要断点续爬
你没有断点续爬:
爬了200页 → 网络中断 → 重新从第1页开始 → 浪费大量时间
你有断点续爬:
爬了200页 → 网络中断 → 重新启动 → 从第201页继续 → 不浪费
不能续爬的爬虫只适合小规模(几百页),大规模采集(几万页以上)必须有断点续爬能力。
二、实现方式一:文件记录进度
最简单的方式,爬到哪一页就写到文件里。
import json
import os
import time
import requests
from bs4 import BeautifulSoup
class ResumeCrawler:
"""支持断点续爬的爬虫"""
def __init__(self, start_page=1, end_page=100):
self.state_file = "crawl_state.json" # 状态文件
self.data_file = "crawled_data.jsonl" # 数据文件
self.end_page = end_page
self.current_page = self._load_state() or start_page
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"
})
def _load_state(self):
"""加载上次的进度"""
if os.path.exists(self.state_file):
with open(self.state_file, "r") as f:
state = json.load(f)
print(f"📂 发现上次进度:第 {state['current_page']} 页")
return state["current_page"]
return None
def _save_state(self):
"""保存当前进度"""
with open(self.state_file, "w") as f:
json.dump({"current_page": self.current_page}, f)
def _save_data(self, items):
"""保存爬到的数据"""
with open(self.data_file, "a", encoding="utf-8") as f:
for item in items:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
def crawl_page(self, page):
"""爬取单页"""
url = f"https://example.com/products?page={page}"
resp = self.session.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
items = []
for product in soup.select(".product"):
items.append({
"title": product.select_one(".title").text.strip(),
"price": product.select_one(".price").text.strip(),
"page": page,
"crawl_time": time.time(),
})
return items
def run(self):
"""主运行逻辑"""
print(f"开始爬取,从第 {self.current_page} 页到第 {self.end_page} 页")
while self.current_page <= self.end_page:
try:
print(f"正在爬取第 {self.current_page} 页...")
items = self.crawl_page(self.current_page)
if items:
self._save_data(items)
print(f" 第 {self.current_page} 页完成,获取 {len(items)} 条")
# 保存进度
self._save_state()
self.current_page += 1
time.sleep(1) # 礼貌延时
except Exception as e:
print(f"❌ 第 {self.current_page} 页出错: {e}")
print(f"已保存进度到第 {self.current_page} 页,等待 30 秒后重试...")
self._save_state()
time.sleep(30) # 等一会再重试
print(f"✅ 全部完成!共爬取到第 {self.end_page} 页")
# 使用
crawler = ResumeCrawler(start_page=1, end_page=500)
crawler.run()
# 如果中途中断了,重新运行会自动从断点继续
三、实现方式二:Redis 记录进度(分布式环境)
多台机器同时爬时,文件方式就不行了——文件在 A 机器上,B 机器看不到。用 Redis 存进度,所有机器共享:
import redis
import json
class RedisResumeCrawler:
"""基于 Redis 的断点续爬"""
def __init__(self, name="crawler", start_page=1):
self.name = name
self.redis_client = redis.Redis(host="localhost", port=6379, db=0)
self.progress_key = f"{name}:progress"
self.data_key = f"{name}:data"
self.current_page = self._load_progress() or start_page
def _load_progress(self):
"""从 Redis 加载进度"""
progress = self.redis_client.get(self.progress_key)
if progress:
page = int(progress)
print(f"📂 上次爬到第 {page} 页,继续...")
return page
return None
def _save_progress(self, page):
"""保存进度到 Redis"""
self.redis_client.set(self.progress_key, page)
def _save_data(self, items):
"""数据存到 Redis 列表"""
for item in items:
self.redis_client.rpush(
self.data_key,
json.dumps(item, ensure_ascii=False)
)
def run(self, end_page=1000):
"""运行爬虫"""
while self.current_page <= end_page:
# ... 爬取逻辑同上 ...
self._save_progress(self.current_page)
self.current_page += 1
四、增量采集——只爬新数据
断点续爬解决的是"中断后重爬",增量采集解决的是"已经爬过的不再爬"。
1. 基于 URL 去重
import hashlib
import redis
class IncrementalCrawler:
"""增量爬虫:只爬新 URL"""
def __init__(self):
self.redis = redis.Redis(host="localhost", port=6379, db=0)
self.seen_key = "crawler:seen_urls"
def _url_hash(self, url):
"""URL 指纹"""
return hashlib.md5(url.encode()).hexdigest()
def is_crawled(self, url):
"""检查 URL 是否已经爬过"""
return self.redis.sismember(self.seen_key, self._url_hash(url))
def mark_crawled(self, url):
"""标记 URL 已爬"""
self.redis.sadd(self.seen_key, self._url_hash(url))
def extract_new_urls(self, page_url, new_urls):
"""从新页面中筛选出未爬过的 URL"""
fresh_urls = []
for url in new_urls:
if not self.is_crawled(url):
fresh_urls.append(url)
return fresh_urls
2. 基于内容哈希去重(相同内容不再爬)
import hashlib
class ContentDedupCrawler:
"""基于内容去重的爬虫"""
def __init__(self):
self.content_hashes = set()
def _content_hash(self, text):
"""计算内容的指纹"""
# 去掉空白字符后再算哈希,避免微小的格式差异导致重复
clean = "".join(text.split())
return hashlib.md5(clean.encode()).hexdigest()
def is_duplicate(self, text):
"""判断内容是否重复"""
h = self._content_hash(text)
if h in self.content_hashes:
return True
self.content_hashes.add(h)
return False
3. 基于时间戳(只爬新闻/文章类)
from datetime import datetime, timedelta
class TimeBasedCrawler:
"""基于时间的增量爬虫"""
def __init__(self, max_days=7):
self.max_days = max_days
self.cutoff_date = datetime.now() - timedelta(days=max_days)
def should_crawl(self, article_date_str):
"""判断是否应该爬取(只爬最近 N 天的)"""
try:
article_date = datetime.strptime(article_date_str, "%Y-%m-%d")
return article_date >= self.cutoff_date
except:
return True # 日期解析失败就默认爬
def run_daily(self):
"""每天定时执行"""
today = datetime.now().strftime("%Y-%m-%d")
print(f"开始增量采集:{today}")
# 只爬当天的数据
self.crawl_by_date(today)
五、完整示例:增量爬取新闻
import requests
from bs4 import BeautifulSoup
import json
import hashlib
import os
import time
class NewsIncrementalCrawler:
"""新闻增量爬虫"""
def __init__(self, state_file="news_state.json"):
self.state_file = state_file
self.state = self._load_state()
self.session = requests.Session()
def _load_state(self):
"""加载状态"""
if os.path.exists(self.state_file):
with open(self.state_file, "r") as f:
return json.load(f)
return {"crawled_urls": [], "latest_crawl_time": ""}
def _save_state(self):
"""保存状态"""
with open(self.state_file, "w") as f:
json.dump(self.state, f, ensure_ascii=False, indent=2)
def _is_crawled(self, url):
"""是否已经爬过"""
url_hash = hashlib.md5(url.encode()).hexdigest()
return url_hash in self.state["crawled_urls"]
def _mark_crawled(self, url):
"""标记已爬"""
url_hash = hashlib.md5(url.encode()).hexdigest()
self.state["crawled_urls"].append(url_hash)
# 只保留最近 10000 条,防止状态文件越来越大
if len(self.state["crawled_urls"]) > 10000:
self.state["crawled_urls"] = self.state["crawled_urls"][-10000:]
def crawl_news_list(self):
"""爬取新闻列表页"""
url = "https://example.com/news"
resp = self.session.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
new_articles = []
for item in soup.select(".news-item"):
title = item.select_one(".title").text.strip()
link = item.select_one("a").get("href")
date = item.select_one(".date").text.strip()
# 只处理新文章
if not self._is_crawled(link):
new_articles.append({
"title": title,
"url": link,
"date": date,
})
return new_articles
def crawl_detail(self, url):
"""爬取新闻详情"""
resp = self.session.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
return {
"content": soup.select_one(".article-content").text.strip(),
"author": soup.select_one(".author").text.strip(),
}
def run(self):
"""执行增量采集"""
print(f"开始增量采集,上次共爬取 {len(self.state['crawled_urls'])} 条")
new_articles = self.crawl_news_list()
print(f"本次发现 {len(new_articles)} 条新文章")
for article in new_articles:
try:
detail = self.crawl_detail(article["url"])
article.update(detail)
# 保存到文件
with open("news_data.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(article, ensure_ascii=False) + "\n")
# 标记已爬
self._mark_crawled(article["url"])
self._save_state()
print(f" ✅ {article['title']}")
time.sleep(1)
except Exception as e:
print(f" ❌ {article['title']}: {e}")
print(f"增量采集完成,新增 {len(new_articles)} 篇")
# 每天定时执行
crawler = NewsIncrementalCrawler()
crawler.run()
六、数据去重的三种粒度
| 去重粒度 | 方法 | 适用场景 |
|---|---|---|
| URL 去重 | 记 URL 是否爬过 | 通用,最常用 |
| 内容去重 | 算内容的 MD5 | 同一篇文章有不同 URL(转载) |
| 字段去重 | 按标题/ID去重 | 数据库已有唯一约束 |
推荐组合: URL 去重为主 + 内容去重为辅。
七、断点续爬的最佳实践
① 每次爬完一页/一条就保存进度(不要等全部爬完才存)
② 状态文件和数据文件分开存
③ 定期清理 URL 去重集合(防止无限增长)
④ 爬虫启动时先恢复进度,而不是从头开始
⑤ 每页爬完加 1-3 秒延时,给后续重试留时间
最重要的原则: 宁可重复保存进度,也不要丢失进度。每爬一页存一次是安全的,每 10 页存一次就有丢失风险。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。

556

被折叠的 条评论
为什么被折叠?



