ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

使用 browser-harness 绕过携程(ctrip)反爬门:酒店列表与价格的浏览器自动化抓取实战 浏览器控制GUI 自动化AI Agent人工智能MCP 服务AI 技能【免费下载链接】browser-harnessBrowser 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 ?flexType1 cityIdn # 2 上海, 1 北京, 等 provinceId0 districtId0 countryId1 # 1 中国国内 checkinYYYY-MM-DD checkoutYYYY-MM-DD 酒店详情页规范形态 https://hotels.ctrip.com/hotel/hotelId.html?checkin...checkout... → 服务器会重写为 /hotels/detail/?... —— 两种写法都可用数据一致 登录页被判定可疑时重定向到这里 https://passport.ctrip.com/user/login?backurlurl-encoded-target关键结论简化形态https://hotels.ctrip.com/hotels/list?city2checkin...checkout...注意是city而非cityId且缺少provinceId/districtId/countryId/flexType会被重定向到登录页。这就是携程区分真实客户端与脚本客户端的核心手段。登录重定向信号与触发条件当携程判定你的会话可疑时会重定向到passport.ctrip.com/user/login?backurl...。登录表单会自动预填用户 Chrome 中最近使用的手机号。看到这个 URL不要输入任何凭据——应终止当前流程并按以下任一方式重启a从首页表单重新走一遍流程下文 Path B修复 URL 参数模式不匹配的问题b请用户交互式登录。实测触发重定向的行为直接访问/hotels/list?city2...注意city而非cityId会话中从未先加载过hotels.ctrip.com/首页就直接 GET。实测不会触发重定向的行为冷启动预设零个携程 cookie直接访问/hotels/list?flexType1cityId2provinceId0districtId0countryId1...直接访问/hotel/id.html?...。由此可以断定关卡基于 URL 参数模式而非行为模式。把参数模式写对就不需要走登录流程。三、环境准备browser-harness 与本地 Chrome本仓库的browser-harness通过 CDPChrome DevTools Protocol直接控制真实浏览器适合需要交互点击、输入、导航、需要用户已登录会话、需要 JS 渲染、或面对反爬页面的场景。调用方式为 CLI heredochelpers 已预先导入run.py在exec前会自动调用ensure_daemon()拉起守护进程见 src/browser_harness/run.pybrowser-harness PY print(page_info()) PY安装与首次连接请参考 install.md若连接失败运行诊断browser-harness --doctor本文涉及的核心 helpers 全部定义在 src/browser_harness/helpers.pyHelper作用源码位置new_tab(url)后台新建标签页并导航helpers.pywait_for_load(timeout)轮询document.readyState completehelpers.pyjs(expression)在页面中执行 JS 表达式并取回值helpers.pyclick_at_xy(x, y)通过 CDP 鼠标事件在坐标处点击合成器层面穿透 iframe/Shadow DOMhelpers.pytype_text(text)通过Input.insertText输入文本helpers.pypress_key(key)派发真实按键事件Enter、Tab、Backspace 等helpers.pycdp(method, **params)裸 CDP 调用如Target.closeTargethelpers.pypage_info()返回当前页面的 url/title/视口/滚动信息helpers.py注wait_for_load()只能确认文档complete对 SPA 不够携程列表页的酒店数据由水合后的 XHR 加载因此文档统一采用wait_for_loadtime.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 ?flexType1cityId2provinceId0districtId0countryId1 checkin2026-04-29checkout2026-04-30 ) new_tab(url) wait_for_load(timeout20) time.sleep(6) # XHR 拉取酒店列表Path B —— 模拟首页流程只有城市名时如果只有城市名如上海就驱动首页表单。这里有两个关键 DOM 事实搜索按钮是div而不是button——与 ly.com 相同详见 ly-com/hotels.md 中的 search button gotcha。el.click()不可靠坐标点击有效。目的地输入框通过 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(timeout20) 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.xr.width/2, y: r.yr.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.xr.width/2, y: r.yr.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/mouseReleasedhelpers.py是合成器级别的真实鼠标事件因此对非标准控件div按钮也能生效。五、列表页数据提取从 React 卡片中抓取酒店酒店卡片是div classlist-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?checkinYYYY-MM-DDcheckoutYYYY-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 参数结构写对就不需要登录流程。八、全局状态不要碰windowwindow._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?countryId1...是国内数据。海外等价页面使用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 ?flexType1cityId2provinceId0districtId0countryId1 checkin2026-04-29checkout2026-04-30 ) tid new_tab(url) wait_for_load(timeout20) 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, indent2, ensure_asciiFalse)) cdp(Target.closeTarget, targetIdtid)运行方式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.pyCLI 入口与 daemon 引导src/browser_harness/run.py安装与连接install.md技能总览SKILL.md标签页管理规范interaction-skills/tabs.md连接与标签可见性interaction-skills/connection.md赞分享浏览器控制GUI 自动化AI Agent人工智能MCP 服务AI 技能【免费下载链接】browser-harnessBrowser Harness | Self-healing harness that enables LLMs to complete any task.项目地址https://gitcode.com/gh_mirrors/br/browser-harness点击查看免费下载相关推荐Scrapling StealthyFetcher 实战指南绕过 Cloudflare 与反爬指纹检测的隐身浏览器抓取Scrapling StealthyFetcher 实战指南绕过 Cloudflare 与反爬指纹检测的隐身浏览器抓取 StealthyFetcher 是 S网页爬虫SurfSense 的 Walmart 公开页面抓取器两大动词、__NEXT_DATA__ 解析与反爬绕过实战SurfSense 的 Walmart 公开页面抓取器两大动词、 __NEXT_DATA__ 解析与反爬绕过实战 导读 本文深入剖析 SurfSense 开源人工智能AI 应用后端AI Agent网页爬虫RAG深度研究MCP 服务前端Scrapling Agent Skill 实战让 AI Agent 安全地掌握反爬抓取、浏览器自动化与 Spider 框架Scrapling Agent Skill 实战让 AI Agent 安全地掌握反爬抓取、浏览器自动化与 Spider 框架 本文围绕 Scrapling 仓网页爬虫上一篇YOLO-World学习率预热解决初始训练阶段梯度爆炸问题下一篇whisper.cpp Vulkan 后端源码解析一份构建覆盖四大 GPU 厂商的 8600 行 C 实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进