在数据采集领域,爬虫任务的中断和重复爬取是开发者最头疼的问题之一。想象一下:当你已经爬取了90%的电商商品数据时,程序突然因为网络波动崩溃——传统方案只能重新开始,这不仅浪费资源,还可能触发目标站点的反爬机制。分页存储与断点续爬技术正是为解决这类痛点而生。
我曾在一次跨境电商价格监控项目中,因为未实现断点续爬,服务器宕机导致3天采集成果全部作废。痛定思痛后,我开发了一套基于文件分片和状态快照的解决方案,使后续采集任务即使中断,也能从最近的成功分页继续。这套方案后来成为我们团队的标配,将数据采集效率提升了60%以上。
分页存储的核心是将大数据集拆分为可管理的块。常见的分页方式包括:
按数量分页:每N条数据存为一个文件
page_1.json包含1-50条,page_2.json包含51-100条按时间分页:每小时/天生成独立文件
2023-08-20_15.json表示15点采集的数据混合分页:结合数量和时间维度
python复制# 示例:每小时最多存储5000条,超量创建新文件
file_name = f"{date}_{hour}_{page_index}.json"
关键经验:分页大小需考虑内存限制和后续处理效率。我通常将单个分页控制在10-50MB之间,避免文件过大导致的IO瓶颈。
格式选择直接影响后续数据使用效率,以下是三种主流方案的对比:
| 格式类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON | 可读性强,兼容性好 | 体积较大,无模式约束 | 中小规模数据,需要人工查看 |
| Parquet | 列式存储,压缩率高 | 需要专用工具读取 | 大数据分析场景(配合Spark等) |
| SQLite | 支持复杂查询,事务安全 | 写入性能较低 | 需要频繁检索的中间结果 |
在我的实践中,推荐以下组合方案:
断点续爬的本质是保存爬取进度。我设计的状态管理器包含以下核心字段:
python复制{
"current_page": 42, # 当前正在处理的页码
"last_success_url": "https://...?page=41",
"failed_attempts": 0, # 连续失败次数(用于熔断判断)
"checkpoint_time": "2023-08-20T15:30:00Z",
"data_signature": "a1b2c3d4" # 当前数据校验码
}
实现要点:
task_checkpoints表根据不同的中断原因,需要采取差异化恢复策略:
网络中断:
python复制def request_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
return requests.get(url, timeout=10)
except Exception as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
反爬拦截:
数据解析失败:
/failed_pages/目录当爬虫运行在集群环境时,需要额外考虑以下问题:
为避免多个节点重复处理相同分页,我采用Redis实现分布式锁:
python复制def acquire_page_lock(page_id, expire=300):
lock_key = f"page_lock:{page_id}"
return redis_client.set(lock_key, 1, nx=True, ex=expire)
# 使用示例
if acquire_page_lock(target_page):
try:
process_page(target_page)
finally:
release_page_lock(target_page)
将分页数据均匀分配到不同节点的算法实现:
python复制from hashlib import md5
def get_worker_node(page_url, node_count):
hash_val = int(md5(page_url.encode()).hexdigest()[:8], 16)
return hash_val % node_count
这种方案确保相同分页始终由同一节点处理,避免状态同步问题。
大规模爬取时,内存泄漏是常见问题。我的解决方案是:
使用生成器逐条处理数据
python复制def parse_items(html):
soup = BeautifulSoup(html, 'lxml')
for item in soup.select('.item-list'):
yield {
'title': item.select_one('.title').text,
'price': float(item.select_one('.price').text[1:])
}
每处理100条数据强制GC回收
python复制import gc
if counter % 100 == 0:
gc.collect()
通过缓冲写入提升IO性能:
python复制from collections import deque
class BufferedFileWriter:
def __init__(self, file_path, buffer_size=1000):
self.buffer = deque(maxlen=buffer_size)
self.file = open(file_path, 'a')
def write(self, record):
self.buffer.append(json.dumps(record) + '\n')
if len(self.buffer) >= self.buffer.maxlen:
self.flush()
def flush(self):
self.file.writelines(self.buffer)
self.buffer.clear()
可能原因及解决方案:
分页边界重叠:调整分页逻辑确保区间不重复
python复制# 错误示例:每页100条,但未考虑新增数据
# 正确做法:基于最后一条记录的ID作为下一页起始
next_page_url = f"?after_id={last_item['id']}"
状态文件损坏:增加校验和检查
python复制def save_checkpoint(data):
data['signature'] = calculate_md5(data)
with open('checkpoint.tmp', 'w') as f:
json.dump(data, f)
os.rename('checkpoint.tmp', 'checkpoint.json')
典型故障处理流程:
df -h)bash复制cp checkpoint.bak checkpoint.json
在实际生产环境中,我通常将爬虫封装为Airflow DAG任务:
python复制from airflow import DAG
from airflow.operators.python import PythonOperator
def crawl_with_checkpoint(**context):
last_state = context['ti'].xcom_pull(task_ids='previous_run')
# 恢复爬取逻辑...
dag = DAG(
'ecommerce_crawler',
default_args={'retries': 3},
schedule_interval='@daily'
)
task = PythonOperator(
task_id='crawl_products',
python_callable=crawl_with_checkpoint,
dag=dag,
provide_context=True
)
这种方案实现了:
为确保分页数据的完整性,我采用以下验证手段:
分页哈希校验:
python复制def verify_page(page_data):
expected = page_data['metadata']['checksum']
actual = hashlib.sha256(str(page_data['items']).encode()).hexdigest()
return expected == actual
全量数据审计(每日执行):
sql复制-- 检查缺失的分页编号
SELECT generate_series(1, max(page)) AS expected
EXCEPT
SELECT DISTINCT page FROM crawled_data;
最终一致性补偿:对验证失败的分页启动重新爬取任务,但标记为"补偿数据"以便后续区分。