
做数据采集这几年最让我神经紧张的就是半夜手机突然震动——监控面板上爬虫稳定性指标从98%一路跌到40%。这种事故我经历过太多次了后来总结出一个教训真正的稳定性不是靠侥幸而是靠分层兜底从提取式API到隧道代理每一层都要能扛事。这篇内容适合正在被目标站点反爬、代理失效、任务凌晨挂掉折磨的采集开发者我把这几年沉淀下来的5个核心手段一次性讲清楚希望能帮少走点弯路。我跟很多人聊过稳定性问题发现大家的第一反应是“换更贵的代理”。其实代理只是其中一环页面解析、请求节奏、会话保持、监控预警哪个环节断了都白搭。这篇文章按我实际踩坑的顺序来写先说提取式API为什么值得用再讲请求调度和会话一致性然后切入隧道代理的选择与配置最后用监控指标把整套东西串起来。1. 提取式API不是偷懒是把最不稳定的环节交出去1.1 为什么提取式API自带“稳定性光环”先说结论纯自建爬虫稳定性大头在“页面抓不抓得到”“数据拿不拿得对”这两件事恰恰是最需要持续投入人力的。提取式API要解决的就是这个。它的工作方式很简单你传一个URL或查询条件API服务商负责真正的抓取、渲染、解析最后返回结构化的JSON。你可能觉得“这不就是套壳吗”但实际操作中差别巨大。比如目标页面是典型的SPA数据靠JavaScript异步加载。自建方案里你得先搞清楚XHR请求、模拟浏览器渲染还要处理验证码弹出和WebDriver识别。用提取式API的话这些都被服务商打包处理了你的代码只需要关心“这个字段叫什么名字”。稳定性来自哪里来自服务商替你养了一个庞大的IP池和渲染集群页面改版、验证码升级这些事他们比单一团队反应快得多。我自己的经验是爬虫事故里不少不是网络问题而是页面结构变更导致解析规则失效。自建解析的话目标站点前端团队改个class名你的生产任务可能就崩了。而提取式API的数据字段是服务商维护好的页面怎么改返回的JSON结构大概率不变。这一层外包本质上是把“跟对端页面版本赛跑”的负担甩出去。1.2 选型核心参数成功率、P95响应与Schema变更用提取式API不是花钱就行选型时我建议死磕四个参数。参数关注原因我的建议成功率服务商承诺的请求成功比例要求至少99.5%低于这个数不值得考虑P95响应时间高峰期的真实体验平均值容易被美化控制在3秒以内不然上游环节全被拖住Schema稳定性返回字段是否会频繁改名、增删层级确认服务商有版本化接口和变更公告机制单位成本按成功请求计费还是所有请求计费优先选“按成功请求计费”失败单也要钱的套路要警惕这几个参数里我最看重Schema稳定性。有的服务商为了适配业务会很随意地给字段加一层嵌套或者把字符串改成对象。写个兼容逻辑也不难难的是信息差——你都不知道哪一天变的等发现时数据已经落库错了。所以选服务商时我会专门看它的文档和更新日志如果三个月内改过不止一次字段哪怕便宜也会一票否决。1.3 我踩过的提取式API三个坑第一是偶发null。服务商返回200HTTP层面完全正常但某个业务字段是null。这种数据直接写库会把下游统计搞崩。处理办法是增加一层字段非空校验不满足条件的记录放到待重试队列。第二是配额耗尽后从报错变成返回值异常。有的服务商在月配额不足后会降级到低质量数据源返回的结果看起来对但缺失率和延迟都明显上升。这个坑很隐蔽建议自己在监控里加上“单日成功请求数”指标而不是完全依赖服务商后台。第三是免费Key的波动性。之前接手过一个老项目用的免费档提取式API白天跑得好好的晚上高峰时延飙到十几秒成功率掉到30%。后来排查半天发现免费档在忙时会被服务商主动限流。生产环境还是得用付费档这不是钱的问题是服务等级协议的问题。说到合规用提取式API也好自建爬虫也好采集对象必须是你有权访问的公开数据别去碰登录后私有内容也别把目标站点拖到不可服务。稳定性是技术问题不该用对方站点的“灾难”来换。2. 请求频控与指数退避稳定性的调度层基本功2.1 把错误分类才是重试的前提很多人写爬虫时对重试的理解就是“失败了循环再试一次”。这种粗暴做法的后果要么是重试了一百遍还是一个样白白耗流量要么是把目标服务器打到报错最后自己的IP被彻底拉黑。我处理重试的第一件事是给错误分类。HTTP状态码不是所有非2xx都该重试的比如404说明资源不存在重试一万次也没意义403一般是权限或风控问题直接重试往往还加重嫌疑429就是被限流了必须等只有5xx和网络层异常连接超时、DNS解析失败才值得立刻重试。状态码/错误类型含义是否直接重试404页面或接口不存在不重试通知人工检查URL403请求被拒绝谨慎先检查身份信息和请求头429触发限流重试但要指数退避且大幅增大间隔5xx服务器错误或维护中重试配合退避算法超时/连接重置网络链路不稳定重试限制次数必要时切换代理2.2 指数退避加抖动用公式和代码说话指数退避的核心思路是每次失败后的等待时间不应该是固定的3秒而是按失败次数指数放大。这不是玄学是大家都在用的标准做法——交握失败或者限流后服务器需要时间恢复连续高频打过去只会越打越死。加抖动的原因也很简单。假设你有10个线程同时失败都按同一个公式算出延时1.5秒那10个线程会在同一时刻重试产生新的尖峰流量。加入随机抖动可以打散这个时间点。下面是我常用的实现用requests和手写退避逻辑import random import time import requests def fetch_with_retry(url, paramsNone, headersNone, max_retries5): for attempt in range(max_retries): try: resp requests.get( url, paramsparams, headersheaders, timeout(3.05, 15) # 连接超时3秒读取超时15秒 ) if resp.status_code in (429,) or resp.status_code 500: raise RuntimeError(fretryable status {resp.status_code}) return resp except Exception as exc: if attempt max_retries - 1: raise # 1.5^attempt 指数放大再加0~2秒的随机抖动 delay min(30.0, 1.5 ** attempt) random.uniform(0, 2.0) time.sleep(delay)如果你用Python里的tenacity库代码会短一些from tenacity import retry, stop_after_attempt, wait_random_exponential retry( waitwait_random_exponential(multiplier1.0, max30.0), stopstop_after_attempt(5) ) def fetch(url): resp requests.get(url, timeout10) if resp.status_code 429: raise RuntimeError(rate limited) return resp关键点是max30.0这个封顶值很重要。一个被降级的服务可能一分钟内恢复也可能需要十分钟。如果退避时间无限制增长任务会拖到天荒地老。封顶30秒是我实际调试中比较合适的值万一超时确实不恢复你该做的是触发告警而不是无限等待。2.3 减少请求量才是终极的稳定性策略说完重试我想补充一个经常被忽略的维度少发请求比把重试玩出花更重要。如果同一份数据你天天全量跑一遍那不仅浪费自己带宽也白白增加被限制的概率。落地层我主要做三件事。一是URL去重。对已经成功解析过的资源URL做布隆过滤器判断重复的请求直接跳过。这个在增量采集场景收益非常明显我之前维护的某电商价格监控去重后请求量直接降了70%。二是缓存密集型小查询。有些页面整体抓下来很贵但实际只需要其中两个数字。可以在数据库里维护一张已采集缓存表命中缓存就不再发起新请求。设置一个合理的过期时间比如小时级数据就设15分钟TTL既保证新鲜度又不浪费。三是把冷热任务分开调度。像价格、库存这类实时性强的数据用小线程池、高优先级队列去跑像历史详情这类低优先级数据压到凌晨低峰期慢慢跑。这么一拆高峰期对目标站点的压力小成功率自然高。一次任务挂掉的数据量也被限制在可控范围。3. 会话与客户端一致性别让自己看着像个“机器人”3.1 Session保持是底线爬虫稳定性不只是IP问题还牵涉到身份连续性。如果你抓的是一个需要登录或带Cookie的站点每次请求都新建客户端、重新登录不仅慢还会因为频繁更换会话被判定为异常。我见过不少新手踩这个坑用requests.get直接裸发请求每次都是新的连接。正确做法是用带会话管理的客户端比如requests.Session或httpx.Client。Session内部会维护Cookie和连接池遇到Set-Cookie可以自动带上TCP连接也能复用。import requests session requests.Session() session.headers.update( { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } ) resp session.get(https://target.example.com/list, timeout15)这个例子很简单但意义很大同一个Session发送的所有请求在服务端看来都是从同一个客户端会话过来的。对依赖会话状态的站点成功率会有明显提升。3.2 客户端指纹和Header顺序的“一致性思维”更深入一层目标服务器除了看IP和Cookie还会收集客户端指纹——TLS加密握手方式、HTTP头顺序、支持的压缩算法、字体渲染信息等等。一个HTTP请求如果其他特征和主流浏览器差太远就容易被风控模型识别出来。这里我先说个原则我们的目标不是“伪装成不是机器人的东西”而是尽量让自己采集端的网络特征和主流浏览器保持一致的“低差异”降低被误伤的概率。很多站点封IP根本不是因为你爬得多而是因为请求特征太“非主流”。拿Header举例用Python字典传入Header时requests的strategy会按字母序排列而浏览器发送的顺序是固定的。某些WAF严格时会对比Sec-Fetch-*系列头、Accept-Language这些字段的配合度。我在碰到这类站点时会保留一组预设Header并手动固定顺序。更极端的场景比如需要应对TLS指纹差异时可以用curl_cffi这类库来处理from curl_cffi import requests as ccurl resp ccurl.get( https://target.example.com/data, impersonatechrome, # 保持和Chrome一致的TLS指纹 timeout15, )这类工具不是用来破解鉴权的只是让合法采集的请求不那么扎眼。如果目标站点本身是你有权限访问的公开数据这样做没有法律和道德问题如果涉及非公开数据建议先拿到授权再说。3.3 行为层面的随机延迟与页面轨迹最后一个容易被忽略的层面是行为节奏。哪怕是正常API接口如果你每秒固定发5个请求、间隔精确到100毫秒也会被现有风控系统盯上。人类的访问节奏是弹性的不是定时的。我的做法是大循环里加随机延迟import random import time delay random.uniform(1.5, 4.5) time.sleep(delay)延迟区间会在每次请求前随机取样长期看每分钟请求量会稳定在一个区间内不会出现一眼假的固定周期。如果是用Playwright或Selenium驱动浏览器做前端渲染页面的采集我还会模拟鼠标滚动和停留时间而不是打开页面后0.5秒就点下一步。这些行为级特征对稳定性有正向帮助尤其当你已经通过隧道代理换了一个家庭或移动IP时配套的人性化节奏能让这个IP活得更久。4. 隧道代理动态IP池如何把封禁风险摊薄4.1 静态代理的痛一个IP坏掉全盘皆输很多人在自建爬虫的初期会买一些静态住宅代理或数据中心代理每个请求绑定固定出口IP。静态代理的好处是稳定同样一个IP可以配合登录态保持但痛点也很明显只要这个IP被目标站点标记整个任务线就废了。我试过自己维护一个代理池流程是这样的批量买入IP清单写一个脚本来测连通性和是否被限制然后给任务分配可用IP挂掉再从池里替换。听起来可行实际上你会被三件事折磨死。第一代理的时效性比想象中差很多便宜IP存活时间不到一天第二被限制不是全或无的有时候IP还能用但目标站点会返回验证码或者故意拖慢响应这种半死状态光靠发心跳包测不出来第三维护代理池本身也要消耗请求量和机器资源。于是后来我把精力转向了隧道代理这条路对稳定性改善是质的飞跃。4.2 隧道代理的原理与使用姿态隧道代理在爬虫圈的技术定义很清晰服务商维护一个庞大的动态IP池你通过一个相对固定的域名端口发起请求服务网关在每次会话或每个请求时自动从池中选取可用出口IP完成转发。你不需要关心具体用了哪个IP也不需要写代码处理IP失效。从调用方的角度看它和用普通代理很像import requests proxy { http: http://user-wh-the-xxx:passwordproxy.tunnel.example.com:8000, https: http://user-wh-the-xxx:passwordproxy.tunnel.example.com:8000, } resp requests.get( https://target.example.com/page, proxiesproxy, timeout15, )隧道代理的价值在于目标站点看到的是不断变化的出口IP。单IP被封的风险被摊薄到整个代理池中即使某个出口IP被识别网关会在下个请求自动切换不需要你感知。选择隧道服务商时我主要看三个参数。第一是会话保持时长有的网关支持在10秒到30分钟内保持同一个出口IP这在需要长连接或登录态时很有用第二是地区覆盖目标站点在国内就选国内节点目标站点在海外必须选对应区域延迟高会拖垮整个采集链路第三是并发通道数这个决定了你能同时跑多少线程而不触发限流建议按业务高峰预估量的1.5倍来买。4.3 隧道代理的降级与提取式API的组合隧道代理不是银弹我也遇到过隧道网关自身抖动的情况。它的表现通常是503或连接超时短则几秒长则三五分钟。所以使用隧道代理后稳定性逻辑变成两层外层是任务调度和重试内层是代理网关的异常补偿。我的降级策略是给请求做两次重试第一次重试仍然走隧道代理如果第二次还是失败就切换到备用通道——要么换另一个隧道代理套餐要么临时切到提取式API兜底。这套组合在实战里非常管用当某个隧道节点质量下降时任务不会整体卡死而是自动滑到备选通道。def fetch_with_failover(url, primary_proxy, fallback_api_call): try: resp requests.get(url, proxiesprimary_proxy, timeout15) if resp.status_code not in (503, 502, 504): return resp except Exception: pass return fallback_api_call(url)用隧道代理时务必要在代码里区分“目标站点返回错误”和“代理本身返回错误”。如果目标站点返回429你换代理之后可能直接就好了如果隧道网关返回503换目标站没意义反而可能加重网关压力。我习惯在日志的error_type字段里明确标注proxy_error还是target_error这能让排障效率高不少。5. 稳定性度量与告警把“感觉还行”变成数据5.1 三个比“成功率”更值得盯的指标很多人理解的稳定性就是“请求100次成功99次”。但实际采集系统里HTTP 200不代表成功因为目标站点可能返回一个200页面里面却是验证码或空模板。所以我监控系统里同时维护三个核心指标。请求成功率HTTP层面2xx响应占全部请求的比例。低于95%就要警觉低于90%基本是代理或目标站点出大事了。解析成功率所有成功响应中能按预期模板提取到核心字段的比例。这个指标比请求成功率更低因为页面改版、验证码弹窗、接口返回结构变化都会先体现在这里。任务时效性一批次采集任务是否在指定时间窗口内完成比如要求凌晨2点前跑完全量数据。最早我把解析成功率当成事后数据去看等发现大量字段丢失已经污染了两天的数据。现在我会把解析失败的消息直接落一张error_table带完整请求信息和错误码方便回溯。5.2 结构化日志与全链路追踪爬虫系统排查问题难难在链路长调度器、代理网关、目标服务器、解析器、消息队列、数据库任何一环都可能出问题。没有统一日志规范你会连“这条响应到底用没用代理”都说不清。我现在的做法是每个任务生成一个trace_id从任务开始一直到数据落库所有日志里都带上这个字段。而且不是日志文本拼接是JSON结构化输出。{ trace_id: 20240123-10001, task: product_price_check, url: https://target.example.com/product/123, status_code: 200, proxy: tunnel-zone-gz, elapsed_ms: 1250, error_type: , parse_ok: true }这种日志用ELK或简单点用日志文件加grep都能快速过滤。比如我看到解析成功率骤降时只需要grepparse_ok:false并按error_type分组几秒就能看出是所有页面都解析失败还是某个特定URL的字段规则失效。5.3 告警分级与自愈动作稳定性系统没有告警等于没搭。但我对告警的要求是不要只会“钉钉群里艾特人”要有配套的自愈动作。我按严重程度分了三级。P0请求成功率跌到50%以下持续3分钟这时候基本是代理全废或目标站点大规模拦截自愈动作是立刻停止所有任务、切换到备用通道。P1解析成功率跌破80%自愈动作是重新尝试用备选解析模板如果还不成就通知人工。P2某个来源的成功率波动超过10%触发重跑或增加延迟不需要人为干预。阈值不是一次定死的。刚开始可以激进一点比如成功率低于98%就告警跑两周看噪音情况再放宽。告警过多会导致“狼来了”大家选择无视告警过少又等于裸奔。我最终形成了这样的监控循环每分钟统计三个核心指标对比上一分钟数据触发阈值后按级别走自愈策略所有动作再写回日志和指标系统。这套循环跑稳后半夜被打醒的次数从每周四五次降到了每月一两次。最后说点实际的我现在接任何一个采集项目第一周不会写业务代码而是把稳定性架构先画出来。哪些页面交给提取式API哪些走自建加隧道代理重试策略怎么配置告警阈值怎么设。把这些搞清楚了后续就是填业务逻辑。这是拿无数个凌晨的告警电话换来的习惯。如果你的采集任务还在“跑挂了再手动重启”的阶段强烈建议先把第2部分和第5部分补上这两块的投入产出比是最高的。