403是服务器明确拒绝可疑身份,非单纯故障;requests需补全User-Agent、Accept、Accept-Language、Sec-系列、Sec-Fetch-、Referer等字段,并复用Session与TLS指纹,否则基础校验即失败。

403 不是“服务器坏了”,而是服务器明确告诉你:“你身份可疑,不给进”。光换 User-Agent 90% 的情况根本没用——它只是第一道门禁卡,后面还有指纹、步态、访问路径、TLS握手细节等一整套安检流程。
requests 请求头必须补全哪些字段才可能过基础校验
很多同学复制一个 User-Agent 就以为万事大吉,结果还是 403。真实浏览器发出的请求头远不止 UA,WAF(如 Cloudflare、Akamai)会校验字段组合与语义一致性:
-
Accept和Accept-Language必须匹配 UA 所声明的浏览器版本和地区(比如 Chrome 128 默认带zh-CN,zh;q=0.9,写成en-US,en;q=0.5可能被拒) -
Sec-Ch-Ua、Sec-Ch-Ua-Mobile、Sec-Ch-Ua-Platform这类 Chromium 独有头,新版站点强校验;缺一个就可能触发 403 -
Sec-Fetch-Site、Sec-Fetch-Mode、Sec-Fetch-Dest描述请求上下文,直接打 API 接口时值为same-origin/cors,但若页面是通过点击跳转来的,实际值可能是same-site+navigate,硬写错会暴露 -
Referer不是可选:有些接口只允许从特定页面发起请求,空 Referer 或错误域名直接 403
实操建议:用 Chrome DevTools → Network → 刷新目标页 → 找到对应请求 → 右键「Copy as cURL (bash)」→ 粘贴到在线工具(如 curlconverter.com)转成 Python requests 代码,再人工剔除 Cookie 和敏感字段。别自己手拼。
代理和 fake_useragent 常见静默失效点
你以为加了代理和随机 UA 就安全了?这两处最容易“看着生效,其实裸奔”:
立即学习“Python免费学习笔记(深入)”;
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
-
proxies字典里写"https": "http://user:pass@host:port"是对的,但写成"https": "https://..."或漏掉认证信息,requests会静默降级走本机出口,你还在那儿等代理 IP 返回 - 验证代理是否真生效,必须跑这行:
requests.get("https://httpbin.org/ip", proxies=proxies).json()["origin"],返回值得是你期望的代理 IP,不是你本地公网 IP -
fake_useragent.UserAgent()默认会尝试远程拉取 UA 池,但官方源已不可用,首次运行必抛fake_useragent.errors.FakeUserAgentError;正确写法是UserAgent(use_cache_server=False, path="ua.json"),并确保ua.json存在或提供 fallback 字符串 -
ua.random返回值可能含不可见字符(如\n),必须.strip()后再塞进 headers,否则某些 WAF 会因格式异常直接返回 403
什么时候该果断放弃 requests 改用 Selenium
当你已经做完以下所有事,仍稳定返回 403 或跳转到 Cloudflare 验证页,说明服务器启用了客户端环境检测,requests 无解:
- 完整复刻了浏览器全部请求头(含 Sec-* 系列)
- 代理已验证生效,且 IP 未被封(用
curl -I对目标站发极简请求确认) - Cookie 已从真实登录会话中导出并注入
Session - 请求间隔已拉长至 3 秒以上,且启用指数退避重试
这时再死磕 header 就是浪费时间。Selenium 的价值不在“能执行 JS”,而在于它提供了完整的 navigator 对象、WebGL 指纹、Canvas 渲染特征、字体列表等真实浏览器上下文——这些是反爬系统真正比对的维度。注意:启动前加 options.add_argument("--disable-blink-features=AutomationControlled"),否则 navigator.webdriver === true 会被秒杀。
Session 复用和 TLS 指纹才是高阶破防关键
多数人忽略的一点:requests.get() 每次都是全新 TCP 连接 + 全新 TLS 握手,而现代 WAF 会提取 TLS Client Hello 中的 SNI、ALPN、扩展顺序、密钥交换参数等生成“连接指纹”。真人浏览器复用连接池,TLS 上下文连续,你的脚本却像一群陌生人轮流刷门禁卡。
- 用
requests.Session()而非裸requests.get(),开启连接池复用 - 给 Session mount 自定义适配器,强制复用 TLS 会话:
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=3) - 更进一步,用
urllib3.util.ssl_.create_urllib3_context()控制 TLS 版本和密码套件,逼近 Chrome 128 的默认行为(但需谨慎,改错反而更可疑)
最常被忽略的其实是“请求路径顺序”:真实用户先加载 HTML,再并发请求 CSS/JS,最后才调用数据接口。如果你直奔 /api/v1/data,服务器看一眼 Referer 和请求时间戳就知道你是爬虫——这个逻辑没法靠 header 模拟,只能靠 Selenium 或 Playwright 走真实导航流。

















