反爬机制与浏览器自动化采集实战:从403到稳定数据同步

发布时间:2026/9/9 5:05:28
反爬机制与浏览器自动化采集实战:从403到稳定数据同步 前段时间接了一个内容监测的小项目对方给了一个演示环境我们内部代号叫 Libvio.link。站点本身不复杂登录后有一个按热度排序的内容列表页业务方希望每小时把列表同步到分析表里。我一开始想得很简单requests 直接 GET 一下 JSON 接口抽字段入库就完事。结果第一轮请求就吃了 403第二轮在接口层被签名拦截第三轮直接弹了验证码。折腾两三天后才把整条链路的“反爬机制”摸清楚也把抓取策略从“暴力模拟请求”改成了“让页面自己跑、我们记录结果”的思路。这篇文章就是这次完整复盘把遇到的反爬层级、绕过思路、代码实现和坑位都记录下来既有技术拆解也有合规提醒适合正在做爬虫、自动化测试或者网站风控研究的朋友。1. 先搞清一件事反爬到底在防什么很多人一说反爬就想到验证码其实验证码只是最后一道闸门。真正的反爬机制通常是一层一层叠加的比如请求头层、访问行为层、浏览器环境层、接口参数层每一层都有自己的判定逻辑。Libvio.link 这个演示站的防护结构就很有代表性它基本把常见的几类拦截方式都集成了一遍。1.1 请求头与 Cookie 基础校验第一层拦截往往最简单但最有效检查请求头是否像真实浏览器。早期很多爬虫脚本只改一个 User-Agent其他字段全是 Python 默认值服务器端一眼就能识别出来。但近两年的风控早就升级了重点看 Sec-Fetch-*、Accept-Language、Referer 这类字段之间的关系。我测试时发现只带 UA 请求会立刻被拒补齐 Accept、Accept-Language、Accept-Encoding 后能通过一部分这说明目标站会比对请求头之间的“一致性”。例如一个声明自己是 Chrome 的请求却连 Sec-CH-UA 平台信息都没有在服务端眼里就很可疑。还有 Cookie 校验很多站点首页会先在浏览器里执行一段 JS通过后写入一个校验 Cookie后续接口才会放行。用裸 HTTP 库直接请求没有这层 Cookie接口直接 403。1.2 浏览器指纹与自动化环境检测第二层就看你的运行环境像不像真实浏览器。服务器可以通过下发 JS 脚本让浏览器生成一个 canvas 绘图、读取 WebGL 渲染参数、检测 AudioContext 的音频处理结果等组合成一个指纹 ID。如果每次请求的指纹差异很大就可能被标记为异常。对爬虫来说更致命的是自动化特征检测比如navigator.webdriver是否为 true、浏览器窗口是否真实可见、有没有加载某些自动化插件、调用链里有没有 CDP 协议痕迹等。这个演示环境里我试过 Selenium 裸起动即使加了--headlessnew加载几个页面之后依然被识别出来最终走向验证码流程。老实说现在成熟的设备指纹方案识别自动化浏览器准确率已经非常高硬在特征层面做隐藏会陷入永无止境的攻防战。1.3 接口参数加密与签名逻辑第三层是业务接口层的安全设计也是“看起来最硬核”的部分。真实页面展示数据时并不是简单地向/api/list发一个 GET 请求而是会带上通过 JS 生成的签名参数例如timestamp、nonce、sign等。服务端收到请求后先校验时间是否合理、随机数是否用过、签名是否和参数对得上。这种设计意味着即使你把请求头伪装得完美、Cookie 也都带上只要缺少签名还是会拿不到数据。如果只从 JS 源码里搜索签名逻辑往往会看到巨大且混淆过的代码想逆向出完整的签名算法非常耗费时间。我在这个演示环境里发现它的签名里还掺了页面某个 DOM 节点的内容如果离开真实页面根本没法生成合法签名。1.4 访问频率与行为轨迹模型最后一层并不是单次请求能触发的而是需要积累一段时间的访问数据。服务器会按用户维度维护一个行为画像每分钟请求多少次、每次请求间隔是否均匀、是否总是跳过图片和静态资源、点击路径是否自然等。如果特征像机器即使前面每一层都通过了也会被降级处理或者直接弹验证码。Libvio.link 演示站的行为模型里配置了滑动窗口频率限制。用我后来写好的正常采集逻辑测试如果每秒发 2 个请求以上跑到第 20 个请求左右必然触发验证码把频率降到每 5 秒一个请求同时模拟先访问列表页、再点击详情页的路径跑一晚上都不会触发。可见频率和轨迹往往比请求伪装更重要。反爬机制小结可以参考这个表格层级主要识别点典型拦截结果请求头UA、Sec-Fetch、Cookie、Header 一致性403 / 拒绝访问浏览器环境Canvas、WebGL、自动化检测放行到验证码接口签名timestamp、nonce、sign数据接口 401 / 参数错误行为模型频率、点击轨迹、静默资源限流、封禁、验证码2. 思路转变别从零模拟请求改用“页面自取”方案搞清楚反爬机制之后我最初还是想着怎么去逆向签名、伪造指纹但越做越觉得这条路不划算。只要目标站老板换了安全方案所有逆向工作都可能作废。后来我换了一个思路也是业内现在更主流的玩法用无头浏览器驱动真实页面让页面自己加载、自己执行 JS、自己生成签名我们只负责记录页面发出的网络请求把已经通过全部校验的数据解析出来。2.1 浏览器自动化为什么会有效核心原因是只要我们的浏览器是真实 Chromium并且是以接近真实用户的方式在使用那么服务器下发的 JS 会正常执行生成的 Cookie、指纹、签名都是合法的。服务端的每一层校验都是基于“这是一个正常用户在正常浏览器里操作”的假设浏览器自动化正好可以完整复现这个假设。这比直接用 requests 去逆向要省事得多。你不需要去读混淆后的 JS 找签名算法不需要手动补 Cookie也不需要手工维护加密参数。你只需要让页面完成一次真实加载然后从浏览器网络层把接口响应“接住”。2.2 “接住”接口响应而不是下载 HTML很多人写爬虫时还停留在抓 HTML 再解析的思路上这在面对动态页面时效率很低。现代前端站点普遍用 Vue、React 这类框架HTML 里只有空壳真实数据都来自 XHR 或 Fetch 请求。与其去解析渲染后的 DOM不如直接监听浏览器里每一个网络响应筛选出包含需要数据的 JSON 接口。好处有几点第一JSON 结构稳定不会像 DOM 那样因为改版而全乱第二省去解析 HTML 的步骤性能更好第三接口返回的数据往往比页面上展示的更完整字段命名更清晰。这个方法的前提是页面能正常加载出数据也就是说前面的 Cookie、指纹、签名都由页面自己处理掉了。2.3 该省的地方就省不做过度设计有人觉得既然用了浏览器自动化就干脆全程 Playwright 跑到底。我的实际经验是浏览器负责复杂环节纯 HTTP 负责简单环节能省则省。比如拿到了接口返回的 JSON 后后续入库、清洗、通知这些动作完全没必要再开一个浏览器去执行直接把数据交给 Python 脚本处理就行。让浏览器一直开着本身就是资源浪费还更容易触发风控。这个原则也可以再延伸一下不是所有站点都需要浏览器自动化。如果目标站只是静态页面或者有公开 API那就直接用 requests 或 httpx别给自己找事。Libvio.link 这个场景特殊因为它的接口签名和页面状态强绑定所以浏览器自动化才是正确答案。判断标准就是一条只用 HTTP 库能不能稳定拿到数据答案不确定的时候就用浏览器方案先跑通再说。3. 项目里的核心技术点拆解三层方案如何落地大概梳理一下这个项目里最终落地了三套抓取方案对应不同的访问路径。第一套是纯 HTTP 请求用于加载静态资源和登录页第二套是浏览器上下文操作用于需要完整执行 JS 的页面第三套是接口响应拦截用于获取最终数据。这三套方案不是互相替代的而是配合使用。3.1 纯 HTTP 请求也要做到“像浏览器一样请求”先看纯 HTTP 方案。这里说的“像浏览器”不只是一个 UA而是一个完整的 Headers 集合。我在代码里维护了一份真实浏览器的请求头并在每次请求时动态更新其中的 Cookie 和 Referer 字段。一个能用的请求头配置大致是这样的import httpx headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.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, Sec-CH-UA: Google Chrome;v125, Chromium;v125, Not.A/Brand;v24, Sec-CH-UA-Platform: Windows, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: same-origin, Sec-Fetch-User: ?1, Upgrade-Insecure-Requests: 1, Referer: http://127.0.0.1:8080/console/list, } with httpx.Client(headersheaders, follow_redirectsTrue) as client: resp client.get(http://127.0.0.1:8080/console/list) print(resp.status_code)这里要注意几个细节Referer 必须是指向当前页面的来源不能为空Accept-Encoding 不要手动去掉服务端可能根据这个决定是否返回压缩内容Sec-CH-UA 这类 Client Hints 一旦声明就必须和实际浏览器版本对得上否则反而会被识别为异常。实际项目中我更建议直接打开浏览器开发者工具从真实的网络请求里复制完整 Headers比自己手写靠谱得多。3.2 遇到 TLS 指纹识别怎么办在测试过程中还遇到过一个特殊情况即使 Headers 完全复制了浏览器的某些接口仍然返回 403。后来发现这不是 Header 的问题而是 TLS 指纹层面的拦截。Python 的 httpx、requests 默认使用 OpenSSL 发起 TLS 握手其 ClientHello 参数和真实 Chrome 有明显区别服务端从 TLS 握手阶段就能判断出访问者不是浏览器。解决这个问题的是curl_cffi这个库它封装了 curl 的 TLS 实现能够模拟 Chrome、Firefox、Safari 等浏览器的 TLS 指纹。实测下来使用impersonatechrome参数后原本被 403 的接口就能正常返回。代码非常简单from curl_cffi import requests session requests.Session(impersonatechrome) resp session.get(http://127.0.0.1:8080/console/list, headersheaders)需要注意的是TLS 指纹模拟并不适合所有场景。如果服务端同时校验 HTTP/2 指纹、QUIC 协议指纹单靠 curl_cffi 不一定能解决。这时候最稳的做法仍是回到浏览器自动化方案因为真实的 Chromium 发出的网络请求天然具备完整的指纹特征。3.3 用浏览器接管 Cookie 和签名整个流程里最核心的一步是通过浏览器自动化去“接管”那些必须由真实环境生成的参数。简单说就是让 Playwright 启动一个 Chromium 实例打开目标页面等页面稳定后把浏览器里的 Cookie 和 LocalStorage 导出交给后续的 HTTP 请求使用。为什么这么做最关键的原因是签名参数往往有时效性比如 5 分钟或者 10 分钟后就失效。我们不能每打一个请求都重启浏览器那样太慢。正确的做法是启动浏览器完成初始化通过开发者协议读取到合法的 Cookie 和时间戳参数然后在一个有效期内复用它去请求其他接口。导出 Cookie 的代码片段大致如下from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() page.goto(http://127.0.0.1:8080/console/list, wait_untilnetworkidle) cookies context.cookies() storage_state context.storage_state() with open(state.json, w, encodingutf-8) as f: import json json.dump(storage_state, f, ensure_asciiFalse, indent2) browser.close()这个state.json文件里保存了登录态和页面生成的关键 Cookie。后续如果把定时采集脚本部署到服务器不需要每次重新登录直接把这个状态文件加载到新的浏览器上下文里就行。当然这也有一个前提Cookie 的过期时间不能太短如果目标站设置了几分钟就过期那还是得把完整浏览器常驻运行或者定时刷新状态。3.4 接口数据拦截与二次解析最后一步就是真正拿数据。这一步要用 Playwright 的网络监听能力。我们不关注 HTML 内容只监听包含数据接口的响应把返回的 JSON 结构保存下来。这样做的优势是由于页面是真实加载的服务端校验过的签名、Header、Cookie 都已经在内我们几乎可以稳定拿到数据。以下是一个典型的拦截逻辑import json from playwright.sync_api import sync_playwright API_PATTERN **/api/items** results [] def on_response(response): if response.url API_PATTERN.replace(**, ) or /api/items in response.url: if json in response.headers.get(content-type, ): data response.json() results.append(data) with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context(storage_statestate.json) page context.new_page() page.on(response, on_response) page.goto(http://127.0.0.1:8080/console/list) page.wait_for_selector(.item-row, timeout10000) for item in results: print(item.get(id), item.get(title)) browser.close()这段代码里有几个细节值得注意监听器要在页面跳转前挂载否则会漏掉请求wait_for_selector的目的是等待页面渲染出真实数据确保接口已经返回打印的字段取决于具体接口结构实际项目中可以解析成 Pydantic 模型再入库。4. 实操过程复盘从 403 到稳定采集的完整路径这轮项目的完整实施路径其实可以概括成一个固定的流程。遇到任何一个反爬严重的站点都可以先按这套流程走一遍快速定位问题出在哪个层级再对症下药而不是一上来就开浏览器做暴力渲染。4.1 第一阶段手工分析接口请求拿到 Libvio.link 演示环境后我第一件事不是写代码而是用浏览器开发者工具手工访问一次页面打开 Network 面板把关键的接口请求找出来。这一步的目的是搞清楚三个问题页面最终从哪个接口取数、这个接口携带了哪些参数、哪些参数是静态的、哪些是动态生成的。用 Network 面板看请求时我习惯先勾选“Preserve log”。有一次页面发生了跳转如果没有勾选跳转前的请求日志就全丢了。然后按网络耗时排序往往最耗时的那几个请求就是真正的数据接口。找到接口后逐个字段看它的来源如果是 JS 生成的就记下对应的堆栈调用从而判断它绑定在哪个页面生命周期事件上。4.2 第二阶段搭建本地可复现的最小 Demo这一步非常关键。当目标环境的反爬逻辑还不清楚时千万不要直接在生产环境反复试探。我在同一个局域网内复制了一份前端静态资源搭建了一个同样的接口服务构造出和线上几乎一致的校验逻辑然后所有测试都在这套本地 Demo 上完成。本地 Demo 的好处很多频率限制可以调高方便测试加密参数可以从源头观察生成过程最重要的是不会因为大量测试请求给目标服务器造成压力也不会触发不必要的风控。代码开发稳定后再切换到真实环境做小流量验证大大降低了封 IP 和账号的风险。4.3 第三阶段优先用浏览器自动化验证可行性在本地 Demo 上我开始尝试三种不同的采集方式纯 HTTP 请求、curl_cffi 模拟 TLS、Playwright 浏览器自动化。运行结果非常直观纯 HTTP 请求在 Header 层就被拦截成功率最低curl_cffi 能通过 Header 和 TLS 校验但接口签名无法生成拿不到真实数据Playwright 自动化加载页面、读取接口响应一次性成功。这个结果说明当目标站有较强的签名校验和浏览器环境校验时纯 HTTP 方案的天花板很有限。我最终确定的技术方案是Playwright 负责页面加载拦截接口响应将数据落地到本地 JSON 文件随后再用纯 Python 脚本做数据清洗和入库。4.4 第四阶段设计定时任务和错误重试采集稳定后我把它封装成一个定时任务。基本流程是先启动浏览器检查页面是否能正常打开如果能就说明 Cookie 和状态还有效执行数据拦截如果打开后进入了登录页说明状态过期需要重新登录并更新 state.json。import time from playwright.sync_api import sync_playwright def job(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context(storage_statestate.json) page context.new_page() page.goto(http://127.0.0.1:8080/console/list, timeout30000) if login in page.url: print(登录态失效需要重新登录) # 这里触发通知机制而不是自动打码 browser.close() return page.wait_for_selector(.item-row, timeout10000) browser.close() if __name__ __main__: job()这里有一个重要的设计原则当登录态失效或者遇到验证码时不要试图通过自动化方式自动绕过而是降级为人工介入或者干脆跳过这一轮。自动过验证码不仅成功率不稳定而且很容易导致账号被重点监控风险收益比很低。5. 运行了两周后复盘几个容易踩的坑把采集脚本跑起来只是开始真正的考验来自长期运行中的各种意外情况。Libvio.link 这种演示环境因为防护规则比较固定踩坑数量不算多但有几个问题非常典型也很有代表性。5.1 固定间隔请求反而比随机间隔更容易被识别我第一次做的定时任务用的是非常稳定的间隔每 10 分钟请求一次。结果跑了不到半天就发现接口开始随机返回空数据重启浏览器也无济于事。后来才意识到真实用户的使用节奏不可能是完美的 10 分钟一次这种过于规律的请求本身就是机器特征。解决方案很简单把请求间隔改成随机值。例如在 480 秒到 720 秒之间随机取一个值作为下一次请求等待时间。随机范围模拟了不同类型用户的操作习惯也能有效降低被行为模型识别出来的概率。import random import time interval random.randint(480, 720) time.sleep(interval)5.2 浏览器状态多实例共存导致数据冲突在测试阶段我经常同时打开多个浏览器实例各自维护独立的上下文。这带来一个问题不同实例生成的 Cookie 和状态会互相覆盖导致最后拿到的一个实例状态可能丢失了登录态进而引发采集失败。正确做法是固定使用同一个持久化上下文避免一个任务里反复创建新的浏览器实例。Playwright 支持持久化上下文创建方式可以指定一个 user_data_dir 目录这样浏览器每次启动都会自动加载之前的会话状态不需要每次手动导入导出 Cookie。from playwright.sync_api import sync_playwright with sync_playwright() as p: context p.chromium.launch_persistent_context( user_data_dir./user_data, headlessTrue, ) page context.new_page() page.goto(http://127.0.0.1:8080/console/list)这种方式可以解决两个问题一是登录状态不会频繁丢失二是每次启动的指纹相对稳定不会因为重新生成浏览器指纹而被风控系统盯上。5.3 数据解析时不能只依赖页面 DOM最初我打算直接从页面 DOM 里抓取内容因为接口返回的 JSON 字段不够直观。但后来发现页面 DOM 经过框架渲染后类名可能带 hash 前缀并且列表的排序规则在页面上做了二次处理直接解析 DOM 得到的顺序和接口原始顺序不一致。改成解析接口 JSON 后数据干净整齐字段完整。这件事给我的教训是能从网络层拿数据就不要从 DOM 层抠文本网络层的结构稳定性远高于页面渲染结果。5.4 常见问题速查表把这两周遇到的高频问题汇总一下问题现象可能原因解决方案一直 403请求头不完整缺少 Sec-Fetch 或 Cookie浏览器中复制完整请求头TLS 握手失败库的 TLS 指纹被识别使用 curl_cffi impersonate接口报签名错误签名与页面状态绑定用 Playwright 加载真实页面登录态频繁丢失每次新建上下文未持久化使用 launch_persistent_context请求频率很高时触发验证码触发行为模型降低频率并加入随机间隔页面能打开但接口空数据Cookie 过期或接口防重复调用更新 state.json检查接口签名时效6. 合规与安全的边界什么能做什么不能做这部分本来不在技术复盘范围内但我还是想专门用一节来聊。因为爬虫、反爬、绕过的技术讨论如果缺少边界意识很容易从“技术研究”滑向“违规采集”。做这个项目之前我首先确认了目标环境是业务方提供的演示站点数据内容和接口逻辑都已经授权用于测试分析。这意味着我在本地做的所有实验都不会对真实服务造成影响也不涉及版权内容或个人隐私数据。如果是真实站点尤其是有明确禁止爬取的声明、需要登录后才能访问的内容或者包含会员专属版权素材就该谨慎评估是否继续。合规爬虫有几个底线可以供大家参考第一目标站 robots.txt 或用户协议明确禁止自动化采集时应当停止相关工作第二尊重版权和个人信息保护不能把抓取的数据用于商业变现或公开传播第三控制请求频率避免对目标服务器造成压力不要用分布式高频请求去“击穿”别人的风控系统第四不破解用户身份验证机制不绕过付费墙不采集非公开数据。写代码之前先想清楚这几个问题可以避免很多后续麻烦。技术能力是一个方面判断力是另一个方面后者往往更重要。最后分享一点个人体会整个项目做完我最深的体会是搞定反爬并不等于把每一层防护都强行破解掉。真正高效的路线是选择与目标站点安全机制“兼容”的访问方式让页面自己完成复杂的校验流程我们只取最终结果。这既减少了代码维护量也降低了对目标环境的影响。以后再遇到类似内容监测需求我大概率会直接考虑浏览器自动化方案并且从一开始就把合规边界和请求频率设计进去。这样写出来的采集工具不仅结构更清晰生命周期也更长。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询