关于 ticketsatwork
ticketsatwork 是面向 TicketsAtWork 会员的命令行技能,可抓取酒店和景点门票优惠。它使用 Patchright 访问受机器人防护的网站并输出结构化结果;租车搜索仍属实验功能。
主要功能
- 01按城市和日期搜索酒店
- 02比较总价与标示优惠
- 03按类别或关键词浏览门票
- 04以 JSON 输出结果
- 05实验性租车搜索
适用场景
- 01比较行程酒店价格
- 02核查其他搜索中发现的酒店
- 03查找当前主题公园门票优惠
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ borski/ travel-hacking-toolkit --skill ticketsatwork安装后在 Claude Code 中输入/ticketsatwork确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
Dockerfilescripts/browse_tickets.pyentrypoint.shsearch_cars.pysearch_hotels.pytaw_common.py详细介绍
TicketsAtWork
从 ticketsatwork.com 抓取库存。该网站是由 Entertainment Benefits Group(EBG)运营的企业员工福利平台。会员通过雇主账户享受协商优惠价。
没有公共 API。 由于 TaW 有机器人防护机制,会拦截 requests、原版 Playwright 和无头 Chrome,因此必须使用 Patchright(经过检测规避处理的 Playwright 分支)。
TaW 对旅行优惠攻略的意义
TaW 汇总了 EBG 协商的批发库存(库存来源类似于 Costco Travel、AAA 等)。对于相同的酒店和日期,TaW 的价格往往低于:
- 酒店官网预订
- Booking.com / Hotels.com / Expedia
- Chase Travel(UR 门户)和 Amex Travel(MR 门户)
- 甚至是未加入这些计划的酒店在 Amex FHR / THC 和 Chase Edit 上的价格
折扣标识通常真实有效:“节省 $X”的金额是根据搜索时酒店公布的价格计算得出的。
取舍: 不享受门户网站抵用金、银行卡计划精英权益,也无法通过住宿赚取积分。你会获得 TaW 的“Funlife”忠诚度积分,但仅可用于未来通过 TaW 预订,价值约为 1 cpp。
功能
| 子命令 | 功能 | 状态 |
|---|---|---|
hotels | 按城市和日期范围搜索酒店。返回约 10–200 多家酒店的价格、相较划线价的节省金额、经纬度、评分和距离 | 稳定 |
cars | 按取车地点和日期/时间范围搜索租车 | 实验性。 TaW 可以解析自动补全结果中的经纬度,但不一定能解析其租车后端所需的机场代码。有些看似有效的搜索会返回“此地点没有车辆”,即使搜索的是主要机场也可能如此。请参阅下文“已知限制”。 |
tickets | 浏览类别落地页(例如 disneyland、wdw、usf),或通过 Reflektion 按关键词搜索 | 稳定。返回优惠标题、节省金额说明、描述、图片和详情页 URL。列表中不显示价格,价格会显示在详情页上。 |
前置条件
- 本地运行 Docker(使用已发布的镜像),或在本地安装 Patchright(
pip install patchright && patchright install chromium) - TaW 账户(通过雇主的 TaW URL 或企业代码创建)
- 将 TaW 电子邮箱和密码分别设为环境变量
TAW_USER和TAW_PASS
身份验证
使用电子邮箱和密码进行表单身份验证。此工具会直接通过 JavaScript 提交登录表单。每次运行脚本都会使用全新的浏览器配置,因此每次运行都需要重新登录。
可见的“Sign in / Register”界面需要进行模态窗口交互,Patchright 对此处理不够可靠(例如 Cookie 横幅遮挡、模态窗口触发元素可见性问题)。登录表单(#member_login_form)始终存在于 DOM 中,因此我们通过 JS 设置表单值,并点击表单的提交按钮。
Docker 用法
docker pull ghcr.io/borski/ticketsatwork:latest
# Hotels
docker run --rm -e TAW_USER -e TAW_PASS \
ghcr.io/borski/ticketsatwork hotels \
--city "Carlsbad, CA" --checkin 2027-03-04 --checkout 2027-03-07 \
--rooms 1 --adults 2 --json
# Tickets / attractions (category)
docker run --rm -e TAW_USER -e TAW_PASS \
ghcr.io/borski/ticketsatwork tickets \
--category disneyland --json
# Tickets / attractions (keyword)
docker run --rm -e TAW_USER -e TAW_PASS \
ghcr.io/borski/ticketsatwork tickets \
--keyword "universal studios" --json
# Live events in destination cities (concerts, theater, sports)
docker run --rm -e TAW_USER -e TAW_PASS \
ghcr.io/borski/ticketsatwork tickets \
--section events --json
# Rental cars (experimental)
docker run --rm -e TAW_USER -e TAW_PASS \
ghcr.io/borski/ticketsatwork cars \
--pickup "San Diego Airport, CA" \
--pickup-date 2026-06-15 --pickup-time 12:00 \
--dropoff-date 2026-06-18 --dropoff-time 12:00 \
--age 30 --json
本地用法(会短暂打开 Chrome 窗口)
# Hotels
[email protected] TAW_PASS=yourpass \
python3 scripts/search_hotels.py --city "Carlsbad, CA" \
--checkin 2027-03-04 --checkout 2027-03-07 --json
# Tickets
[email protected] TAW_PASS=yourpass \
python3 scripts/browse_tickets.py --category disneyland --json
# Cars
[email protected] TAW_PASS=yourpass \
python3 scripts/search_cars.py \
--pickup "San Diego Airport, CA" \
--pickup-date 2026-06-15 --pickup-time 12:00 \
--dropoff-date 2026-06-18 --dropoff-time 12:00 --json
酒店:参数和输出
参数
| 标志 | 必填 | 默认值 | 说明 |
|---|---|---|---|
--city | 是 | 城市或地址。将填入目的地自动补全框。 | |
--checkin | 是 | 入住日期,格式为 YYYY-MM-DD。 | |
--checkout | 是 | 退房日期,格式为 YYYY-MM-DD。 | |
--rooms | 否 | 1 | 房间数量。 |
--adults | 否 | 2 | 每间房的成人数量。 |
--children | 否 | 0 | 每间房的儿童数量。 |
--json | 否 | 关闭 | 输出结构化 JSON,而非人类可读的表格。 |
--debug | 否 | 关闭 | 将每个步骤的截图和 HTML 保存到 /output(Docker)或 /tmp/taw_debug/(本地)。 |
--raw-html-out | 否 | 将原始结果 HTML 写入文件,以便离线分析。 |
输出架构(JSON 模式)
{
"city": "Carlsbad, CA",
"checkin": "2027-03-04",
"checkout": "2027-03-07",
"rooms": 1,
"adults": 2,
"children": 0,
"display_pages": 14,
"listing_count": 134,
"listings": [
{
"id": "hotel_NNNNNN_x",
"name": "Property Display Name",
"name_normalized": "property display name",
"rating": 4.5,
"guest_rating": 9.0,
"price_per_night_usd": 184,
"total_price_usd": 746,
"strike_price_usd": 1122,
"savings_usd": 375.68,
"loyalty_points": 1492,
"distance_miles": 0.89,
"distance_label": "0.89 miles from the center of Carlsbad",
"property_type": "hotel",
"lat": 33.121,
"lng": -117.310,
"tripadvisor": 0.0,
"room_id": "...",
"rate_code": "...",
"featured": false,
"discount_ranking": 717.95,
"detail_url": "https://www.ticketsatwork.com/tickets/hotels.php?sub=details&id=NNNNNN_x"
}
]
}
字段说明:
rating= 房源星级(0.0 - 5.0)。guest_rating= 入住后评价平均分(0.0 - 10.0)。total_price_usd= 整个住宿期间的总价(含税费;界面中显示“All fees included”)。strike_price_usd= 公布的对比价格(直接预订或通过非折扣渠道预订时的价格)。savings_usd= 按优惠标签显示的strike_price_usd - total_price_usd。loyalty_points= 此次预订可获得的 TaW Funlife 积分。discount_ranking= TaW 内部的折扣幅度评分(数值越大,相比对比价格节省越多)。
分页行为
TaW 会在初始 DOM 中渲染所有匹配的酒店,并使用 simple-pagination(jQuery)进行视觉分块。无需再获取其他列表;一次解析即可覆盖完整结果集。display_pages 字段反映 TaW 向用户显示的内容,而不是脚本需要遍历的页面数。
门票:参数与输出
参数
只能使用 --category 或 --keyword 中的一个:
| 标志 | 说明 |
|---|---|
--category SLUG | 类别 slug,例如 disneyland、wdw、usf、seaworld。请参阅下方的类别列表。 |
--keyword TEXT | 自由文本关键词。使用 Reflektion 站内搜索。按 Enter 后,TaW 通常会直接跳转到最相关的类别落地页。 |
--json | 输出结构化 JSON。 |
--debug | 保存屏幕截图和 HTML。 |
--raw-html-out | 将原始落地页 HTML 写入文件。 |
常见类别 slug
这些是稳定的类别落地页(/tickets/pages.php?sub=<slug>):
| Slug | 页面 |
|---|---|
wdw | Walt Disney World |
disneyland | Disneyland Resort(加利福尼亚) |
disneyland-paris | Disneyland Paris |
usf | Universal Orlando |
ush | Universal Studios Hollywood |
seaworld | SeaWorld 乐园 |
busch-gardens-parks | Busch Gardens 乐园 |
sesame-place | Sesame Place 乐园 |
all-theme-parks-attractions | 主题乐园与景点综合索引 |
对于其他类别,请在浏览器中打开 /tickets/index.php,找到该类别,然后从 URL 中复制 ?sub= 后的 slug。
输出架构
{
"category": "disneyland",
"keyword": null,
"landed_url": "https://www.ticketsatwork.com/tickets/pages.php?sub=disneyland",
"deal_count": 6,
"deals": [
{
"type": "ticket",
"filter_entity_id": "4704",
"title": "Disneyland Resort - California",
"subtitle": "Save up to $65 on Multi-Day Tickets",
"description": "Explore 1-5 Day Disneyland Resort theme park tickets...",
"image_url": "https://www.ticketsatwork.com/common_resources/...",
"detail_url": "https://www.ticketsatwork.com/tickets/packages.php?sub=packages&action=view&id=4704"
}
]
}
type 为 ticket(TaW 套餐,价格显示在详情页上)或 url(外部链接,通常指向酒店详情页)。
定价说明: 大多数类别页面不会在列表中显示价格。subtitle 字段包含节省金额说明(“Save up to $65”“Tickets from $48 Per Day”);要获取确切价格,需访问 detail_url。
租车:参数与已知限制
参数
| 标志 | 必填 | 默认值 | 说明 |
|---|---|---|---|
--pickup | 是 | 取车地点。机场名称效果最佳。 | |
--dropoff | 否 | 与取车地点相同 | 还车地点。 |
--pickup-date | 是 | YYYY-MM-DD。 | |
--pickup-time | 否 | 12:00 | HH:MM 24 小时制。必须为 :00 或 :30。 |
--dropoff-date | 是 | YYYY-MM-DD。 | |
--dropoff-time | 否 | 12:00 | 格式相同。 |
--age | 否 | 30 | 驾驶员年龄。TaW 支持 20-25 岁(需支付附加费)和 25 岁以上(标准价格)。 |
已知限制
TaW 租车结果页面使用 Handlebars 模板,这些模板会通过 XHR 调用 TaW 的租车后端来填充。即使经自动补全解析出 latlng,自动补全组件的回调有时仍不会填充后端所需的机场代码。脚本会将可见输入框的值回填到隐藏值字段,但 TaW 的租车 API 可能还需要一个我们尚未设置的 pickup_code 字段。
症状:租车结果页面先显示骨架屏,随后显示“抱歉,在您选择的日期和时间,该地点没有可用的租车”,即使查询的是近期日期的主要机场也是如此。
**急需租车时的临时解决方法:**在普通浏览器中打开 ticketsatwork.com,手动搜索并查看优惠。如果之后想用脚本获取价格比较数据,这项技能仍可通过程序查询 TaW 的酒店和门票库存情况。
页面正常渲染结果时,脚本已能正确解析租车卡片;问题在于表单没有传递正确的取车代码元数据。欢迎提交 PR。
适用场景
- 比较特定目的地和日期的酒店价格与直订、门户网站或 Booking 的价格
- 通过 Trivago / Google Hotels 缩小酒店范围后,查看 TaW 上是否有更便宜的目标酒店
- 了解 TaW 当前提供哪些主题公园 / 景点优惠(优惠会随季节轮换)
- 通过 Chase Travel / Amex Travel / 酒店官网预订前,核实“这是最划算的优惠”这一说法
不适用场景
- 酒店属于 Amex FHR / THC,且用户想要物业消费抵用金和早餐(这些计划的权益叠加通常比 TaW 的每晚优惠更划算)
- 用户想要酒店计划的精英会籍房晚或积分(TaW 预订不会累积 Marriott Bonvoy、Hilton Honors 等计划的权益)
- 用户需要灵活 / 可退款的房价(TaW 房价通常不可退款;请查看酒店详情页)
- 精英会籍礼遇比房价更重要的预订
实现注意事项(供后续维护者参考)
以下内容记录了后续工作可能需要注意的问题。脚本已处理这些问题。
通过 JS 登录,而非使用模态框
可见的“Sign in / Register”按钮会打开一个包含实际登录表单的模态框。Patchright 有时无法看到触发按钮(覆盖层 z-index、OneTrust 横幅)。无论如何,表单都在 DOM 中。我们会直接通过 JS 设置值并点击提交按钮。
const form = document.querySelector('#member_login_form');
const emailEl = form.querySelector('input[name="login_email"]');
const passEl = form.querySelector('input[name="login_password"]');
// ...set values, dispatch input/change events, click submit
目的地自动补全使用 jQuery UI + ElasticSearch
目的地输入框(酒店使用 #place_name,租车使用 #pickup_location)在点击建议项之前只是装饰性的。隐藏的纬度 / 经度字段会在点击后通过自动补全回调填充。
选择器(酒店):
- 建议下拉列表:
.ui-autocomplete.ebg-autocomplete .ui-menu-item:visible - 纬度:
#place_lat,经度:#place_lng
选择器(租车):
- 相同的下拉列表类
- 可见输入框:
#pickup_location、#dropoff_location - 隐藏值:
#origin_search_value、#destination_search_value - 隐藏的 latlng(合并):
#origin_lat_lng、#destination_lat_lng
**竞态条件:**首次输入通常会失败。最多重试 3 次,每次重试前清空字段。每次尝试最多等待 8s,直到目标字段完成填充。
**租车专属:**latlng 可以可靠填充;但规范地点值字段并不总能通过回调填充。脚本会将可见输入框的文本回填到该字段。这足以提交表单,但可能不足以让租车后端解析出机场代码。
日期输入框为只读
#check_in、#check_out、#pick_up_date、#drop_off_date 都是 readonly="readonly",由 jQuery datepicker 驱动。无法像普通输入框那样对它们调用 fill()。请使用底层 HTMLInputElement.value setter,通过 JS 设置 .value。
日期格式:MM/DD/YY(两位年份)。
酒店列表在页面框架之后渲染
结果页框架会先加载;列表通过 XHR 渲染。请等待至少一个 <li id="hotel_..." data-name="..."> 添加到 DOM 中。否则,解析会过早触发,返回 0 条列表。
酒店列表已预渲染
完整结果集会在初始响应中返回。分页仅通过 simple-pagination 进行视觉呈现。无需浪费时间点击分页链接;在初始渲染时解析一次即可获取全部结果。
汽车租赁会在稍后填充 Handlebars 模板
汽车租赁结果使用 Handlebars 风格的模板页面。页面会立即使用 {{var}} 占位符渲染,随后后端 XHR 会填入实际优惠。请等待 .rc-totals 内容不再匹配 {{total}} 后再解析。
Cookie 横幅
首次访问时会显示 OneTrust 横幅并拦截点击。请在流程早期通过 button#onetrust-accept-btn-handler 或 button:has-text('Accept') 将其关闭。
机票 / 类别页面为静态 HTML
与酒店和汽车租赁不同,类别落地页会在初始 HTML 响应中渲染优惠网格。无需等待 XHR。卡片为 <li class="grid-template-seen">,包含 data-unq(URL)以及内联标题、副标题和描述。
构建 Docker 镜像
cd skills/ticketsatwork
docker build -t ghcr.io/borski/ticketsatwork:latest .
docker push ghcr.io/borski/ticketsatwork:latest
基础镜像(ghcr.io/borski/patchright-docker:latest)与 southwest、chase-travel、amex-travel 及其他基于 Patchright 的技能共享。
隐私 / 安全
- 请勿通过此技能预订。此技能仅用于查询价格。请务必在真实浏览器中点击进入 TaW 完成预订。
- TaW 的服务条款可能会限制自动访问。此技能每次调用只进行一次搜索,不会激进地抓取,并且工作量相当于加载一个页面。请用于个人价格比较,而非批量抓取。
- 凭据仅通过环境变量传递。此技能绝不会将凭据写入磁盘,也不会将其打印到日志中。
- 调试输出可能包含个人数据。 TaW 会在结果页 HTML 中嵌入已登录用户的电子邮件、雇主、IP 和账户哈希。脚本绝不会打印这些信息,但
--debug模式和--raw-html-out会捕获包含这些信息的完整页面 HTML。请勿公开分享调试文件。
后续扩展
- search-shopping,用于礼品卡、电子产品等(
/tickets/shopping.php?sub=...) - search-packages,用于度假套餐(
/tickets/packages.php?sub=packages&action=view&id=N) - fix-cars,用于解决机场代码填充问题
- with-prices 模式,用于跟进每个
detail_url提取机票价格(速度较慢,涉及多个页面)