Python爬虫必看:90%新手踩过的10大反爬坑与实战解法
2026/9/20 8:01:02 网站建设 项目流程

凡是写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 坑1:User-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/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "keep-alive", }

我见过很多人在UA上栽了跟头,跑本地没事,一到生产环境就出问题,最后排查半天发现是生产环境的默认UA太老,被站点标记了。UA池这个习惯一定要养成,成本极低,收益却很直接。

2.2 坑2:Headers信息带不全,缺少关键字段

有些新手知道要带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;q=0.9", "Sec-Ch-Ua": '"Chromium";v="120", "Google Chrome";v="120"', "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, headers=headers) return resp

如果抓的是批量列表页,还可以把延时控制在1到3秒之间,并在夜间调大延时。再进一步,一天内不同时段的请求频率也可以不同,模拟真人的活跃规律。频率控制做到位,大部分静态站点的请求层防爬都能绕过去。

2.4 坑4:IP被封后没有代理方案

频率再低,数据量大起来之后,单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}, timeout=10) 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, path="cookies.pkl"): with open(path, "wb") as f: pickle.dump(session.cookies, f) # 下次直接加载Cookie def load_cookie(path="cookies.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(headless=True) page = browser.new_page() page.goto("https://example.com/data", wait_until="networkidle") 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(level=logging.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_selector=None): with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context( viewport={"width": 1920, "height": 1080}, 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", ) page = context.new_page() page.goto(url, wait_until="domcontentloaded", timeout=30000) if wait_selector: page.wait_for_selector(wait_selector, timeout=15000) html = page.content() browser.close() return html

这个封装里有两个细节值得注意:wait_until="domcontentloaded"比默认的load更快,避免等待所有图片和广告资源加载完;wait_for_selector用来等核心数据渲染出来,比固定sleep更可靠。

7. 常见问题排查与避坑速查

7.1 十大高频坑排查维度表

现象可能原因优先排查项
返回403UA被禁、IP被封换UA、换代理
页面有数据但抓不到动态渲染抓XHR接口或上Playwright
抓到的是空白或乱码字体反爬解析WOFF映射
第一页正常第二页失败Cookie动态刷新从响应中提取并更新Token
请求直接卡在验证码频率过高、IP质量差降频、换IP、考虑打码
数据时好时坏Header不全、代理不稳补齐Header、检查代理时效
今天能跑明天报错网站改版、签名逻辑变了检查页面结构和加密逻辑
多线程后封禁率骤增并发过高降低线程数、加随机延时
采集结果数量明显变少数据防爬升级检查是否出现CSS偏移或字体伪装
登录态反复失效Cookie未持久化用Session登录并定期更新Cookie

7.2 我踩过的几个坑

第一个坑是代理池没有做过滤。图便宜买了一批透明代理,结果请求发出后响应头里带着真实IP,等于白跑了。从那以后,凡是要接代理,我一定会先做一轮匿名度验证。

第二个坑是截图式的调试。以前遇到页面渲染不出来,习惯性地截个图看,但headless模式下截图常常是白屏,容易误判。后来改成把页面HTML和console日志一起存下来,排查效率高很多。

第三个坑是忽略编码问题。页面编码没声明或者声明错误,导致抓到手里的中文字符全是乱码。解决方式很简单,优先用resp.encoding显式指定编码,或者通过requestsapparent_encoding自动检测。

7.3 写在最后的合规提醒

写爬虫这几年,我最大的体会是:技术能力再强,也要守住边界。抓取公开数据做分析、做聚合、做学术研究,这些属于正当用途,前提是不要对目标网站造成访问压力,不要抓取个人隐私数据,不要绕过登录做未授权访问。

很多网站的robots.txt已经写明了哪些路径不允许抓取,认真看一眼花不了几分钟,却能避免后面很多麻烦。做爬虫项目的时候,也建议给自己定一条原则:只抓你需要的数据,不碰不该碰的,用合理频率去请求,给对方服务器留一点喘息空间。

8. 十五分钟自查清单

最后分享一张我一直放在手边的自查清单。新写好的爬虫上线之前,按这个清单过一遍,能省掉大部分常见事故:

  • 请求头是否完整,UA是否来自真实浏览器?
  • 登录态是写死还是自动维护?
  • 请求之间是否有随机延时?
  • IP被封后是否有代理兜底?
  • 动态数据是抓接口还是自动化浏览器,选型是否合理?
  • 验证码策略是否明确?
  • 参数签名是否有单独模块?
  • 字体反爬是否有动态解析?
  • 解析失败是否有日志和告警?
  • 网站改版后能否快速定位?

这十条对应着前面讲的10个坑。每一条都做到位,不敢说能应付所有反爬策略,但市面上90%的站点,基本拿你没什么办法。剩下10%的高防护场景,考验的就不再是某个具体技巧,而是你综合调试、分析和长期维护的能力了。

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

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

立即咨询