ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

携程机票爬虫实战:从接口分析到字体反爬破解全解析

携程机票爬虫实战:从接口分析到字体反爬破解全解析 做爬虫这些年携程一直是个绕不开的对手。机票、酒店、旅游线路几乎每个做出行数据的人都会盯上它。这篇文章不聊那些虚的概念直接用我自己的实操经历把携程旅行机票抓取这件事从零到一拆开讲清楚包括接口怎么找、反爬怎么破、字体加密怎么解、数据怎么存以及我踩过哪些坑。先说明一下文章里涉及的技术方案都以学习和研究为目的抓取频率和数据使用都要控制在一个合理、合法的范围内。做这行越久越明白技术本身没问题关键看用来干什么。1. 为什么大家都在折腾携程机票抓取1.1 真实需求场景谁在抓、抓什么、为什么抓先说需求。很多人一听到“抓取携程机票”第一反应是做个比价软件其实真实场景要多得多。我这几年接触到的项目主要有这么几类价格监控提醒盯住某条航线比如北京到上海、上海到成都价格跌到某个阈值就触发通知。这种需求个人用户最多有的是为了买特价票有的是做旅行规划的。竞品数据对比分析不少做旅游产品的创业公司会定时抓取平台价格用来设计自己的定价策略或者看某个时间段的市场热度。出行趋势研究通过航线的搜索量、价格波动、航班数量变化分析哪些目的地正在变热哪些航线运力在增加。这类需求通常来自咨询公司或者做旅游投资的人。真正动手做的时候大家会发现最难的不是“怎么解析页面”而是“怎么稳定地拿到数据”。携程的反爬策略一直在升级能在这种对抗中稳定跑几个月的数据采集任务对爬虫工程师来说本身就是一次很好的技术检验。1.2 数据源对比为什么选携程而不是航司官网有人会问机票数据直接从航司官方接口拿不就行了理论上当然可以实际执行会发现两条路都不好走但各有各的优劣。航司官网的数据是最准的但每个航司一个系统国航、东航、南航加起来几十个数据源你得一个个去对接。有的航司官网用极验验证码有的会有非常严格的频率风控有的干脆连接口都不暴露在公网只支持App内查询。如果就是想研究某一条航线的价格走势这个工作量有点大。相比之下携程这类OTP平台聚合了绝大多数航司的航班和价格数据一次抓取就能覆盖大部分航线。而且携程的网页端和H5端提供了相对规范的接口结构返回的是JSON数据解析成本低得多。更关键的是携程的反爬虽然复杂但它的策略是“可逆”的通过Python、JS逆向等手段可以还原真实数据这是很多小平台都不具备的特征。1.3 技术方案选型requests直连、浏览器模拟还是混合方案动手之前先选方案。市面上主流的有三条路requests/httpx直接请求后端接口。优点是速度快、资源占用小适合大规模抓取缺点是需要处理签名、Cookie、字体反爬等环节逆向成本高。Selenium/Playwright/Pyppeteer浏览器自动化。好处是浏览器帮你执行了大部分JS逻辑突破签名验证几乎没有成本缺点是吃内存、并发低开10个浏览器实例机器基本就满了。混合方案。用requests处理大部分常规请求遇到强校验页面用浏览器自动化补充Cookie或签名。这也是我目前在实际项目中用的方案。如果你是第一次搞携程我的建议是先别急着上浏览器自动化。虽然它看起来省事但隐含的并发瓶颈和IP风控问题会把后续维护成本拉到很高的位置。先用requests硬着头皮把签名和字体反爬解出来后面做分布式的时候会轻松很多。2. 携程反爬机制拆解从请求头到字体加密2.1 请求头与Cookie校验最基础也最容易忽略的关卡任何网站的第一道反爬都在请求头里。携程也不例外而且做得比一般站点更严格。UA、Referer、Cookie这三样缺一不可特别是Cookie很多新手一上来就栽跟头。我第一次抓携程机票的时候写了一个简单的requests脚本带上浏览器UA去GET航班列表页结果返回了一串异常JSON提示“当前访问人数较多请稍后再试”。排查了很久发现问题是Cookie缺失。携程会在你首次访问首页时下发一系列Cookie后续接口请求都必须带上这些Cookie才能通过校验。解决方法是先用requests访问一下携程的首页拿到Set-Cookie里写入的会话信息然后再去请求查询接口。如果还是不稳定直接手动从浏览器开发者工具里复制一份完整的Cookie字符串硬编码到Headers里先跑通流程。注意Cookie会过期生产环境要设计定时更新的机制后面我会细说。还有一点容易被忽略Referer字段。携程的部分接口会校验请求来源如果Referer是空的或者指向别的域名会直接拒绝服务。所以我在构造请求时始终带着Referer一般就是对应的航班列表页URL。2.2 接口定位与参数分析逛Network面板是基本功脚本写好了下一步就是找到真正返回航班数据的接口。这一步不涉及什么高深技术全靠开发者工具。操作路径如下用Chrome打开携程机票列表页按F12进入Network面板勾选Fetch/XHR过滤然后手动输入一个出发地和目的地选一个日期点查询。接下来观察面板里冒出来的请求按时间排序找返回内容里包含航班号或价格的那个请求。说个更具体的例子我在抓“上海到成都”这条航线时发现查询后页面里出现了一个以api开头的请求Response是标准JSON结构里就有航班号、起降时间、舱位价格这些字段。这说明携程的网页端走的是前后端分离架构页面上的数据是异步加载的分析好了可以直接拿JSON。拿到接口URL之后先别急着写代码把Request Headers、Query String Parameters、Payload全部复制下来。这里有几个关键点接口URL里可能会带时间戳、渠道号等动态参数要看清楚哪些是固定的哪些每次都会变。请求方式有的是GET有的是POSTPOST的话要看Payload格式是form-data还是JSON。有些参数看着像明文其实是从JS里动态生成的这就是后面要处理的“签名参数”。2.3 签名参数与JS逆向绕不开的硬骨头携程的接口里有一个参数让我印象深刻每次请求都会变而且取值范围看起来毫无规律。最初我以为是随机数后来把携程的JS文件拉下来一看发现这个参数是在页面加载过程中通过一段加密函数生成的输入是当前时间戳、一个固定字符串还有若干环境信息输出经过MD5和Base64的多重处理。破解这种签名参数有两种思路。第一种是把加密函数用Python重写一遍但遇到混淆严重的JS时工作量巨大。第二种是直接用pyexecjs或者node.js环境来执行原始的JS代码让Python调用JS函数生成签名。我自己更倾向用pyexecjs因为环境搭建简单不用额外起服务。具体做法是从携程的静态资源文件里定位到包含加密函数的那段JS。把函数单独摘出来做成一个纯函数模块确保不依赖浏览器环境。在Python里通过execjs.compile加载这个模块每次请求前调用一次拿到新鲜的签名参数。把签名拼到请求参数里再发请求。要注意的是携程会不定期更新加密逻辑一般表现为签名参数的名字变了、长度变了或者生成逻辑变了。这时候需要重新回到Network面板对比新旧请求的差异定位变动的代码位置。我经历过一次升级对方只是把字符串拼接顺序变了我排查了两个小时才找到问题很折磨人但也很有成就感。2.4 字体反爬数字看着正常背地里全是“暗号”说到携程的反爬就不得不提字体反爬这是很多爬虫新手最头疼的地方。简单说网页上显示的价格、航班号看起来是正常的数字但你在HTML或JSON里拿到的却是一些特殊字符或者编码直接保存下来根本没法用。携程的字体反爬做法是定义一个自定义字体文件woff格式把标准数字0-9映射到一些自定义的Unicode编码上。浏览器加载字体文件后显示出来的是正常数字而爬虫拿到的源码里是一堆奇奇怪怪的字符。处理办法如下从HTML里找到字体文件的URL一般是css样式表里通过font-face定义的下载下来保存为.woff文件。用fontTools库读取这个字体文件解析cmap表拿到Unicode编码到字形名称的映射关系。把字形名称再对应到真实数字上就能建出一个“编码转数字”的字典。抓取时遇到被加密的数字直接查字典转换。最开始做的时候我也搞不清楚字形名称和数字怎么对应后来发现每个数字对应的字形名称一般都有规律比如“one”“two”“three”或者简单的序号。拿到第一份映射之后后面就可以自动处理了。但是注意携程会定期更换字体文件你不能把映射表写死必须在每次请求之后动态解析最新的字体文件否则一段时间后会发现数据全部错乱。这也是爬虫系统里最隐蔽的一个坑不是报错而是数据静默变错排查起来非常浪费时间。3. 完整实操从拿到接口到数据入库的全流程3.1 环境准备与依赖安装先把基础环境列一下我用的是Python 3.10依赖库如下pip install requests pip install fonttools pip install pyexecjs pip install pandas pip install pymysql除了这些提前装好Node.js因为pyexecjs在部分系统上需要Node来解释执行JS。Windows和macOS都测试过Node版本建议14以上。3.2 构造一个可用的机票查询脚本拿“上海到成都查询当天航班”为例先写一个最基础的请求函数确认能拿到原始JSONimport requests import execjs import time session requests.Session() session.headers.update({ 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, Referer: https://flights.ctrip.com/online/list/oneway-shanghai-chengdu, Accept: application/json, text/plain, */* }) # 先访问首页拿到初始Cookie session.get(https://flights.ctrip.com/online/list/oneway-shanghai-chengdu) # 调用JS生成签名参数 with open(sign.js, r, encodingutf-8) as f: js_code f.read() ctx execjs.compile(js_code) sign_params ctx.call(generateSign, int(time.time() * 1000)) payload { departureCity: SHA, arrivalCity: CTU, date: 2025-01-15, sign: sign_params[sign], timestamp: sign_params[timestamp] } resp session.post(https://flights.ctrip.com/itinerary/api/12808/flightList, datapayload) print(resp.json())这个方法跑通之后你会看到一个完整的航班列表JSON。里面包含每个航班的基本信息、起降时间、机型、舱位、价格等字段数据量很大后续做解析时可以用JSONPath或者直接按层级取字段。3.3 字体反爬解析与数据清洗拿到JSON之后下一步是价格字段的清洗。携程返回的数字可能有两种情况一种是明文数字一种是经过字体加密的字符。我在实操中发现价格字段经常会混着出现所以不能写死哪种方式要做一次判断。下面是我实际用的解析片段from fontTools.ttLib import TTFont def build_font_map(woff_path): font TTFont(woff_path) cmap font.getBestCmap() # cmap里的key是十进制编码value是字形名 # 通过字形名对应数字映射 num_map {} for code, glyph_name in cmap.items(): if glyph_name in [one, two, three, four, five, six, seven, eight, nine, zero, period]: num_map[code] glyph_name elif glyph_name.startswith(num): # 有些版本的字体用 num1 num2 这种命名 num_map[code] glyph_name.replace(num, ) return num_map拿到映射字典之后遍历价格字段里的每个字符如果字符的Unicode编码在字典里就替换成对应的数字如果不在字典里就保持原样。大部分情况下价格字段里的加密数字都能被还原出来。清洗数据这一步别忽略因为后面做价格趋势分析时如果数据里有半个乱码或者类型不对整个图表都会出问题。我一般会转成float类型然后做一致性校验比如价格大于0、起飞时间早于到达时间、航班号格式正确等。3.4 数据存储与去重设计数据解析完之后存储方案要提前想好。如果只是自己研究存CSV就够了如果想做长期监控建议直接上MySQL。建表语句可以参考这个结构CREATE TABLE flight_prices ( id INT AUTO_INCREMENT PRIMARY KEY, route_code VARCHAR(32) NOT NULL, flight_date DATE NOT NULL, flight_number VARCHAR(16) NOT NULL, departure_time VARCHAR(8), arrival_time VARCHAR(8), cabin_class VARCHAR(16), price DECIMAL(10,2), crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_route_date_flight (route_code, flight_date, flight_number, cabin_class) );去重逻辑很重要。同一个航班一天之内会被反复抓取如果没有唯一键数据库很快就会被重复数据塞满。我的做法是对“航线日期航班号舱位”做联合唯一索引这样即使重复抓取也只会更新价格字段不会新增记录。入库时不建议一条一条insert性能太差。先用pandas把数据拼接好再用executemany做批量插入。实测下来10万条数据从解析到入库耗时可以控制在两分钟左右这个效率对个人项目完全够用。4. 常见问题与排查技巧实录4.1 请求返回“当前访问人数较多”怎么办这个提示是携程风控触发后的典型响应。常见原因有三个Cookie缺失或过期检查headers里是否有完整的Cookie尤其是登录态相关的几个字段。请求频率太高同一个IP在短时间内请求了太多次触发频率限制。解决方法是加延时或者用IP轮换。签名参数无效确认签名函数是否还能正常执行有时候JS文件更新了本地缓存的旧JS生成的签名已经被服务端判为无效。我的排查顺序是先看Cookie再看签名最后才考虑IP问题。因为Cookie的问题最好定位而IP问题需要观察是否持续出现才能判断。4.2 字体映射对不上、数据全是乱码这个问题在爬虫上线几天后可能会出现。原因基本就是字体文件更新了但程序还在用旧的映射表。解决办法是让程序每次请求页面时都重新检查一下字体文件有没有变化可以通过对比字体文件的MD5来实现。变化了就去重新解析顺便把旧的映射表替换掉。另外有些页面里同一类字体会有多个文件不要只抓第一个font-face要把页面里所有的woff文件都下载下来分析我曾经因为漏掉第二个字体文件导致部分航班号一直解析不对。4.3 数据字段突然变空了怎么定位还有一种情况更迷惑请求状态码正常JSON也返回了但航班列表是空的。这通常意味着请求参数里有字段的值不正确比如城市代码写错了或者日期格式不对。城市代码在携程里是固定的三字码比如上海PVG/SHA、成都CTU。出发地和目的地要区分机场还是城市用错的话返回的数据就是空的。这不是反爬是参数校验对照网页上的请求Payload逐项检查就能发现。4.4 并发抓取时的性能优化心得单线程抓取肯定慢但一上来就上分布式又容易把风控触发得太快。我的建议是先从并发4-6个线程开始观察一段时间如果稳定再逐步增加。多线程方案在Python里直接用concurrent.futures的ThreadPoolExecutor就能实现。每个线程维护一个独立的requests.Session避免共享Session导致的Cookie串号问题。另外建议把数据写入改成异步批量模式而不是每个线程各写各的这样能避免数据库连接过多引发的锁等待。说一个我踩过的比较典型的坑一开始我让每个线程都开一个MySQL连接结果跑到第50个请求的时候数据库直接报“Too many connections”整个任务崩掉。后来改成全局共享一个连接池每次取连接用上下文管理器管理问题就解决了。5. 抓取边界与长期维护的几点体会5.1 学会识别反爬的“软性信号”反爬不一定都是403或者验证码很多是软性的。比如返回的JSON里数据顺序变了价格字段突然加上了额外的字符或者部分航班数据被随机替换成相近的值这些都是风控系统在尝试干扰你让你拿到的数据失真。遇到这种情况我一般会做个数据完整性校验脚本每天跑完抓取后自动检查数据量是否正常价格范围是否合理如果连续几次抓到异常数据就会触发报警及时人工介入。这个策略救了我很多次毕竟爬虫系统跑着跑着数据源变了而你还不知道这才是最可怕的。5.2 不要挑衅风控也不要主动对抗技术上有能力做更多事不一定代表就该做。抓取速度控制在对方可以接受的范围内只获取自己需要的数据不要尝试去爬取用户个人信息或者超出合理范围的敏感数据。我在和不少同行交流时发现很多新手倾向于把频率拉得特别高结果不仅账号被封连公司IP段都被拉黑。说到底爬虫是解决数据需求的工具不是炫技的手段。稳定的抓取节奏对双方都友好数据质量也会有保障。5.3 爬虫学习能给你带来什么如果说这几年做爬虫最大的收获不是写了多少行代码而是培养了一种“遇到黑盒不慌”的解决问题的能力。看到页面上一个动态变化的参数能顺着线索去定位到对应的JS代码看到字体乱码能从字体文件结构里分析出映射逻辑。这些能力不仅在爬虫领域有用放到任何需要逆向分析的工作里都很有价值。携程机票抓取这件事说难也难说简单也简单。难在其中的反爬链路长、细节多每一步都有坑简单在有技巧、有方法的条件下其实就是一个接口分析和数据清洗的过程。希望我这些实操记录能帮你少走一点弯路。
返回列表