☰
爬虫伪装实战:请求头、Cookie、指纹与行为节奏全攻略
2026/10/13 18:32:16 网站建设 项目流程

做数据采集这几年,我见过太多人把精力花在“藏身份”上,结果还是会收到一排403。真正好用的爬虫伪装,不是堆一屏幕的UA,而是让每一次请求都像一个坐在屏幕前的人类。这篇文章我会把一套可复用的伪装方案拆开讲,从请求头、Cookie、浏览器指纹到访问节奏,每一步都有可以直接跑的代码。无论你是刚写爬虫的初学者,还是被反爬打到怀疑人生的工程师,都可以对照着做。

先说清楚边界:以下所有内容只用于你自己的网站、公开接口开发、或者明确允许抓取的数据源。抓取数据前请查看目标网站的robots协议和服务条款,别把技术用到不该用的地方。伪装不是为了突破什么,而是让合法的采集请求保持礼貌、减少误伤。

1. 爬虫伪装的核心逻辑:让请求通过“行为侧写”

1.1 服务端到底在怀疑你什么

反爬系统判断一个请求是不是爬虫,从来不是看单一指标,而是看一组特征是否自洽。最常见的是下面四类。

第一是频率。一个普通用户不可能每秒请求同一个页面五次。服务端会统计单位时间内的请求数量,一旦超过阈值,直接拒绝访问或者弹出验证码。这个阈值往往不是固定的,而是根据来源、时段、路径动态调整的。

第二是请求头完整性。浏览器每次发出请求,都会带上一套固定的Header字段,包括User-Agent、Accept、Accept-Language、Referer、Sec-Fetch-*等。爬虫请求如果缺字段,或者字段之间明显矛盾,比如UA是安卓手机,操作系统却是macOS,就很容易被标记异常。

第三是会话一致性。正常用户的Cookie、登录态、页面浏览顺序是连续的。你第一次访问某个页面拿到Cookie,第二次带着它继续访问,这是正常逻辑。但如果每个请求都是新的会话,没有任何Cookie,且请求序列完全是随机的,就会触发“新访客暴增”的告警。

第四是行为模式。人类打开网页会停顿、滚动、移动鼠标、点击链接,阅读时间也不均匀。爬虫的行为通常是直线型的:发起请求,拿结果,再发起下一个请求,中间没有任何自然停顿。服务端通过日志能轻松发现这种“机器节奏”。

理解了这四点,你就明白了所谓的伪装,其实是在制造一个“合理的人设”:一个真实、连续、有节奏的人类访客画像。而大多数人的误区,恰恰是想通过隐藏来源地址来解决问题,这反而会让整条请求链缺少上下文,更容易被看穿。

1.2 伪装的分层策略

我自己的做法是把伪装分成四个层级,优先级从高到低,成本从低到高。

层级主要工作相对成本说明
请求头层UA、Referer、Accept等字段低入门必做,直接决定第一印象
会话层Cookie、登录态、Session保持中维持“同一人”的身份连续性
行为层随机延时、鼠标轨迹、滚动低到中让访问节奏像真实用户
指纹层TLS指纹、HTTP/2指纹、Canvas高对付高级反爬,不是所有场景都需要

不要一上来就追求最高级方案。先做好请求头和行为节奏,大多数场景已经够用。指纹层是最后的手段,因为它会显著降低请求效率和代码可维护性。

2. 请求头伪装:构建一套自洽的浏览器身份

2.1 User-Agent不是越长越好

很多人收集一堆UA字符串,随机选一个塞进请求里就完事。其实UA只是浏览器身份的一小部分,真正起作用的是整套Header组合是否自洽。

比如一个典型的Chrome UA是这样的:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36

注意看括号里的信息:Windows NT 10.0表示操作系统是Windows 10,Win64表示64位。如果你的请求是从Linux服务器发出去的,却带一个Windows UA,服务端从TCP握手和操作系统特征上就能看出端倪。

所以,UA必须和你的运行环境匹配。如果你在Windows本机调试,可以放心用Windows UA;如果你跑在Linux服务器上,建议用“Linux”内核特征的UA,或者干脆用后面说的curl_cffi去模拟完整浏览器指纹,而不是手工组装。

另一个容易忽略的点是版本一致性。UA里的Chrome版本,应该和后面请求头里的Sec-CH-UA、Sec-CH-UA-Platform、Sec-CH-UA-Mobile这些字段对齐。比如UA是Chrome/124,Sec-CH-UA里却写的是Chrome/91,明摆着是拼凑出来的。

2.2 关键请求头之间的“人设一致性”

