如果你曾把浏览器里的网址复制到 Python 脚本中,却收到一整页写着 Access Denied 的 HTML,那你就遇到了 cloudflare 403 forbidden python requests 这个问题。同一个页面在 Chrome 里加载完美,但只要 requests、httpx 或 aiohttp 一访问,Cloudflare 就返回 HTTP 403 Forbidden——有时还伴随着令人头疼的 error 1020 access denied。感觉就像服务器在专门针对你的代码,某种意义上确实如此。
好消息是:这并不是随机发生的,而且大多数情况下你不需要一整套无头浏览器就能修复。本指南会逐步讲清楚:为什么 Cloudflare 拦截普通的 Python 客户端却放行浏览器、你可以按工作量从小到大尝试的手动修复方法、每种方法在哪里会失效,以及当真正的 managed challenge 或 Turnstile 挡在面前时,自动化场景下最可靠的解决路径。
Cloudflare 403 到底意味着什么
来自 Cloudflare 保护站点的 403 Forbidden 很少意味着你的凭证错误或页面不存在。它的真正含义是:Cloudflare 的 bot 管理层检查了你的请求,判定它来自自动化程序而非真实浏览器,于是拒绝将其转发到源服务器。同样的逻辑也会产生 error 1020 access denied,这是 Cloudflare 在防火墙(WAF)规则拦截请求时使用的代码——在 Python requests 这类原始 HTTP 客户端中,它通常表现为一个普通的 403。
关键在于:Cloudflare 在你的代码还没说完话之前就已经做出了这个判断。早在它读取你的请求头或 URL 路径之前,它就已经根据你建立连接的方式给你打了分。这正是为什么仅仅设置一个聪明的 User-Agent,几乎从来都修不好顽固的 403。
为什么浏览器能通过而 Python requests 被拦截
Cloudflare 会在多个层面对你的客户端进行指纹识别,而默认的 Python 技术栈几乎会同时在大部分层面败下阵来:
- TLS / JA3-JA4 指纹:在 HTTPS 握手过程中,你的客户端会发送一个 ClientHello,列出它支持的加密套件、TLS 扩展以及它们的确切顺序。Python 的 requests 基于 OpenSSL,它产生的指纹与 Chrome 的 BoringSSL 握手完全不同。Cloudflare 会对其做哈希(JA3/JA4)并与已知浏览器比对——不匹配就是即时 403,发生在读取任何请求头之前。
- HTTP/2 指纹:真实浏览器使用 HTTP/2 时带有特征性的 SETTINGS 帧、窗口大小和伪头部顺序。大多数 Python 客户端默认使用 HTTP/1.1 或非浏览器的 HTTP/2 配置,这是又一个破绽。
- 不一致的浏览器请求头:浏览器会发送一套连贯的请求头——User-Agent、sec-ch-ua、sec-ch-ua-platform、Accept、Accept-Language、Accept-Encoding——并按特定顺序排列。而 requests 只发送一份简短且明显的列表(如果你忘了修改,UA 里还会带着 python-requests)。
- 不执行 JavaScript:managed 和 JS challenge 要求客户端运行 JavaScript 并返回结果。纯 HTTP 客户端做不到,因此永远拿不到证明它已通过验证的 cf_clearance cookie。
- IP 信誉:来自云服务商的数据中心 IP 信任分很低。一个干净的住宅 IP 会提高你的成功率;一个被标记的数据中心 IP 段则会降低它。
手动修复 1:发送真实、完整的浏览器请求头
先从成本最低的修复开始。许多较轻量的 Cloudflare 配置只做基础的请求头与信誉检查,一套完整且一致的请求头就足以通过。复制真实 Chrome 请求所发送的请求头(可以在浏览器的 DevTools 网络面板中获取),并复用同一个 session 以便在请求之间保留 cookie。
保留 cookie 的重要性超出很多人的预期:如果你在请求之间丢弃 cookie,Cloudflare 会把每次调用都当作一个全新访客来评估,并每次都重新发起验证,这会大大增加触发 1020 的概率。
import requests
session = requests.Session()
session.headers.update({
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"sec-ch-ua": '"Chromium";v="126", "Google Chrome";v="126", "Not.A/Brand";v="24"',
"sec-ch-ua-platform": '"Windows"',
"Upgrade-Insecure-Requests": "1",
})
resp = session.get("https://target-site.com")
print(resp.status_code)手动修复 2:伪装浏览器的 TLS 指纹
如果完美的请求头仍然返回 403,那罪魁祸首就是你的 TLS/JA3 指纹。标准的 requests 和 httpx 库无法改变它,因为指纹来自底层的 OpenSSL 技术栈,而不是你的 Python 代码。解决办法是换用一个能复刻真实浏览器握手的客户端。
curl_cffi 是 2026 年的首选:它封装了 curl-impersonate,使用 BoringSSL,并复刻 Chrome 确切的 TLS 与 HTTP/2 指纹。tls-client 是另一个流行的替代方案(基于 Go)。两者都能攻破让 requests 直接碰壁的纯 TLS 防线。
# pip install curl_cffi
from curl_cffi import requests as cffi
# impersonate a real Chrome TLS + HTTP/2 fingerprint
resp = cffi.get(
"https://target-site.com",
impersonate="chrome",
)
print(resp.status_code)
print(resp.text[:500])手动修复 3:住宅代理(以及这一切在哪里失效)
如果请求头和 TLS 伪装都正确,但在大规模抓取时仍被拦截,那剩下的因素就是 IP 信誉。通过轮换的住宅或移动代理路由,可以把你被标记的数据中心 IP 替换成看起来像普通家庭用户的地址,从而提高你的信任分和成功率。
但这里有一道硬性上限:curl_cffi、tls-client 和住宅代理只能解决被动指纹识别这一层。一旦 Cloudflare 决定真正下发一个 managed challenge、JS challenge 或 Turnstile 组件,它们就都无能为力了。这些验证需要执行 JavaScript 来计算令牌并获取 cf_clearance cookie——而无论一个 HTTP 客户端把 Chrome 的握手伪装得多么逼真,它都无法运行那段 JavaScript。这正是大多数抓取管线卡住的地方:方案能用上几周,然后目标站点收紧配置,每个请求又重新 403 了。
可靠的修复:用 API 解决 challenge
一旦遇到真正的 challenge,可靠的做法就是把它交给一个能替你完成浏览器工作、并返回你的普通 session 所需凭证的服务。NSLSolver 的 Challenge 端点正是这样做的:你 POST 目标 URL(可选附带代理),它会清除 Cloudflare 的 managed/JS challenge,并返回 cf_clearance cookie 以及与之匹配的 user_agent。随后你把两者附加到自己的 requests session 上,就能以原生速度继续抓取——你的管线里不必运行任何无头浏览器。
这套契约很小且精确。向 https://api.nslsolver.com/solve 发送一个带 X-API-Key 的 POST 请求。响应中的 cookies 字段是一个以 cf_clearance 为键的字典,user agent 以 user_agent(蛇形命名)返回。关键一点:你在后续请求中发送的 user_agent 必须与绑定在 cf_clearance cookie 上的那个一致——Cloudflare 会把它们绑定在一起,所以请使用 API 返回的值,而不是你自己的。
import requests
API_KEY = "nsl_YOUR_API_KEY"
TARGET = "https://target-site.com"
# 1) Ask NSLSolver to clear the Cloudflare challenge
resp = requests.post(
"https://api.nslsolver.com/solve",
headers={"X-API-Key": API_KEY},
json={
"type": "challenge",
"url": TARGET,
"proxy": "http://user:pass@host:port", # optional
},
timeout=120,
)
data = resp.json()
# 2) Reuse the cf_clearance cookie + matching user_agent
session = requests.Session()
session.cookies.update(data["cookies"]) # dict keyed by cf_clearance
session.headers["User-Agent"] = data["user_agent"]
# 3) Now your normal requests pass straight through
page = session.get(TARGET)
print(page.status_code) # 200
print(page.text[:500])什么时候用哪种方案
先尝试手动修复——它们免费且快速。先加上完整的浏览器请求头,如果还不够,就切换到 curl_cffi 来匹配 TLS 指纹,再叠加住宅代理。对于只做被动指纹识别的站点,这套组合往往就足够了,而且不花一分钱。
一旦面对真正的 managed challenge、Checking your browser 过渡页或 Turnstile 组件——也就是任何请求头或 TLS 技巧都无法取胜的场景——就切换到 NSLSolver 的 Challenge 端点。定价为按量付费,Challenge 每 1,000 次解算 $0.50(Turnstile 为 $0.40/1,000,Kasada 为 $1.50/1,000),成功率 99.9%,失败的解算不收费。新账户注册即送 100 次免费请求,无需信用卡、无需加密货币,因此你可以在花费任何费用之前,先确认它能否清除你的具体目标。