首页 / 资讯中心 / 文章详情

Python爬虫实战:从requests到反爬,构建可持续数据采集

Python爬虫实战:从requests到反爬,构建可持续数据采集 ★ FEATURED ARTICLE
简介这份Python爬虫技术指南面向有一定编程经验、希望高效抓取公开网络数据的开发者和研究人员。内容从爬虫的基本概念、工作原理与Python语言的核心优势讲起逐步拆解发送网络请求、解析网页内容、将数据持久化存储到文件或数据库这三大核心环节同时结合 requests、BeautifulSoup、lxml、Scrapy 等常用库讲解这些库的适用场景与代码示例。针对动态加载、反爬虫机制与效率优化文档进一步介绍了 Selenium 等浏览器模拟工具的使用思路末尾强调法律法规、网站协议与用户隐私保护引导读者合法合规地开展数据采集。资源包共1个docx文档压缩包仅16KB轻量易读目录结构清晰便于随时查阅。目前已有628人学习下载适合作为网络数据采集从入门到进阶的速查手册也能为后续数据挖掘项目或信息监测系统的技术选型提供参考。1. Python爬虫到底能解决什么问题先越过这三道坎再动手我见过不少开发者对 Python爬虫的印象停留在「用 requests 把网页拿下来再用正则抠几个字段」可真要把一套高效网络数据采集流程跑起来遇到的第一个坎往往不是代码而是「请求发出去、数据没回来或者回来之后你根本不敢用」。这标题把基础到实战连在一起真正想表达的是采集不是拼一两句代码而是从请求、解析、调度到规范一整条链路。这篇面向两类人一类是刚写完第一个采集程序、准备把零散脚本做成可持续任务的初学者另一类是已经能抓到数据但经常被反爬限制、被页面结构变化打乱的老手。你会看到 requests 的最小用法、XPath 的选型依据、翻页和落盘的工程写法也会看到几条具体踩坑记录——这些坑不解决采集只会停留在「运气好能跑一次」的阶段。先把心里那条线立住高频抓取不可持续动态页面不是简单库能直接解决的采集范围要自己画好边界。2. 第一步不是写解析是让 requests 正确拿到页面2.1 最小抓取环境requests、超时与UA先把第一行命令跑通几乎所有采集脚本都是从一个请求开始的但裸 requests 直接 get 一个地址往往过不了最基本的门槛。常见做法是同时带上三样东西超时时间、请求头、状态码检查。超时是为了不让程序死等一个无响应的连接UA 是为了让目标站点把你当成一个正常访问者而非脚本状态码检查是为了尽早发现请求失败而不是拿到错误响应之后继续解析半天。import requests # 用贴近真实浏览器的请求头而不是 requests 默认的 python-requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 } url https://example.com/list response requests.get(url, headersheaders, timeout(10, 20)) # 非2xx状态码直接抛异常避免把错误页面当数据分析 response.raise_for_status() print(response.status_code, len(response.content))timeout 里的两个数字分别是「连接超时」和「读取超时」只写一个数字则两个阶段共用同一个值。连接超时太长会让程序在目标站点不可达时卡很久太短又会在网络波动时误杀10 秒起步比较合适。UA 之所以要写全是因为很多站点对 UA 里有 python、requests 字样会直接返回 403完整浏览器 UA 是低成本高收益的第一道伪装。依赖环境建议用虚拟环境隔离。下面这段命令创建并激活环境再安装后续会用到的库python -m venv crawler-env source crawler-env/bin/activate # Windows 下用 crawler-env\Scripts\activate pip install requests lxml beautifulsoup4requests 负责拿数据lxml 负责 XPath 解析BeautifulSoup4 在少量场景做辅助。虚拟环境不是爬虫专属要求但对反复试错的人来说装坏的包不会污染系统 Python换版本也方便。很多新手在这步省略虚拟环境后面 pip 装了一堆东西导致依赖冲突只能推倒重来这笔血泪账其实可以提前避免。2.2 判断抓取是否成功的三层检查状态码、编码、内容截断拿到响应之后很多人只看 status_code 等于 200 就继续往下走这是最常见的翻车点之一。200 只代表请求被受理不代表返回的是你要的数据。某些站点会对可疑请求返回一个「验证页面」HTML 内容依然存在状态码依然是 200但你从中解析不到任何目标字段。所以在解析之前至少做三层检查状态码、响应大小、页面特征关键词。from requests import RequestException try: response requests.get(url, headersheaders, timeout(10, 20)) response.raise_for_status() except RequestException as e: print(请求失败:, e) raise # 第一层检查响应体大小太短说明可能被拦截或跳转到了错误页 print(响应长度:, len(response.content)) # 第二层检查编码页面里声称的编码和实际字节流不一定一致 print(响应头charset:, response.encoding) # 第三层检查页面特征用一两个关键词判断是不是验证页/跳转页 check_list [验证, captcha, access denied, 安全验证] body response.text for keyword in check_list: if keyword.lower() in body.lower(): print(命中拦截特征:, keyword)编码问题最隐蔽。response.encoding 优先取响应头里的 charset但很多站点没有声明或者声明错误导致 response.text 把字节流按错误编码解码中文全部乱码。一个经验是不确定编码时用 chardet 或直接根据响应头找而不是盲信 response.text 的默认结果。我一般会在拿到字节流后先打印前 200 个字符看乱不乱再决定是否手动指定编码。乱码问题在采集阶段不解决解析阶段就全盘错位找一个字段错十个字段这个教训值得提前提防。2.3 动手前先确认页面是服务端渲染还是JS渲染requests 只能拿到服务端返回的原始 HTML如果页面里的内容是浏览器通过 JavaScript 动态渲染出来的那么 requests 拿到的只是一个空壳。判断方法很简单打开浏览器的开发者工具切到「网络」面板刷新页面找那个类型为 document 的请求点开查看「响应」标签。如果响应里已经能看到你要的数据说明是服务端渲染requests 可以直接处理如果响应里只有一个空 div 和一堆 script 标签数据是后续通过接口加载的那就得去找数据接口。不必把这类页面交给无头浏览器硬来。大部分动态页面的数据来自某个 XHR 接口返回的是 JSON采集 JSON 比解析 HTML 更舒服。这个调研动作放在写代码之前能省下后面一整轮方案推倒重来的时间。判断渲染方式属于采集前的基本功很多人跳过这一步把请求发出去、拿到一份没有数据的 HTML然后在解析环节反复怀疑自己的 XPath 写错了最后才发现问题出在数据源本身。3. 解析数据XPath和BeautifulSoup不是二选一是按场景选3.1 解析工具的选型边界lxmlXPath 是主力正则只做清洗采集程序的第二个主战场是解析。我把常用方案按使用频率排个序lxml 配合 XPath 是主力BeautifulSoup 适合快速验证正则只用于文本清洗。它们之间不是简单的性能差距而是「按哪个维度定位数据」的差别。XPath 定位元素靠的是 DOM 结构和属性正则定位靠的是字符串特征BeautifulSoup 的 find 方法本质上是走了一遍 CSS 选择器逻辑。同一个页面三种工具都能干活但代码的健壮性差别很大。工具定位方式适用场景主要代价lxml XPath按DOM层级和属性定位列表页、详情页字段抽取需理解 HTML 结构和 XPath 语法BeautifulSoup按标签、属性选择器定位简单页面快速验证语法写起来顺手嵌套复杂时定位不准正则字符串特征匹配从文本里抽 JSON、长摘要遇到标签嵌套和换行容易误匹配HTML 在解析器眼里和你在浏览器里看到的不一样。浏览器有容错机制会补全缺失的标签、修正嵌套结构解析器也按自己的规则构建节点树由此产生的 DOM 树可能与「直觉上的结构」有出入。所以用 XPath 时最忌凭肉眼从浏览器里看到的页面结构直接推导路径。稳妥做法是先把某一条数据里的目标节点在真实 HTML 里找到再顺着它的父节点逐级往上确认路径。正则我基本只用在两处一是从 script 或内嵌 JSON 字段里抽值二是做清洗——把解析出来的日期、金额字段里的杂字符去掉。用正则拆整个 HTML 是另一条弯路页面稍微改一改标签结构正则就碎一地性格不合尽量别碰。3.2 从HTML到字段的最小编码流程先取列表再遍历取字段实际工程里采集页面的解析可以按「定位列表容器 → 遍历容器 → 逐条取字段」三层来做。先拿到每一条的父级节点再在这个节点内部继续查子字段比自己凭感觉写一个从根路径开始的完整 XPath 更贴身。原因是页面改版时通常只改某个字段的 class容器层级很少大变而且先定位列表单元可以避免「标题取了上一行、链接取了下一行」的错位问题。下面这段是抓列表页的通用结构from lxml import html as lxml_html # 先把字节流转成DOM树注意要用 content 而不是 text避免编码二次错误 doc lxml_html.fromstring(response.content) # 第一步定位每条记录的容器节点 item_nodes doc.xpath(//div[contains(class, list-item)]) print(本条页面条目数:, len(item_nodes)) # 第二步在容器节点内部继续查找字段 for node in item_nodes: title node.xpath(.//h2[contains(class, title)]/a/text()) url node.xpath(.//h2[contains(class, title)]/a/href) pub_time node.xpath(.//time/datetime) # 取文本时统一去除首尾空白避免换行符干扰后续写入 title_text title[0].strip() if title else print(title_text, url[0] if url else , pub_time[0] if pub_time else )XPath 里凡是节点内部的查找都用点开头.//表示当前节点下的任意层级不加点会从整个文档重新找。contains(class, list-item) 处理的是多 class 的情况直接用 classlist-item 要求精确相等页面里多一个样式类就会匹配失败。取文本时 text() 可能返回多个元素比如标题里既有文字又有 span稳妥做法是先取第一个再判断是否为空。这个判断可以省但省了之后你会常在某个字段上遇到索引越界尤其是条目结构不全的时候。字段取完不要急着存先把解析结果打印一遍。打印不仅确认每个字段取到了想取的内容也顺便检查有没有错位、漏取、混入空白字符。解析是最依赖手感的环节打印确认这一步看似多此一举实际能把后面「数据入库后才发现某字段全空」得返工成本压到最低。3.3 XPath 取不到内容时的排查路径把页面落盘再验证XPath 写好后取不到数据最常见的反应是「改一改路径碰碰运气」这是低效的。我的排查顺序往往是把页面保存到本地用编辑器打开再结合实际页面结构检查路径。这样做的意义在于你不用每次验证都重新请求目标站点——页面结构有缓存排查不发请求既避免加大目标站压力也避免因为请求频率变化引发额外的拦截。# 把 HTML 保存到本地便于离线反复查看 with open(page_debug.html, w, encodingutf-8) as f: f.write(response.text) # 也可以在程序里直接打印某段HTML检查目标字段是否真的在响应里 keyword 目标标题关键词 start response.text.find(keyword) print(response.text[start - 200:start 200])如果保存的 HTML 里根本没有这个关键词问题就不在 XPath 写法而在数据源页面是 JS 渲染的或者请求被拦截了。这时候再去考虑找接口或者调整请求头而不是对着路径死磕。另一个容易忽略的点是 HTML 里有命名空间或者标签被注释掉页面在浏览器里显示正常但注释块里的内容在 DOM 里是不存在的XPath 按正常节点处理自然拿不到。还有一种情况是页面结构里存在多层嵌套的相同 class 名。XPath 的//div[contains(class, list-item)]会匹配所有层级如果页面上既有容器又有子元素都用了这个 classitem_nodes 会拿到很多噪音节点。此时要往上找更外层的包裹元素或者用//ul[contains(class, list)]/li这种更贴近语义的路径。判断标准很简单先看 item_nodes 的数量对不对数量都不对别急着写字段路径。4. 把「能抓一次」变成「能反复抓」翻页、去重与落盘4.1 翻页循环的正确写法URL生成与页面解析解耦翻页是所有列表型采集都要面对的问题也是稍不注意就出乱子的地方。第一页的 URL 和第二页往往不在同一个 format 里——有些站点的第二页 URL 是page2有些是?page2.html还有的翻页靠 POST 提交。写翻页循环前先到浏览器里手动点两页把 URL 的变化规律搞清楚再决定是字符串拼接还是用 params 传参。import requests import time base_url https://example.com/news params_list [ {page: 1, size: 20}, {page: 2, size: 20}, {page: 3, size: 20}, ] failed_pages [] for params in params_list: try: resp requests.get(base_url, paramsparams, headersheaders, timeout(10, 20)) resp.raise_for_status() # 解析和落盘逻辑在这里执行建议单独拆成函数 parse_page(resp) except Exception as e: # 单页失败不影响整个采集流程先记录后面统一重试 failed_pages.append(params[page]) print(f第 {params[page]} 页失败: {e}) continue # 每两页之间停顿几秒避免连续高频请求 time.sleep(random.uniform(1, 3)) print(失败页码:, failed_pages)翻页循环里我习惯把「生成 URL」和「解析页面」拆成两个独立部分。生成 URL 集中在循环开头解析逻辑抽成单独函数这样页面一旦改版只需要改解析函数而不动请求逻辑。另一个细节是用 params 传参而不是手动拼 URL 字符串requests 会帮你处理参数编码遇到、之类特殊符号不会打乱地址结构。翻页还有一个隐藏陷阱页码区间不一定线性。有些列表页超过一定页数后不按 page 递增而是转向一个「下一页」链接有些站点的翻页路径是相对路径丢一个链接后面全断。我一般会让程序优先探测下一页按钮对应的 URL 是否存在存在就循环跟下去不存在或已到最后一页再退出。写死页码范围虽然简单但页面数量变动一次你的采集就得返工一次。4.2 去重与落盘给批量采集装两道保险批量采集跑起来之后最怕的不是抓不到而是同一批数据被反复写入。去重的最低成本方案是维护一个已采集 URL 的集合每拿到一条记录前先检查 URL 是否已在集合里。集合只存在于内存时程序一重启就丢配合一个追加写入的文件才能实现跨会话去重。落盘我建议用 JSON 追加写入每条记录一行既是 JSON 又是文本后续导入数据库或者做清洗都很方便。import json seen_urls set() output_file data.jsonl # 从已有文件里恢复已采集的URL实现「续跑不重采」 try: with open(output_file, r, encodingutf-8) as f: for line in f: record json.loads(line) seen_urls.add(record[url]) except FileNotFoundError: pass new_records [] # 假设 item 是刚从页面上解析出来的一条字典 for item in items: if item[url] in seen_urls: continue # 已采集过跳过 seen_urls.add(item[url]) new_records.append(item) # 追加写入配合 flush 让数据及时落到磁盘 with open(output_file, a, encodingutf-8) as f: for record in new_records: f.write(json.dumps(record, ensure_asciiFalse) \n) f.flush()这里的 seen_urls 从旧文件里恢复是断点续抓的简化版。程序中断后下次启动会把已经写进文件里的记录重新加载不会重复采集已经拿到的 URL。ensure_asciiFalse 保证中文按明文写入而不是转成\uXXXX文件可读性大幅提升。flush 每次写入都调用是为了降低「程序中途崩溃导致缓冲区数据丢失」的概率——频繁 flush 有少量磁盘 IO 开销但对采集场景来说数据的可靠性远比这点性能损耗重要。我看到不少人把采集到的数据攒在内存里到最后一次性写文件测试时没问题一旦采集了上万条后程序崩了前面积累的数据全部白干。分页落盘的价值就在这种时刻体现每一页处理完都有文件产出最坏情况也只是重跑最后几页。这也是「高效」背后真正被低估的部分——高效不只是抓得快更是失败之后恢复得快。4.3 限速与采集规范从第一行循环代码就开始设置频率标题里把「规范」放在重要位置落到工程上最直接的体现就是限速。很多采集者的第一反应是「能不能跑得更快」而实际被反爬限制的往往不是速度是请求节奏。连续的、均匀的、没有间隔的请求序列对任何站点来说都是明显的非人类行为。同样 1000 个请求分散在 30 分钟里和集中在 3 分钟内发出去被限制的概率完全不同。import random import time # 在每次请求前随机休眠而不是固定 sleep(2) time.sleep(random.uniform(1.5, 3.5))为什么要随机而不是固定固定间隔会被限流系统识别为程序行为——请求间距恒定的序列在统计上非常显眼。随机异步区间让请求节奏更接近真人同时也能平滑掉网络抖动的影响。至于间隔取多少我的建议是从保守值开始每页请求间隔不少于 1 秒单域并发不超过一两个。先跑通流程再逐步调整而不是一上来就把间隔设成 0.1 秒去试探对方底线。另一个规范是把请求量控制在「对目标站点无压力」的范围内。你可以把自己定位成站点的普通访客一天只抓几个小时、每分钟不超过几十个请求、避开业务高峰时段。这样既能长期平稳地采集也给自己留了退路——站点运营方看到的是合理访问流量而非威胁性爬取。限速做不好后面无论解析写得多好都会因为被限制而前功尽弃。规范不是空话它是保证采集任务可持续运行的前提条件。5. 反爬应对与规范踩过的坑、排查顺序和不要碰的边界5.1 只带UA仍被识别请求头特征不完整的现象与修复现象代码里已经加了 UArequests 也能正常返回内容但连续请求几十条之后开始出现 403或者在页面里能看到「安全检查」的字样。原因UA 只是请求头的一小部分真实浏览器每次请求还会带上 Accept、Accept-Language、Accept-Encoding、Connection 等一系列头部。如果把 UA 设成浏览器却缺少其他配套头这种「半套伪装」在服务端看来反而比默认 UA 更可疑。解决不要只填 UA而要把浏览器请求头整体复制过来最省事的做法是直接在开发者工具的「网络」面板里找到那个 document 请求右键复制为 cURL然后转成 Python 字典。headers { User-Agent: Mozilla/5.0 ... Chrome/124.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1, }需要说明的是Accept-Encoding 如果写了 gziprequests 自己也必须能处理压缩响应——它默认会自动解压所以通常没问题。但如果你手动设置了某些头又写错值反而会暴露程序身份。另外一个隐蔽点是请求头顺序在多数简单反爬系统里不会被检查但在特征分析模型里会有影响——不同库发送时头的排列顺序不同请求头顺序和浏览器不一致也容易被识别。所以尽量从一个完整的浏览器头复制过来不要只挑几个字段自己拼。5.2 加了延时还是被限流时间窗口和并发尖峰的问题现象明明每两次请求之间都 sleep 了两三秒跑一会儿还是收到「请求过于频繁」的提示。原因限流系统往往不是按「相邻请求间隔」来判定的而是按时间窗口内的总请求数。比如 1 分钟内超过 60 次就触发限制如果你的程序前 10 秒内密集发出 50 个请求后面 50 秒虽然在 sleep但窗口内的总数已经超了。解决除了请求间隔随机化之外还要控制每分钟的总请求数上限并把请求分布到整个时间窗口里。import time import random request_times [] def rate_limited_request(max_per_minute30): 按每分钟总请求数进行限速控制 now time.time() # 把超过1分钟的旧记录清掉 request_times[:] [t for t in request_times if now - t 60] if len(request_times) max_per_minute: # 距离最早那次请求满1分钟还差多少秒就等多少秒 wait_time 60 - (now - request_times[0]) print(窗口限速等待:, round(wait_time, 1), 秒) time.sleep(wait_time random.uniform(1, 2)) request_times.append(time.time())这个函数的思路是记录每次请求的时间戳在发起新请求前检查 1 分钟窗口内的请求数是否达到上限。达到就等待直到窗口里最早那次记录滑出时间范围。它比固定 sleep 更严格因为它不关心你上次请求是多久之前只关心这一分钟里发了多少。实际使用时把 max_per_minute 设为 20~30 即可这是一个对绝大多数目标站点都友好的频率。还有一个被我踩过多次的细节请求时间戳记录的应该是「发起请求前」而不是「等到响应后」。如果响应很慢服务端看到的实际请求间隔和你的时间记录会错位窗口统计也就不准。我这个函数把时间戳追加在检查之后、请求发出之前就是为了对齐服务端的计数口径。5.3 浏览器能看到、代码抓不到JS渲染页面的处理顺序现象浏览器里打开目标页面数据整整齐齐列在那里requests 去抓却拿到一堆 script 标签正文区域是空的。原因页面数据由前端 JS 发起接口请求后动态填充初始 HTML 里压根没有内容。解决先去开发者工具里找真正返回数据的 XHR 接口绝大多数站点的数据接口是 JSON 格式直接请求它比渲染页面高效得多。找到接口的注意点是接口地址可能有 token、时间戳等动态参数要单独处理。如果找不到接口或者接口参数被加密得很严才轮到无头浏览器方案。这时候可以用 Playwright 或 Selenium 启动一个真实浏览器内核去加载页面等它执行完 JS 再取内容。但这是有代价的方案内存占用高、运行速度慢、头容易被识别。无头浏览器必须隐藏自动化的痕迹这本身就是一个持续攻防的过程。我的习惯判断顺序是「优先找接口、次之找静态资源里的 JSON、最后才考虑浏览器渲染」——接口方案是最省力的落点性能高出好几个量级。如果你发现目标站点加载的数据来自接口但接口校验了 Referer 和 Origin可以尝试在请求该接口时也带上这两个头。之前请求页面用的 headers 不能直接套到接口上接口请求是独立的会话要在代码里单独维护。还有一点某些接口返回的数据经过了编码混淆需要先解码才能用但这已经进入对抗范畴量力而行。5.4 验证码和登录墙超出采集边界尽早绕开现象连续采集几十条后页面突然返回一个带着验证码的页面或者某些数据必须登录后才能看到未登录状态直接跳转到登录页。原因站点对异常请求做出了升级响应——验证码用于区分脚本和真人登录墙用于保护需要特定身份才能访问的内容。解决验证码出现时基本说明你的请求频率或特征已经触发了警戒线正确的做法不是去处理验证码而是立刻停手调低频率、检查请求头是否完整。登录墙同理——需要登录态才能访问的页面意味着这部分数据并非完全公开采集它也可能与站点条款冲突。这类场景我见过太多人花大量精力去做验证码识别、自动登录、保持会话最后换来的是更大范围的封禁和站点加固。站在成本角度算一笔账如果目标数据在别处也有公开副本或者有官方提供的数据接口绕开需要登录的部分比纠结怎么爬登录后的内容划算得多。我通常会把「登录后才能看到的数据」直接列入不碰清单这不是能力问题是边界意识。尤其要提醒的是登录采集涉及用户身份的使用往往会触碰站点服务条款中关于账户使用的约定。很多站点的条款明确禁止使用自动化程序登录。这类行为可能带来的风险完全配不上它带来的数据收益。做采集前先给自己画一条线公开数据可以碰需要特殊身份的坚决不碰。5.5 采集规范清单公开、低频、不绕认证、不抓隐私、留余地这里把我个人会遵守的几条规范列得具体一点它们可以作为你开展采集前的默认底线。第一只采集公开数据即不需要登录、不需要特殊权限就能访问的内容第二限制请求速率到目标站点可承受的范围内不让你的采集对站点的正常运行产生影响第三不绕过身份认证、不破解访问控制遇到验证码或登录墙就停下第四避开个人隐私字段比如姓名、手机号、住址、证件号这些内容即便在公开页面上出现也不要纳入你的存储范围采集和存储是两回事存储会把风险放大第五留余地指的是设置自定义 UA 和来源说明让站点运营方在有疑问时能找到你也让自己在沟通时有据可依。robots 协议也应该纳入流程参考。它不是强制法律但代表了站点所有者对爬取行为的态度。在发起批量采集前用标准库读取一下目标站点的 robots.txt如果明确禁止了某些路径就绕开这些路径。注意 robots 只约束合规的爬虫如果你觉得自己「反正不会被发现」那这条规范对你就是空谈——但后面真出了问题空谈就是教训。from urllib.robotparser import RobotFileParser rp RobotFileParser() rp.set_url(https://example.com/robots.txt) rp.read() can_fetch rp.can_fetch(*, https://example.com/list) print(对通用爬虫是否允许访问:, can_fetch)有人会争论 robots 不是法律采了也没事。我的态度是不争论这个把它当作项目风险控制的一部分允许就做不允许就换方案。合规的采集方案会更容易被接纳而打擦边球的采集方式稳定性永远是最大的敌人——站点一次改版或一次屏蔽你的整个采集管道就废了这才是做技术的人真的要避免的事。6. 给采集程序装上重试、持久化和日志让重复执行不再靠运气批量采集真正考验人的不是首次跑通的兴奋而是第二次、第三次执行时的可靠性。没有重试机制的话网络抖动、站点超时、偶发的 503 都会让任务在半夜中断第二天起来发现只跑了一半。我习惯给请求环节做多层重试连接超时重试、读取超时重试、状态码异常重试每次重试之间按指数退避加随机抖动而不是固定间隔。import time import random from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry session requests.Session() # 最多重试3次对连接错误和5xx状态码生效 retry Retry( total3, connect3, read3, backoff_factor0.5, # 退避基数首次重试等待0.5秒之后翻倍 status_forcelist[500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) resp session.get(url, headersheaders, timeout(10, 20))Retry 里的 total 表示所有类型的重试次数总和connect 和 read 分别限制连接失败和读取失败的重试次数backoff_factor 控制退避时间0.5 表示第一次重试前等 0.5 秒第二次等 1 秒第三次等 2 秒。status_forcelist 指定哪些状态码值得重试5xx 是服务端临时问题重试有意义4xx 通常是请求本身有问题重试只会浪费资源所以不放进列表。配合持久化的是进度记录。中断恢复的机制我做得比较轻每完成一页就在日志文件里追加一行完成第N页下次启动时先读日志跳过已经完成的页。这样比把所有状态放进内存可靠得多——程序崩掉时日志里记录的进度就是最后的真相。日志最好同时输出到控制台和文件控制台让你盯实时状态文件让事后排查有据可查。我吃过不写日志的亏某个采集任务跑了大半夜进程被系统杀掉重启后完全不知道已经处理到哪个位置只能从第一页重新开始。那次之后凡是超过几十页的任务我都坚持写进度日志和原始响应缓存。用两三个小时的开发换几十个小时的重复执行时间不吃亏。这套组合下来采集程序就不再是一个「跑一次就废」的一次性脚本而是一个可以重复使用、出了问题可控恢复的工具这大概就是标题里「高效」两个字最实在的落脚点。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站