news 2026/9/21 16:14:57

使用 browser-harness 绕过携程(ctrip)反爬门:酒店列表与价格的浏览器自动化抓取实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 browser-harness 绕过携程(ctrip)反爬门:酒店列表与价格的浏览器自动化抓取实战
  • 浏览器控制
  • GUI 自动化
  • AI Agent
  • 人工智能
  • MCP 服务
  • AI 技能

【免费下载链接】browser-harness

Browser Harness | Self-healing harness that enables LLMs to complete any task.

项目地址:https://gitcode.com/gh_mirrors/br/browser-harness
点击查看免费下载

本文基于 agent-workspace/domain-skills/ctrip/hotels.md 这份实测领域技能文档整理成文。该文档于 2026-04-29 针对hotels.ctrip.comPC 网页版(国内酒店)完成实测验证。本文以它为核心骨架,结合本仓库browser-harness的源码实现,系统讲解携程酒店价格的抓取原理、URL 参数模式、列表页/详情页 DOM 提取、登录重定向信号识别与完整可运行的 Python 示例。读完本文,你将掌握:如何用完整的"六参数"URL 模式免登录访问携程国内酒店列表页、如何从 React 渲染的 DOM 中稳定提取酒店名称/评分/价格对、如何识别并规避passport.ctrip.com登录重定向,以及如何用浏览器坐标点击绕过非<button>的搜索控件。


一、TL;DR:携程能免登录看到价格,但有一个前提

实测结论(与同为 OTA 平台的 同程 ly.com 形成鲜明对比):

  • 价格无需登录即可见——但前提是你必须通过"自然"的导航流程到达列表页,使 URL 携带完整的参数结构。对简化 URL 直接 GET 会被重定向到登录页。
  • 必须使用浏览器会话。页面外壳只有 144 KB,酒店数据由 XHR 在水合(hydration)之后加载,http_get抓不到任何酒店数据(对应源码见 src/browser_harness/helpers.py 中http_get的纯 HTTP 实现——它不执行 JS)。
  • 一旦进入合法列表页,价格以¥<原价> ¥<当前价> 起的形式成对渲染,没有¥?占位符,也没有"请登录"弹层。
  • 详情页 URLhttps://hotels.ctrip.com/hotel/<hotelId>.html?checkin=...在未登录状态下可直接访问,并展示完整的房型/房价明细。
  • 相比之下,ly.com 把每一个价格都藏在登录墙之后;携程只要尊重 URL 参数模式,对爬取要友好得多

二、理解携程的反爬门:URL 参数模式(URL patterns)

携程的"签名式"反爬关卡是基于参数模式而非基于行为。它通过区分"真实客户端"(通过首页表单构建的 URL)和"脚本客户端"(手工输入 URL)来决定是否放行。三种 URL 形态:

列表页(规范形态,匿名友好): https://hotels.ctrip.com/hotels/list ?flexType=1 &cityId=<n> # 2 = 上海, 1 = 北京, 等 &provinceId=0 &districtId=0 &countryId=1 # 1 = 中国(国内) &checkin=YYYY-MM-DD &checkout=YYYY-MM-DD 酒店详情页(规范形态): https://hotels.ctrip.com/hotel/<hotelId>.html?checkin=...&checkout=... → 服务器会重写为 /hotels/detail/?... —— 两种写法都可用,数据一致 登录页(被判定可疑时重定向到这里): https://passport.ctrip.com/user/login?backurl=<url-encoded-target>

关键结论:简化形态https://hotels.ctrip.com/hotels/list?city=2&checkin=...&checkout=...(注意是city而非cityId,且缺少provinceId/districtId/countryId/flexType会被重定向到登录页。这就是携程区分"真实客户端"与"脚本客户端"的核心手段。

登录重定向信号与触发条件

当携程判定你的会话可疑时,会重定向到passport.ctrip.com/user/login?backurl=...。登录表单会自动预填用户 Chrome 中最近使用的手机号。看到这个 URL,不要输入任何凭据——应终止当前流程并按以下任一方式重启:

  • (a)从首页表单重新走一遍流程(下文 Path B),修复 URL 参数模式不匹配的问题;
  • (b)请用户交互式登录。

实测触发重定向的行为:

  • 直接访问/hotels/list?city=2&...(注意:city而非cityId);
  • 会话中从未先加载过hotels.ctrip.com/首页就直接 GET。

实测不会触发重定向的行为:

  • 冷启动(预设零个携程 cookie)直接访问/hotels/list?flexType=1&cityId=2&provinceId=0&districtId=0&countryId=1&...
  • 直接访问/hotel/<id>.html?...

