资讯详情

资讯详情

网站反爬机制全解析:从请求头到字体混淆的破解之道

你有没有遇到过这种情况爬虫代码写得挺顺本地跑得飞快结果部署上线第二天对方网站直接给你弹出一个验证码页面或者干脆所有请求都返回302重定向。更气人的是从浏览器里看一切正常可你的代码就是拿不到数据。这不是你代码的问题而是撞上了反爬机制。反爬机制这四个字在爬虫工程师心里基本等同于“工资的来源”和“发际线的杀手”。很多人一听到反爬就想到各种高大上的加密算法、指纹识别其实大部分网站的反爬远没有那么玄学。它们通常遵循一套固定的分层逻辑从最简单的请求头校验到复杂的浏览器环境检测再到内容层面的字体混淆一层一层像筛子一样过滤掉非人类流量。这篇文章我就按自己的实战经验把这些常见反爬机制做一个系统性的分类梳理并且给出对应每一类的思考路径和破解思路。我不会只扔结论而是尽量讲清楚“对方为什么要这么设计”以及“我们为什么要这么拆”这样你以后再遇到没见过的反爬也能自己顺着链路摸出解法。1. 先想清楚反爬到底在防什么它的本质是一场成本博弈很多刚入门的同学会把反爬理解为“绝对防御”觉得网站方就是想尽一切办法让爬虫彻底爬不到。这个理解是错的。如果真的想彻底防住自动化访问理论上所有网站都可以强制要求必须使用特定浏览器、必须通过复杂的双向认证才能访问但没有任何一个正常网站会这么做因为那会把真实用户也一起挡在门外。反爬的本质是提高爬虫的访问成本直到它高过爬取方预期的收益。这里有个很形象的类比反爬就像给自行车上锁。真正的小偷拿液压剪几秒钟就能剪断那为什么还要锁因为锁的作用是让绝大多数顺手牵羊的人放弃而不是防住专业的盗贼。网站的反爬也是一样它防的是那些批量抓取、不加掩饰的初级爬虫至于那些愿意投时间投精力逆向的团队反爬能做的只是尽量拖延和增加成本。从这个角度出发你会发现所有反爬机制的设计都有一个共同点**尽量把真实用户和爬虫区分开同时又不能误伤真实用户。**这个“不误伤”的约束条件恰恰是我们破解思路的切入点。另一个需要明确的概念是数据的分层。不是所有数据都有同等的反爬强度数据层级典型例子反爬强度完全公开数据首页展示的新闻标题、公司简介弱甚至无反爬半公开数据列表页摘要、商品价格区间中等常见IP限速需操作获取的数据登录后才能看的详情、搜索结果强涉及Cookie和加密参数完全私有数据个人订单、通讯录极强涉及账号风控和设备指纹搞清楚了目标数据在哪一层你就能预估需要投入多少精力去处理反爬避免一上来就大题小做或者相反地踩进坑里。在后面的章节里我会按照请求从发出到渲染完成的链路顺序把反爬机制分成几个层次来讲第一层是请求本身的特征第二层是IP和Cookie维度第三层是行为轨迹和验证码第四层是浏览器环境和JS动态渲染第五层是内容层面的伪装。每一层都对应不同的破解思路。2. 第一道关卡请求特征识别最基础也最容易被忽略2.1 请求头里的身份信息你带全了没有但凡写过几天爬虫的人都听说过要设置User-Agent简称UA但实际见过的请求头不完整导致的封禁案例多得离谱。服务端判断一个请求是不是来自真实浏览器第一件事就是看请求头是否完整且合理。除了UA之外还有Referer、Accept、Accept-Language、Accept-Encoding这几个标准头以及一些现代浏览器会自动加上的头比如Sec-Fetch-Site、Sec-Fetch-Mode、Sec-Fetch-User。如果你用requests默认的请求头去访问服务端立刻就能从UA和Accept的异常组合中判断出这不是浏览器。我之前帮一个朋友排查问题他写的爬虫已经能正常拿到HTML但页面里的某些关键数据接口总是返回403。我让他把浏览器开发者工具里的请求头原样Copy出来对比发现他漏了Sec-Fetch-*这一组头。补上之后问题立刻消失。这只是个很简单的例子但它说明了一个核心思路**破解反爬的第一步永远不是写复杂的代码而是先让你的请求看起来像一个真正的浏览器。**具体操作上我建议直接用浏览器的Copy as cURL功能然后在代码里把对应的请求头完整解析出来而不是自己手写一个简化版。2.2 请求频率与时间窗口服务端怎么判断你是人是机器光有请求头还不够。一个真实用户每秒最多发起两三个请求而脚本可以在毫秒级连续发几十个。服务端只要做一个单位时间窗口内的请求计数就能快速锁定异常流量。常见的频率检测手段有这么几种固定窗口计数比如一分钟内相同IP或相同Cookie的请求数超过阈值就封禁。滑动窗口计数比固定窗口更精细能检测到刚好卡在窗口边缘的突发请求。自定义的业务规则比如访问路径的跳转顺序是否合理、站内搜索的频率是否超出正常范围。我见过最朴素的封禁逻辑就是同一个IP一秒钟内访问超过3次就直接封一小时。这种阈值定得很低几乎不会误伤真实用户但对没做限速的爬虫来说几乎是见一个封一个。2.3 对应的破解思路伪装和节流这一层的破解思路不需要什么高深技巧核心就两个字像人。第一请求头务必齐全且能动态变换。不要所有请求共用同一个UA而是准备一个UA池随机切换。有些网站还会对UA做更细致的校验比如浏览器UA必须搭配对应的Sec-Fetch头使用所以建议成组切换。第二请求频率要控制。不要用固定间隔比如不要写死sleep(3)因为固定间隔本身就是一种机器特征。更合理的方式是用一个随机时间范围比如每次请求之间随机间隔2到5秒遵循短尾分布偶尔连续两次快请求偶尔停个十秒。第三访问路径要有逻辑。如果你爬的是多级页面先从列表页进入详情页是正常逻辑但如果你只疯狂抓详情页而没有任何列表页的访问记录服务端也能看出异常。下面是一个简单的请求头补全和随机延迟示例可以当作基础模板import random import time import requests user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, ] def build_headers(): return { User-Agent: random.choice(user_agents), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Referer: https://example.com/, Sec-Fetch-Site: same-origin, Sec-Fetch-Mode: navigate, Sec-Fetch-User: ?1, Sec-Fetch-Dest: document, } def safe_get(url, sessionNone, max_retries3): for attempt in range(max_retries): try: resp (session or requests).get(url, headersbuild_headers(), timeout10) if resp.status_code 200: return resp except requests.RequestException: pass time.sleep(random.uniform(2, 5)) return None这套东西不复杂但能解决第一层百分之八十的问题。3. 第二道关卡IP维度的风控和Cookie会话校验3.1 IP封禁、限速和地域一致性检查如果你突破了请求头这一层服务端会启动第二层防线IP维度的风控。最常见的策略包括单一IP在单位时间内的请求次数超过阈值直接封禁一段时间。同一IP访问不同账号的频次异常高触发刷号判定。IP地理位置和Cookie里的登录信息地理位置长期不一致触发异地登录风控。数据中心的IP段比如各大云厂商的机房IP被直接标记为高风险即使频率不高也可能被拦截。这一点很多人会忽略。你以为自己加了代理就安全了但如果用的代理是烂大街的免费代理IP段被对方标记了效果反而更差。这里要记住一个原则**代理的质量远比数量重要。**高匿代理、住宅代理、机房代理每一种的存活率和成功率天差地别。实际操作里比较可行的方案是维护一个代理池每次请求随机取一个IP并且对失效IP做淘汰。一个简单的代理池装饰器逻辑如下class ProxyPool: def __init__(self, proxies): self.pool proxies self.failed set() def get_proxy(self): available [p for p in self.pool if p not in self.failed] if not available: self.failed.clear() available self.pool return random.choice(available) def report_failure(self, proxy): self.failed.add(proxy)这只是最基础的框架实际生产中你还需要定期补充新代理、校验代理的响应速度和匿名程度甚至要针对不同目标网站维护不同的IP池。3.2 Cookie的生成、会话保持和动态签名参数IP风控再往上就是Cookie这层。其实很多网站的真实反爬逻辑藏在Cookie里。第一次访问网站时服务端会下发一个Cookie这个Cookie不是随便生成的它可能包含了服务端的Session ID、首次访问时间、一些加密的参数。后续的每一次请求服务端都会校验Cookie的有效性。如果你的爬虫没有正确处理Cookie就会出现这种情况首页能打开但一旦点击进入详情页就被强制跳回首页或者弹出验证码。更麻烦的是现在很多主流网站会在Cookie或者请求参数里加上动态签名。比如某个参数是由当前时间戳、一个固定密钥、还有一些环境信息拼接后做哈希生成的。这个签名的目标就是让请求具有时效性且无法被简单重放。面对这类机制首先要做的事情不是逆向签名算法而是先确认一个问题**这个签名参数是必须在代码里动态生成还是可以直接复用浏览器里的Cookie**很多场景下只要你维持一个足够新鲜的Cookie服务端并不会实时校验签名的生成过程。你可以先把浏览器里的完整Cookie复制出来加到你请求里往往就能绕过去。只有当Cookie失效很快、必须频繁重新获取时你才需要去研究它的生成逻辑。这里推荐的做法是跑一个有头浏览器自动完成首次访问和Cookie获取再把这个Cookie传给requests会话使用。下面是一个混用的思路from playwright.sync_api import sync_playwright def get_fresh_cookie(url): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() page.goto(url) cookies context.cookies() browser.close() return ; .join(f{c[name]}{c[value]} for c in cookies)3.3 破解思路让请求链路保持“温度”这一层的核心思路是维持一个活着的会话。不要频繁创建新会话每次请求都从零开始而是用一个带Cookie的requests.Session保持会话的连续性。另外要注意很多服务的Cookie是和IP绑定的。你今天用代理A获取了Cookie明天用代理B带着这个Cookie去访问对方只要比对一下IP和Cookie的绑定关系就能戳穿你。所以IP和Cookie尽量要配对维护不能混用。4. 第三道关卡行为轨迹和验证码最让人头疼的一层4.1 鼠标轨迹、滚动、点击等行为特征到底怎么判定当请求头、IP、Cookie这些基础维度都过关之后如果网站对你的身份仍然存疑就会祭出行为检测。这一层防的不是你的请求而是你的操作过程。真实用户浏览网页时鼠标移动有加减速曲线滚动页面时快时慢点击按钮之前会有一个短暂的悬停。而脚本的鼠标移动是直线匀速的滚动是瞬间完成的点击之间没有任何停顿。这些差异在行为采集脚本面前异常明显。判断行为是否异常的维度主要包括鼠标轨迹的曲率、速度变化、是否有停顿。从页面加载完成到第一次点击的时间间隔。两次点击之间的间隔是否过于均匀。页面滚动是否出现了超过视口高度的“瞬移”。不过说实话对大部分中小型网站来说做了完整行为检测的还是少数因为需要在前端埋点、后端建模成本较高。只有那些数据价值很高的大平台才会认真做这一块。4.2 验证码形态演进从字符到滑块再到点选和无感验证码是行为检测最常见的落地形式也是爬虫工程师最熟悉的拦路虎。它的演进过程很能说明问题第一代字符验证码歪歪扭扭的数字字母识别靠OCR。第二代滑块验证码拖动滑块到指定位置分缺口滑块和拼图滑块。第三代点选验证码按文字提示点击图片中的对应物体比如“点击图中的红绿灯”。第四代无感验证码用户完全不需要操作后台在毫秒级采集浏览器的各种环境信息和交互特征直接打分判定。从破解难度来看这几代的难度不是一个量级的。字符验证码可以用深度学习模型识别滑块验证码需要模拟拖拽轨迹点选验证码需要目标检测模型定位无感验证码则几乎没有直接破解的入口因为它把判断逻辑放在了海量环境特征的综合评分上。4.3 对应破解思路优先搞清楚触发条件而不是硬刚识别模型很多初学者一遇到验证码就想着训练模型去识别这其实是最笨重的一条路。更高效的做法是先问自己一个问题为什么会触发验证码验证码的出现通常说明前面几层已经暴露了。可能IP太脏可能请求频率太高可能Cookie没有配对好。这时候你先优化IP池、把访问频率降下来、把请求头环境补全验证码出现的概率自然会大幅下降。我在实际项目中超过一半的验证码问题都是通过调整频率和IP质量解决的根本轮不到上识别模型。如果优化之后仍然绕不过验证码那才需要考虑识别方案。对滑块验证码来说核心不是识别缺口的坐标而是模拟拖拽的轨迹。千万别用匀速直线要模拟一个先加速后减速、带一点上下浮动的轨迹。对点选验证码则需要一套目标检测的模型成本明显更高这时候你要权衡一下如果这个网站的数据真的重要到非要爬不可那就值得投入否则不如换个思路走API或者买数据。下面是模拟滑块轨迹的一个简化示例重点是轨迹点的时间间隔和位移变化import random def generate_slide_track(distance): track [] current 0 mid distance * 0.8 t 0.2 while current distance: if current mid: speed random.uniform(3, 5) else: speed random.uniform(1, 2) current speed t random.uniform(0.1, 0.2) track.append((min(current, distance), t)) return track5. 第四道关卡浏览器环境检测和JS动态渲染5.1 数据不在HTML里而在XHR接口里如果你只爬那些在HTML文档里直接输出的数据那还处于爬虫的入门阶段。稍微复杂一点的网站数据都是通过JavaScript动态加载的页面源码里根本看不到核心数据只有一堆script标签和初始化的空壳DOM。这时候爬虫的逻辑要从“抓HTML”变成“抓接口”。打开开发者工具的Network面板刷新页面观察页面加载过程中的所有XHR和Fetch请求找到真正返回业务数据的那个接口。这里有个常见的坑很多接口有自定义加密参数比如一个token或者sign直接请求会返回参数错误或数据为空。5.2 WebDriver特征检测和浏览器指纹要应对JS渲染一种思路是直接用无头浏览器比如Selenium、Playwright让浏览器帮你把JS执行完再读取渲染后的HTML。但这种方案也会触发另一种反爬手段WebDriver特征检测。无头浏览器和真实浏览器在底层环境上有差异可被检测的特征包括navigator.webdriver属性是否为true。window.chrome对象是否存在普通无头模式下该对象缺失。navigator.plugins和navigator.languages是否与正常浏览器一致。Canvas指纹和WebGL渲染字符串是否和正常浏览器不同。浏览器窗口尺寸是否为无头模式的典型值。针对这些特征破解思路就是在页面加载前通过CDPChrome DevTools Protocol注入JS把特征改掉。比如在Playwright里可以通过add_init_script在页面任何脚本执行之前先覆盖这些属性from playwright.sync_api import sync_playwright stealth_js Object.defineProperty(navigator, webdriver, {get: () undefined}); Object.defineProperty(navigator, plugins, {get: () [1, 2, 3, 4, 5]}); Object.defineProperty(navigator, languages, {get: () [zh-CN, zh]}); window.chrome {runtime: {}}; with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() context.add_init_script(stealth_js) page context.new_page() page.goto(https://example.com) print(page.title())但说实话这属于“猫鼠游戏”的范畴。对方如果积累了足够的浏览器真实环境数据用机器学习判断当前上下文是不是headless单纯靠注入JS已经很难完全绕过了。这也是为什么现在的爬虫工程师开始关注大模型方向——大模型可以做更复杂的语义分析和行为模拟但这又是另一个更深的话题了。5.3 破解思路的两条路线渲染层模拟还是接口层逆向这一层有两条路可以走各有利弊。一条路走渲染层模拟用无头浏览器配合行为模拟。优点是实现相对简单不需要深入理解网站的JS逻辑缺点是速度和稳定性差而且永远在追赶对方的特征检测更新。另一条路走接口层逆向直接搜索前端JS代码里生成签名参数的逻辑然后用Python复现。优点是请求效率高不需要开浏览器缺点是需要分析混淆过的JS代码有时还会遇到动态生成的密钥、WebAssembly加密等技术逆向难度很大。对这个选择我个人的建议是目标网站规模不大、数据量少走渲染层模拟更快目标网站数据量大、需要频繁采集走接口层逆向更划算。不要一上来就追求高深的技术先评估投入产出比。6. 内容层的反爬字体、图片和CSS偏移看得见却拿不到6.1 自定义字体的反爬原理这层反爬比较隐蔽往往出现在价格、手机号、评论数字等敏感字段上。它的原理是网站用一套自定义字体文件替换了标准字符编码比如数字“3”在页面源码里对应的是一个Unicode私有区的字符而浏览器加载自定义字体后这个私有区字符显示成“3”。于是你在浏览器里看到了正常的数据但用requests直接抓HTML源码看到的是乱码。要破解字体反爬核心是拿到字体文件解析出字符映射关系。具体流程大致是从HTML或CSS中定位到字体文件的URL一般是woff2格式。下载字体文件用fontTools库读取cmap表。把私有区字符和对应的glyph名称映射到真实字符。不同网站的映射逻辑不同有些直接把字符编码映射到真实Unicode有些则用同一个轮廓反查。每次网站更新字体文件后映射关系可能还会变所以你需要一套定时更新映射的机制。一个简单的字体解析思路如下from fontTools.ttLib import TTFont def parse_font_mapping(woff_path): font TTFont(woff_path) cmap font.getBestCmap() mapping {} for code, glyph_name in cmap.items(): unicode_char chr(code) # 根据实际情况glyph_name可能包含真实字符信息或需要配合多张字体图片比对 mapping[unicode_char] glyph_name return mapping6.2 图片混淆和CSS偏移把文字变成图片或打乱顺序还有两类内容层的反爬也比较常见。一类是图片混淆。比如某些网站的价格数据不是文本而是切成小块的图片前端把图片拼起来展示。这种方式防抓取非常直接因为图片里的内容没法通过正则和XPath直接提取。破解思路一般是OCR识别或者如果你知道图片是从哪张大图上切的也可以直接去抓原图。另一类是CSS偏移。页面上看起来正常的文字顺序其实是被CSS打乱的。源码里字符的顺序可能是“2514”但通过CSS的transform和绝对定位页面显示成了“1542”。这种反爬不涉及加密只是改变了DOM的视觉呈现顺序。破解思路就是解析CSS代码计算出每个字符的偏移量把实际的视觉顺序还原出来。6.3 内容反爬的破解原则先观察页面渲染逻辑再选工具面对内容层的反爬我最大的心得是不要急着写代码先花半小时在浏览器里把元素的渲染方式研究透。字体反爬的核心在字体文件的映射关系图片反爬的核心在图片的切割和拼接规则CSS偏移的核心在样式计算。找到对应的“规则”之后剩下的就是写代码执行规则而已。这一层难度并不高但非常考验细心程度。尤其要注意的是网页改版之后正则表达式和选择器很容易失效所以建议平时就把解析逻辑封装成独立的函数方便快速替换。7. 一次真实的对抗链路复盘从浏览器正常到代码被拒7.1 现象请求200但数据为空去年我帮一个客户处理过一个案例现象非常典型用requests直接请求目标列表页返回的HTTP状态码是200但页面里的核心数据列表是空的其他静态部分都能正常输出。客户的直觉是”页面结构变了“于是改用Selenium结果发现浏览器里数据正常。这说明什么说明同一个URL浏览器请求和代码请求得到的响应是不同的。这背后几乎可以确定是服务端根据请求特征做了分流。7.2 排查链路从Header到签名参数的逐层剥离我给的排查步骤是这样的第一步用浏览器开发者工具的Copy as cURL复制出浏览器请求在本地用requests复现。如果复现成功说明目标和requests之间差的东西全在Header和Cookie里。实际上我们用这种方式确认问题出在缺失的一组自定义Header上补全之后列表数据正常了。第二步继续测试详情页接口发现直接请求detail接口返回403。浏览器里却能正常加载。这时候把关注点转向接口的请求参数发现有一个sign参数而且每次请求都不相同。用浏览器无痕模式重复请求确认这个签名的生成与后端Session绑定。第三步分析前端JS。找到了签名参数的生成位置是一个经过混淆的JS文件通过断点调试定位到它使用时间戳、请求路径和一个写死在JS代码里的密钥做MD5拼接。虽然密钥写在JS里会被任何有心人提取但只要放在混淆后的代码里就能拦住90%的新手爬虫。第四步用Python复现签名逻辑同时对请求头和Cookie做全量模拟最后跑通了整个流程。这个案例里反爬是按层递进的先是请求头再是Cookie和签名参数每一层都在做同一件事——提高门槛。破解的关键不是某项技术多高深而是你有没有耐心一层层去排查和还原。7.3 这个案例给我的三个启发第一永远先复现再逆向。很多同学看到接口返回403就开始找所谓的“JS逆向教程”但其实先把浏览器请求原样复现一遍可能问题就已经解决了。第二签名逆向要有全局思维。找到签名生成的位置只是第一步你还要理解这个签名绑定了哪些变量是时间戳还是随机数还是Cookie绑定关系不同复现的复杂度完全不同。第三不到万不得已别硬刚逆向。如果数据量不大用Selenium或Playwright直接渲染成本更低。逆向签名看着厉害但维护成本高而且对方换一次混淆你就得重新分析一次。8. 破解思路的边界爬虫工程师该有的自我约束聊完五花八门的破解思路最后想聊点技术之外但非常重要的事情。爬虫技术本身是一把双刃剑。它能帮你快速抓取公开数据、做竞品分析、搭建自己的数据集但也容易被滥用。我在这一行干了这么多年见过太多人因为爬虫踩了法律红线也有不少开发者因为爬虫技术被平台起诉。所以我必须把底线说清楚第一优先使用官方API和数据授权通道。很多平台提供了开放的API接口目的就是让开发者合法地获取数据。虽然可能有些限制但胜在稳定、合规、不需要投入维护成本。如果API无法满足需求再考虑其他方式但一定要评估合规风险。第二尊重robots协议和服务条款。robots.txt虽然只是君子协定但它表达的是站点所有者的意愿。你不一定非要完全遵守但至少要清楚自己在做什么、风险有多大。第三不碰个人信息和敏感数据。涉及个人隐私的数据比如手机号、地址、社交关系链抓取和使用的法律风险极高。这类需求坚决不做不是技术做不到是代价承受不起。第四控制爬取频率不要影响目标网站的正常服务。技术对抗归对抗但前提是不要给对方服务器造成压力。一个合格的爬虫工程师应该在代码里默认加上限速和异常退避机制。我在实际工作里有一条不变的原则如果你的爬虫抓的是公开的、非敏感的数据且不会对目标站点造成明显影响那就大胆去做一旦越过了这条线不管技术多有意思都果断停手。回到技术本身。反爬和爬虫的对抗本质上是成本和收益的博弈是整个互联网生态中数据流通与数据保护之间的持续拉扯。作为从业者我们需要做的不是站队而是保持对技术的敏感度和对规则的敬畏心。把每一次反爬对抗当成一次思维训练你会发现它锻炼的不只是代码能力更是你拆解问题、权衡利弊的综合判断力。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →