news 2026/10/15 3:40:17

我写了个爬虫-1000条数据5分钟抓完还不会崩

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
我写了个爬虫-1000条数据5分钟抓完还不会崩

我写了个爬虫,1000 条数据 5 分钟抓完,还不会崩

需要抓取 books.toscrape.com 上 1000 本书的书名、价格、评分,分页共 50 页。脚本一次运行 5 分钟左右抓完,全程零人工干预;最终实收 1000 条,缺失 0、重复 0,与目标条数对账一致。

更值得说的是运行过程中发生的另一件事:**途中出现过 2 次请求失败,都被重试机制自动救回,最终数据一条不少。**所谓"不会崩",不是没出故障,而是出了故障能自己扛过去、扛不过去时也留得下记录。

本文记录这个爬虫"不会崩"的三道防线——重试、断点续跑、频率控制——的具体实现,以及开发阶段遇到的 6 个问题。


一、结果

目标:抓取 50 页、1000 条图书数据(书名 / 价格 / 评分),落盘 Excel 并输出数据质量报告。

一次完整运行的结果:

  • 完成页数 50 / 50,实收 1000 / 1000 条
  • 缺失数据 0 条,重复记录 0 条,获取失败页数 0
  • 请求失败次数(含重试):2 次——全部被重试机制自动恢复,未造成任何数据缺口
  • 报告数字自洽:清洗后 1000 条 = 目标 1000 条

运行环境:Python 3.9 / requests / pandas / lxml / Windows

依赖清单

requests>=2.31 beautifulsoup4>=4.12 lxml>=4.9 pandas>=2.0 xlsxwriter>=3.0 # 输出 Excel 的引擎,以字符串参数传入,pipreqs 扫描不到,需手工补入

二、"不会崩"到底指什么

先界定问题。一次几十页的抓取任务,跑完之前这些事都可能发生:

可能发生的事没有防护时的后果对应防线
网络抖动、单次请求超时脚本当场中断,或永久挂起超时限制 + 重试退避
服务器返回 4xx / 5xx错误页被当正常数据解析,产出乱数据raise_for_status()归位到重试通道
中途 Ctrl+C、断网、电脑休眠全部进度清零,从头再来断点续跑
请求过快触发限流封禁,或拖垮对方站点频率控制 + 诚实标识

崩溃本身不可避免——网络是外部环境,不受脚本控制。**能设计的是每次失败后的行为:要么自动恢复,要么显式停止并留下账目。**下面按三道防线依次展开。

三、防线一:重试——把"偶发失败"从灾难降级成一条日志

网络偶发超时是正常现象,一次失败不值得终止任务。重试逻辑封装为独立函数,与主循环分离:

# book_scraper.pydefget_res_with_retry(url,max_retries):"""带重试与退避的页面请求。"""forattemptinrange(1,max_retries+1):try:res=requests.get(url,headers=headers,timeout=10)# 超时10秒,防止请求永久挂起res.raise_for_status()# 4xx/5xx抛异常,进入重试通道returnres.contentexceptExceptionase:stats["retries"]+=1print(f"第{attempt}次获取失败:{e.__class__.__name__}")ifattempt<max_retries:time.sleep(2*attempt)# 退避:第1次等2秒,第2次等4秒returnNone# 重试耗尽,交由调用方登记并停止

三个设计点:

1.timeout=10必须有。requests不传 timeout 时没有默认超时,网络一旦卡住,脚本会永远停在那里——这比崩溃更糟,因为它看起来像在正常工作。10 秒足够慢速站点响应,也保证了脚本不会无限等待。

**2.raise_for_status()让 4xx/5xx 走进重试通道。**不加这一行,服务器返回 500 错误页时requests不会报错,错误页会一路流进解析环节——轻则解析出空数据,重则把报错文本当数据存进表里。加了这一行,HTTP 错误和超时、断连一样,都被归位到同一个重试通道处理。

**3. 退避间隔逐次拉长。**第 1 次失败等 2 秒再试,第 2 次等 4 秒。连续失败通常意味着对方暂时繁忙,间隔拉长是给对方喘息,也提高自己下一次成功的概率。

重试耗尽之后,脚本不会抛异常崩掉,而是返回None,由调用方显式处理:

# book_scraper.py 主循环res=get_res_with_retry(url,config["max_retries"])ifresisNone:register_failed(page,url)# 失败登记落盘,进入质量报告print(f"第{page}页重试{config['max_retries']}次失败,停止运行")break# 体面停止,已抓数据不受影响

这套机制在真实运行中确实派上过用场:数据质量报告里的「请求失败次数(含重试):2」就是两次真实故障的记录。如果没有重试,脚本会停在其中一页,1000 条的目标直接断掉;实际结果是日志里多了两行"第1次获取失败",任务照常完成。