由此可以断定:关卡基于 URL 参数模式而非行为模式。把参数模式写对,就不需要走登录流程。


三、环境准备:browser-harness 与本地 Chrome

本仓库的browser-harness通过 CDP(Chrome DevTools Protocol)直接控制真实浏览器,适合需要交互(点击、输入、导航)、需要用户已登录会话、需要 JS 渲染、或面对反爬页面的场景。调用方式为 CLI heredoc,helpers 已预先导入,run.pyexec前会自动调用ensure_daemon()拉起守护进程(见 src/browser_harness/run.py):

browser-harness <<'PY' print(page_info()) PY

安装与首次连接请参考 install.md;若连接失败,运行诊断:

browser-harness --doctor

本文涉及的核心 helpers 全部定义在 src/browser_harness/helpers.py:

Helper作用源码位置
new_tab(url)后台新建标签页并导航helpers.py
wait_for_load(timeout)轮询document.readyState == 'complete'helpers.py
js(expression)在页面中执行 JS 表达式并取回值helpers.py
click_at_xy(x, y)通过 CDP 鼠标事件在坐标处点击(合成器层面穿透 iframe/Shadow DOM)helpers.py
type_text(text)通过Input.insertText输入文本helpers.py
press_key(key)派发真实按键事件(Enter、Tab、Backspace 等)helpers.py
cdp(method, **params)裸 CDP 调用,如Target.closeTargethelpers.py
page_info()返回当前页面的 url/title/视口/滚动信息helpers.py

注:wait_for_load()只能确认文档complete,对 SPA 不够;携程列表页的酒店数据由水合后的 XHR 加载,因此文档统一采用wait_for_load+time.sleep的组合等待。


四、可靠地进入列表页:两条路径

Path A —— 构建规范 URL(已知 cityId 时首选)

当你已经知道目标城市的cityId(见文末对照表),直接构建完整六参数 URL 即可:

from browser_harness.helpers import new_tab, wait_for_load import time url = ( "https://hotels.ctrip.com/hotels/list" "?flexType=1&cityId=2&provinceId=0&districtId=0&countryId=1" "&checkin=2026-04-29&checkout=2026-04-30" ) new_tab(url) wait_for_load(timeout=20) time.sleep(6) # XHR 拉取酒店列表

Path B —— 模拟首页流程(只有城市名时)

如果只有城市名(如"上海"),就驱动首页表单。这里有两个关键 DOM 事实:

  1. 搜索按钮是<div>而不是<button>——与 ly.com 相同(详见 ly-com/hotels.md 中的 "search button gotcha")。el.click()不可靠,坐标点击有效。
  2. 目的地输入框通过 placeholder 特征识别(/目的地|城市|酒店/),输入城市名后按 Enter 选择第一个自动补全建议。
from browser_harness.helpers import new_tab, wait_for_load, js, click_at_xy, type_text, press_key import time new_tab("https://hotels.ctrip.com/") wait_for_load(timeout=20) time.sleep(2) # 通过 placeholder 定位目的地输入框 inp = js(""" const i = Array.from(document.querySelectorAll("input")) .find(i => i.placeholder && /目的地|城市|酒店/.test(i.placeholder)); if (!i) return null; const r = i.getBoundingClientRect(); return {x: r.x+r.width/2, y: r.y+r.height/2}; """) if not inp: raise RuntimeError("ctrip homepage: destination input not found — page DOM may have changed") click_at_xy(inp["x"], inp["y"]) time.sleep(0.4) type_text("上海") time.sleep(1.5) press_key("Enter") # 选择第一条自动补全建议 btn = js(""" const b = Array.from(document.querySelectorAll("button, div")) .find(el => (el.innerText||"").trim() === "搜索" && el.getBoundingClientRect().width > 60); if (!b) return null; const r = b.getBoundingClientRect(); return {x: r.x+r.width/2, y: r.y+r.height/2}; """) if not btn: raise RuntimeError("ctrip homepage: 搜索 button not found — page DOM may have changed") click_at_xy(btn["x"], btn["y"]) time.sleep(7) # 现在位于 /hotels/list?... 且带有完整的规范参数结构

这就是真人会走的流程,也正是让参数结构保持完整的原因。这个过程中设置的 cookie 对后续直接访问 URL 的请求也有帮助。

从源码结构看,click_at_xy通过Input.dispatchMouseEvent派发mousePressed/mouseReleased(helpers.py),是合成器级别的真实鼠标事件,因此对非标准控件(<div>按钮)也能生效。


五、列表页数据提取:从 React 卡片中抓取酒店

