爬虫403错误破解指南:普通请求库与Selenium的应对策略对比
写爬虫的人几乎没有谁没被 403 迎面泼过冷水。请求发出去状态码回来一个 403 Forbidden服务器明明白白告诉你我不接受你的访问。这个错误在爬虫开发里太常见了常见到很多人第一反应就是“换 UA”“加延时”“上代理”但实际效果往往时好时坏。尤其是当你手里的方案涉及普通请求库与 Selenium 这两种截然不同的技术路线时403 的处理逻辑完全不在一套思路上。这段时间我集中调试了一批模拟项目反复对比了 requests、httpx 这类普通请求方案和 Selenium 自动化方案的 403 表现发现很多问题并不是“反爬太强”而是我们对 403 本身的判断就偏了。这篇内容我会把两种方案下的 403 处理经验完整拆开讲包括错误根源的分析方法、两种请求机制的差异、具体调试步骤还有我实际踩过的坑。如果你正被 403 折磨或者准备在两种技术路线之间做选型这篇应该能帮你省下不少时间。1. 403 错误不是玄学先搞清楚服务器在拒绝什么1.1 403 状态码背后的决策逻辑HTTP 403 的意思是“服务器理解请求但拒绝执行”。注意这里的关键点服务器已经识别到了你的请求并且有足够的信息做出拒绝决定。这跟 404 不一样404 是“找不到资源”403 是“我知道你要什么但我不给你”。看似差别不大但排查思路完全两条路。在爬虫场景里服务器做拒绝决策时一般会看几类信息请求头是否完整、是否像真实浏览器发出的Cookie 是否合法、会话状态是否符合预期IP 的访问频率、分布特征是否异常TLS 指纹、HTTP/2 指纹是否与浏览器一致行为特征比如访问路径是否规律、点击间隔是否反人类普通请求库和 Selenium 在这几个维度上的表现差异非常大。普通请求库默认的指纹特征非常明显服务器很容易识别“这不是浏览器”Selenium 虽然用真实浏览器内核但自动化特征同样可以被检测。所以处理 403 的底层逻辑就是要搞清楚服务器到底是根据哪个维度把你拒掉的。用一个生活化的类比来理解你去一家只接待会员的店403 就相当于门口保安看了你一眼说“你不像会员”。普通请求库就像一个穿着明显不合身外套的人保安一眼就觉得可疑Selenium 像一个行为有点别扭的会员刚开始能混进去但待久了还是会被注意到。两种情况的应对方式完全不一样。1.2 常见诱因从请求头到 IP 信誉的完整链条根据我调试过的几个模拟项目403 的诱因大致可以分几类按出现频率排序第一类是请求头缺陷。缺少 User-Agent、Accept-Language、Sec-Fetch 等基础头或者头信息自相矛盾比如声明 Chrome 版本但 TLS 指纹不像 Chrome。这类问题在普通请求方案里尤其常见很多人只设置了 UA 就发请求结果照样 403因为服务器校验的维度远不止 UA。第二类是 IP 维度的问题。同一个 IP 在短时间内产生大量请求或者 IP 段本身信誉差比如机房 IP、云服务商 IP这类 IP 即使请求头伪装得再好也容易被直接拒绝。普通请求和 Selenium 都会遇到这个问题但 Selenium 因为浏览器开销大单位时间请求量低触发频率会低一些。第三类是行为特征问题。访问路径过于规律、页面停留时间过短、鼠标轨迹缺失这些对 Selenium 用户来说是比较常见的坑。普通请求反而没有这个问题因为普通请求根本没有“行为”可言服务器主要看请求头和数据特征。第四类是 Cookie 和会话状态问题。有些站点要求必须先访问首页、完成 JS 初始化才能带着有效 Cookie 访问目标页面。直接用普通请求跳过了初始化步骤自然收获 403。理解了这些诱因后面无论是选技术路线还是排查问题都有了判断依据。接下来我会把两种方案的工作机制拆开对比它们面对 403 时的天然差异。2. 普通请求库与 Selenium 的本质差异一场指纹与行为之争2.1 普通请求库的请求特征一场指纹与行为之争requests 和 httpx 是 Python 爬虫里最常用的普通请求库。它们的工作方式本质上是构造一个 HTTP 报文发出去然后等待响应。这个报文由请求库自己生成带的特征非常“素”默认 UA 是 python-requests默认头就那么几个TLS 指纹是 botocore 或者 openssl 的特征没有 Cookie 存储机制requests 的 Session 也只是帮你管理 Cookie不是浏览器那种完整状态。我用一个模拟项目做过实验目标是一个有反爬策略的新闻站点用 requests 直接请求首页第一次就被 403响应头里带了明确的 WAF 标识。然后我完整模拟了浏览器的请求头包括 User-Agent、Accept、Accept-Language、Accept-Encoding、Connection、Sec-Fetch-* 等一整套发现依然 403只是能多撑几次请求才被封。这说明什么问题说明服务器的校验已经不止停留在请求头层面。现代反爬系统普遍会采集 TLS 指纹和 HTTP/2 指纹。不同客户端建立 TLS 连接时密码套件顺序、扩展列表、椭圆曲线参数等几十个特征组合起来形成一把独特的“指纹”。python 请求库的指纹和真实 Chrome 的指纹差别极大服务器瞬间就能判断出这不是浏览器。httpx 相对 requests 有一个优点它支持 HTTP/2且可以自定义一些传输层的参数但 TLS 指纹的问题依然存在。市面上有 curle 这种工具配合 impersonate 参数可以模拟 Chrome 的 TLS 指纹确实能解决一部分 403 问题但这已经属于会话层模拟的范畴脱离普通请求库的“原生”范畴了。2.2 Selenium 的浏览器级优势与隐藏的自动化特征Selenium 的方案则是直接操控一个真实浏览器实例。Chrome、Firefox 或 Edge 启动后它就是一个完整的浏览器环境TLS 指纹、HTTP/2 指纹、JS 执行能力、Canvas 渲染特征全部和真实用户无异。理论上服务器很难从“浏览器指纹”这个层面拒绝你。但 Selenium 有自己的问题。它注入的自动化控制脚本会在页面的 window 对象上留下标记。比如 navigator.webdriver 属性在 Selenium 控制的浏览器里默认是 true这个属性在普通浏览器里是 undefined。对反爬熟悉的人都知道一行navigator.webdriver的检测就能让所有 Selenium 方案集体阵亡。除了 webdriver 标记还有几类自动化特征window.chrome对象的部分属性缺失比如window.chrome.csi、window.chrome.loadTimesnavigator.plugins和navigator.languages的完整性行为特征比如鼠标移动轨迹不自然、页面滚动速度异常、点击间隔极度均匀CDPChrome DevTools Protocol连接特征检测方可以通过Runtime.evaluate的堆栈信息判断是否被自动化工具控制所以 Selenium 的优势是“指纹层面像人”劣势是“行为层面不像人”。反过来普通请求是“行为层面没有破绽因为根本没有行为”但“指纹层面完全不是人”。这就是两种方案处理 403 的思路分水岭。2.3 两种方案的 403 表现对比我把同一个目标站点的表现整理成表格可以更直观地看到差异对比维度普通请求requests/httpxSeleniumTLS 指纹与浏览器差异极大容易被识别与真实浏览器一致JS 执行能力无无法处理动态渲染完整执行页面状态真实请求头完整性需手动构造容易遗漏浏览器自动生成完整性高自动化检测特征无没有浏览器环境可被检测webdriver 等标记明显请求频率控制完全由代码控制容易过快浏览器本身有解析耗时天然降速资源开销低单机可跑高并发高多实例对内存和 CPU 压力大反爬识别难度低到中取决于站点校验深度中到高取决于站点是否检测自动化这个表格基本说明了一个核心矛盾普通请求的问题是“你承认自己不是浏览器”Selenium 的问题是“你假装自己是浏览器但演技不行”。搞清楚这个矛盾你就知道遇到 403 时应该在哪个维度上找突破口。3. 定位 403 的根源用最小成本做交叉验证3.1 一探到底先用 curl 摸清基础请求的基线调试 403 的第一步不是急着换方案而是先建立基线。我建议你装一个 curl用最原始的请求去访问目标 URL观察返回值。这一步的目的是搞明白“没有任何伪装的情况下服务器是什么态度”。curl -I https://目标站点.com/某路径如果返回 403说明服务器对陌生客户端有基础准入限制那问题就要从指纹层面解决如果返回 200说明服务器本身不排斥非浏览器客户端那问题出在你的代码和请求头细节上后面加头的思路就对了。常用的一个进阶做法是带 UA 再试一次curl -I -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 https://目标站点.com/某路径这一步能快速判断服务器是否做 UA 基础过滤。很多小型站点在这一层就会放行如果你的代码加了 UA 还 403那就要考虑是不是请求库生成的报文和 curl 有明显差异。curl 的另外一个用途是看响应头的细节。403 响应头里经常藏着“制裁原因”比如Server: cloudflare、CF-Ray这类标识说明走了 Cloudflare 代理X-Silicon之类则是特定 WAF 的标记。看响应头比盲猜有效得多。3.2 三种工具组合判定IP 封禁还是特征拦截面对 403我最常用的组合就是“手机热点对比 浏览器无痕模式 普通请求”三个对照试验。第一步用同一个代码分别在当前 IP 和手机热点 IP 下跑一次。两个 IP 都 403说明问题大概率不在 IP 维度当前 IP 403、手机热点 200说明当前 IP 已经被服务器记录或信誉降级这时候需要换 IP 或等解封。第二步在浏览器无痕模式下手动访问目标页面。注意是无痕模式可以排除插件和 Cookie 的干扰。无痕模式能正常打开但 requests 请求 403说明服务器对浏览器和脚本请求做了差异化处理特征拦截的可能性很大。第三步如果无痕模式也打不开直接提示 403那说明服务器做了比较严格的准入控制要么校验了 IP 归属要么对整个机房 IP 段做了封禁。这种场景下普通请求和 Selenium 都难逃一劫换网络环境才是正路。这套组合下来403 的根源基本能定位到一个大致范围IP 信誉问题、请求特征问题还是浏览器准入问题。有了方向后续的针对性处理才有效率。4. 普通请求方案下的 403 突破思路4.1 构造可信请求头从“要素齐全”到“逻辑自洽”如果你确认 403 来自请求特征拦截最直接的优化就是请求头。但注意请求头的核心不是“多”而是“自洽”。服务器校验请求头时会看整体逻辑比如你声明自己是 Chrome 120但 Sec-Fetch-Site 的值和实际访问场景不匹配这种小矛盾就可能触发风控。我实际使用的一套请求头模板以普通新闻站点为例import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: same-origin, Sec-Fetch-User: ?1, Cache-Control: max-age0, }这套头的问题在于“固定不变”。真实用户的请求头并不是每次完全一致的比如 Accept-Language 的优先级顺序、Cache-Control 的空值、Sec-Fetch-Site 是否带值都可能因浏览器缓存、系统设置而变化。如果你的爬虫目标站点风控精细化程度高需要做成请求头池随机切换不同组合而不是一套头打天下。我自己踩过的坑是 Accept-Encoding。有一段我带着brBrotli去请求但代码里没有针对 br 的解压处理结果服务器返回的是 br 压缩后的内容解析乱码。后来把br去掉只留gzip, deflate问题就解决了。这件事提醒我请求头不是为了“看起来像浏览器”而是为了“服务器给的响应我能正确处理”。4.2 Session 与 Cookie 管理从第一步就要有“会话”概念requests.Session 的好处是自动管理 Cookie但很多人的用法有问题——他们用一个全新的 Session 直接去请求目标数据页忽略了目标站点可能要求的“先访问入口页种下 Cookie再请求数据”的流程。遇到这类情况我一般分两步走import requests session requests.Session() session.headers.update(headers) # 第一步访问入口页触发 Cookie 下发 entry_url https://目标站点.com/ session.get(entry_url, timeout10) # 第二步携带已获得的 Cookie 请求目标数据 target_url https://目标站点.com/api/data response session.get(target_url, timeout10)有时候第二步直接就能 200原因是第一步种下的 Cookie 被服务器视为“会话初始化成功”的凭据。同理如果你发现 403 在“直接请求”时出现而在“访问首页后再请求”时不出现基本可以断定服务器的校验依赖 Cookie 会话状态这时候用 Session 串起访问流程就能解决问题。4.3 代理池的正确使用姿势让请求族谱更真实IP 维度的 403 也很常见尤其是大规模抓取场景。但代理池不是“换个 IP 就行”这么简单最关键的是代理的“身份一致性”。正经代理池的使用逻辑是同一个 Session 在生命周期内尽量绑定同一个 IP不要让每个请求轮换不同 IP。原因是服务器会结合“IP Cookie 请求头”的三元组来判断用户身份。如果你每次请求都换 IP但 Cookie 不变服务器反而会认为“同一个人在短时间内出现在多个地区”触发异常规则。我给普通请求项目配置代理的方式proxies { http: http://user:passwordproxy_ip:port, https: http://user:passwordproxy_ip:port, } response session.get(target_url, headersheaders, proxiesproxies, timeout10)配好代理之后一定要检查出口 IP 是否真的变了。有些代理服务商在请求量大的时候会自动漂移 IP你以为自己在用固定 IP实际上每个请求的出口都在变。检查方法很简单请求一个返回 IP 的接口打印出来对比即可。5. Selenium 处理 403 的实操方案与关键参数5.1 什么时候值得动用 SeleniumSelenium 的启动开销不小单个浏览器实例轻松吃掉几百 MB 内存多实例跑起来对机器性能要求高。所以我一般只在两种情况下用它一种是目标页面依赖 JS 动态渲染普通请求拿不到完整 HTML。典型表现是普通请求返回的 HTML 里没有目标数据只有一堆空壳节点数据要等 JS 执行完才填充。另一种是普通请求已经全面被 403且确认服务器对指纹校验非常严格比如 TLS 指纹层面的拦截。这种情况下 Selenium 的浏览器级真实指纹就体现出价值了。如果目标数据在初始 HTML 里就有或者通过接口能拿到 JSON 数据那就别用 Selenium。硬上的结果是速度慢、资源占用高、被封概率反而更大。5.2 关闭自动化特征从 webdriver 标记到 CDP 参数老实说原生 Selenium 直接访问目标站点大概率会在navigator.webdriver这一层被识别。这也是 Selenium 跑起来照样 403 的最常见原因。解决办法不是绕过而是尽量让浏览器实例的自动化特征收敛。核心做法是通过 ChromeOptions 设置排除自动化控制开关from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-blink-featuresAutomationControlled) # 排除自动化控制相关开关 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions)然后通过 CDP 在页面加载前覆盖那几个关键属性driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}); Object.defineProperty(navigator, plugins, {get: () [1, 2, 3, 4, 5]}); Object.defineProperty(navigator, languages, {get: () [zh-CN, zh, en]}); window.chrome window.chrome || {}; window.chrome.csi function() {}; window.chrome.loadTimes function() {}; })这套组合能解决大量基础自动化检测但注意它只能覆盖 JS 层面的检测。如果目标站点使用 CDP 协议检测、或者收集浏览器启动参数中的--enable-automation痕迹这些 JS 层面的掩盖就不一定有效了。5.3 请求拦截与隐身策略让浏览器“说人话”Selenium 本身的请求是浏览器自动发出的请求头完整、TLS 指纹真实、Cookie 自动管理这些都不是问题。但 Selenium 发起的请求有两个隐形风险多余请求和自动化痕迹。多余请求来自浏览器初始化阶段。启动 Firefox 或 Chrome 的时候浏览器会默认请求一些扩展组件、更新检查等资源。某些 WAF 会记录这些请求路径发现“一个浏览器实例竟然请求了这些非用户行为资源”从而判断这是自动化环境。一个有效的做法是拦截这些请求from selenium.webdriver.common.by import By # 以 selenium-wire 方式或 CDP 的 Network 域做请求拦截 driver.execute_cdp_cmd(Network.enable, {}) # 对特定域名或路径做 block driver.execute_cdp_cmd(Network.setBlockedURLs, { urls: [ *://update.googleapis.com/*, *://*.gvt1.com/*, *://*.gvt2.com/*, ] })隐藏浏览器自动化痕迹的另一个方向是使用 undetected-chromedriver 这类第三方库。它的原理是重写 ChromeDriver 的启动参数和通信协议让目标站点更难从 CDP 握手阶段识别自动化。值得注意的是这类库本质上是在做对抗站点的反爬策略升级后库也需要同步更新不存在一劳永逸的方案。5.4 用普通请求加速策略收集用 Selenium 攻坚核心数据我现在的做法是在同一个项目里混用两条路线。普通请求负责试探规律、摸清接口、做规模化采集Selenium 负责处理那些普通请求拿不下来的核心页面和数据接口。先让普通请求快速跑一遍目标站点把能拿的数据都拿了确认哪些 URL 返回 403。 对 403 的 URL再分析到底是缺 Cookie 还是缺渲染。缺 Cookie 就补齐流程缺渲染就换 Selenium。Selenium 只目标性地打开那几个真正需要的页面不在 Selenium 里做全站爬取。这样既能利用两类方案的长处又避免了 Selenium 资源开销大导致的性能瓶颈。整站爬下来Selenium 打开页面数量不到普通请求的 1%但拿到的数据完整度提升了不少。6. 常见问题与排查技巧实录6.1 requests 请求带齐了头照样 403这是最让人头疼的情况之一。头都带齐了UA 也换了你甚至用了和浏览器一模一样的请求头结果还是 403。我遇到过的可能原因有这么几个TLS 指纹被识别服务器不看你带什么头直接看你建立 TLS 连接时的指纹特征python 请求库的指纹和浏览器差太远直接拒。请求顺序不对服务器要求先访问某个入口 URL 种下 Cookie你直接请求目标页被拒。HTTP/2 指纹如果服务器支持 HTTP/2它还会比对 HTTP/2 的帧特征比如 SETTINGS 参数、窗口更新策略这些和请求头无关。排查方法换手机热点访问确认 IP 没问题用浏览器无痕模式访问确认页面能打开再用 curl 带同样请求头验证是不是 requests 客户端本身被识别。三步走完方向基本清晰。6.2 Selenium 打开页面照样 403Selenium 条件下页面也能打开、JS 也能执行但还是 403这时候通常是自动化特征已经被识别。先用一个简单方法验证——在页面上下文执行 JavaScript查看navigator.webdriver的值webdriver_flag driver.execute_script(return navigator.webdriver;) print(webdriver_flag) # True 说明自动化特征未掩盖如果输出 True说明前面的 CDP 覆盖失效或者顺序不对需要检查Page.addScriptToEvaluateOnNewDocument是否在页面加载前注册了代码。另一个高频原因是浏览器启动参数暴露。Chrome 在 Selenium 控制下会带--enable-automation、--remote-debugging-port等参数一些站点的风控会通过扫描进程命令行来发现这些痕迹。undetected-chromedriver 能处理一部分但它的原理是改掉这些参数的显式特征不是隐藏整个浏览器进程所以也不是绝对安全。6.3 403 出现规律一会儿能访问一会儿被封这种情况通常是频率限制。服务器不是一开始就拒绝你而是在单位时间内请求次数超过阈值后触发封禁。我的经验是把请求频率降到阈值以下同时做指数退避重试。举个例子如果你测试发现连续 10 次请求后第 11 次开始 403那就把单次请求间隔设置在第 11 次对应的累计请求时间之上或者控制每分钟不超过某个请求次数。用代码实现一个降频重试import time import random def request_with_retry(session, url, max_retries3): for attempt in range(max_retries): response session.get(url, timeout10) if response.status_code ! 403: return response # 指数退避基础间隔 2 秒上限 30 秒 wait_time min(2 * (2 ** attempt) random.uniform(0, 1), 30) time.sleep(wait_time) return None注意点在于随机因子的引入。固定间隔实际上也是一种机器特征统一间隔的请求序列很容易被识别。加一点随机抖动反而更像真实用户行为。6.4 403 问题排查速查表现象可能原因排查手段解决方向所有请求全部 403IP 被记录或信誉差手机热点交叉验证更换网络环境/代理浏览器能访问requests 403TLS 指纹或请求头缺陷curl 对照测试使用指纹模拟方案或 Selenium带齐请求头依然 403指纹校验或请求顺序问题对比无痕模式补 Cookie 流程或改用浏览器方案Selenium 打开页面 403自动化特征暴露检查 navigator.webdriverCDP 覆盖或使用 undetected 方案请求一段时间后 403频率超限记录封禁触发点降频 退避重试仅特定 URL 403页面级风控对比不同 URL 表现检查页面是否要求先访问入口6.5 我踩过的三个隐蔽坑第一个坑是“校验 Referer”。某次我抓一个图片资源站请求图片 URL 一直 403单独看 URL 没有任何问题手机热点也不行。后来发现图片页面的加载依赖前一个页面的 Referer如果直接请求图片而 Referer 为空服务器判定为“外链盗图”直接拒。解决方案是先请求源页面拿 Referer再带着 Referer 请求图片。这个坑在普通请求方案里很常见Selenium 场景下因为浏览器自动带 Referer 反而不会触发。第二个坑是“Cookie 中的签名参数时效性”。有些站点在首页种下的 Cookie 里带了一个时效签名比如_m_h5_tk这类签名过期后即使 Cookie 还在也无法访问数据接口。解决办法是定期重新访问首页刷新 Cookie而不是一个 Session 用到底。我一开始没注意这个导致爬虫跑半小时后突然所有请求都 403排查了很久才发现是 Cookie 签名过期。第三个坑是 Selenium 的“窗口大小与视口不匹配”。启动浏览器时如果只设置了window-size参数而没有设置视口viewport某些站点的版面自适应脚本会异常导致页面加载不完整进而引发风控误判。解决方法是同时设置--window-size和设备像素比参数或者用driver.set_window_size()配合driver.execute_script()调整视口。7. 从 403 处理经验反推方案选型7.1 普通请求和 Selenium 的适用边界处理了足够多的 403 之后我对两条技术路线的适用场景有了更清晰的判断。普通请求方案适合以下场景目标页面是服务端渲染HTML 里直接有数据数据接口返回结构化数据JSON/XML请求量大、对速度敏感愿意投入时间构造请求头、管理 Cookie、配置代理目标站点的反爬集中在基础头校验和频率限制层面Selenium 方案适合以下场景页面依赖 JS 动态渲染普通请求拿不到完整数据目标站点对客户端指纹校验严格普通请求容易被识别对请求频率要求高但单次抓取量不大愿意接受较高的资源开销和较慢的速度关键判断公式可以这么理解如果普通请求能拿到数据的 80%就别轻易上 Selenium如果 Selenium 要开的页面数量很大也要重新评估因为浏览器实例的资源消耗会随数量线性增长。两种情况交叉出现时就用普通请求处理能处理的部分Selenium 只负责攻坚。7.2 不要迷信“伪装成浏览器”反爬的本质是判定“你是不是真人”处理 403 越久越能体会到一个事实反爬系统真正想判断的不是“你是不是浏览器”而是“你是不是真人”。浏览器只是真人的常见载体反爬系统通过浏览器特征来推断背后是不是人类操作。普通请求的问题是“没有浏览器载体”Selenium 的问题是“有了载体但没有真人行为”。想让普通请求更像真人你需要补单位时间内的请求形态比如随机延时、请求顺序乱序、请求头组合变化。想让 Selenium 更像真人你需要模拟鼠标移动、滚动、点击位置和停留时间。这些行为的复杂度远高于请求头伪造但也是突破高阶反爬的必经之路。我目前的做法是在 Selenium 场景里加入随机滚动和鼠标移动import random from selenium.webdriver.common.action_chains import ActionChains # 随机滚动页面 for _ in range(random.randint(2, 5)): driver.execute_script(fwindow.scrollBy(0, {random.randint(100, 500)});) time.sleep(random.uniform(0.3, 1.2)) # 随机移动鼠标 action ActionChains(driver) action.move_by_offset(random.randint(10, 200), random.randint(10, 200)) action.perform()这些行为虽然简单但确实能降低自动化识别的命中率。要注意的是行为模拟不能完美覆盖所有检测维度遇到强风控站点时仍然会有一批请求被判 403这时候只能依赖重试和代理换 IP。7.3 一个值得长期维护的调试清单最后分享一个我很私人的调试清单每当遇到 403我按顺序排查一遍手机热点访问目标页面确认资源本身可访问curl 裸访问记录基础响应和响应头curl 带完整 UA 访问对比与裸访问的差异requests 模拟完整请求头观察是否 403requests 增加 Cookie 初始化的前置访问流程如果仍 403用 Selenium 无头模式访问检查页面状态如果 Selenium 也 403检查 webdriver 标记和浏览器启动参数最后才考虑代理池、IP 更换、频率降低等流量层面优化这套流程按照“成本从低到高”排列每一步都能得到明确的诊断信息。我自己用这套流程处理了多个模拟项目的 403绝大多数问题能在第 4 步到第 6 步之间定位到根源。写到这里想起之前某个模拟项目连续调试到凌晨反复在 requests 和 Selenium 之间切换结果最后发现只是 Session 没有维持好 Cookie 的新鲜度。403 这类问题最折磨人的地方就在这里——它不会直接告诉你哪里错了只给你一个冷冰冰的状态码。但你只要把维度拆开逐个做对照实验大部分问题都能在一个可预期的时间范围内找到答案。希望这篇里的经验能让你在处理 403 时少走几步弯路。