四、防线二:断点续跑——任意时刻中断,损失不超过一页

第二类故障是任务被中断:手动 Ctrl+C、断网、电脑休眠。对无防护的爬虫,这意味着全部进度清零;对几百页的任务,重跑一次的代价难以接受。

方案是两个文件配合:

  • 数据文件books.jsonl:JSONL 格式,一行一条记录,逐页追加写。中断后已写盘的数据天然保留。
  • 进度文件done_book_url.json:字典结构,记录{已完成页URL: 页码}。

关键设计是写回时机——每成功一页就立即写回进度文件,而不是全部跑完再写:

# book_scraper.py 主循环withopen(data_path,"a",encoding="utf-8")asf:# 数据逐条追加落盘foritemincards:f.write(json.dumps(item,ensure_ascii=False)+"\n")done_url[url]=page# 每成功一页,立刻写回进度withopen(progress,"w",encoding="utf-8")asf:f.write(json.dumps(done_url,ensure_ascii=False)+"\n")

这样无论在第几页中断,进度损失的上界就是当前这一页。

再次启动时,脚本从进度文件的最后一项恢复断点:

# book_scraper.py 启动时done_url=json.loads(progress.read_text(encoding="utf-8"))ifprogress.exists()else{}url,page=next(reversed(done_url.items()),(url,config["pages"]))# 最后一项即上次断点

这里有个容易想当然的细节:恢复点是"最后完成的页",而不是字面理解的"下一页"。因为下一页的链接要从当前页的 HTML 里解析出来,必须重抓这一页才能继续往下走。为避免数据重复,重抓只用于翻页,跳过数据收集:

# book_scraper.py 主循环ifurlindone_url:print(f"第{page}页数据已存在,跳过")url=get_next_url(url,res)# 只解析"下一页"链接,不收集数据ifurl:page+=1time.sleep(1)continue

代价只有 1 次额外请求,换来恢复逻辑无需任何持久化的"下一页"状态。验证方法很直接:跑到一半按 Ctrl+C,重新运行,观察它是否从断点继续、总条数是否仍为 1000。

五、防线三:频率控制——1 秒间隔是成本最低的稳定措施

第三道防线针对的是自己:请求过快不仅可能触发对方限流,也会让脚本自己变成不稳定因素。

做法有三条:

# book_scraper.pyheaders={"User-Agent":"personal-learning-scraper/1.0"}# 自定义标识,不伪装浏览器time.sleep(1)# 主循环内,每页请求之间固定间隔1秒

**1. 每页之间固定间隔 1 秒。**50 页多花 50 秒,换来的是不进入对方限流名单——被限流后的重试、封禁等待,消耗的时间远超这 50 秒。慢一点,是快的前提。

**2. 诚实标识。**请求头使用自定义的 User-Agent,说明脚本身份,不伪装成浏览器。伪装浏览器绕过的是站点的访问策略,这条线不碰。

**3. 只在授权范围内抓取。**抓取前查看目标站 robots.txt;目标站为公开的、专为数据抓取场景搭建的站点;不碰需要登录的数据,不破解任何防护。

从交付角度看,这条防线最实际的意义是:甲方的数据源往往带访问频率防护,一个礼貌、克制的爬虫才能在对方站点上长期稳定运行——"还不会崩"的另一半含义,是不崩在别人的黑名单里。

六、开发阶段遇到的 6 个问题

**问题 1:500 错误页被当正常数据解析。**最初版本没有raise_for_status(),站点返回 500 时requests不报错,错误页 HTML 一路流进解析环节。同一套机制在另一个站点实测时真实遇到过:末页是一个不存在的伪页,服务器返回 500。修复后,HTTP 错误统一归位到重试通道,重试耗尽则登记失败、显式停止——错误页再也不会伪装成数据。

问题 2:中断窗口造成重复数据。主循环先写数据、再写进度,两个动作之间有一个极小的崩溃窗口:数据已落盘、进度未更新,重启后这一页会被重抓重写。彻底消除需要原子写,对这个量级不划算;实际处理是承认这个窗口——清洗环节用drop_duplicates兜底去重,质量报告里如实展示 JSONL 原始行数与清洗后条数的差异,重复数据有账可查而非悄悄混入。

数据落地的两步写(数据文件、进度文件)不是原子操作,这是设计时就明确的边界。工程上的选择是:与其追求理论上的完美原子性,不如让不一致可被发现、可被修正。质量报告的价值正在于此——它不是美化结果的宣传页,而是暴露运行事件的对账单。

**问题 3:断点恢复后数据重复。**最初恢复逻辑从"下一页"开始,但下一页链接无从取得(需要上一页内容才能解析)。改为从最后完成页恢复、重抓但跳过收集后,问题消失,代价仅 1 次请求。

问题 4:失败记录跨站点污染。图书和电影两个抓取目标最初共用同一个失败记录文件,A 站的历史失败记录混进 B 站报告,清空一个站的记录会误删另一个站的。修正为各站独立命名失败文件,互不干扰。

**问题 5:无限循环风险。**翻页循环的终止条件若只依赖"下一页"元素,一旦站点结构异常(比如 Next 链接自引用),脚本会原地打转。最终采用三重终止:结构信号(Next 按钮)+ 常量上限(MAX_PAGES=50)+ 空页兜底(解析不到卡片即停)。循环类代码的终止条件必须在所有异常路径上都能触发,否则失败处理不当就会变成死循环。

**问题 6:失败后如何停止。**最初用sys.exit()处理请求失败——简单粗暴,但已抓数据的状态、失败页的信息都来不及交代。改为"登记失败 → 打印归因 → break 停止"后,无论从哪一页退出,数据质量报告都能完整交代:目标多少、实收多少、失败在哪、重试了几次。

七、运行结果

执行python book_scraper.py,最终落盘的数据质量报告:

==================== 数据质量报告 ==================== 目标页数:50,目标数据条数:1000 完成页数(含历史断点): 50 ,JSONL 原始行数: 1000 (重跑叠加时大于清洗后条数) 缺失数据条数:0 缺失数据数量: title 0 price 0 rating 0 dtype: int64 重复记录:0 获取失败页数:0 失败获取清单: 请求失败次数(含重试):2 清洗后数据1000条,与目标数据1000条数一致 =====================================================

输出books.xlsx:1000 行图书数据,书名 / 价格 / 评分三列无缺失、无重复,价格统一为数值类型,评分统一为整数,可直接排序与筛选。

图 1|运行过程:控制台逐页输出抓取进度与失败重试日志。

图 2|断点续跑验证:中途 Ctrl+C 中断后重新运行,脚本从断点页继续

八、总结

复盘这个爬虫,解析 HTML 本身只占工作量的零头,真正投入在"不会崩"上,也真正值钱的是三点:

  1. **稳定不是不失败,而是每次失败都有去处。**要么被重试自动恢复,要么被登记进报告显式呈现——不存在第三种"悄悄失败"的状态。
  2. **进度每成功一步就落盘。**恢复的损失上界决定交付质量:按页写回,中断的代价就是一页;跑完再写,中断的代价就是全部。
  3. **对目标站克制是最便宜的稳定措施。**1 秒间隔、诚实标识、遵守 robots.txt,换来的是长期可运行——被限流封禁的爬虫,重试机制写得再好也救不回来。

如有数据抓取、自动化脚本类需求,欢迎交流:

  • Gitee 仓库:https://gitee.com/ridgeline/Web-Scraper
  • GitHub 仓库:https://github.com/mokong-J/Web-Scraper
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 3:39:55

大麦网自动抢票指南:如何用 Python 脚本 4 步跑通自动购票流程

大麦网自动抢票指南&#xff1a;如何用 Python 脚本 4 步跑通自动购票流程 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 19:30:00&#xff0c;大麦网开票倒计时归零的那一刻…

作者头像 李华
网站建设 2026/10/15 3:39:06

时序网络瓦解Ⅲ--时序供应链网络中的级联失效和恢复机制

一、概念背景 三种经典级联失效模型。级联失效是指局部节点 / 边的失效会通过负载重分配、邻接影响等方式连锁传导,最终引发全网大范围崩溃的过程,是网络鲁棒性、网络瓦解研究的重要基础方向。 一、容量 - 负载模型(Motter-Lai 模型,2002) 核心原理 这是最经典的负载…

作者头像 李华
网站建设 2026/10/15 3:38:51

华为ENSP实战:ACL访问控制列表从原理到配置排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 3:38:41

SSM框架在扶贫惠农系统开发中的应用与实践

1. 项目背景与核心价值在乡村振兴战略背景下&#xff0c;如何利用信息化手段提升扶贫惠农政策落地效率&#xff0c;成为基层工作的重要课题。SSM233定西扶贫惠农推介系统正是针对这一需求开发的数字化解决方案。这个系统基于SSM&#xff08;SpringSpringMVCMyBatis&#xff09;…

作者头像 李华
网站建设 2026/10/15 3:37:55

Go零基础实战第3期:Mutex与数据竞争

文章目录引言&#xff1a;Mutex与数据竞争一、本篇学习目标与准备二、核心概念与实现思路1. 这个程序在模拟什么2. 什么是数据竞争3. sync.Mutex 是什么4. 为什么 Unlock 要配合 defer5. 为什么读也要加锁6. 为什么不要把内部 map 直接暴露出去7. Reserve 的执行过程8. 锁保护的…

作者头像 李华