除了UA,下面这几个Header对爬虫伪装同样重要:

  • Accept:浏览器通常会写text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8,表示接受的内容类型及优先级。
  • Accept-Language:比如zh-CN,zh;q=0.9,en;q=0.8,表示语言偏好及优先级。
  • Referer:表示来源页面。如果你直接打开某个网址,Referer通常为空;如果你从搜索结果点进去,Referer就是搜索引擎地址。爬虫抓数据时,Referer要和你的访问路径一致,否则会被认为是“无来源请求”。
  • Sec-Fetch-*:这是新一代浏览器自带的安全元数据,包括Sec-Fetch-Site、Sec-Fetch-Mode、Sec-Fetch-Dest。比如从网站首页跳转到详情页,Sec-Fetch-Site应该是same-origin;如果直接访问一个图片资源,Sec-Fetch-Dest就是image。

下面这段Python代码,展示了一个尽可能贴近真实浏览器的请求头组合:

import random import requests URL = "https://example.com/data" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.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": "zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Referer": "https://example.com/", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Dest": "document", "Upgrade-Insecure-Requests": "1", "Connection": "keep-alive", } session = requests.Session() resp = session.get(URL, headers=headers) print(resp.status_code)

注意Accept-Encoding不要随意写,因为服务器会根据它来决定是否压缩响应。如果你写了br,但没有对应的解压能力,会收到乱码;requests一般会自动处理gzip和deflate,所以这里用gzip, deflate, br也问题不大。

使用requests.Session而不是直接requests.get,是为了让Cookie和连接状态在多次请求之间保持连续,这正好衔接下一节要说的会话伪装。

2.3 请求头顺序和大小写的隐藏作用

你可能觉得Header大小写无所谓,但HTTP/2协议里,浏览器发送的Header顺序是固定的,例如:method、:authority、:scheme、:path,然后是accept、accept-language等。反爬系统完全可以通过Header的排序和大小写格式,判断出请求是来自真实浏览器,还是来自某语言的标准库。

requests库默认把Header字段首字母大写,并且顺序不一定和浏览器一致。遇到比较严格的反爬,这可以被当作判断依据。

解决办法是换用能控制发送顺序的HTTP客户端,比如httpx的http2模式,或者用curl_cffi直接模拟浏览器。下面这段代码用curl_cffi发送请求,Header顺序和TLS指纹都由底层库接管:

from curl_cffi import requests as curl_requests resp = curl_requests.get( "https://example.com/data", impersonate="chrome", ) print(resp.status_code, resp.text[:200])

impersonate="chrome"表示模拟Chrome浏览器的TLS指纹和Header顺序。它是目前应用层伪装里比较省事的做法,后面第4节还会详细展开。

3. Cookie与会话状态:维持“同一人”的身份连续性

3.1 无状态请求和有状态会话的差别

服务端识别用户身份,很大程度靠Cookie。第一次访问网站时,服务器会在响应里下发一个或多个Cookie;浏览器把Cookie存下来,下一次请求时自动带上,服务器一看就知道“你之前来过”。

如果用requests.get而不是Session,每次请求都是全新的无状态连接,没有携带任何Cookie。在服务端看来,这相当于一个永远记不住你是谁的新访客。当“新访客”的请求量超过正常水平时,评分系统会自动把你归为高风险来源。

所以,凡是需要多次请求才能拿到的数据,一定要用Session。

import requests session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 ...", }) # 第一次请求,服务端下发Cookie resp1 = session.get("https://example.com/login") print(resp1.cookies) # 第二次请求自动携带上一次的Cookie resp2 = session.get("https://example.com/profile")

Session对象会维护一个CookieJar,自动处理响应里的Set-Cookie,并在后续请求中带上。这比手动拼接Cookie头要可靠得多。

3.2 从浏览器一键提取Cookie

有些目标网站需要登录才能访问数据。你当然可以用爬虫模拟登录,但流程复杂,还容易触发验证码。很多时候,更稳妥的做法是从自己的浏览器里复制登录后的Cookie,然后手动塞给请求客户端。

具体方法是:在浏览器里打开目标页面,按F12进入开发者工具,切换到Network面板,刷新页面,找到第一个文档请求,右键选择“Copy as cURL”。你会发现这段命令里有一长串Cookie:头。把Cookie值取出来,填到你的请求里:

import requests from http.cookiejar import CookieJar cookies = { "sessionid": "复制来的值", "user_token": "复制来的值", } session = requests.Session() session.cookies.update(cookies) resp = session.get("https://example.com/dashboard")

如果你用的是Playwright做浏览器自动化,还可以直接读取浏览器的Cookie,无缝传递给requests:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") # 手动登录一次,或通过UI自动登录 cookies = context.cookies() browser.close() # 把cookies转成requests要的格式 cookie_jar = {c["name"]: c["value"] for c in cookies} session = requests.Session() session.cookies.update(cookie_jar)

