携程国际机票查询API逆向分析:从sign参数到完整数据抓取的避坑指南
携程国际机票查询API逆向工程实战从加密参数破解到数据采集全流程1. 逆向工程入门理解携程国际机票查询的核心机制逆向工程在数据采集领域一直是个充满挑战又极具价值的技术方向。携程作为国内领先的在线旅游服务平台其国际机票查询接口的设计尤其精妙包含了多层防护机制。要成功获取数据我们需要先理解整个查询流程的运作原理。国际机票查询的核心接口是/api/search/batchSearch这个POST请求需要两个关键参数transactionid和sign。与国内机票查询不同国际机票查询在参数验证和反爬策略上更为严格。请求流程分解首先获取transactionid通过一个预查询接口获得然后生成sign参数基于transactionid和航班信息本地计算最后携带这两个参数发起正式查询// 典型请求头示例 headers: { accept: application/json, content-type: application/json;charsetUTF-8, transactionid: 8c12c83315fb42fe8a73b3dd9afab405, sign: 4e4ca18f296fc7742e262f116a742f3c }注意直接复制这些参数是无效的因为它们具有时效性和唯一性必须按照正确算法实时生成。2. 关键参数解析transactionid的获取与验证transactionid是一个32位的唯一标识符由服务器生成并返回。获取它的接口设计十分巧妙GET /international/search/api/flightlist/oneway-{出发地}-{目的地}?_1depdate{日期}cabiny_sadult1containstax1URL参数说明参数名示例值说明出发地bjs北京首都机场的三字码小写目的地sel首尔仁川机场的三字码小写depdate2023-08-15出发日期YYYY-MM-DD格式cabiny_s舱位类型固定值adult1成人数量containstax1是否含税1表示含税这个接口的响应是一个JSON结构其中data.transactionID字段就是我们需要的值{ status: 0, msg: success, data: { transactionID: a24b0ed8040540bab8cea5f1c7dced62, // 其他字段... } }常见问题排查返回403错误检查请求头是否包含accept: application/json返回空transactionID确认日期格式和机场代码是否正确请求超时注意接口有频率限制过快的请求会被暂时屏蔽3. 签名算法破解sign参数的生成逻辑sign参数是整个逆向工程中最具挑战性的部分。经过分析我们发现它是通过以下方式生成的sign MD5(transactionID 出发地三字码(大写) 目的地三字码(大写) 出发日期)以北京到首尔为例如果transactionID a24b0ed8040540bab8cea5f1c7dced62出发地 BJS目的地 SEL日期 2023-08-15那么待加密字符串为a24b0ed8040540bab8cea5f1c7dced62BJSSEL2023-08-15MD5加密后得到32位小写哈希值就是最终的sign参数。Python实现示例import hashlib def generate_sign(transaction_id, dep_code, arr_code, dep_date): raw_str f{transaction_id}{dep_code.upper()}{arr_code.upper()}{dep_date} return hashlib.md5(raw_str.encode(utf-8)).hexdigest() # 使用示例 sign generate_sign( a24b0ed8040540bab8cea5f1c7dced62, bjs, sel, 2023-08-15 ) print(sign) # 输出4e4ca18f296fc7742e262f116a742f3c技术细节携程使用的是标准的MD5算法没有加盐或额外的变形处理这大大降低了逆向难度。但要注意所有字母都必须大写日期格式必须严格匹配。4. 完整请求构建与数据采集掌握了两个关键参数后我们就可以构建完整的查询请求了。以下是使用Python的requests库实现的完整示例import requests import json # 第一步获取transactionID dep_code bjs # 出发地代码 arr_code sel # 目的地代码 dep_date 2023-08-15 # 出发日期 tx_url fhttps://flights.ctrip.com/international/search/api/flightlist/oneway-{dep_code}-{arr_code}?_1depdate{dep_date}cabiny_sadult1containstax1 tx_response requests.get(tx_url, headers{accept: application/json}) tx_data tx_response.json() transaction_id tx_data[data][transactionID] # 第二步生成sign sign generate_sign(transaction_id, dep_code, arr_code, dep_date) # 第三步构建查询参数 query_params { flightWayEnum: OW, arrivalProvinceId: 0, # 其他必要参数... flightSegments: [ { departureDate: dep_date, departureCityCode: dep_code.upper(), arrivalCityCode: arr_code.upper(), # 其他航班段信息... } ], transactionID: transaction_id } # 第四步发送查询请求 search_url https://flights.ctrip.com/international/search/api/search/batchSearch headers { accept: application/json, content-type: application/json;charsetUTF-8, transactionid: transaction_id, sign: sign } response requests.post( search_url, headersheaders, datajson.dumps(query_params) ) flight_data response.json() print(flight_data)响应数据结构关键字段flightItineraryList: 航班列表flightSegments: 航段信息priceList: 价格信息adultPrice: 成人票价adultTax: 税费baggage: 行李额信息penalty: 退改签规则5. 高级技巧与反反爬策略在实际操作中直接使用上述代码可能会遇到各种反爬措施。以下是几个经过验证的有效策略请求头优化必须包含完整的headers特别是User-Agent和Referer模拟真实浏览器的headers组合headers { authority: flights.ctrip.com, accept: application/json, accept-language: zh-CN,zh;q0.9, cache-control: no-cache, content-type: application/json;charsetUTF-8, origin: https://flights.ctrip.com, pragma: no-cache, referer: https://flights.ctrip.com/, user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 }请求频率控制每个IP每分钟不超过5次查询在请求间加入随机延迟1-3秒对重要查询使用代理IP轮换参数动态生成每次请求都重新获取transactionID实时计算sign参数模拟真实用户操作流程错误处理机制捕获429状态码请求过多自动重试机制最多3次异常情况下的IP切换from time import sleep import random def safe_request(url, methodGET, headersNone, dataNone, max_retries3): for attempt in range(max_retries): try: sleep(random.uniform(1, 3)) # 随机延迟 if method GET: response requests.get(url, headersheaders) else: response requests.post(url, headersheaders, jsondata) if response.status_code 429: sleep(10) # 遇到限流等待10秒 continue return response except Exception as e: print(fAttempt {attempt 1} failed: {str(e)}) if attempt max_retries - 1: raise6. 数据解析与存储优化获取到原始数据后如何高效解析和存储也是关键。以下是几个实用建议数据结构优化提取核心字段去除冗余信息将嵌套的JSON结构扁平化为常用查询字段建立索引Python解析示例def parse_flight_data(raw_data): flights [] for itinerary in raw_data.get(data, {}).get(flightItineraryList, []): for segment in itinerary.get(flightSegments, []): for flight in segment.get(flightList, []): flight_info { flight_no: flight.get(flightNo), departure: { city: flight.get(departureCityName), airport: flight.get(departureAirportName), time: flight.get(departureDateTime) }, arrival: { city: flight.get(arrivalCityName), airport: flight.get(arrivalAirportName), time: flight.get(arrivalDateTime) }, duration: flight.get(duration), aircraft: flight.get(aircraftName), prices: [] } for price in itinerary.get(priceList, []): flight_info[prices].append({ type: price.get(eligibility), price: price.get(adultPrice), tax: price.get(adultTax), cabin: price.get(cabin), baggage: price.get(baggage, {}).get(dataList, [{}])[0].get(adultBaggage, {}) }) flights.append(flight_info) return flights存储方案对比存储类型优点缺点适用场景MySQL事务支持完善查询能力强需要预先定义schema结构化数据存储MongoDB灵活适合JSON数据占用空间较大原始数据存储Elasticsearch搜索性能优异维护成本高需要全文检索的场景CSV/JSON文件简单易用查询效率低小规模临时存储7. 实战经验与避坑指南在实际项目中我们积累了一些宝贵经验机场代码问题携程使用的是三字码如PEK不是IATA的两字码部分城市有多个机场如上海有SHA/PVG需要明确指定国际机场代码大小写敏感必须与服务器端完全一致日期格式陷阱请求参数中的日期格式为YYYY-MM-DD响应中的日期时间格式为YYYY-MM-DD HH:MM:SS时区问题所有时间都是当地时间没有UTC转换价格显示规则adultPrice是基础票价不含税adultTax是税费实际支付金额为两者之和不同舱位Y/S/M等价格差异可能很大反爬升级应对定期检查接口变化大约每3-6个月会有小调整准备多套请求头轮换使用监控成功率低于90%时需要检查算法是否变更常见错误排查表错误现象可能原因解决方案返回status:-1sign计算错误检查MD5算法和输入参数顺序空结果集日期太近/太远尝试调整查询日期连接超时IP被封更换IP增加延迟数据不全参数缺失检查是否传递了所有必需参数