
做酒店数据采集这行携程是个绕不开的关卡。它的动态接口和反爬强度放在全行业里都属于头部难度光是一个搜索列表页的签名参数就能劝退不少刚入门的朋友。这篇文章不是理论科普是我自己从定位接口、分析参数、破解签名到部署反反爬策略的完整实战记录全程用真实抓包和代码说话。如果你有 Python 和 requests 基础但对“动态接口破解”和“反反爬策略”还停留在听说过、没实操过的阶段这篇内容可以直接当作业抄。1. 项目目标与整体思路动手之前先把目标拆清楚不然很容易在调试中迷失方向。我当时的需求是从携程抓指定城市、指定日期区间的酒店列表与价格数据用于后续的价格分析和市场调研。听起来简单但真正跑起来才发现网页版的数据全在 JS 里动态渲染直接 requests 拿到的 HTML 里根本没有价格信息。如果不先把数据源和接口链路摸清楚后面所有工作都是白费。1.1 先搞清楚携程酒店页面的数据流携程的酒店业务有 PC 网页、H5、小程序、App 等多端入口每端对应的后端接口和风控策略都不一样。我的建议是优先研究移动端接口也就是 H5 或小程序因为它们的接口规律性更强、参数复杂度相对可控而且 JSON 结构比网页版的大 HTML 好解析得多。你要是真从 PC 端入手光是从一堆混淆后的 JS 里找出数据渲染逻辑就能耗掉一半时间。定位接口的标准动作是打开 Chrome 开发者工具切到 Network 面板勾选 Preserve log然后在页面里完成一次完整的酒店搜索操作。搜索关键词用城市名、酒店名、价格区间都行重点看 XHR 类型的请求。一般酒店列表接口的 URL 里会带 hotel、list、search 这类特征词Response 预览里能直接看到酒店名称、房型、价格等结构化字段。找到之后先别急着写代码把请求的 URL、Method、Headers、Request Body 全部完整记录下来这就是后续所有分析的基准。1.2 为什么不能直接爬网页源码很多新手习惯直接请求页面 URL然后从 HTML 里用正则或 XPath 提取数据。但携程这类大型站点早就用前后端分离架构了页面呈现的数据几乎全部由异步接口加载HTML 里只有空壳和首屏骨架。你就算把整个页面下载下来也只是一堆 JS 引用和没有数据的 DOM 节点价格、房态、评分这些核心字段全在后面的 XHR 响应里。这也是“动态接口破解”这个说法的由来你真正要爬的不是网页而是网页背后那个动态变化的接口。所谓破解不是去攻击对方服务器而是把接口的请求规律和签名机制摸清楚然后用代码模拟出合法的请求让对方服务器认为你是正常用户在访问。这中间最核心的工作量集中在两件事一是找到真实数据接口二是让伪造的请求能通过服务端校验。1.3 整体技术选型与流程规划我最终的技术栈是 Python httpx 做主请求库Charles 加 Chrome DevTools 做抓包分析PyExecJS 调 Node.js 来执行抽取出来的加密 JS 函数MySQL 做数据存储。选 httpx 而不是 requests 的原因很简单httpx 对 HTTP/2 的支持更好而携程的部分接口已经切到了 HTTP/2requests 在 HTTP/2 场景下会有兼容性隐患。当然你用 requests 也并非完全不行只是踩坑概率更高。整体流程分四步走。第一步抓包分析把请求链路和参数体系全部搞清楚。第二步破解签名定位加密函数并本地复现。第三步构建爬虫脚本把请求头、Cookie、代理池、频率控制全接上。第四步解析入库把返回的 JSON 清洗后落到数据库。这套流程看起来不复杂但每一步都有不少细节任何一个环节处理不对都会导致请求失败或者数据质量问题。2. 动态接口定位与请求链路分析接口定位是整个项目的基石这一步错了后面全白搭。我见过不少朋友在没定位到正确接口的情况下就埋头写解析逻辑最后发现数据全是从错误源头来的白白浪费好几天。定位接口的核心思路是利用浏览器的开发者工具观察页面行为与网络请求之间的对应关系再用关键词搜索锁定返回目标数据的请求。2.1 从 Network 面板锁定核心列表接口在 Network 面板里刷新页面并执行搜索后会看到几十个甚至上百个请求光靠肉眼一个个翻是不现实的。我的方法是先在页面里搜一个具有唯一性的酒店名比如“上海外滩W酒店”然后回到 Network 面板的搜索框里输入同样的关键词。能搜到这个关键词的请求就是可能的数据源通常这类请求就是返回酒店列表的主接口。锁定的接口往往不止一个有的返回列表摘要有的返回价格明细还有的返回营销文案。这时候还需要进一步甄别优先选择返回 JSON 且数据结构完整的请求查看它的 Preview 里是否包含酒店 ID、名称、星级、评分、最低价、房型等字段。把这些字段和页面上显示的最终价格对照一下确认没有偏差后再确定这是核心接口。如果页面价格和接口里的某个字段对不上说明价格可能是二次计算或来自另一个接口需要进一步追踪。2.2 请求头与 Cookie 的完整链路确定接口地址后接着要把请求头完整复制下来。这里有个很容易忽略的细节携程的接口校验非常依赖 Cookie而 Cookie 里有些值是首次访问页面时才种下的如果你跳过访问首页直接请求接口服务端会因为缺少关键 Cookie 而拒绝响应。所以爬虫的请求流程必须模拟真实用户路径先 GET 一次首页或搜索页拿到种子 Cookie再带着这些 Cookie 去请求真正的数据接口。我在实际操作中观察到携程会在 Cookie 里写入大量标记字段比如_bfa、_bdfB、MKT_Pages这类里面通常带有时间戳和访问路径信息部分字段会随着你的请求行为而更新。最简单的处理方式是维护一个真实的浏览器 Cookie 池定期手动从浏览器复制新鲜 Cookie 给爬虫使用或者用自动化工具模拟访问来持续刷新 Cookie。另外一个容易踩的坑是 User-Agent、Referer、Accept-Language 这些常规请求头也要保持一致我当时就遇到过由于 Referer 没带导致接口返回业务异常的情况。2.3 动态参数的初步分类与识别把请求完整记录后下一步就是逐字段判断哪些是静态参数、哪些是动态参数。静态参数比如城市 ID、入住日期、离店日期这些值是固定的或可预测的。动态参数则麻烦得多常见的有时间戳、随机数、签名值等需要你在多次请求中对比观察。比较直观的方法是连续请求三次同一接口然后把三个请求的 URL Query 和 Request Body 放在一起做 diff。规律型的差异一眼就能看出来比如一个叫click_ts的参数每次都在变而且值看起来像毫秒级时间戳另一个叫sign的参数每次都不同而且长度固定很可能就是通过哈希算法生成的签名。我做了一个简单的分类表帮助理清思路这里分享给你参考。参数名出现位置动态性可能来源cityIdQuery固定城市编码由用户选择决定checkIn / checkOutQuery固定入住离店日期业务输入pageQuery递增分页游标click_tsQuery每次变化前端生成的时间戳signQuery每次变化多个参数拼接后哈希_bfaCookie会话内变化首次访问种下后续更新abtestHeader动态A/B 实验标识不参与签名把这些参数区分清楚后才能决定哪些写死、哪些用变量动态生成、哪些需要进一步逆向算法。对于明显的时间戳类参数直接用 Python 生成同一时间格式即可对签名类参数就得进入下一阶段从 JS 代码里找算法。3. 签名机制破解与反反爬核心策略签名是动态接口里最难啃的部分也是整个项目能否跑通的关键。携程的签名机制不是简单的 MD5 一把梭而是会把请求参数、时间戳、甚至业务密钥按特定规则拼接后做哈希。好在大部分加密逻辑都在前端 JS 里我们有充分的观察和分析空间。只要静下心找对函数入口复现算法并不难。3.1 从 JS 文件中定位加密函数拿到接口后在开发者工具的 Sources 面板里找到发起这个请求的 JS 文件搜索前面发现的动态参数名比如sign或click_ts就能直接跳到赋值语句附近。携程源码经过混淆但变量名可以改语义不会变搜索参数名的成功率很高。找到关键字后把所在函数整体截图或复制下来格式化后仔细读逻辑。通常你会看到类似这样的流程先取当前时间戳然后拼接一堆参数最后调用某个哈希函数算出 sign。这个过程里还会涉及一些全局变量或固定的密钥字符串也需要一并找出来。把 JS 函数完整提取后下一步就是在本地还原它的执行环境。这里有个 90% 的人都会犯的错误直接把这串 JS 塞到 Python 里用 hashlib 模仿一遍结果总是不对。原因可能是 JS 里加密前做了隐式类型转换或者拼接顺序有讲究也可能是编码方式不一致稳妥的做法是用 Node.js 直接执行原始 JS。3.2 用 Node.js 在本地复现签名算法我采用的方案是把从浏览器里完整抠出来的加密函数保存成一个独立的 JS 文件然后在本地用 Node.js 直接执行传入参数得到签名结果。这样做的好处是最大程度保留了原始算法的逻辑不容易因为“重写翻译”而引入偏差。我的代码结构大致是这样// sign.js const crypto require(crypto); function getSign(params, timestamp) { const keys Object.keys(params).sort(); let raw ; for (let k of keys) { if (params[k] ! params[k] ! undefined) { raw k params[k] ; } } raw key SECRET_KEY ts timestamp; return crypto.createHash(md5).update(raw).digest(hex).toUpperCase(); } // 供外部调用 const args JSON.parse(process.argv[2]); console.log(getSign(args.params, args.timestamp));# python 调用 Node 执行签名 import subprocess, json, time def make_sign(params): ts int(time.time() * 1000) payload json.dumps({params: params, timestamp: ts}) res subprocess.run( [node, sign.js, payload], capture_outputTrue, textTrue ) return res.stdout.strip()用真实参数跑一次 Node 签名再和抓包数据里的 sign 对比如果一致就说明算法分析对了。这一步需要反复调整常见的坑有三个拼接参数时要不要过滤空值、排序是字典序还是数组原序、时间戳用毫秒还是秒。我的排查技巧是拿一次真实请求的参数组合做基准不断微调条件直到本地算出的签名和抓包值完全一致为止。一旦对上了签名模块就算落地了。3.3 反反爬策略的部署签名破解只是第一步真正让爬虫长期稳定运行的是反反爬策略。携程的风控系统会从多个维度识别爬虫请求频率、IP 行为、Cookie 生命周期、请求头的一致性、TLS 指纹特征等。只搞定签名但请求频率过高照样会被封。我这边整理了一套相对完整的组合方案按优先级排列如下。IP 池是刚需我用的是自建代理池加付费代理的混合方案。自建池负责搜集免费代理和自家拨号服务器的地址付费代理负责兜底保证核心请求的稳定性。地域上尽量选择和服务器同城或相邻城市的 IP延迟和成功率都会好很多。Cookie 方面要维护一个可轮换的会话池每个会话模拟一个独立用户定期用真实浏览器更新 Cookie避免长时间使用同一个 Cookie 触发异常。请求频率上做随机化处理每次请求之间的间隔在 3 到 8 秒之间随机让流量看起来更像真人操作。还有个容易被忽略的细节是行为路径。真实用户访问酒店列表时一定会先加载首页可能还会点击酒店详情再返回列表。爬虫如果一上来就疯狂请求列表接口风控很容易识别出异常。我的做法是先请求两次首页或搜索页再请求列表接口每请求几个列表后插入一次详情页请求这样整个流量曲线更接近真人浏览。如果你是重度使用者建议研究一下 TLS 指纹伪装携程部分接口对 HTTP/2 的指纹也有检测用成熟的开源指纹库可以降低被识别概率。3.4 常见的风控特征与规避原则爬虫和风控对抗的核心是伪装得像真人而不是用魔法打败魔法。我总结了几种最容易触发风控的行为单 IP 每秒多次请求、请求间隔完全相同、请求头固定不变、Cookie 长期不更新、直接跳过首页访问深层接口。这些特征每个都像指纹一样指向“非人类”集齐几个基本必封。万一被风控了最典型的表现是接口开始返回验证码页面或者返回一段 HTML 而不是正常 JSON严重的甚至会直接重置连接。遇到这种情况不要硬刚先降低频率更换 IP清掉 Cookie 重新初始化会话让服务端认为是一个新用户在开始浏览。如果某个 IP 反复被限制就把它拉黑换新的代理池的优势这时候就体现出来了。从项目管理角度看跑数据时还要留意抓取规模和数据用途控制在合理范围不搞暴力抓取既是对目标站点的尊重也是避免在大规模跑批时给自己惹麻烦。4. 数据解析、清洗与入库接口数据拿到后不等于任务结束。原始 JSON 里充满嵌套结构和杂质字段不经过解析清洗直接入库后续做统计分析时会异常痛苦。我在这块踩过不少坑整理一下完整流程给你一个可以直接复用的方案。4.1 酒店列表与详情的数据结构差异列表接口返回的 JSON 通常是嵌套多层的数据结构酒店信息在hotelList或类似 key 下面每个酒店对象包含基本字段和扩展信息。房型、价格、促销信息这些字段往往不在列表接口里或者只有摘要值完整数据需要再请求酒店详情接口。列表接口和详情接口可以根据酒店 ID 关联这个 ID 通常是数字或字母数字混合的唯一标识。举个例子列表接口的返回格式可能是这样最外层有status、msg、datadata里是hotelList数组每个元素有hotelId、name、star、score、minPrice等字段。但当你点进详情页看到的最低价格可能比minPrice低因为详情接口里还有.price、.taxFee、.memberDiscount等字段参与最终计算。我在初期就因为直接用列表接口的价格做分析导致统计数据比页面实际价格偏高后来发现是没把促销和优惠字段考虑进去。解析这类嵌套 JSON我强烈建议用jsonpath而不是手写多层的 for 循环和 if 判断代码简洁太多。比如要提取所有酒店名称一行jsonpath.jsonpath(data, $..hotelName)就直接搞定了。清洗时重点处理编码乱码、空值、特殊字符价格类字段全部转成浮点数日期统一成标准格式评分保留一位小数。每清洗完一批数据我习惯先抽样对比几个样本和页面上显示的值是否一致确认没跑偏再批量入库。4.2 多接口数据的关联与去重同一个酒店在列表接口、详情接口、价格接口里都可能出现如果没有做好关联和去重数据库里会出现大量重复记录。我的做法是以hotelId为唯一键先建一个hotels主表存静态信息再建一个hotel_prices子表存每日价格快照两表通过hotelId关联。这样既能保证酒店基础信息不重复存储又能完整保存历史价格变化后续做价格趋势分析非常方便。去重逻辑上要注意一个细节同一酒店的minPrice在不同时间点会变化所以主表的minPrice只更新最新值子表则始终追加新记录。为了避免同一分钟内重复入库我在子表上建了(hotelId, checkIn, checkOut, price_date)的唯一索引入库时用ON DUPLICATE KEY UPDATE或者先查询再插入来保证幂等。特别是跑定时任务时重复请求同一个接口导致重复数据的情况非常多没有唯一索引的话数据库很快就会被垃圾数据占满。4.3 增量更新与任务调度数据抓取不是跑一次就完事的价格和房态每天都在变增量更新才是常态。我的调度策略是按城市和日期粒度组织任务比如每天凌晨 2 点抓取未来 30 天所有目标城市的价格快照。为了支持断点续爬我在数据表里加了一个task_status字段每个任务从待执行、执行中到完成、失败都有明确状态。请求失败的任务进入失败队列由另一台机器上的定时任务统一重试。调度框架我用的是简单可靠的方案Python 的apscheduler做定时触发任务配置放在数据库表里每个任务由唯一 ID 标识执行时先查一下上次状态避免重复执行。数据量大的时候还会做分片比如把一个大城市的所有酒店按页码拆分每一页作为一个子任务分配不同的代理节点去抓。整个调度链路就像一个高效率的流水线每个节点只负责自己那块工作出了问题也能快速定位。增量更新过程中的数据质量监控也很重要。我每天会跑一次校验脚本检查价格字段是否有异常值比如房价为 0、评分超过 5 分、酒店名称重复这类问题。一旦发现异常脚本会自动把相关数据标记为可疑并推送告警。这样即使某天的数据因为接口改版出了差错也能第一时间发现而不是等到做分析时才发现整整一周的数据全是脏的。5. 常见问题与排查技巧实录写爬虫最耗时间的从来不是写业务代码而是排错。签名算法突然不对、请求又反爬拦截、抓回来的数据和页面不一样这些问题几乎天天能看到。我把实战中碰到的高频问题整理成一个速查表再逐个展开讲清楚排查思路。现象可能原因排查方向签名返回 401时间戳过期、密钥失效、算法缺参数对比成功请求与失败请求的参数差异接口返回 HTMLIP 或 Cookie 被风控换 IP、刷新 Cookie、降低频率数据与页面不一致选错接口、漏掉优惠字段用唯一酒店名搜索页面请求核实字段请求全部超时代理质量差、目标服务器限速测试代理延迟与连通率换优质代理价格字段为 0JSON 路径错误、接口改版查看原始返回数据更新 jsonpath 表达式5.1 签名校验失败时的排查思路签名相关的报错最难排查因为信息不明确但你手里永远有一张王牌浏览器里那个能够成功的真实请求。把报错请求和真实请求的所有参数放在一起逐字段比对范围和值都要看。有个容易遗漏的点是请求路径里可能也有签名参数不只是 Request Body 里URL Query String 中的参数同样会参与签名计算。排查时可以尝试“少参数法”和“多参数法”做二分定位。所谓少参数法就是先删掉非核心参数只保留最基本的必备字段再跑一遍看服务端是否会返回不同的错误码。多参数法则是把真实请求里所有字段原样复制到爬虫里如果这样还报错那一定是 Cookie 或 Headers 出了问题与签名本身无关。我用这个方法解决过好几次看似是签名问题的诡异报错最后发现问题出在 Cookie 里的一个旧值。另外要警惕签名算法里的时间陷阱。有的接口把时间戳直接参与签名服务端收到请求后会自动校验时间差。只要本地时钟和服务器时间偏差超过几十秒签名就会失效这时候你检查半天算法发现完全没错其实是时间同步的问题。遇到这种情况用 NTP 协议同步一下本地时间或者把 Python 请求前的时间戳强制调成最近一次成功请求的时间就能快速验证是不是时间问题。5.2 请求频率过高被风控后的恢复策略被风控是每个爬虫工程师的必修课但很多新手不知道被风控后怎么优雅地恢复访问。刚被限制时千万不要立即重试越急着重试越容易被标记为恶意流量。我的经验是停止所有高频请求让当前 IP 和 Cookie 静默 15 到 30 分钟等风控的封禁窗口过去。期间可以做点别的工作比如完善数据解析逻辑或者构建新的代理节点。恢复访问时也不要直接回到之前的请求频率要先以较低的频率试水比如每 10 秒一次连续成功 20 次后再逐步加快到目标频率。这个过程就像慢慢加温给风控一个“正常用户”的感觉。如果换了新 IP 依然被秒封检查一下这个新 IP 是不是曾经被用于爬虫流量很多免费代理池里的 IP 早就被风控系统标记了用之前最好先测试一下。除了频率控制请求路径模拟也很关键。我刚被风控的那几次几乎都是因为跳过了首页直接请求列表接口。后来每次都模拟完整的浏览路径包括首屏加载、列表页滑动、详情页点击被限制的概率直线下降。这套逻辑的本质是让你的请求序列更贴近真实用户的行为漏斗而不是像扫描器一样直奔目标资源。5.3 数据与页面显示不一致的核对方法爬到的数据和页面上展示的数据有出入这个问题处理不好会让下游分析误入歧途。最常见的根源是页面上的价格经过了多轮计算你只抓了原始值没抓加价和税费。携程的价格体系很复杂有酒店挂牌价、促销价、会员价、优惠券抵扣等列表接口返回的只是其中某一个维度。核对的正确姿势是在页面上打开一个具体的酒店详情把这个酒店在页面上显示的每一项费用和金额都记录下来然后逐字段到抓取结果里找对应。比如页面显示“¥450起”这个 450 可能是原价 500 减了 50 的优惠价也可能是含税价。找到对应字段后如果还是对不上就去详情接口找更细粒度的价格组成字段把它们组合计算出最终展示价。我建议在建表时直接预留一个display_price字段存储页面最终展示的价格这样后续做定价分析时省去大量重复计算工作。还有一种情况是页面上的数据经过了二次渲染比如评分来自于列表接口但点评数来自另一个接口两个接口更新频率不同导致数据不一致。这种问题的处理办法是多接口数据做交叉关联时以权威接口的数据为准并且记录抓取时间方便后续对账时判断哪个字段是旧值。5.4 接口改版后的快速适配机制携程这样的平台接口迭代非常频繁可能今天还能正常抓取的接口明天就多了一个必填参数或者换了一个签名算法。为了避免每次改版都手忙脚乱我建议从设计层面就把参数配置化。所有请求参数、签名规则、接口地址都放到配置文件或数据库表里代码只做通用逻辑参数一变只改配置不动代码。发现接口异常时先用抓包工具手动请求一次页面看新的真实请求长什么样和代码里配置的差异在哪里。如果只是新增了一个动态参数把参数名和生成规则补进配置就能恢复如果是签名算法大变那就回到前面的 JS 分析流程重新抽取函数更新算法。为了第一时间发现改版我给爬虫加了一个定时的探针任务每天用低频率请求一次目标接口如果返回的数据结构和预期不符就触发告警这样能在正式任务跑批前发现问题。这里分享一个我常用的快速对比工具Charles 的 Compare 功能它能清晰对比两次请求或响应的差异非常适合在接口改版后快速发现新增字段或参数。还有一个土办法把正常请求的完整 URL 复制下来存成一个文本文件报错时和新抓的 URL 做一次文本 diff即使没有专业工具也能快速找到差异点。6. 合规边界与爬虫心态聊了这么多技术实现最后必须把合规这件事放到桌面上说。爬虫本身不违法但非法抓取、破坏系统、侵犯隐私都踩在法律红线上。我从来只抓公开数据严格遵守目标网站的协议和条款不碰用户个人信息不做任何可能导致系统故障的高频请求。如果你是从我这里学到了接口分析的方法请把它用在合法的数据分析、学术研究或个人学习上不要去抓那些需要登录才有权限的数据更不要用爬虫做商业竞争情报或骚扰性抓取。行业里其实有很多人只讲技术不讲边界但我个人实际操作中的体会是把“不添乱、不越界”当成底线反而能让你走得更远。技术上你越懂一个系统的防御逻辑越知道哪些行为会让对方难受这些知识用来自保和优化远比用来进攻有价值。反爬的本质是成本和收益的博弈服务商每加一道验证都要牺牲一部分真实用户体验所以真正成熟的爬虫方案从来不是硬碰硬而是把自己伪装成最普通的那一个。最后再分享一个调试层面的小技巧任何接口在写代码之前先用 curl 把完整请求复现一遍。如果 curl 能正常返回数据说明身份没有问题再用代码模拟时会少很多干扰噪音。如果 curl 也失败就优先排查 Cookie 和签名不要把时间浪费在代码逻辑里。这个习惯帮我节省了无数排查时间也让我对每个接口的请求构成记得特别牢。做爬虫的核心能力不完全是写代码而是快速定位问题和理解系统逻辑这两样本事在任何技术方向上都值钱。