这算是“半自动”方案,兼顾了Requests的效率和浏览器的真实指纹。

3.3 Session保持的注意事项

Cookie不是设置一次就永远有效的。它有Domain(所属域名)、Path(生效路径)、Expires(过期时间)。手动设置Cookie时容易踩几个坑:

第一,Cookie值里如果包含特殊字符,比如分号、逗号、空格,不要直接拼在Header字符串里,用字典传参可以避免格式问题。

第二,Session的Cookie会自动更新,但如果你手动设置的Cookie和服务器下发的同名Cookie冲突,可能会被覆盖。遇到这种情况,建议以服务端下发的为准,观察响应头里的Set-Cookie。

第三,登录态可能因为服务器校验“请求头中的UA”和“登录时记录的UA”不一致而失效。所以提取Cookie的浏览器UA,最好和你的请求UA保持一致。

我自己就踩过这样的坑:在Chrome里登录后,拿到Cookie,却在代码里换了另一个UA去请求,结果服务器返回302重定向到登录页。后来把UA改成和浏览器完全一致,问题立刻消失。

4. 浏览器指纹伪装:让特征不再“一眼假”

4.1 为什么普通requests会被一眼识别

一个成熟的爬虫工程师,早晚会遇到“请求头全对,Cookie也有,但依然被拒”的情况。这时候大概率是卡在了指纹层。

requests底层使用Python标准库的socket和TLS实现,和Chrome、Firefox等浏览器的TLS握手特征完全不同。反爬系统通过分析TLS握手中的ClientHello参数,可以生成一个哈希值,业内常称其为“TLS指纹”。不同的HTTP客户端有不同的TLS指纹,这个指纹很难通过单纯设置Header来自定义。

类似的还有HTTP/2指纹。浏览器在HTTP/2连接中发送的帧顺序、SETTINGS参数、窗口大小等,和Python的requests库有显著差异。服务器可以用这些特征识别出你用的是爬虫工具,而不是浏览器。

这也解释了为什么有的网站你用浏览器打开一切正常,用requests一访问就马上被拦。

4.2 用curl_cffi直接模拟Chrome指纹

目前比较省事的方案是使用curl_cffi,它把curl库的TLS指纹模拟能力封装成了类似requests的API。你不需要自己去构造TLS握手,直接指定impersonate参数即可。

pip install curl_cffi
from curl_cffi import requests as curl_requests resp = curl_requests.get( "https://example.com/data", impersonate="chrome", )

impersonate可以填chrome、firefox、safari等,也可以指定具体版本,比如chrome124。curl_cffi会自动设置TLS指纹、HTTP/2指纹和部分Header顺序。

为什么推荐这个库,而不是自己写底层协议?因为指纹模拟涉及大量二进制参数,自己调优的周期太长,而curl_cffi本身就在持续跟进各浏览器版本的指纹特性,更新速度远快于个人维护。

使用时有两点经验:

  • 优先选目标环境下主流的浏览器版本。如果你的采集目标主要是国内网站,国内用户Chrome占比最高,那就默认用Chrome;如果目标是苹果生态的工具站,可以考虑用Safari。
  • 不同版本的指纹差异会体现在细节上,不要盲目追新。一个很新的Chrome版本,UA和指纹特征之间可能出现不匹配,反而暴露。

4.3 更重的方案:真实浏览器自动化

如果需要执行JavaScript、渲染页面,或者目标网站对webdriver特征做了检测,那就要用Playwright或Puppeteer启动一个真实的内核。

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", locale="zh-CN", viewport={"width": 1366, "height": 768}, ) page = context.new_page() page.goto("https://example.com") page.wait_for_timeout(2000) page.mouse.move(100, 100) page.mouse.wheel(0, 300) page.wait_for_timeout(1500) html = page.content() browser.close()

浏览器自动化最大的优势是,所有的JavaScript钩子、Canvas指纹、WebGL指纹、字体列表、时区、屏幕分辨率,全部是真实值,不存在逻辑矛盾。缺点是慢,非常慢。一个页面从打开到渲染完可能需要三五秒,远不如HTTP客户端直接请求来得高效。

所以我的思路是分层:能用curl_cffi拿到的静态数据,绝不上浏览器;只有遇到需要执行JS的页面,才让Playwright上场。

5. 行为伪装:把“节奏感”伪装出来

5.1 随机延时不是简单random

反爬系统最擅长抓规律。如果你每隔两秒请求一次,间隔时间完全一致,图表上看就是一条直线,这在统计上是极不自然的。真正的用户行为,访问间隔会呈现一种抖动,有时候连着点几个链接,有时候半天不动。

