
凡是写Python爬虫的朋友一定经历过这种场景本地调试一切正常数据抓得飞起一放到服务器上跑几分钟要么IP被封要么返回一堆验证码要么直接出现一段“访问过于频繁”的提示。更气人的是有些网站数据明明在页面上肉眼可见用requests就是抓不到换成浏览器看又一切正常。这些问题的背后几乎都是反爬机制在起作用。我做了很长时间的爬虫开发也带过不少新人总结下来90%的爬虫新手翻车都集中在10个高频防爬坑里。这篇文章不做空泛的理论讲解直接把这10个坑逐个拆开说清楚每个坑背后的原理、常见的错误写法以及我实测下来有效的解决思路。无论你刚接触爬虫还是已经写过一阵子requests这篇都能帮你少走不少弯路。1. 为什么90%的爬虫新手会栽在防爬这关1.1 爬虫与反爬的攻防本质反爬的本质是网站试图区分“真实用户”和“自动化脚本”。真实用户的行为特征是有正常的User-Agent、有浏览器指纹、访问频率不均匀、会加载页面里的各种资源、会在页面停留一段时间。而爬虫脚本的特征往往是请求头缺这少那、每秒请求几十次、Cookie不稳定、行为轨迹平直得可怕。很多新手想不明白一件事为什么我明明加了User-Agent对方还是封我因为反爬系统从来不是靠单一维度做判断的它会把IP、请求频率、Header完整性、Cookie状态、JS执行环境、鼠标轨迹、浏览器指纹等几十个信号综合起来打分。你只解决了其中一个维度其他维度照样暴露你的自动化身份。理解这一点很重要。与其说你在写爬虫不如说你是在“模拟一个真实用户”。所有防坑思路本质上都是往“更像真人”的方向靠拢。1.2 新手最常犯的三个认知错误第一个错误是把反爬当成“加密解密”以为找到某个sign参数的生成算法就一劳永逸了。实际上签名只是反爬的一道锁就算你破解了签名频率失控照样封IP。第二个错误是一上来就追求“高并发”。用多线程开50个线程去抓同一个网站不出3分钟IP就会被封然后反过来抱怨网站反爬太强。真实场景里单线程加合理延时往往比暴力并发更高效。第三个错误是忽视“维护成本”。很多人写完爬虫能跑就算完事不做日志、不做告警、不做结构化管理结果网站一改版爬虫悄无声息地挂了三天攒了一堆脏数据才发现。这三个认知不纠正后面所有的技术方案都是空中楼阁。2. 请求层的高频深坑先把基础打好2.1 坑1User-Agent裸露一眼被识别这是最基础的坑但也是最多人犯的。很多教程里直接写requests.get(url)连headers都不带或者复制了一个默认的UA字符串写到死。到了某些防护严格的网站上这种请求连页面都进不去直接返回403或跳转验证。真正的做法是维护一个UA池每次请求随机挑选一个真实的浏览器UA。更讲究一点把Sec-Ch-Ua、Sec-Fetch-*这类现代浏览器会带的Header也一并补上。import random UA_POOL [ 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, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76, ] def get_headers(): return { User-Agent: random.choice(UA_POOL), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, }我见过很多人在UA上栽了跟头跑本地没事一到生产环境就出问题最后排查半天发现是生产环境的默认UA太老被站点标记了。UA池这个习惯一定要养成成本极低收益却很直接。2.2 坑2Headers信息带不全缺少关键字段有些新手知道要带User-Agent但只带了UA一个字段其他请求头一概不写。结果遇到某些网站反爬策略比较严格的就会突然发现数据时好时坏或者返回的页面内容对不上。这是因为服务器会校验多个Header字段。Referer用来判断请求来源Origin用来做跨域校验Accept-Language能判断浏览环境Sec-Fetch-*更是现代反爬重点关注的对象。请求一个商品详情页却不带Referer服务器会觉得这个请求“来路不明”。排错的时候有个实用技巧用浏览器开发者工具打开“网络”面板找到真实浏览器发出的请求然后对比自己的爬虫请求把关键字段一个个补齐。headers { User-Agent: Mozilla/5.0 ..., Referer: https://example.com/, Origin: https://example.com, Accept: application/json, text/plain, */*, Accept-Encoding: gzip, deflate, br, Accept-Language: zh-CN,zh;q0.9, Sec-Ch-Ua: Chromium;v120, Google Chrome;v120, Sec-Fetch-Dest: empty, Sec-Fetch-Mode: cors, Sec-Fetch-Site: same-origin, }Header的补齐原则是“按需补齐”不是照抄全部。有些请求加了多余的字段反而会露出马脚比如你用requests去请求一个纯前端接口却在Header里带上了Content-Length这在真实浏览器环境里几乎不可能出现。2.3 坑3请求频率失控IP秒被封频率控制是新手最容易忽略、也是最容易触发封禁的环节。很多人写完循环就闷头跑for url in urls: requests.get(url)一个网页0.1秒就抓完。放在对方服务器眼里这就是一个每秒请求10次的机器人不封你封谁。控制频率的正道是加随机延时而且延时区间要符合人类行为。固定sleep(1)其实也能被识别因为真人不可能每次都精确间隔1秒。import time import random def safe_request(url, headers): time.sleep(random.uniform(2, 5)) resp requests.get(url, headersheaders) return resp如果抓的是批量列表页还可以把延时控制在1到3秒之间并在夜间调大延时。再进一步一天内不同时段的请求频率也可以不同模拟真人的活跃规律。频率控制做到位大部分静态站点的请求层防爬都能绕过去。2.4 坑4IP被封后没有代理方案频率再低数据量大起来之后单IP仍然会被盯上。尤其是爬取竞品数据、公开信息聚合类场景IP封禁是早晚的事。新手的典型反应是换Wi-Fi、重启路由器或者等IP自己解封。这些在测试环境可以对于生产级爬虫完全不可行。成熟的方案是搭建代理池。自建代理池可以买一批住宅代理或动态拨号VPS也可以直接用第三方代理服务商提供的API轮换IP。代理池的关键不只是“有代理”而是要具备自动剔除失效代理、按目标站点分配出口IP、失败自动重试等能力。import requests def fetch_with_proxy(url, proxy_list): for proxy in proxy_list: try: resp requests.get(url, proxies{http: proxy, https: proxy}, timeout10) if resp.status_code 200: return resp except requests.RequestException: continue return None代理方案的选择要视预算和目标站的防护级别而定。普通站点用高匿代理池就够防护严的重点站点可能要用到动态住宅IP。需要注意代理池用之前一定要先验证匿名度很多透明代理会在响应头里暴露真实IP等于白搭。3. 状态与渲染层Cookie、动态页面和验证码3.1 坑5登录态处理不当Cookie经常失效很多需要登录才能查看的数据新手犯的错误是把浏览器里复制的Cookie写死在代码里。今天能跑明天就失效于是反复复制粘贴陷入手工维护的泥潭。真正该做的是用requests.Session模拟登录流程把用户名密码发给登录接口拿到服务器下发的Cookie和Token后自动维护。对于带验证码或者登录逻辑复杂的站点可以先把登录后的Cookie用pickle序列化保存到本地过期后再重新登录。import requests import pickle session requests.Session() # 手动登录一次并保存Cookie def save_cookie(session, pathcookies.pkl): with open(path, wb) as f: pickle.dump(session.cookies, f) # 下次直接加载Cookie def load_cookie(pathcookies.pkl): with open(path, rb) as f: return pickle.load(f)Cookie失效的排查思路也很固定先确认是不是登录态过期再看是全局Cookie失效还是某个局部Token失效。很多网站的Cookie里会带一个随请求动态刷新的token需要从响应里提取并更新这一步漏了也会导致“第一页能抓第二页就失效”。3.2 坑6动态渲染页面requests抓不到数据现在有大量网站采用前后端分离架构页面上的数据不是后端直接渲染在HTML里的而是前端页面加载后再通过JavaScript异步请求接口拿到的。你用requests去请求页面URL拿到的只是一个空壳子HTML里面根本没有数据。遇到这种情况有两条路可以走。一条路是抓接口。用浏览器开发者工具去看XHR请求找到真实返回数据的接口然后直接请求这个接口。这个方案效率高、写起来也简单适合接口参数没有加密的网站。很多新手不知道这个技巧一遇到页面没数据就以为要做自动化浏览器其实绕过了接口这一步走了远路。另一条路是直接用Playwright或Selenium这类自动化浏览器工具。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://example.com/data, wait_untilnetworkidle) html page.content() browser.close()Playwright相比直接解码爬虫的优势在于它本身就是真实浏览器环境JS会正常执行动态数据能完整渲染出来大部分JS挑战反爬也能直接通过。缺点是资源占用大、速度慢。我个人的经验是能用接口解决的优先抓接口接口太复杂才上自动化浏览器这个顺序不要颠倒。3.3 坑7验证码拦截请求直接被卡住验证码是反爬里最让人头疼的一环。常见的有图形验证码、滑块验证码、点选验证码、无感验证码。新手遇到验证码第一反应是“我用OCR识别”但识别率低得可怜。这两年更流行的是接入打码平台把验证码图片或滑块轨迹数据发给平台平台返回验证结果。对接简单、识别率高成本也低。图形验证码和滑块的对接思路完全不同。滑块验证码本质上是在校验拖动轨迹需要模拟人类的“先快后慢再微调”的动作曲线直接拉一条直线过去反而会被判定为机器人。import random import time def human_slide_track(distance): track [] current 0 mid distance * 0.7 t 0.2 while current distance: if current mid: v random.uniform(2, 4) else: v random.uniform(0.5, 2) current v track.append(round(current, 2)) time.sleep(t) return track不管用哪种方案都要记住一个原则验证码是反爬的“哨兵”不是“主战场”。如果一个网站的验证码出现频率特别高说明你前面的请求行为已经暴露了优先排查IP质量、请求频率和Header完整性而不是一味地跟验证码死磕。4. 数据防爬层参数签名、字体与伪装数据4.1 坑8接口参数加密改动一个参数就报错现在稍微讲究一点的网站接口参数都不会是裸奔的明文。要么对参数值做Base64编码要么对所有参数按特定规则做MD5签名要么引入更完整的加密逻辑。新手抓接口时常常一脸懵明明所有参数都传了服务器却返回“参数错误”。遇到这种问题首先要做的是“断点定位”。在浏览器开发者工具的Sources面板里给发送请求的JS代码下断点跟踪参数从生成到拼接的整个过程。把加密逻辑所在的函数找出来用Python重写一遍。举个例子一个常见的签名逻辑是把所有参数按key排序拼成字符串加上一个固定盐值再做MD5。import hashlib def sign_params(params, salt): sorted_keys sorted(params.keys()) raw_string .join(f{k}{params[k]} for k in sorted_keys) salt return hashlib.md5(raw_string.encode()).hexdigest() params { page: 1, size: 20, keyword: 手机 } params[sign] sign_params(params, your_salt_here)参数逆向这块没有捷径就是耐心跟JS、反复验证。值得提醒的是很多网站的加密参数会在某个版本迭代后突然改变写爬虫的时候要把签名逻辑单独封装成一个模块方便后续维护和替换。4.2 坑9字体反爬与CSS偏移看到的是假的字体反爬是数据防爬里比较高级的手段主要出现在一些点评、招聘、小说网站。原理是网页里数字或文字的显示依赖一个自定义字体文件HTML里存的是一种特殊字符编码浏览器加载字体文件后展示出来的才是正常文字。你用requests抓到HTML看到的全是乱码或错乱数字自然没法直接用。解决办法是解析网站返回的字体文件通常是WOFF格式用fontTools库提取字体映射表把特殊字符映射回真正的数字。from fontTools.ttLib import TTFont def parse_font(woff_path): font TTFont(woff_path) cmap font.getBestCmap() mapping {} for code, name in cmap.items(): # 根据字形名称推测真实字符需要结合页面上下文 mapping[chr(code)] name return mapping字体反爬的难点在于网站的字体文件可能会定期更换映射关系所以爬虫里要加一道“动态解析”的工序每次请求后自动拉取最新字体文件重新计算映射不能写死。CSS偏移是另一种伪装手段常见于一些电商平台。页面上看起来是一个正常的数字但源码里是把几位数字以随机顺序排列再用CSS样式把位置错乱调整回正确显示。用requests抓到的源码里数字顺序是乱的。这类问题没有通用解法只能一个站一个站地分析CSS结构然后针对性地做字段还原。4.3 坑10网站改版频繁爬虫失效却无人察觉这个坑不算技术难题但伤害最大。很多爬虫项目上线时跑得好好的过了一个月突然发现数据不全。排查半天发现网站的HTML结构在第N次改版时彻底变了而你的解析规则还在用老的选择器抓回来的全是空值。应对方案有两个层面。技术层面解析时不要用死板的选择器尽量用稳定的属性锚点。比如用>import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def parse_page(html): items [] # 解析逻辑... if not items: logging.warning(解析结果为空可能页面结构已变化) return items这个监控习惯新手可能觉得没必要但做久了你会发现它比任何技术技巧都值钱。5. 工程化与长期维护让爬虫活得更久5.1 分布式爬虫与代理池配合解决规模问题单机单IP的爬虫数据量一旦上去再怎么控制频率也会遇到瓶颈。这时候就该考虑分布式方案。Python生态里Scrapy本身支持分布式扩展配合scrapy-redis可以做到多台机器共享请求队列、去重队列和调度状态。分布式不是银弹它的复杂度比单机高一个量级。需要考虑任务分配、去重一致性、节点异常恢复、调度策略等一堆问题。我的建议是数据量没到每天百万级不要轻易上分布式先用单机加代理池顶住把采集逻辑本身做扎实。5.2 爬虫管理平台调度、监控、告警一体化当爬虫数量多了手工管理就是一场灾难。行业内有一些开源的爬虫管理平台比如Crawlab、SpiderFlow支持定时调度、爬虫部署、结果展示和告警通知。使用这类平台至少能让爬虫项目从“脚本时代”进入“工具时代”。我把爬虫管理平台比作“调度中心”它不帮你解决反爬问题但能把反爬问题可视化出来。IP被封了、采集量为0、解析出错率升高这些都能在平台上看到不用再靠人肉盯着服务器日志。5.3 我的工程化配置清单分享一份我常用的配置清单不一定每条都适用但能覆盖大部分爬虫项目的共性需求用requests.Session复用连接减少TCP握手开销所有请求统一走一个带重试和延时控制的函数代理池要做健康检查失效代理自动剔除数据落库前做好去重避免重复采集解析规则独立成类方便改版时快速替换抓取量和异常率写入日志配合定时任务做巡检每天备份一份采集结果防止意外覆盖6. 完整实操案例从requests到playwright的渐进改造6.1 一个典型场景假设要抓取某个网站的列表页数据页面是动态渲染的接口参数做了签名列表里的内容还有字体反爬。很多新手看到这个需求就头大想着直接上Playwright解决一切。我的做法是先分层尝试。第一步用requests直接请求列表页URL看看HTML里有没有数据。通常得到的只是一个空壳。第二步用浏览器开发者工具找到真实的XHR接口分析接口参数。如果签名逻辑较复杂先看有没有可能通过修改请求头或Cookie绕过。第三步如果接口签名实在复杂就上Playwright。Playwright的核心优势是它天然具备完整的浏览器环境签名逻辑由页面里的JS自己完成我只需要等待数据渲染完成、取回页面内容即可。6.2 三层方案对比方案开发成本反爬对抗能力性能适用场景requests直接请求低弱高静态页面、无加密接口requests模拟接口中中高接口参数可逆向Playwright中高强低JS动态渲染、参数加密复杂这个表格值得反复看。很多人的问题在于一上来就用第三层方案既慢又费资源。先确认能用第一层解决再往第二层、第三层走这个顺序才是最高效的。6.3 一个Playwright的实用封装我用Playwright时通常会做一个简单封装把等待、重试、日志都处理掉。from playwright.sync_api import sync_playwright def fetch_dynamic_html(url, wait_selectorNone): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, ) page context.new_page() page.goto(url, wait_untildomcontentloaded, timeout30000) if wait_selector: page.wait_for_selector(wait_selector, timeout15000) html page.content() browser.close() return html这个封装里有两个细节值得注意wait_untildomcontentloaded比默认的load更快避免等待所有图片和广告资源加载完wait_for_selector用来等核心数据渲染出来比固定sleep更可靠。7. 常见问题排查与避坑速查7.1 十大高频坑排查维度表现象可能原因优先排查项返回403UA被禁、IP被封换UA、换代理页面有数据但抓不到动态渲染抓XHR接口或上Playwright抓到的是空白或乱码字体反爬解析WOFF映射第一页正常第二页失败Cookie动态刷新从响应中提取并更新Token请求直接卡在验证码频率过高、IP质量差降频、换IP、考虑打码数据时好时坏Header不全、代理不稳补齐Header、检查代理时效今天能跑明天报错网站改版、签名逻辑变了检查页面结构和加密逻辑多线程后封禁率骤增并发过高降低线程数、加随机延时采集结果数量明显变少数据防爬升级检查是否出现CSS偏移或字体伪装登录态反复失效Cookie未持久化用Session登录并定期更新Cookie7.2 我踩过的几个坑第一个坑是代理池没有做过滤。图便宜买了一批透明代理结果请求发出后响应头里带着真实IP等于白跑了。从那以后凡是要接代理我一定会先做一轮匿名度验证。第二个坑是截图式的调试。以前遇到页面渲染不出来习惯性地截个图看但headless模式下截图常常是白屏容易误判。后来改成把页面HTML和console日志一起存下来排查效率高很多。第三个坑是忽略编码问题。页面编码没声明或者声明错误导致抓到手里的中文字符全是乱码。解决方式很简单优先用resp.encoding显式指定编码或者通过requests的apparent_encoding自动检测。7.3 写在最后的合规提醒写爬虫这几年我最大的体会是技术能力再强也要守住边界。抓取公开数据做分析、做聚合、做学术研究这些属于正当用途前提是不要对目标网站造成访问压力不要抓取个人隐私数据不要绕过登录做未授权访问。很多网站的robots.txt已经写明了哪些路径不允许抓取认真看一眼花不了几分钟却能避免后面很多麻烦。做爬虫项目的时候也建议给自己定一条原则只抓你需要的数据不碰不该碰的用合理频率去请求给对方服务器留一点喘息空间。8. 十五分钟自查清单最后分享一张我一直放在手边的自查清单。新写好的爬虫上线之前按这个清单过一遍能省掉大部分常见事故请求头是否完整UA是否来自真实浏览器登录态是写死还是自动维护请求之间是否有随机延时IP被封后是否有代理兜底动态数据是抓接口还是自动化浏览器选型是否合理验证码策略是否明确参数签名是否有单独模块字体反爬是否有动态解析解析失败是否有日志和告警网站改版后能否快速定位这十条对应着前面讲的10个坑。每一条都做到位不敢说能应付所有反爬策略但市面上90%的站点基本拿你没什么办法。剩下10%的高防护场景考验的就不再是某个具体技巧而是你综合调试、分析和长期维护的能力了。