资讯动态

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

发布时间:2026/10/15 3:40:18 来源:尧图企业网站定制
我写了个爬虫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依赖清单requests2.31 beautifulsoup44.12 lxml4.9 pandas2.0 xlsxwriter3.0 # 输出 Excel 的引擎以字符串参数传入pipreqs 扫描不到需手工补入二、不会崩到底指什么先界定问题。一次几十页的抓取任务跑完之前这些事都可能发生可能发生的事没有防护时的后果对应防线网络抖动、单次请求超时脚本当场中断或永久挂起超时限制 重试退避服务器返回 4xx / 5xx错误页被当正常数据解析产出乱数据raise_for_status()归位到重试通道中途 CtrlC、断网、电脑休眠全部进度清零从头再来断点续跑请求过快触发限流封禁或拖垮对方站点频率控制 诚实标识崩溃本身不可避免——网络是外部环境不受脚本控制。**能设计的是每次失败后的行为要么自动恢复要么显式停止并留下账目。**下面按三道防线依次展开。三、防线一重试——把偶发失败从灾难降级成一条日志网络偶发超时是正常现象一次失败不值得终止任务。重试逻辑封装为独立函数与主循环分离# book_scraper.pydefget_res_with_retry(url,max_retries):带重试与退避的页面请求。forattemptinrange(1,max_retries1):try:resrequests.get(url,headersheaders,timeout10)# 超时10秒防止请求永久挂起res.raise_for_status()# 4xx/5xx抛异常进入重试通道returnres.contentexceptExceptionase:stats[retries]1print(f第{attempt}次获取失败{e.__class__.__name__})ifattemptmax_retries:time.sleep(2*attempt)# 退避第1次等2秒第2次等4秒returnNone# 重试耗尽交由调用方登记并停止三个设计点1.timeout10必须有。requests不传 timeout 时没有默认超时网络一旦卡住脚本会永远停在那里——这比崩溃更糟因为它看起来像在正常工作。10 秒足够慢速站点响应也保证了脚本不会无限等待。**2.raise_for_status()让 4xx/5xx 走进重试通道。**不加这一行服务器返回 500 错误页时requests不会报错错误页会一路流进解析环节——轻则解析出空数据重则把报错文本当数据存进表里。加了这一行HTTP 错误和超时、断连一样都被归位到同一个重试通道处理。**3. 退避间隔逐次拉长。**第 1 次失败等 2 秒再试第 2 次等 4 秒。连续失败通常意味着对方暂时繁忙间隔拉长是给对方喘息也提高自己下一次成功的概率。重试耗尽之后脚本不会抛异常崩掉而是返回None由调用方显式处理# book_scraper.py 主循环resget_res_with_retry(url,config[max_retries])ifresisNone:register_failed(page,url)# 失败登记落盘进入质量报告print(f第{page}页重试{config[max_retries]}次失败停止运行)break# 体面停止已抓数据不受影响这套机制在真实运行中确实派上过用场数据质量报告里的「请求失败次数含重试2」就是两次真实故障的记录。如果没有重试脚本会停在其中一页1000 条的目标直接断掉实际结果是日志里多了两行第1次获取失败任务照常完成。四、防线二断点续跑——任意时刻中断损失不超过一页第二类故障是任务被中断手动 CtrlC、断网、电脑休眠。对无防护的爬虫这意味着全部进度清零对几百页的任务重跑一次的代价难以接受。方案是两个文件配合数据文件books.jsonlJSONL 格式一行一条记录逐页追加写。中断后已写盘的数据天然保留。进度文件done_book_url.json字典结构记录{已完成页URL: 页码}。关键设计是写回时机——每成功一页就立即写回进度文件而不是全部跑完再写# book_scraper.py 主循环withopen(data_path,a,encodingutf-8)asf:# 数据逐条追加落盘foritemincards:f.write(json.dumps(item,ensure_asciiFalse)\n)done_url[url]page# 每成功一页立刻写回进度withopen(progress,w,encodingutf-8)asf:f.write(json.dumps(done_url,ensure_asciiFalse)\n)这样无论在第几页中断进度损失的上界就是当前这一页。再次启动时脚本从进度文件的最后一项恢复断点# book_scraper.py 启动时done_urljson.loads(progress.read_text(encodingutf-8))ifprogress.exists()else{}url,pagenext(reversed(done_url.items()),(url,config[pages]))# 最后一项即上次断点这里有个容易想当然的细节恢复点是最后完成的页而不是字面理解的下一页。因为下一页的链接要从当前页的 HTML 里解析出来必须重抓这一页才能继续往下走。为避免数据重复重抓只用于翻页跳过数据收集# book_scraper.py 主循环ifurlindone_url:print(f第{page}页数据已存在跳过)urlget_next_url(url,res)# 只解析下一页链接不收集数据ifurl:page1time.sleep(1)continue代价只有 1 次额外请求换来恢复逻辑无需任何持久化的下一页状态。验证方法很直接跑到一半按 CtrlC重新运行观察它是否从断点继续、总条数是否仍为 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 个问题**问题 1500 错误页被当正常数据解析。**最初版本没有raise_for_status()站点返回 500 时requests不报错错误页 HTML 一路流进解析环节。同一套机制在另一个站点实测时真实遇到过末页是一个不存在的伪页服务器返回 500。修复后HTTP 错误统一归位到重试通道重试耗尽则登记失败、显式停止——错误页再也不会伪装成数据。问题 2中断窗口造成重复数据。主循环先写数据、再写进度两个动作之间有一个极小的崩溃窗口数据已落盘、进度未更新重启后这一页会被重抓重写。彻底消除需要原子写对这个量级不划算实际处理是承认这个窗口——清洗环节用drop_duplicates兜底去重质量报告里如实展示 JSONL 原始行数与清洗后条数的差异重复数据有账可查而非悄悄混入。数据落地的两步写数据文件、进度文件不是原子操作这是设计时就明确的边界。工程上的选择是与其追求理论上的完美原子性不如让不一致可被发现、可被修正。质量报告的价值正在于此——它不是美化结果的宣传页而是暴露运行事件的对账单。**问题 3断点恢复后数据重复。**最初恢复逻辑从下一页开始但下一页链接无从取得需要上一页内容才能解析。改为从最后完成页恢复、重抓但跳过收集后问题消失代价仅 1 次请求。问题 4失败记录跨站点污染。图书和电影两个抓取目标最初共用同一个失败记录文件A 站的历史失败记录混进 B 站报告清空一个站的记录会误删另一个站的。修正为各站独立命名失败文件互不干扰。**问题 5无限循环风险。**翻页循环的终止条件若只依赖下一页元素一旦站点结构异常比如 Next 链接自引用脚本会原地打转。最终采用三重终止结构信号Next 按钮 常量上限MAX_PAGES50 空页兜底解析不到卡片即停。循环类代码的终止条件必须在所有异常路径上都能触发否则失败处理不当就会变成死循环。**问题 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.xlsx1000 行图书数据书名 / 价格 / 评分三列无缺失、无重复价格统一为数值类型评分统一为整数可直接排序与筛选。图 1运行过程控制台逐页输出抓取进度与失败重试日志。图 2断点续跑验证中途 CtrlC 中断后重新运行脚本从断点页继续八、总结复盘这个爬虫解析 HTML 本身只占工作量的零头真正投入在不会崩上也真正值钱的是三点**稳定不是不失败而是每次失败都有去处。**要么被重试自动恢复要么被登记进报告显式呈现——不存在第三种悄悄失败的状态。**进度每成功一步就落盘。**恢复的损失上界决定交付质量按页写回中断的代价就是一页跑完再写中断的代价就是全部。**对目标站克制是最便宜的稳定措施。**1 秒间隔、诚实标识、遵守 robots.txt换来的是长期可运行——被限流封禁的爬虫重试机制写得再好也救不回来。如有数据抓取、自动化脚本类需求欢迎交流Gitee 仓库https://gitee.com/ridgeline/Web-ScraperGitHub 仓库https://github.com/mokong-J/Web-Scraper

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