更好的模拟方式是让延时服从某种分布,比如正态分布或者对数正态分布,而不是完全均匀的随机数。

import random import time def human_delay(): # 以均值为3秒、标准差为0.8秒的正态分布随机延时 delay = random.gauss(3, 0.8) # 防止出现负数 return max(0.5, delay) for page_url in url_list: resp = session.get(page_url) # 处理数据... time.sleep(human_delay())

如果你要请求的页面比较多,还可以加入指数退避逻辑:一旦收到429状态码,第一次等待5秒,第二次等待10秒,第三次等待20秒,逐次翻倍,直到恢复200。

import requests import time max_retries = 5 base_wait = 5 for attempt in range(max_retries): resp = session.get(url) if resp.status_code == 200: break if resp.status_code == 429: wait = base_wait * (2 ** attempt) print(f"被限流,等待 {wait} 秒") time.sleep(wait)

5.2 模拟鼠标移动和滚动

使用纯HTTP客户端时,你无法模拟鼠标行为,但服务器可以通过请求顺序间接推断:真实用户阅读一个页面通常会滚动,然后点击页面里的链接,而不是从第1页直接跳到第5页。所以抓取列表页时,从第1页、第2页、第3页依次往后翻,比随机跳页更自然。

如果上了Playwright,可以直接模拟鼠标和滚动。

page.goto("https://example.com/list") # 向下滚动页面,模拟阅读 for _ in range(5): page.mouse.wheel(0, 500) page.wait_for_timeout(random.uniform(300, 800)) # 偶尔移动一下鼠标 page.mouse.move(random.randint(200, 800), random.randint(300, 600)) page.wait_for_timeout(random.uniform(500, 1200))

需要注意的是,不要把鼠标移动和滚动写得过于规律。每一次的位移、停顿时间、滚动距离都要有随机性,才像真人。

5.3 遇到验证码时的正确姿势

无论伪装得多好,总有一刻会撞上验证码。很多人的第一反应是“换个方式重试”,但高频重试只会让风控判定更确认你是机器。我的建议是:

第一步,立即停止当前请求,把访问频率降到一个更低的水平。第二步,观察验证码的类型。如果是滑块或点选,可以使用Playwright人工介入处理一次。第三步,等那个Session稳定后,再以更慢的速度继续任务。

还有个小技巧:不要让所有线程都同时收到验证码然后一起重试,那会把风控警报拉满。串行处理,一个线程解决验证码后,其他线程再继续,会安全很多。

6. 常见问题与排查技巧实录

这里把我踩过的坑整理成一张速查表,方便你对照排查。

现象可能原因排查与解决
返回403 ForbiddenHeader缺失、UA不一致、来源被标记对比真实浏览器的Header,逐项补齐;换用curl_cffi模拟指纹
返回199或空内容响应被压缩、字符集不对检查Accept-Encoding是否包含br;用resp.content而不是.text
验证码频繁弹出访问频率过高、行为节奏太规律增加随机延时,参考第5.1节;必要时降速
Cookie明明设置了,但状态丢失Session未复用、Cookie被覆盖统一使用requests.Session;观察Set-Cookie;避免手动和自动混用
请求头看起来完整,还是被识别TLS指纹或HTTP/2指纹异常启用curl_cffi的impersonate参数
同一个Session请求多页后开始被限流没有翻页间隔、请求路径过短加入阅读行为模拟,先滚动再翻页,增加页面停留时间

排查看起来复杂,其实遵循一个思路就行:先抓包,拿真实浏览器的请求作为样本;再对比,看自己的请求少了哪个字段或者多了哪个字段;最后做减法,先只加必需的伪装,跑一段时间没问题再逐步叠加,不要一次把所有招都用上。

遇到403时,我的排查顺序是:查看请求头是否和浏览器一致,再看Cookie是否带上了,最后才考虑指纹层。很多新手在这一步会直接上重量级工具,结果反而把问题搞复杂。

最后分享一个小技巧

说了这么多,最后说个实际经验。我在做一个新采集任务时,不会上来就追求“完美伪装”,而是先只加请求头和随机延时,跑半天看看封禁率。如果封禁率能接受,就不碰指纹和浏览器自动化;如果不行,再按需往上加。伪装不是叠buff,做对了,一层就够。

还有一点很重要:请求模块的版本要保持更新。尤其是curl_cffi这类指纹模拟库,浏览器一发新版本,底层指纹就会变。定期升级依赖,比你手动改几十行Header更省心。

这篇文章里的代码都不复杂,真正的难点在于理解“为什么这样设计”。你只要把握住“像真人”这个核心,请求头、Cookie、指纹、行为,每个层级都围绕这一点展开,就能做出一个稳定、可维护的采集方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询