被判成反爬封IP,真凶却是无头浏览器:携程页面抓不到数据的复盘
上周要做会展酒店的竞品价格监控,抓携程某个城市的热门酒店列表。第一版脚本跑得很顺,返回 HTTP 200,日志里没有报错,但提取出来的酒店数量是 0。第一反应是 IP 被封了,换了个出口,还是 0。来回折腾了半小时,才定位到真正的原因:不是网络,是我用的无头浏览器。
背景:为什么选 Playwright
这类页面我一开始就判断是 JS 渲染的,所以直接上 Playwright 无头模式,等 networkidle 之后取 content。这种做法在大多数站点都没问题,也省去了手写异步请求。问题在于我把它当成了万能解,没做验证。
真实症状:200 但内容是错的
判断封 IP 的常见依据是 403 或 429,或者响应极短。我这次拿到的是完整的 HTML,长度几万字符,节点也能选到,只是里面全是「登录」「注册」的字样,酒店列表一个都没有。也就是说页面结构和预期完全不是一回事,但我当时只盯着「有没有报错」,没有校验「有没有目标数据」。
改成三步验证之后真相很快出来:
第一,同一个出口 IP,换成最朴素的 urllib 加真实 UA 请求一次。如果这个能拿到数据,就不可能是 IP 被封。
第二,把两种方式拿到的 HTML 存下来 diff 标题和关键文本。浏览器版是我之前设置过的登录页面,纯 HTTP 版是完整的酒店列表页。
第三,对比请求头。无头浏览器会带一串自动化特征,服务端识别到之后不给你 403,而是安静地把你导向登录页,HTTP 状态依然是 200。这就是最容易误判的地方:反爬没有拒绝你,它只是给了你一份你不想要的内容。
结论很直接:纯 HTTP 先上,浏览器只做兜底。同 IP 情况下,简单请求反而没被抓,说明这类站点认的是浏览器指纹而不是出口地址。
第二个坑:类名是哈希,老选择器永远匹配不到
换成纯 HTTP 之后数据出来了,但地址字段一直是空。排查发现站点用的是 CSS Modules,类名形如 addressText__a1b2c,后面带一段随构建变化的哈希。我早期写法是包含 address_text 这种下划线命名,一直匹配不到,因为源码里是驼峰。
解决办法是用前缀模糊匹配,把哈希忽略掉,只认稳定部分。这条路在大多数前端构建产物里都适用,前提是先看一遍真实 DOM,别按自己想象的类名写选择器。
第三个坑:有些数据静态 HTML 里根本没有
页面上的房型和实时房价是异步加载的。静态 HTML 能拿到的只有酒店名称、星级、评分、地址、开业年份和图片,房型列表拿不到。这不是解析问题,是数据压根没在那份响应里。
还有一个反直觉的结论:页面上看到的 ¥150、¥288 不是房价,那是餐饮或 SPA 的报价。真实房价对未登录访客永远显示「登录看低价」。这是源站的策略,靠改请求头绕不过去,需要的是登录态,而登录态本身有合规成本,我最后放弃了这部分字段,只做主表信息。
可复用的四条经验
一,判断被反爬之前先换一种请求方式做对照,同 IP 两套客户端的结果差异,比换 IP 更能定位问题。
二,只看 HTTP 状态码会误判,必须校验「拿到的内容里有没有目标字段」,把这条写成断言。
三,选择器先看真实 DOM 再写,遇到哈希类名用前缀匹配,不要按命名习惯猜。
四,先摸清哪些字段在静态 HTML 里,哪些要异步加载,再和需求方对齐能交付的范围。省下的是后半程的返工。