ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

携程酒店数据爬虫实战:接口逆向与反爬应对全指南

携程酒店数据爬虫实战:接口逆向与反爬应对全指南 直接爬携程的价格数据这事放几年前还算新鲜现在基本是入门级实战里绕不开的经典场景。但经典归经典携程的反爬和风控从来没停过网上很多教程跑两天就失效核心原因在于只教了怎么发请求没讲清楚酒店数据在页面里到底是怎么加载的、哪些参数会被风控盯上、被封之后该怎么退。这篇博文就把我实际跑通的这套流程完整拆开从请求构造、数据解析、并发策略到被封后的处理思路一步步讲清楚。1. 项目整体设计与思路拆解1.1 核心需求解析先明确一件事你要抓的不是携程前台的 HTML 页面本身而是它背后返回 JSON 数据的接口。酒店列表页、详情页、价格日历这三个页面的数据来源和参数结构完全不同如果一上来就对着页面元素硬解析大概率会死在动态加载和字体反爬这两关上。这个项目的核心目标是给定一个城市和入住日期拿到该城市下所有酒店的基础信息酒店名、星级、评分、地址和价格数据最低价、房型价、价格日历。实际工作中你可能只需要其中一部分但架构上最好一次把数据模型设计全避免后面反复改表。1.2 方案选型背后的考量先说我为什么不用 Scrapy也不用 Playwright。Scrapy 是框架功能全但学习曲线陡对于这种单城市、单日期的定向抓取用 requests parsel 反而更轻。你不需要调度器、不需要中间件、不需要 Item Pipeline这些在项目初期全是负担。等数据量真的大到需要分布式了再迁移到 Scrapy 也不迟核心的解析逻辑是可以平移的。Playwright 是最后的手段。携程的列表页和详情页确实有动态渲染的部分但价格数据全部来自 XHR 接口只要能把接口参数仿对requests 完全够用。浏览器自动化最大的问题是慢——一个页面平均 3 到 5 秒一万家酒店跑下来就是十几个小时而接口直连的方案能做到每请求 0.3 秒以内。不要一上来就上重武器先看轻量方案能不能解决。requests parsel 这套组合requests 负责发 HTTP 请求拿 JSONparsel 负责在你确实需要解析 HTML 片段比如某些详情页的设施列表时做 XPath 提取。两个库都很成熟社区资料多踩坑时容易搜到解决方案。2. 核心细节解析与实操要点2.1 接口定位与参数逆向打开携程酒店列表页按 F12 进入开发者工具切到 Network 面板刷新页面后筛选 XHR 请求。你会看到一批以list、hotel、search等关键词开头的请求。逐个点开看响应体找到那个返回酒店列表 JSON 的接口。这个接口的完整 URL 长得吓人几十个参数堆在一起。但你不需要全部理解只需要抓住几个核心的cityId城市 ID携程内部的城市编码北京是 2上海是 1这个映射可以从携程的城市列表接口拿到checkIn/checkOut入住和离店日期格式 YYYY-MM-DDpageIndex页码从 1 开始pageSize每页数量默认 20实测最大能调到 100sort排序方式默认是推荐排序其余的uuid、utm、flash这类参数大部分是埋点或标识用途。我实测下来即使不加也能正常返回数据但为了稳妥建议还是从浏览器里原样复制一份完整 Cookie 和请求头作为底稿。2.2 请求头与 Cookie 的构造策略携程的风控主要看三样东西User-Agent、Cookie 的完整性、请求频率。User-Agent 别用 Python 默认的改成 Chrome 最新版的 UA 字符串。Cookie 这件事不同人有不同做法。有人用 session 自动维护有人每次请求都带固定 Cookie。我的经验是先用浏览器登录一次携程把 Cookie 复制出来存到配置文件中每次请求都带上。这么做的好处是稳定坏处是 Cookie 有有效期一般 3 到 7 天会失效到时候重新复制一次就行。还有个容易被忽略的点Referer字段。请求列表接口时Referer 必须是列表页的 URL请求详情页接口时Referer 必须是详情页的 URL。平台会校验这个字段对不上会返回 403。2.3 字体反爬与价格字段处理携程的价格数据在接口返回的 JSON 里是明文数字不存在字体反爬的问题。字体反爬主要出现在网页版的评论内容上价格接口是干净的。这一点比很多同类平台做得好也降低了抓取难度。但价格字段有个坑接口里返回的价格是含税价还是不含税价不同接口不一样。列表接口返回的是最低价的裸价详情页的房型接口返回的才是包含服务费和税费的最终价。如果你的业务需要展示最终价一定要以详情页的paymentInfo字段为准。价格日历的数据结构稍微特殊一点。priceCalendar接口返回的是一段包含多个日期价格的对象日期以时间戳形式存储需要转换成YYYY-MM-DD格式才能对应上。3. 实操过程与核心环节实现3.1 环境准备Python 版本建议 3.9 以上我本地用的是 3.10。依赖库就四个requests、parsel、pandas、retry。安装命令一条pip install requests parsel pandas retry如果你用的是国内网络记得换清华源不然下载速度会让你怀疑人生pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests parsel pandas retry编辑器我这里用的是 VS Code配好 Python 插件后就够用了。不需要 PyCharm 那么重的工具这个项目的代码量控制在 500 行以内轻量编辑器反而更专注。3.2 请求发送与重试机制先封装一个基础的请求函数。这里有两个关键点一是超时时间必须设置Python 的 requests 默认不设超时对方服务器一挂你就会无限等待二是重试机制必须有网络抖动和风控拦截是常态不重试的项目活不过一个晚上。import requests import time from retry import retry HEADERS { 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, Accept: application/json, text/plain, */*, Referer: https://hotels.ctrip.com/hotels/list, Cookie: 你的浏览器Cookie写在这里, } retry(tries3, delay2, backoff2) def fetch_json(url, params): resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() return resp.json()retry装饰器是retry库提供的tries3表示最多重试 3 次delay2是第一次重试前等 2 秒backoff2是每次重试的等待时间翻倍。这个配置实测比较合理既不会因为重试太频繁加重风控风险也不会因为等待太久拖慢整体速度。3.3 酒店列表接口的完整实现列表接口的 URL 我整理了一下去掉无关参数后的最小可用版本长这样def get_hotel_list(city_id, check_in, check_out, page_index1, page_size20): url https://m.ctrip.com/restapi/h5api/searchapp/hotels/list params { cityId: city_id, checkIn: check_in, checkOut: check_out, pageIndex: page_index, pageSize: page_size, sort: default, source: ho1, from: hotel_list, } data fetch_json(url, params) hotels [] for item in data.get(hotelList, []): hotels.append({ hotel_id: item.get(hotelId), hotel_name: item.get(hotelName), star: item.get(star), score: item.get(commentScore), address: item.get(address), min_price: item.get(minPrice), original_price: item.get(originalPrice), }) return hotels, data.get(totalCount, 0)这里totalCount是整个城市符合条件的酒店总数用它除以每页数量就能得到总页数控制循环的终止条件。3.4 详情页与价格日历的抓取列表接口给的是最低价要拿到房型级别的价格需要请求详情接口。这个接口的参数相对复杂核心是hotelId和日期参数def get_hotel_detail(hotel_id, check_in, check_out): url https://m.ctrip.com/restapi/h5api/searchapp/hotel/detail params { hotelId: hotel_id, checkIn: check_in, checkOut: check_out, source: ho1, } data fetch_json(url, params) result { hotel_id: hotel_id, rooms: [], payment_info: [], } for room in data.get(roomList, []): result[rooms].append({ room_name: room.get(roomName), bed_type: room.get(bedType), breakfast: room.get(breakfastName), price: room.get(priceInfo, {}).get(price), }) return result价格日历接口返回的是未来 30 天或 90 天的价格数组用于判断价格趋势非常有价值。它的实现比详情页还简单只是需要处理时间戳from datetime import datetime, timedelta def get_price_calendar(hotel_id, check_in, days30): url https://m.ctrip.com/restapi/h5api/searchapp/hotel/priceCalendar params { hotelId: hotel_id, startDate: check_in, days: days, } data fetch_json(url, params) calendar [] for item in data.get(priceList, []): timestamp item.get(date) / 1000 date_str datetime.fromtimestamp(timestamp).strftime(%Y-%m-%d) calendar.append({ date: date_str, price: item.get(price), }) return calendar3.5 并发设计与速率控制这里要聊一下热词里反复出现的“并发设计”。携程这类平台对单 IP 的请求频率限制得很死我实测下来每秒超过 5 个请求持续一分钟左右就会被限流返回的 JSON 会变成一段无意义的乱码严重时会直接让你输入验证码。用线程池可以提升效率但必须配合限速器。我的做法是ThreadPoolExecutor配合一个全局的rate_limiter控制在每秒 3 个请求以内。这个速度看起来慢但一个小时能跑 10800 个请求足够覆盖大部分城市的全部酒店from concurrent.futures import ThreadPoolExecutor import threading import time class RateLimiter: def __init__(self, max_calls_per_second3): self.min_interval 1.0 / max_calls_per_second self.lock threading.Lock() self.last_call_time {} def wait(self, keydefault): with self.lock: now time.time() last self.last_call_time.get(key, 0) if now - last self.min_interval: time.sleep(self.min_interval - (now - last)) self.last_call_time[key] time.time() limiter RateLimiter(max_calls_per_second3) def fetch_with_limit(hotel_id, check_in, check_out): limiter.wait() return get_hotel_detail(hotel_id, check_in, check_out) with ThreadPoolExecutor(max_workers5) as executor: futures [executor.submit(fetch_with_limit, hid, check_in, check_out) for hid in hotel_ids] results [f.result() for f in futures]max_workers设置成 5配合每秒 3 个请求的限速实际跑起来很稳。注意这里的关键不是并发数高不高而是单位时间内的请求总量有没有超过阈值。并发 5 和并发 20 如果都控在每秒 3 个请求效果是一样的但并发过高会带来 CPU 和内存的无谓消耗。3.6 数据存储数据量不大的时候直接用 pandas 存 CSV 就够了。我习惯把酒店基础信息、房型价格、价格日历分别存成三个 CSV方便后续用 Excel 或 Tableau 做分析import pandas as pd hotel_df pd.DataFrame(hotel_results) hotel_df.to_csv(hotels.csv, indexFalse, encodingutf-8-sig) room_df pd.DataFrame(room_results) room_df.to_csv(rooms.csv, indexFalse, encodingutf-8-sig)utf-8-sig编码很重要。直接用utf-8存的文件用 Excel 打开会乱码因为 Excel 默认按 GBK 解码。加上-sig前缀后文件会带上 BOM 头Excel 就能正确识别了。4. 常见问题与排查技巧实录4.1 请求被风控拦截的表现与判断平台的风控拦截不是直接拒绝连接而是假装正常返回一个错误响应。我整理了一个速查表方便你快速判断自己是不是被针对了现象可能原因处理方式返回 JSON 正常但 hotelList 为空被风控返回假数据停止请求等待 10-15 分钟返回 403 ForbiddenReferer 缺失或 Cookie 失效核对请求头更新 Cookie请求超时TimeoutErrorIP 被临时限流降低频率检查代理返回 HTML 字符串而非 JSON触发了验证码页面更换 IP 或使用代理池第四种情况最麻烦说明风控已经盯上你的 IP 了。这时候继续请求没有任何意义最快的方式是切换出口 IP。如果是家庭宽带重启路由可能换 IP如果不行就得准备代理池。4.2 代理池的选型和配置热词里提到了“python 爬虫ip代理”这块确实绕不开。但我的建议是能不用代理就别用代理的质量参差不齐很多高匿代理实际响应速度很慢反而拉低整体效率。只有当单个 IP 的风控问题严重到影响数据完整性时才考虑代理方案。我的选择标准是只选支持 HTTP/HTTPS 的短效代理5 分钟一换的那种每个代理 IP 分配不超过 20 个请求代理失效时自动重试换下一个而不是傻等使用代理的代码改动很小requests 直接支持proxies { http: http://your_proxy_ip:port, https: http://your_proxy_ip:port, } resp requests.get(url, paramsparams, headersHEADERS, proxiesproxies, timeout10)4.3 数据完整性与校验跑完一批数据后建议做一个完整性校验。我踩过几次数据缺失的坑总结下来最主要的原因是并发场景下部分请求异常被静默吞掉了。最简单的校验方式统计每个城市的酒店总数对比列表接口返回的totalCount和实际抓取到的hotel_id去重数量。对不上的话用缺失的 ID 列表重新跑一遍补漏missing_ids set(all_ids) - set(fetched_ids) for hid in missing_ids: detail get_hotel_detail(hid, check_in, check_out) # 写入数据库还有一个常见的坑是日期格式。携程接口对日期格式很敏感有的接口接受2024-01-01有的接口只接受2024/01/01。统一在入口处做格式转换不要在每个函数里各自处理def normalize_date(date_str): return date_str.replace(/, -)4.4 Playwright 的适用场景前面说了不需要 Playwright但有一种情况例外当你发现某个页面数据不是通过 XHR 接口返回而是由 JavaScript 渲染到 DOM 里的时候这种情况在携程的个别子页面依然存在requests 方案就无能为力了。如果你只是偶尔抓一两个页面用 Playwright 的page.goto()page.content()组合配合 parsel 解析是可行的。但不要把它作为主方案因为它的性能和资源开销都远超 requests。真到了必须频繁处理 JS 渲染页面的程度我会重构方案让 Playwright 只负责渲染和抓取接口响应把数据吐给 requests 模块去处理后续逻辑各司其职。4.5 关于合规的提醒最后说一个所有教程都不会细讲但你必须知道的事。抓取携程数据用于个人学习、学术研究问题不大但如果你要商用或者抓取频率高到影响对方服务器稳定就会涉及法律风险。我的建议是严格遵守 robots.txt 和网站服务条款控制请求频率不要把对方服务器打挂只抓你需要的字段不要全量存储个人信息如果可能优先使用官方开放平台接口爬虫技术本身是中性的用得好是效率工具用不好就是给自己挖坑。做一个有底线的开发者比做一个技术最牛的开发者更重要。5. 扩展思路与本项目可复用的资产酒店价格抓取这个项目本身做完你会发现自己积累的这套能力是可以横向复用的。请求库的封装、限速器的实现、Cookie 管理方案、缓存策略这些代码模块换个 URL 和参数就能用在其他平台的数据抓取上。我后来做的机票价格监控、景点门票比价都是在这个项目的基础上改改参数就跑起来的。遇到新的平台最耗时间的永远是逆向分析接口参数那段但只要把思路理顺——找接口、找参数、试请求、控频率——基本半天就能搞定一个全新的数据源。还有一个方向值得提把抓到的历史价格数据积累起来做价格预测。携程的酒店价格受节假日、大型活动、淡旺季影响非常明显如果你能连续抓一个月的数据用简单的时间序列分析就能发现不少有意思的规律。这些数据如果配合机器学习的时序预测模型可以做酒店价格低谷提醒工具在旅游省钱这件事上非常实用。爬虫接入了持续的数据采集能力之后项目的价值就不单是一次性的抓取而是一个能持续产生数据资产的体系。这也是我建议你把代码写成模块化、配置可外置的原因——为了后期扩展。
返回列表