尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

携程酒店数据采集实战:动态接口分析与反反爬策略

携程酒店数据采集实战:动态接口分析与反反爬策略 做数据采集这行干久了会发现一个真实存在的分水岭能抓到静态HTML的人很多能玩转动态接口的人很少。之前帮一位做OTA竞对分析的朋友跑了一遍携程酒店数据的采集链路从页面分析到动态接口定位再到参数加密和反反爬策略的取舍整个过程的典型程度非常高。这篇文章把这次实践经验完整复盘出来适合已经开始用Requests、Scrapy但一遇到动态渲染页面就无从下手的开发者参考。先行提醒一句本篇所有分析都以技术学习和研究为前提实际操作前务必阅读并遵守目标网站的robots协议和用户协议不对生产环境造成压力更不要把抓取数据用在商业牟利或非法途径上。1. 携程酒店页面为什么抓了个寂寞从静态解析转向接口分析1.1 浏览器里看得到代码里抓不到先说我自己最初踩的坑。按照以前抓静态页面的思路我直接用Requests请求酒店列表页拿到HTML后打印出来看酒店名、价格、评分这些核心字段通通不在。再往后翻几页发现页码和列表内容全是动态插入的整个DOM树里只有一个空壳页面。这就是典型的SPA应用逻辑浏览器里最终呈现的完整页面是前端JavaScript在本地拼接并渲染出来的服务端只回传一个框架和一堆脚本资源。这种设计的初衷并不是为了反爬更多是为了性能和交互体验。但对做数据采集的人来说这意味着过去那套拿到HTML、写XPath/CSS选择器、批量提取的链路直接断掉了。你面对的不是一个文档页面而是一整套运行的应用程序。只有理解这一点才会心甘情愿地放弃静态解析转向接口层。1.2 我用三个方法验证了页面的动态渲染属性第一次碰到这种情况的人容易慌其实判断页面是否依赖动态渲染有三个很直接的土办法。打开浏览器右键查看网页源代码在源码里搜索页面上能看到的酒店价格关键词搜不到基本就是动态渲染。打开Chrome DevTools的Network面板刷新页面后过滤JS类型的请求如果发现大量脚本文件在运行而HTML文档本身只占很小一部分说明页面逻辑高度依赖JavaScript。在浏览器设置里禁用JavaScript后刷新页面动态渲染的页面会直接变成空白或只剩顶部导航核心内容全部消失。这三个方法从不同角度交叉验证基本就能确定下一步方向。这里插一句个人经验很多新手卡在第一步就动不了了反复尝试各种XPath写法却从没想过问题根本不是解析规则不对而是数据根本不在这个HTML里。及时止损、换赛道是进阶的第一个重要认知。2. 动态接口定位从Chrome开发者工具的网络面板说起2.1 XHR过滤与关键词搜索快速缩小候选接口确认页面是动态渲染之后接下来最核心的任务是找到填充数据的那些AJAX接口。打开DevTools的Network面板刷新列表页在过滤器里选择Fetch/XHR会发现一次页面加载背后有几十上百个接口调用如果全看根本看不过来。我常用的缩小范围方法有两招。按关键词过滤在Filter输入框里输入城市拼音、日期格式或酒店名关键词和页面内容相关的接口会立刻浮出来。按响应内容搜索在Network面板里按CtrlF打开全局搜索输入页面上某个独特的酒店名或价格数字DevTools会直接告诉你这个数据藏在哪个接口的响应体里。两个方法组合使用基本可以在半分钟内锁定目标接口。但锁定接口只是开始真正麻烦的是请求参数里那一串疑似动态生成的内容以及某些接口返回前经过加密处理的JSON。这些问题如果不处理就算找到了接口也拿不到干净数据。2.2 看懂酒店列表接口的数据组织方式被锁定的接口通常返回JSON结构里面包含酒店ID、酒店名、价格、售卖状态、地理位置、图片封面等字段。下面是我在分析过程中记录的一份字段结构示意实际生产环境字段更多而且命名不会是规范的英文多数是缩写或经过加密的字符串。{ hotelList: [ { hotelId: 1234567, hotelName: 示例酒店脱敏, price: { amount: 899, unit: CNY }, saleStatus: 1, location: { cityCode: 001, districtCode: 0101 } } ], pageInfo: { pageIndex: 1, pageSize: 10, total: 320 } }拿到这份JSON不代表任务完成因为价格字段会出现起价、优惠券、限时折扣等不同含义的子字段需要结合页面的展示文案来确认。我处理这类问题的经验是每次抓完一批数据后抽几组数据回到页面做人工比对比对项包括酒店名称、价格区间和售卖状态确认无误再进入下一步。这种笨办法看起来很低效却是防止抓到了但抓错了的最可靠手段。2.3 跟着翻页和筛选操作重新认识请求参数定位到接口后不要急着写代码先在页面上手动做一次翻页操作同时盯着Network面板看接口发出了几次新请求哪些参数变了。通常酒店列表接口的核心参数包括城市代码、入住日期、离店日期、页码、排序方式以及几个看起来毫无意义的字符串。那个毫无意义的字符串往往是整个动态接口分析里的难点。它可能在每次请求时都变化也可能在一段时间内保持不变。为了确认生成频率我一般会在不刷新页面的情况下连续翻几页对比每次请求里这个参数有没有变化。如果翻页时参数不变说明它和页面会话或用户态绑定如果每次都变说明它参与了请求签名流程每次调用都会重新生成。到这里动态接口破解才算真正进入正题——需要弄明白这个参数是怎么生成的以及是否有办法在自己代码里复现。3. 反爬机制不只是封IP签名、加密与行为风控拆解3.1 最常见但最容易忽略的请求头校验先说一个很多人看不起却实际拦截率很高的点请求头。很多爬虫脚本的UA就是默认的Python-requests/2.x这种请求头在服务端风控里属于一票否决级别的信号。真实的浏览器UA、Referer、Accept-Language、Accept-Encoding、Accept这些字段之间是有内在一致性的。服务端的校验思路也不复杂先看UA是不是常见浏览器再看Referer是不是从本站页面跳转而来最后看Cookie有没有携带访问首页时种下的会话标识。如果只带上UA而不带Referer就可能直接触发风控。这个校验逻辑并不高深却能在第一道防线拦住大量低水平爬虫。所以我处理任何网站采集前都会先用真实浏览器完成一次完整访问把请求头里的关键字段逐项记录下来再对照着配置到脚本里。这个过程花不了多少时间但对成功率的影响是决定性的。3.2 动态签名参数的生成逻辑与逆向入口签名参数通常由页面里的JavaScript脚本在运行时计算出来作用很直接防止有人绕过页面直接调接口。实现方式五花八门但大体逃不开两个套路。一是把固定字符串加上时间戳做MD5或SHA1哈希拼接后再加一段盐值二是把业务参数按规则排序后做加密编码最后拼成签名。为了增加破解难度有些实现还会混入随机数让每次的签名结果都不一样。定位签名生成入口时我常用的路线是在DevTools的Sources面板里全局搜索参数名比如搜sign、token、headerAuth这类关键词找到生成签名的函数后再往上游追查它的输入依赖。这个过程做起来很有成就感但我要强调一个容易被忽略的现实不少团队在签名参数之外还叠加了设备指纹和环境检测。也就是说就算你把签名算法完整还原了换了台设备或换了网络环境依然可能拿不到数据。这类防护已经不是简单的签名验证而是整套风控体系。3.3 响应体加密与行为指纹为什么更难缠有些接口不仅请求参数有签名连响应内容本身都是加密的前端在拿到密文后会用脚本动态解密再渲染。这种设计的潜台词是就算你成功调用了接口拿到的也只是一团乱码。要破响应加密逻辑上和破请求签名类似核心都是找到解密函数和密钥但实现成本明显更高有时候还涉及多次密钥交换。行为指纹则是更抽象也更难伪造的存在。风控系统会记录鼠标移动轨迹、页面滚动速度、点击间隔、按键耗时等特征构建一套这个访问者到底是不是真人的模型。脚本在行为特征上和真人差异巨大一旦触发阈值就会进入滑块或点选验证流程。这也是我为什么始终坚持一个策略——不要试图和风控对抗而是保持低感知、克制地采集。说直白点反反爬的核心不是破解验证码而是让自己看起来不像爬虫。4. 反反爬的正确姿势不是对抗而是模拟与克制4.1 请求频率控制与随机化策略很多开发者一提反爬就想到IP池、代理池其实单机采集先把频率控制好远比盲目堆代理更有效。我的经验是每次请求之间至少间隔3到5秒并且加入随机扰动让节奏不规律。用Python写出来大概是这样import time import random # 每次请求前随机等待3~5秒 delay random.uniform(3, 5) time.sleep(delay)列表页到详情页的连续请求间隔要再拉长每分钟总请求数控制在15到20个以内。也许你会觉得这个速度太慢但换个角度算账单机慢一点可以用多机分布式来扩展但前提是每一台机器都保持低频率。狂跑一小时被封IP再花两小时换IP、重试、等解封反而更慢更伤。4.2 Cookie与登录态管理的边界登录态能拿到的数据通常更完整但风险也更大。有些平台的风控会把登录账号和IP、设备指纹捆绑一旦检测到同一账号短时间高频请求轻则限流重则要求二次验证。所以我在做酒店数据采集时会优先尝试不登录态下的匿名访问只有在确认匿名拿不到完整数据时才考虑登录同时严格控制单账号的并发请求数。另一个容易忽略的细节是Cookie生命周期。很多网站的会话Cookie几个小时就失效如果脚本把Cookie写死了过了有效期所有请求都会报错。比较好的做法是采集前自动访问一次首页刷新Cookie让请求会话始终保持新鲜。这种预热步骤看似多余但对长周期采集任务来说几乎是必须的。4.3 保持低感知代理、UA池与请求会话复用如果确实需要扩大采集规模可以引入代理但要注意细节。同一秒内不要用同一个代理IP请求同一个站点两次这是基本常识。UA池要选取真实常见的浏览器版本最好配合对应的平台、浏览器内核一起使用不要随手在网上下载一个乱编的UA列表因为设备指纹不一致反而更显眼。请求会话复用也值得养成习惯。用同一个Session对象保持Cookie和连接池避免每次新建TCP连接这种方式一方面速度快另一方面也更接近正常浏览器的行为特征。我不建议一上来就上Playwright或Selenium模拟浏览器它们虽然能绕过大部分请求头检测但资源占用大、稳定性差而且鼠标轨迹这些行为特征依然可能被识别性价比并不高。5. 一套可落地的酒店数据采集工程方案5.1 采集框架选型与任务拆分我日常做这类项目的主力框架是Scrapy它对请求调度、超时重试、去重、并发控制都有现成的封装不用自己造轮子。一个完整的酒店数据采集任务我习惯拆成三层城市列表页任务负责拿到所有目标城市或商圈的信息。酒店列表页任务负责按城市、日期条件拉取酒店列表。酒店详情页任务负责进入每家酒店详情页获取房型、设施、评价等更细的数据。每一层是一个独立的Spider用一个Pipeline串起来下一层任务的URL由上一层解析结果生成。这样做的好处是某一层出错不会影响其他层而且方便做断点续跑。如果怕Scrapy太重也可以用httpx加asyncio写轻量脚本但并发控制、重试机制和日志记录都要自己处理工程量其实更大。5.2 数据清洗与超长文本处理酒店详情页通常带大量描述文本抓下来之后要先做一轮清洗。我的做法是先用正则去掉script和style标签再移除剩下所有标签最后把连续的空白字符替换成单个空格得到一个干净的纯文本。价格字段则要统一单位有的页面显示¥899起有的显示US$120这个过程必须在落库前完成否则后续统计分析会很痛苦。import re def clean_text(html_text: str) - str: # 去掉脚本和样式 text re.sub(rscript.*?/script, , html_text, flagsre.S) text re.sub(rstyle.*?/style, , text, flagsre.S) # 移除所有标签 text re.sub(r[^], , text) # 合并空白字符 text re.sub(r\s, , text) return text.strip()另外我建议在数据库里同时保留原始字符串和处理后的数值字段方便哪天想回溯核对时不至于抓瞎。数据清洗这种事做得越细后面分析阶段就越省心。5.3 去重、增量更新与数据质量监控酒店数据的核心主键是hotelId天然适合用来做去重和更新。我在数据库里会为它建立唯一索引重复抓取时使用ON DUPLICATE KEY UPDATE只更新价格和售卖状态字段保留历史快照。这样每小时或每天跑一轮增量任务数据库里的数据会越来越接近实时又不用全量重刷。INSERT INTO hotel_price ( hotel_id, hotel_name, price, room_type, check_in, check_out, crawl_time ) VALUES ( %s, %s, %s, %s, %s, %s, %s ) ON DUPLICATE KEY UPDATE price VALUES(price), crawl_time VALUES(crawl_time);数据质量监控是很多人忽略的一环。我每次落库后都会跑一个简单统计脚本检查当次抓取的有效记录数、价格为空的比例、缺失字段的酒店数量。这些指标一旦异常就说明接口可能变更、参数可能失效或者已经被风控识别。没有这道监控数据出了问题可能要过好几天才能发现损失就大了。6. 实战中我最常被问到的几个棘手情况6.1 接口参数明明一样为什么返回却是空的这是最常见的问题。表面上看参数没变化但可能有隐藏的时效性参数比如时间戳只在几分钟内有效过了窗口期就返回空数据。另一种可能是接口要求先访问某个预热接口生成一次会话标识之后的列表接口才允许正常调用。遇到这种情况我会回到Network面板把请求顺序从头到尾重现一遍而不是只盯着单个接口看。很多时候问题不在接口本身而在于缺少了前置步骤。6.2 单机采集被限制后的降级方案如果单一IP被限流最简单的降级方案是切换网络出口比如换用手机热点。更稳妥的做法是暂停一段时间把请求频率降得更低再继续。不要在同一时间把所有采集任务都压在一个IP上可以把任务按城市拆分分时段、分批执行把压力摊开。这里也提醒一句不要一被限流就想着换代理硬刚先搞清楚是频率问题还是参数问题否则换再多IP也白搭。6.3 价格数据存在哪些一致性陷阱酒店价格从来不是固定值它跟入住日期、离店日期、房型、是否含早都有关系。列表页显示的往往是起价点击进入详情页后才发现不同房型价格差异巨大。采集的时候一定要把列表页价格、详情页最低价、各房型价格区分开存储并且记录查询日期和入住日期。数据表里如果没有这两个日期字段后续做趋势分析或者竞对分析时根本分不清价格是涨了还是只是换了查询条件。6.4 日志里最常见的三类异常排查问题最怕没日志。我在项目里会把每次请求的状态码、响应耗时、异常信息都记录下来长期观察后发现高频异常集中在这三类。ConnectionError大概率是网络波动或IP被临时断开重试一次往往能恢复。Timeout请求超时可能是当前频率过高被延迟响应需要拉长间隔。403 Forbidden或418 Im a teapot通常是风控明确拒绝的信号此时应该立即停止当前批次任务进入降级流程而不是继续死循环重试。这三类异常对应三种完全不同的处理方式如果只用一种重试策略硬扛很容易把临时问题放大成封号或封IP的级别。6.5 关于封装数据出售我一定要给读者提的醒最后说句掏心窝的话爬虫技术本身是中性的但把抓取到的酒店数据封装成产品出售或公开传播极大概率触及法律红线。酒店价格、库存这类数据涉及平台的核心商业资产如果真的有OTA竞对分析需求更建议走正规渠道比如与平台方协商获取数据授权或者使用对方官方开放接口。这套动态接口分析的方法论也可以平移到得物、美团等其他动态渲染站点做学习研究但都要把握好边界。技术能力最好用来提升自己的工程水平而不是用在可能惹上官司的路上。整个项目做下来我最深的体会是动态接口分析的关键不是破解有多炫酷而是对页面运行机制的深刻理解。只要你能像浏览器一样思考知道什么时候发请求、带什么参数、保持什么节奏采集这件事就能做到既稳定又低调。每个参数背后都是一次前端和服务端的对话读懂了这场对话反反爬就不再是黑魔法。
返回列表