酒店卡片是<div class="list-item">卡片上没有data-id,也没有<a href=...>——酒店 ID 只存在于 React 组件闭包中。要跳转详情页,要么视觉上点卡片(坐标点击),要么抓取酒店名、再用其他途径(如逐步积累的映射表)解析出 ID 后重新访问/hotel/<id>.html

// 列表提取 —— 通过 js(...) 在浏览器内运行 return Array.from(document.querySelectorAll(".list-item")).slice(0, 30).map(card => { const text = (card.innerText || "").replace(/\s+/g, " "); const name = text.match(/^([^\s]{2,40}?(?:酒店|宾馆|公寓|民宿)(?:\([^)]*\))?)/)?.[1]; const score = text.match(/[1-5]\.\d/)?.[0]; // 先剥离 "1,234条点评" 再抓价格,否则 "1,234" 里的 ¥1 会泄漏进来 const priced = text.replace(/\d{1,3}(?:,\d{3})+/g, "").replace(/\d+条点评/g, ""); const prices = (priced.match(/¥\s*(\d{2,})/g) || []).map(s => parseInt(s.replace(/[^\d]/g, ""))); const [original, current] = prices; return {name, score, price_original: original ?? null, price_current: current ?? null}; });

陷阱(phantom price):1,234条点评5,678条点评这样的点评数如果不先剥离,会匹配到¥\s*\d+——¥1¥5会作为幽灵价格出现。必须先剥离千分位数字和\d+条点评再应用价格正则。

单张卡片的文本形状:

<name> [4.8] [超棒][1,994条点评] <tagline> 近<area> · <metro>查看地图 <room-type> 订单确认后30分钟内免费取消 ... ¥<original> ¥<current> 起 查看详情

后缀表示"起价";实际预订价可能因房价方案、早餐、取消政策而更高。


六、详情页:免登录查看完整房价明细

https://hotels.ctrip.com/hotel/<hotelId>.html?checkin=YYYY-MM-DD&checkout=YYYY-MM-DD

该 URL 免登录可用。服务器会将其规范化为/hotels/detail/?...,但数据完全一致。页面标题格式为:🟢 <hotelName>预订价格,联系电话位置地址【携程酒店】🟢前缀来自browser-harness的标签页标记机制,见 helpers.py 中的_mark_tab(),并非携程内容)。

详情页渲染多个房型/房价行,形态与列表页相同的¥orig ¥current。查找文本恰为预订<button><a>,再向上回溯到房价卡片(与 ly.com 的做法一致,可对照 ly-com/hotels.md 的"价格三元组"一节)。

失效酒店检测:如果访问的酒店已不存在,URL 会静默存活(返回 200),但只显示通用标题"预订价格,联系电话位置地址【携程酒店】"且没有价格——用bodyText.match(/¥\s*\d+/) === null检测即可。


七、登录重定向:检测与处理

检测代码:

if "passport.ctrip.com" in page_info()["url"]: raise RuntimeError("ctrip wants login — restart from homepage")

记住两条实测规则:

  • 参数模式正确(六参数 + 冷启动)→ 不触发登录重定向;
  • 参数模式不完整(city=简化形态)或未先访问首页 → 触发登录重定向。

因此核心策略是:把 URL 参数结构写对,就不需要登录流程。


八、全局状态:不要碰window

window._objAllSearchResult存在但在真实会话中是空的。携程的酒店数据存放在React 组件状态中,不在window上。请使用 DOM 提取。这里没有__NEXT_DATA__、没有__APOLLO_STATE__、也没有window.__INITIAL_STATE__。(这与 ly.com 形成对照:同程基于 Nuxt,数据在window.__NUXT__的 SSR 状态里,见 ly-com/hotels.md。)


九、陷阱清单(完整版)

  • 简化 URL(?city=)会被重定向到登录页。使用规范六参数结构,或驱动首页表单。
  • 首页默认标签是"海外酒店"。推荐酒店显示"暂无价格",并预选了新加坡/普吉岛的日期。不要从首页读价格——去/hotels/list?...
  • ¥1¥5幽灵价格:来自点评数正则泄漏。在应用¥\d+之前先剥离\d+,\d+\d+条点评
  • 卡片上没有详情 URL。data-id、无<a>。没有外部 ID 来源时,坐标点击是唯一可靠的跳转详情方式。
  • 不存在的酒店 ID 返回 200/hotel/{id}.html会带品牌外壳返回,无报错——通过正文中缺少价格来检测。
  • 搜索按钮是<div>与 ly.com 相同的坑——坐标点击有效,el.click()不可靠。
  • hotels.ctrip.com是国内主机。首页标题写着"海外酒店预订",但/hotels/list?countryId=1&...是国内数据。海外等价页面使用countryId≠ 1(尚未映射)。

