使用 Python 数据库驱动提供的批量执行方法(如 executemany)配合事务提交,是优化爬虫写入性能的标准方向。适用高频写入场景,风险在于批量过大会占用内存或导致长事务锁表。
先说结论:批量插入能显著减少网络往返和事务提交开销,但需控制单次提交数据量。
- 先定位:确认当前写入瓶颈是网络 IO 还是磁盘事务提交。
- 先做:使用驱动层的批量方法并将多条插入包裹在一个事务中。
- 再验证:监控内存占用和数据库锁等待时间,避免批量过大。
命令速用版
以下是基于 pymysql 和 SQLAlchemy 的批量写入最小可用代码片段,直接替换原有的单条循环插入。
# 方案 A:原生驱动 executemany
import pymysql
conn = pymysql.connect(host='localhost', user='user', password='pass', db='dbname')
cursor = conn.cursor()
data_list = [(id1, val1), (id2, val2), (id3, val3)] # 积累一批数据
try:
cursor.executemany("INSERT INTO table_name (id, val) VALUES (%s, %s)", data_list)
conn.commit() # 必须手动提交事务
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
# 方案 B:SQLAlchemy bulk_insert_mappings
from sqlalchemy import create_engine
engine = create_engine("mysql+pymysql://user:pass@localhost/dbname")
conn = engine.connect()
data_list = [{"id": id1, "val": val1}, {"id": id2, "val": val2}]
conn.execute(table.insert(), data_list) # 自动优化为批量
conn.commit()为什么会这样
数据库写入慢的核心原因通常是频繁的网络往返和事务日志刷盘,而不是插入逻辑本身。
单条插入时,每写入一行都需要一次网络请求和一次事务提交确认,大部分时间消耗在等待数据库响应上。批量插入将多行数据合并为一次网络请求,并将多次事务提交合并为一次,大幅降低了协议握手和磁盘同步的开销。公开资料中没有看到可靠的量化数据表明具体提升倍数,因为性能取决于网络延迟、磁盘 IO 速度和单条数据大小,但减少交互次数是数据库优化的通用原则。
分步处理
按以下步骤调整爬虫管道中的数据库写入逻辑,确保在提升性能的同时不丢失数据。
步骤 1:积累数据批次
在爬虫回调函数中不要立即写入数据库,先将解析好的数据存入内存列表。设置一个阈值(如 100 条或 500 条)触发写入动作。
步骤 2:包裹事务
确保批量写入代码位于事务块内。对于自动提交的驱动,需显式关闭自动提交(autocommit=False)。如果中途报错,必须执行 rollback 回滚,防止脏数据。
步骤 3:调整批次大小
初始批次建议设为 100 条。观察运行稳定后,可逐步增加至 500 或 1000 条。如果涉及大文本字段,批次大小应适当减小。
步骤 4:异常重试机制
批量写入失败时,不要直接丢弃整个批次。建议将失败的批次拆分降级为单条重试,或记录到本地日志文件后续补偿。
怎么验证是否生效
通过对比单位时间写入行数和系统资源占用,确认优化效果。
检查写入速率
在爬虫日志中记录每批次写入的耗时。计算 每秒写入行数 = 批次大小 / 耗时。优化后该数值应明显上升。
检查数据库锁
使用数据库监控命令查看锁等待情况。MySQL 可查询 information_schema.innodb_trx 表,确认没有长时间未提交的事务。
检查内存波动
使用 Python 的 tracemalloc 模块或系统监控工具观察爬虫进程内存。如果内存随批次积累持续升高不释放,说明存在内存泄漏或批次清理不及时。
常见坑
批量插入虽快,但实现不当容易引发数据一致性问题或资源崩溃。
事务过长导致锁表
如果积累数据时间过久才提交事务,会长时间占用数据库行锁,影响其他查询。建议增加时间阈值,例如每 5 秒强制提交一次,即使数据量未达到批次上限。
内存溢出(OOM)
爬虫速度过快而数据库写入过慢时,内存列表会无限增长。必须设置列表最大长度限制,达到上限后暂停爬虫抓取或丢弃旧数据。
部分写入成功
批量插入通常具有原子性,一旦某条数据报错,整个批次可能失败。需确保数据格式在送入数据库前已完成清洗,避免非法字符导致整批回滚。
常见问题
SQLite 也需要批量插入吗?
需要,SQLite 同样受事务提交开销影响。
SQLite 默认每条语句都是一个事务,使用 executemany 配合 BEGIN/COMMIT 能显著提升速度,否则磁盘 IO 会成为瓶颈。
ORM 的 bulk 操作会跳过验证吗?
是的,大多数 ORM 的批量方法会跳过对象生命周期管理。
SQLAlchemy 的 bulk_insert_mappings 不会触发事件钩子或验证逻辑,速度更快但需确保数据本身合法。
网络不稳定时批量插入安全吗?
不安全,需配合重试机制。
网络波动可能导致整个批次丢失,建议在代码层捕获数据库异常,并将失败数据存入本地队列等待重发。
参考来源
- Python Software Foundation, "sqlite3 - DB-API 2.0 interface for SQLite databases", docs.python.org
- SQLAlchemy, "Core Event Extensions - Bulk Insert Operations", docs.sqlalchemy.org
- MySQL Documentation, "INSERT Statement", dev.mysql.com