资讯详情

Python爬虫403错误的终极解决方案

📅 2026/10/15 9:22:42 | 华诺云谱 👁 阅读
Python爬虫403错误的终极解决方案
前言这个标题本身有问题得先改掉。403 没有「终极解决方案」因为它不是一个待解决的技术故障而是对方一个明确的答复。HTTP 状态码 403 的官方定义是服务器理解了你的请求但拒绝处理它它和 401 的区别在于——对 403 来说你重新做身份认证也没有任何区别因为这个拒绝是绑在应用逻辑上的比如权限不足、资源不允许访问。换句话说403 是站点在说「我知道你要什么我不给」。因此把 403 当成「需要被攻克的技术障碍」然后去找「换 IP 轮换 User-Agent 伪造 Referer 继续请求」这套组合拳方向从一开始就错了。那套做法不是在解决 403而是在规避对方明确表达的技术措施它不仅可能违反站点的服务条款也把一个待授权的问题伪装成了技术问题。本文不会给出任何这类代码。真正算得上「解法」的只有一条线四步顺序不能反先自查是否合规robots、频率、User-Agent 是否按对方要求标识→ 降速重试针对 429 这类「慢一点」的信号→ 改用官方 API 或授权数据源 → 联系站点申请许可。本文还会讲清 401 / 403 / 429 / 503 四种状态码各自在说什么因为把这四个混为一谈是「越修越糟」的主要来源。示例用标准库urllib写本机没有 Python 解释器代码未在本机运行验证仅作逐行人工推演涉及第三方库的地方会注明需先安装。一、四种状态码到底在说什么很多人把它们统称为「被封了」但它们的含义和正确反应完全不同状态码官方语义是「重新认证」能解决的吗正确的第一反应401 Unauthorized请求缺少有效的身份认证凭据是需要提供正确凭据补上认证头检查凭据是否过期403 Forbidden服务器理解请求但拒绝处理不是重新认证也没有区别停止请求该资源检查自己是否越权走官方渠道429 Too Many Requests一定时间内请求过多即「限流」不是慢下来并尊重响应里的Retry-After503 Service Unavailable服务暂时不可用过载或维护不是退避等待不要加重对方负担几个关键点展开说403 与 401 的区别是官方明确强调的对 403 而言「认证或重新认证都无济于事」。所以一个常见的误判是——看到 403 就去换凭据、加 Token白费功夫因为它根本不是身份问题。429 是站点在明说「慢一点」。它的定义是客户端在给定时间内发送了过多请求这种「要求客户端降低请求速率」的机制通常被称为限流rate limiting。429 响应里可以带一个Retry-After头用来告诉客户端应该等多久再发下一次请求而且限流的维度可能是整个站点级的也可能是按资源粒度的通常会基于客户端 IP在带认证或带 Cookie 的请求里则可能按用户或应用来限。所以 429 的正确反应是读Retry-After并按它等待而不是换个 IP 去绕开——换 IP 恰好是「规避基于 IP 的限流」属于本文明确不写的那一类做法。503 多半不是你的错是对方过载或维护。这时正确做法是退避指数退避越等越久而不是加大并发。在对方已经过载时继续加压是最典型的「把目标站打挂」的思路本文不接受这种方案。把这四个区分开之后你会发现真正需要「修复」的只有 401补认证429 和 503 需要的是「退让」而 403 需要的是「停止并走正式渠道」。没有一个是靠伪装请求头能解决的。二、先自查你是不是本来就不该这么请求收到 403 或 429 之后第一件事不是改代码是检查自己这边。按顺序过一遍读对方的 robots.txt。标准库自带解析器用urllib.robotparser.RobotFileParser就能判断某个路径是否允许抓取也能读Crawl-delay和Request-rate# 适用于 Python 3.8import urllib.parseimport urllib.robotparserdef site_url(path):# 换成你实际要访问的站点这里用 RFC 2606 保留的示例域名做占位return urllib.parse.urlunsplit((https, example.invalid, path, , ))rp urllib.robotparser.RobotFileParser()rp.set_url(site_url(/robots.txt))rp.read() # 拉取并解析print(rp.can_fetch(*, site_url(/some/page)))print(rp.crawl_delay(*)) # 没有该参数时返回 None注意crawl_delay(useragent)在「没有该参数」或「参数语法非法」时返回None所以调用方要处理None的情况不能直接拿去做算术。request_rate(useragent)返回一个RequestRate(requests, seconds)命名的元组同样是查不到就返回None。看对方是否要求标识 User-Agent。很多站点在 robots.txt 或页面的服务条款里写明请用能识别你的名字的User-Agent并留下联系方式。按要求把 UA 设成能认出你的标识而不是伪装成浏览器是合规的一部分。这一点和「轮换 UA 池」是完全相反的两件事——前者是自报家门后者是隐藏身份。看频率。你的请求是不是太密了有没有并发跑几十个线程打同一个站点先降下来再说。看你是不是在请求需要授权的内容。如果你在抓的是需要登录、需要订阅、或者明确标注了版权限制的内容那 403 只是结果根因是「你本来就没有这个权限」。这一步做完你往往已经知道 403 是怎么来的了。三、正确的处理顺序只有一条线按顺序执行前面能解决就不要跳到后面第一步降速。如果你的问题是频率类429 通常对应这个把并发降到 1、给每次请求之间加固定间隔、把间隔按Crawl-delay或Request-rate的指示来设。遇到 429 时读Retry-After它可能是秒数也可能是一个 HTTP 日期两种情况都要能解析。第二步改用官方 API 或授权数据源。如果站点提供 API就走 API如果数据来自有授权的数据提供方就用对方给的正规接口。用 API 往往比抓页面更稳定、更省事而且是对方允许的方式。第三步联系站点申请许可。如果你的用途正当学术研究、你自己的数据、公开数据集的补充直接找到站点公布的联络方式说明来意很多站点会给出白名单、测试配额或明确答复。这一步的性价比经常被低估一封邮件省下的可能是几周的对抗式调试。下面这段代码演示遇到 403 时正确的动作是停止遇到 429 时按Retry-After等待其余情况退避重试且全程不做任何伪装# 适用于 Python 3.8import timeimport urllib.errorimport urllib.requestUA MyCrawler/0.1 (联系方式见站点服务条款) # 能识别出你的标识def fetch_once(url, timeout10.0):req urllib.request.Request(url, headers{User-Agent: UA})try:with urllib.request.urlopen(req, timeouttimeout) as resp:return resp.status, resp.read(), resp.headersexcept urllib.error.HTTPError as e:return e.code, b, e.headers # 4xx/5xx 会以 HTTPError 抛出def parse_retry_after(value, default60.0):if not value:return defaultvalue value.strip()if value.isdigit():return float(value)return default # 日期格式的 Retry-After 需要额外的解析这里退避为默认值def fetch_responsibly(url, max_attempts3):for attempt in range(max_attempts):code, body, headers fetch_once(url)if code 200:return bodyif code 429:wait parse_retry_after(headers.get(Retry-After))time.sleep(wait) # 站点说了「慢一点」就按它说的等continueif code 403:raise PermissionError(f站点明确拒绝403{url}停止对该目标的请求改走官方 API 或联系站点申请许可)if code 500:time.sleep(2 ** attempt) # 对方过载指数退避别加压continueraise RuntimeError(f未预期的状态码 {code}: {url})raise TimeoutError(f重试 {max_attempts} 次仍未成功: {url})这段代码里有几个刻意的设计UA是能识别出你的标识而不是伪装的浏览器遇到 429 才等待重试遇到 403 直接抛异常终止500 段用指数退避。它没有任何一行是「换 IP」「换 UA」「加 Referer」因为那些正是本文拒绝提供的方向。四、为什么「换 IP 继续打」不是解法把这一点单独讲清楚因为它是最容易被当成「终极方案」的误解。假设一个站点已经封了你。它可能有几种理由你的频率超了、你没有权限、你的行为触发了它的安全策略、或者它就是不希望被自动化访问。无论哪一种换一个 IP 继续请求都不会改变那个理由——它只是让对方的判断失效了一次。对方一旦发现通常会升级措施更长时间的封禁、验证码、行为分析于是对抗升级而你原本想拿的数据可能一个都没多拿到。更要紧的是合规层面当站点通过技术手段明确表达了拒绝绕过它去继续访问在很多司法辖区和服务条款下都不是一个中性的技术选择。本文反复强调「停止」不是回避技术问题而是因为这本来就是正确的处理。那什么时候代理是有正当用途的当你是在做你自己有权发起的任务时——比如测试你自己应用在不同地区的表现、或者按对方允许的并发分摊你自己的请求。这和「被拒绝后继续打」是两回事区别在于对方是否已经明确说不。常见坑点❌ 收到 403 就去换 IP、轮换 User-Agent、伪造 Referer继续请求。✅ 403 是「重新认证也无用」的明确拒绝正确动作是停止对该目标的请求改走官方 API 或联系站点申请许可。❌ 把 401 和 403 当成同一回事看到 403 就去刷新 Token。✅ 401 是缺少有效凭据补认证可能解决403 是应用逻辑层面的拒绝重新认证没有任何区别。❌ 收到 429 就换一个 IP 继续发因为「对方限的是这个 IP」。✅ 429 的语义就是「慢一点」应读取Retry-After并按它等待换 IP 属于规避基于 IP 的限流。❌ 不管Retry-After的具体内容直接time.sleep(1)后重发。✅Retry-After可能是秒数也可能是 HTTP 日期两种都要能解析解析不了时用一个足够大的默认退避值。❌ 在 5xx服务过载时加大并发想「压过去」。✅ 5xx 说明对方过载或维护应指数退避、降低并发绝不加压。❌ 为了「看起来像浏览器」而把 User-Agent 伪造成 Chrome不留任何可追溯信息。✅ 用能识别出你和你用途的 User-Agent这是合规的一部分伪装身份和自报家门是相反的两件事。❌ 完全跳过 robots.txt直接开抓。✅ 用urllib.robotparser.RobotFileParser先读can_fetch、crawl_delay、request_rate并注意它们在查不到时返回None。❌ 把「重试」写成无限循环403 也一直重试到天荒地老。✅ 限制重试次数403 不重试429 按Retry-After等待超过上限就报错退出让人来决策。总结状态码含义该做什么不该做什么401缺少有效凭据补上正确认证把它当成 403 而放弃403理解请求但拒绝处理认证无效停止请求检查权限走官方渠道换 IP、轮换 UA、伪造 Referer429请求过多被限流读Retry-After降速等待换 IP 规避限流503服务暂时不可用指数退避降低并发加大并发加压200正常正常处理响应—「终极解决方案」这个说法之所以不成立是因为 403 不是要修的 bug是要听的答复。工程上你真正该做的是把合规检查写进流程robots、标识 UA、控制频率、把状态码处理写对区分 401/403/429/503、把「停止」当成一个合法且优先的分支。剩下拿不到的数据用官方 API、授权数据源或一次沟通来解决而不是用对抗。参考各类状态码的语义以 MDN 的 HTTP 状态码文档为准robots.txt解析器的接口以 Python 官方文档urllib.robotparser章节为准本文代码未在本机运行仅作人工推演。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