从requests到异步采集:为什么你写的爬虫总被对方一眼识破?
三年前我在做一个电商价格监控项目,用requests写好了采集逻辑,本地跑得好好的。部署到云服务器后,不到半小时就收到对方安全团队的警告邮件。当时我以为是IP被限了,连夜换代理池、加随机延时、用session保持cookie……一通操作下来,第二天照样被封。后来用wireshark抓了自己服务器的包,再看一眼浏览器正常访问的流量,才发现问题根本不在我们天天聊的UA或headers顺序上,而是在TCP连接行为本身。
现在很多教程都在讲随机UA、代理池、验证码识别,但这些本质上都是“应用层”的优化。而服务端在检测爬虫时,第一道关卡根本不是HTTP层,而是传输层的指纹——比如你的TCP窗口大小默认是65535,而Chrome在Linux上的窗口大小是29200,加上其他六七个TCP参数组合,就构成一个独一无二的指纹,就像人的DNA。requests底层用的urllib3,它发连接的方式和乔叟英语一样古老。你用requests去访问一个采用Cloudflare或阿里云WAF的站点,对方不用看你的header就知道你是个脚本。为什么?因为你的TLS握手消息顺序、TLS扩展顺序、supported groups偏好、椭圆曲线优先级,全部和主流浏览器不一样。
我做了一个很简单但残酷的实验。一个模拟登录接口,只设置了划选验证码,没有任何频控。我写两个爬虫去跑:一个requests自带session,一个是curl -A 模仿最新版Edge——也就是说请求头几乎一模一样,但底层签名不同。结果requests的请求有大概22%直接被拒在WAF层,返回403或者JS挑战;而curl模拟的请求,只要验证码过了(用了打码平台),几乎100%能拿到数据。同样的网址、同样的请求头、同样的IP,差别就在底层。这个结果颠覆了我之前对反爬的认知——原来应用层伪装只是表面功夫,底层协议栈才是真正暴露身份的地方。
所以到了2024年,如果你还在问“我怎么用Scrapy设置代理”,说明你还没踩到真正的门槛。你要解决的问题不是怎么少被封,而是怎样让你的爬虫在网络行为上“看起来像一个真实用户”。商业采集服务像ScrapingFish或Scrapy Cloud,他们从来不说自己是靠随机UA取胜的,他们靠的是浏览器级指纹模拟——把TLS握手、TCP参数、HTTP2头帧顺序、甚至js渲染的canvas指纹全部包装好在云端。你本地写个requests去调他们的API,本质上是把他们采集好的HTML回传给你。这不是“高级爬虫”,这是“代购”。
真正的进阶路线应该分成两层。如果只是做小规模采集、对数据实时性要求不高,我的建议是老老实实去学Playwright或Puppeteer无头浏览器再配合自定义启动参数。比如用undetected-chromedriver启动一个带真实用户数据目录的Chrome实例,这个目录里存着你日常登录的cookie、localStorage、甚至IndexedDB的数据。这样服务端看到的不是一个全新的浏览器,而是有历史痕迹的、活生生的浏览器指纹。但代价是内存占用常常拉到1GB以上,CPU也是持续高负载,跑20个并发就足以拖垮一台2C4G的小服务器。如果做大规模异步采集,这条路走不通,性价比太低。
另一种更轻量的做法是改造自己的请求库的底层签名。用curl_cffi替代requests,它能够模拟浏览器的TLS指纹和HTTP2指纹,且API几乎兼容requests——只需把import requests改成from curl_cffi import requests。实测下来,curl_cffi的impersonate参数设置成chrome, 在同一个代理IP下采集豆瓣top250,连续跑了2000个页面,只触发了两次验证码;而用普通requests,200个页面不到就出现滑块。这个差距你换多少轮UA都追不回来。另外,如果你坚持要自己实现TCP指纹修改,那就得深入Linux内核参数设置——从socket层去调整TCP window scale、MSS、时间戳选项,甚至要用golang或者rust封装一个自定义的net.Dialer。这种做法适合极少数需要完全掌控的网络环境,对于大多数业务场景没必要因为开发成本太高。
回到开头那个电商项目,最后我放弃了requests全家桶,改用curl_cffi + 自建代理池(2000个住宅代理,按需轮换)+ 验证码打码,并且把定时任务从纯随机改成模拟人工“工作时间”——早上九点到晚上十一点间不定时触发,每次运行时间控制在15-40分钟。之后两个星期没有再收到警告。这个转变让我意识到:做爬虫的根本不是什么“对抗”或者“反反爬”,而是理解客户端在正常使用时的所有约定俗成的细节。你不需要让服务器觉得你是真人,你只需要让服务器懒得证明你是机器人。
如果你已经开始用采集框架,请先停一下,抓个tcpdump对比一下自己的请求和浏览器请求的TCP层差异。你会发现在“被封IP”、“代理质量差”、“触发风控”这些常见问题背后,藏着一个极简单却极少被公开讨论的原因:你的http库太原始了。与其继续折腾那些伪随机延时,不如花一晚上把底层依赖换掉。爬虫这行当,拼到最后拼的是采样——人写代码的思维很难跳出常规教程的框架,但传输层不会说谎。
顺便说一句,很多所谓“零基础爬虫速成”的文章让你用requests.get一把梭就爬这个爬那个,全是坑。对于只想拿几天数据的个人项目,可以这么干,毕竟被封了再换方案也不迟。可要是你有长期采集需求,从第一天就该把浏览器指纹和HTTP客户端指纹纳入架构设计。不要信网上那种“设置好headers就能伪装浏览器”的老话——时代变了。