十、已观测的 cityId 对照表

城市cityId
上海2
北京1
广州32
深圳30

获取更多 cityId 的办法:驱动首页流程一次,然后读取规范 URL 中的参数。


十一、Quick start:一键抓取上海酒店列表

完整的端到端示例(开新标签 → 等待加载与 XHR → DOM 提取 → 打印 JSON → 关闭标签):

from browser_harness.helpers import new_tab, wait_for_load, js, cdp import time, json url = ( "https://hotels.ctrip.com/hotels/list" "?flexType=1&cityId=2&provinceId=0&districtId=0&countryId=1" "&checkin=2026-04-29&checkout=2026-04-30" ) tid = new_tab(url) wait_for_load(timeout=20) time.sleep(6) hotels = js(""" return Array.from(document.querySelectorAll(".list-item")).slice(0, 20).map(card => { const raw = (card.innerText || "").replace(/\\s+/g, " "); const cleaned = raw.replace(/\\d{1,3}(?:,\\d{3})+/g, "").replace(/\\d+条点评/g, ""); const name = raw.match(/^([^\\s]{2,40}?(?:酒店|宾馆|公寓|民宿)(?:\\([^)]*\\))?)/)?.[1] || null; const prices = (cleaned.match(/¥\\s*\\d{2,}/g) || []) .map(s => parseInt(s.replace(/[^\\d]/g, ""))); const score = raw.match(/[1-5]\\.\\d/)?.[0] || null; return {name, score, price_original: prices[0] ?? null, price_current: prices[1] ?? null}; }); """) print(json.dumps(hotels, indent=2, ensure_ascii=False)) cdp("Target.closeTarget", targetId=tid)

运行方式(heredoc 或直接保存为脚本后通过 CLI 执行均可)。Target.closeTarget对应 helpers.py 中close_tab的底层实现,用于清理本次任务创建的标签页。


十二、延伸阅读

  • 领域技能原始文档:agent-workspace/domain-skills/ctrip/hotels.md
  • 同城 OTA 对比:agent-workspace/domain-skills/ly-com/hotels.md
  • 核心 helpers 源码:src/browser_harness/helpers.py
  • CLI 入口与 daemon 引导:src/browser_harness/run.py
  • 安装与连接:install.md;技能总览:SKILL.md
  • 标签页管理规范:interaction-skills/tabs.md;连接与标签可见性:interaction-skills/connection.md
  • 浏览器控制
  • GUI 自动化
  • AI Agent
  • 人工智能
  • MCP 服务
  • AI 技能

【免费下载链接】browser-harness

Browser Harness | Self-healing harness that enables LLMs to complete any task.

项目地址:https://gitcode.com/gh_mirrors/br/browser-harness
点击查看免费下载

相关推荐

上一篇:YOLO-World学习率预热:解决初始训练阶段梯度爆炸问题
下一篇:whisper.cpp Vulkan 后端源码解析:一份构建覆盖四大 GPU 厂商的 8600 行 C++ 实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 16:14:44

DSSAD与EDR:解读汽车黑匣子如何记录碰撞数据

做过几次事故车的数据恢复之后&#xff0c;我彻底改变了对“汽车黑匣子”的看法。很多朋友以为碰撞数据只存在于飞机或者高端赛车上&#xff0c;其实今天一台二十多万的蔚来&#xff0c;或者一台特斯拉Model 3&#xff0c;都在悄悄记录着碰撞瞬间的完整时间线。这个隐藏在行车记…

作者头像 李华
网站建设 2026/9/21 16:14:03

单文件Bash实现配环境Agent:探测-计划-执行-验证

如果你管过哪怕一台开发机&#xff0c;大概都经历过这种时刻&#xff1a;照着文档敲完几十条命令&#xff0c;以为环境终于好了&#xff0c;结果node -v能跑、npm install报错&#xff1b;PyCharm 里解释器怎么都选不对&#xff1b;git push 提示找不到 ssh key。问题不是“命令…

作者头像 李华
网站建设 2026/9/21 16:07:45

PowerPMAC上位机开发实战:用C#构建Winform运动控制界面

去年接手一个三轴检测设备的上位机项目&#xff0c;厂家只留了一台装着 PowerPMAC 调试软件的工控机。操作员每天开工要盯着命令行窗口&#xff0c;敲一堆类似#1j/#2j/的指令做回零和点动&#xff0c;稍微按错一个符号&#xff0c;轴就停在半路。于是"做一个能给人用的 Wi…

作者头像 李华