403 禁止访问在网络爬虫中的解决阶梯:真正有效的方法

403 并不是权限问题——而是检测问题。四个阶梯将被阻止的脚本与 200 状态码分隔开来,大多数人停在第一个阶梯上。

网络爬虫中的 403 禁止访问错误几乎从不意味着状态码所说的内容。HTTP 403 被定义为服务器理解你的请求但拒绝授权——但当它击中爬虫时,几乎与权限或缺少登录无关。这意味着网站查看了你的请求,判断是机器发送的,并关闭了大门。这告诉你需要改变什么:不是你的凭证,而是你的流量形态。以下是解决阶梯,按成本从低到高排列,并附上识别你卡在哪个阶梯的检查。

403 vs 401 vs 429:每个状态码告诉你的信息

在编写代码之前,先正确诊断。401 未授权要求提供凭证——提供后即可解决。403 禁止访问无论凭证如何都拒绝,因此如果触发了机器人检测,登录也无济于事。429 请求过多与请求量有关,当窗口重置时会自动清除;403 则与身份有关,直到你改变请求的外观才会消失。如果你的爬虫收到的是 429 而不是 403,解决方法是调整节奏,而不是伪装——我们在修复 429 请求过多中讨论了这一点。

在更改任何代码之前,60 秒内进行诊断

三个命令几乎告诉你一切。运行裸请求,再次运行时仅替换为浏览器 User-Agent,然后读取响应体——阻止原因通常写在其中。

# 1. Bare request: what does the target give a naked client?
curl -sS -o /dev/null -w '%{http_code}\n' https://target.example/page

# 2. Same request, browser User-Agent only
curl -sS -o /dev/null -w '%{http_code}\n' \
  -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
  https://target.example/page

# 3. Read the body and the response headers - the reason is in there
curl -sS -D - https://target.example/page | head -c 600

这样解释。如果步骤 2 返回 200,问题全在 headers,你在第一个阶梯就完成了。如果响应体提到 Cloudflare、Ray ID 或错误 1020,你被 WAF 规则挡住了——请参阅Cloudflare 错误 1020。一个普通的 Apache 或 nginx 禁止访问页面通常意味着一个服务器模块,如 mod_security,自现代机器人管理存在之前就已阻止已知的机器人 User-Agent。如果同一台机器上的浏览器加载页面而你的客户端没有,请查看curl 403 但浏览器工作

网络爬虫的 403 禁止访问解决阶梯:headers、TLS 指纹、IP 类型和渲染作为四个递进的阶梯
爬一个阶梯,重新测试,一旦获得 200 就停止。渲染成本最高,修复效果最小。

阶梯 1:停止在 headers 中暴露自己

Python 的 urllib 将自己标识为类似 python-urllib/3.3.0requests 发送 python-requests/2.x。这些字符串是一个自白,关于 Python 爬虫中 403 的规范 Stack Overflow 线程上最受欢迎的答案仅仅是:发送一个浏览器 User-Agent。这在许多网站上仍然有效。但自那个答案写成以来,有两件事发生了变化。首先,一个裸 Mozilla/5.0 现在本身就是一个标志——同一线程上的评论者报告网站直接阻止它,因为没有真实的浏览器会发送一个两标记的 UA。其次,现代服务器比较的是整个 header 集,而不是一个字段。

发送一个一致的集合:一个当前的浏览器 UA,匹配的 Accept 链,一个语言,以及 Chromium 添加到每次导航中的 Sec-Fetch-* 元数据 headers。在更严格的目标上,header 顺序也很重要——使用有序映射并按浏览器使用的顺序放置它们。

import requests

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    ),
    "Accept": (
        "text/html,application/xhtml+xml,application/xml;q=0.9,"
        "image/avif,image/webp,*/*;q=0.8"
    ),
    "Accept-Language": "en-GB,en;q=0.9",
    "Accept-Encoding": "gzip, deflate",   # add 'br' only if brotli is installed
    "Upgrade-Insecure-Requests": "1",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Connection": "keep-alive",
}

with requests.Session() as s:
    s.headers.update(HEADERS)
    r = s.get("https://target.example/page", timeout=20)
    print(r.status_code, len(r.content))

一个一致性陷阱几乎抓住了所有人:一个声称是美国 Chrome 桌面的 UA,配对 Accept-Language: de-DE 和一个在巴西的出口 IP,是任何一个体面系统都会注意到的不匹配。保持用户代理、语言和 IP 地理位置讲述同一个故事。

在这个阶梯上的一些 403 更简单:一个缺失的 Referer。只有当请求看起来来自他们自己页面时才提供资产的服务器会对直接命中返回 403,而当你添加引用 URL 时立即返回 200。这是一个经典,并且测试只需一个 header。

阶梯 2:TLS 指纹 headers 无法修复

如果完美的 headers 仍然返回 403,阻止发生在你的 headers 被读取之前。每个 HTTPS 客户端在 TLS ClientHello 中宣布其密码套件、扩展、椭圆曲线和 ALPN,并且该组合被哈希为一个 JA3 或 JA4 指纹。Python 的 requests、Go 的 net/http 和原生 curl 各有一个独特的指纹,任何反机器人供应商都能轻松区分它们与 Chrome。声称在 header 中是 Chrome 131,而在握手时像 OpenSSL 是爬虫能做出的最大矛盾。

解决方法是一个在 TLS 层模拟真实浏览器的客户端。在 Python 中,这是 curl_cffi,一个绑定到一个经过修补的 libcurl 的库,重现浏览器 ClientHellos:

# pip install curl_cffi
from curl_cffi import requests as cffi

proxy = "http://USER:PASS@gate.quantumproxies.io:8000"

r = cffi.get(
    "https://target.example/page",
    impersonate="chrome",              # Chrome JA3/JA4 + HTTP/2 settings
    proxies={"http": proxy, "https": proxy},
    timeout=20,
)
print(r.status_code)

Node 有基于相同修补 TLS 堆栈构建的等价物。如果你想了解为什么这个单一改变能在受保护网站上将 403 转为 200 的完整机制,请阅读如何 JA3/JA4 指纹识别你的爬虫

阶梯 3:IP 是信息

Headers 和 TLS 描述客户端。IP 描述谁在请求,并且权重很大。托管和云范围内的地址是公开的,易于通过 ASN 映射,并且在检查你的请求的单个字节之前就具有较低的信任分数——这就是为什么 VPS 上的爬虫会收到 403,而同样的代码在家庭连接上却畅通无阻。住宅地址属于消费者互联网提供商,被视为人;移动运营商 IP 位于 CGNAT 后面,每个都有数千名真实用户,这使得它们是最难被大规模阻止的。

所以阶梯 3 是一个交换,而不是重写:从一个住宅出口发送相同的格式良好的请求。QuantumProxies 在 200 多个国家/地区运行 9000 万多个住宅 IP,提供按请求轮换或粘性会话、HTTP 和 SOCKS5 的每个计划,以及按 GB 计费——一行配置更改目标看到的 ASN。已经在住宅 IP 上仍然被阻止?使用我们的免费IP 质量评分检查器检查池的声誉:任何评分超过 75 的都已被烧毁,无论你的 headers 多好,都会收集 403。

跳过阶梯:使用 Scraper API 抓取任何页面

检查表比较触发 403 禁止访问错误的爬虫请求信号与真实浏览器发送的信号
反机器人系统测试一致性。此列表中的一个不匹配信号就足以导致 403。

阶梯 4:渲染和挑战

最后一个阶梯是昂贵的。一些 403 是 JavaScript 挑战的可见一半:服务器发送一个小脚本,期望在几秒钟内得到答案,并拒绝无法执行它的所有请求。没有 header 集和 IP 能够解决这个问题,因为测试的是你是否能运行代码。按成本升序排列的选项:一个带有隐身补丁集的无头浏览器、一个托管的浏览器池,或一个按需渲染的爬虫 API。渲染每页的成本比普通 HTTP 请求高出数倍,因此只对真正需要的 URL 升级。

QuantumProxies 的Scraper API将阶梯 2 到 4 合并为一个请求:浏览器级别的 TLS、住宅出口、当页面需要时的 JavaScript 渲染,以及返回 markdown、JSON 或原始 HTML。这是诚实的交易——你停止维护阶梯,而是按成功页面付费。

在实践中使用阶梯

禁止预防是兄弟学科:一旦你获得 200,保持它是节奏、会话卫生和池健康的问题,而不是伪装。

常见问题

在网络爬虫中导致 403 禁止访问错误的原因是什么?

几乎每种情况下都是检测。常见触发因素是默认库 User-Agent、不完整或矛盾的 header 集、声誉不佳的数据中心 IP、过于规律的请求节奏,或与声称的浏览器不匹配的 TLS 指纹。真正的权限错误确实存在,但它们也会对浏览器返回 403——在假设之前先在浏览器中测试。

如何在 Python 中绕过 403 禁止访问错误?

使用阶梯。将完整的浏览器 header 集添加到 requests.Session;如果失败,切换到模拟浏览器 TLS 的客户端,如 curl_cffi;如果失败,通过住宅代理路由;如果页面发送 JavaScript 挑战,渲染它或使用爬虫 API。只有在实际测试过更便宜的阶梯后才升级。

为什么 httpx 返回 403 而我的浏览器没有?

requests 的原因相同:httpx 发送一个最小的 header 集和一个 Python TLS 指纹。从 DevTools 复制浏览器的确切请求,用 httpx 重放,403 通常会消失——这告诉你区别在于 headers。如果在相同的 headers 下仍然存在,阻止在 TLS 或 IP 层。

如何修复 Scrapy 中的 403 禁止访问?

设置一个现实的 DEFAULT_REQUEST_HEADERSUSER_AGENT,保持 ROBOTSTXT_OBEY 诚实地说明你被允许抓取什么,启用 AUTOTHROTTLE_ENABLED,并通过旋转代理中间件路由请求。Scrapy 重试默认不重试 403——仅在尝试之间旋转 IP 时将其添加到 RETRY_HTTP_CODES

403 是信息,而不是墙。它告诉你是哪四个信号暴露了你,每个阶梯的成本都比下面的高。从最便宜的开始,每次更改后测试,一旦状态码变为 200 就停止爬升。

获取能够通过阶梯 3 的住宅代理