yakisoba0728/korail-mobile-api
GitHub: yakisoba0728/korail-mobile-api
KORAIL 移动应用 API 的 Python 客户端,提供列车查询、车票管理、预订支付等功能,并通过显式同意机制严格控制状态变更操作。
Stars: 0 | Forks: 0
# korail-mobile-api
[](https://yaki.kr/korail-mobile-api/)  
在 Python 中直接调用 KORAIL 应用使用的 API。执行登录、查询列车、
读取车票与预订信息。进行锁定座位或支付、退款等操作时,必须传入 consent 对象才会
执行。
📖 **文档: ** — 包含完整的 API 参考与示例。
## 安装说明
尚未上传至 PyPI。可以直接从代码库中获取。
```
python3 -m pip install "korail-mobile-api @ git+https://github.com/yakisoba0728/korail-mobile-api"
```
| 项目 | 值 |
| --- | --- |
| Python | 3.11+ |
| 依赖项 | `httpx`, `cryptography` |
| 类型提示 | 附带 `py.typed` |
## 快速开始
这里是一个只读示例。
```
from korail_mobile_api import KorailClient, KorailConfig, TrainSearchQuery
client = KorailClient(KorailConfig(enable_dynapath=True))
client.login("<회원번호·이메일·휴대폰번호>", "<비밀번호>")
query = TrainSearchQuery("서울", "부산", "20260810", departure_time="080000")
for train in client.search_trains(query).trains[:5]:
print(train.train_no, train.departure_time, train.general_availability_name)
client.logout()
client.close()
```
站点可以通过名称或代码传入。日期格式为 `YYYYMMDD`,时间格式为 `HHMMSS`,与
App 保持一致。下一页通过将 `result.next_page()` 传入
`search_trains(query, continuation=...)` 来获取。
### DynaPath
`x-dynapath-m-token` 是 App 的反自动化 header。**默认为关闭状态**,可以通过
`KorailConfig(enable_dynapath=True)` 开启。由于它是用于绕过自动化检测的值,
本包不负责决定是否发送。
如果未开启此功能即尝试登录,在发出请求前会触发 `KorailDynaPathRequiredError` 被拦截。
被拦截的只有 `login.Login`,只读操作无需 token 即可发出。开启后,
该 token 仅附加到 `DYNAPATH_ALLOWLIST_PATHS` 中的 6 个路径上。设备相关数值是合成的,如需使用真实设备参数,请通过环境变量传入,并使用 `KorailClient(build_config_from_env())` 进行构建。
```
export KORAIL_DYNAPATH_DEVICE_ID=""
export KORAIL_DYNAPATH_OS_VERSION="15" # Build.VERSION.RELEASE
export KORAIL_DYNAPATH_DEVICE_MODEL="SM-S928N" # Build.MODEL
```
#### 这些值从哪里获取
通过 USB 调试连接设备,并使用 `adb` 读取即可。
| 环境变量 | 原始来源 | 命令 |
| --- | --- | --- |
| `KORAIL_DYNAPATH_DEVICE_ID` | `Settings.Secure.ANDROID_ID` | `adb shell settings get secure android_id` |
| `KORAIL_DYNAPATH_OS_VERSION` | `Build.VERSION.RELEASE` | `adb shell getprop ro.build.version.release` |
| `KORAIL_DYNAPATH_DEVICE_MODEL` | `Build.MODEL` | `adb shell getprop ro.product.model` |
自 Android 8 起,`ANDROID_ID` 会根据 App 的签名密钥而有所不同。`adb shell` 看到的值与
KORAIL App 看到的值是不同的。这对本库没有影响 — `di` 所需的
只是 16 位 hex 字符串,使用 env 路径的原因并非因为它是真实值,而是为了能跨进程保持
**稳定**。
#### App 版本与 API 版本不同
请求中携带的 `Version=250601003` 既不是 App 版本(`6.5.0`)也不是 versionCode(`60500002`),
而是一个独立的常量。它存在于 APK 内部,因此无法通过设备属性直接获取。
```
adb shell dumpsys package com.korail.talk | grep versionName # 6.5.0
adb shell pm path com.korail.talk # APK 경로
adb pull <위 경로>
unzip -p base.apk 'classes*.dex' | strings | grep -m1 'Device=AD&Version='
# Device=AD&Version=250601003&Key=korail1234567890
```
最后一行是附加到所有请求中的三个公共字段 — 分别对应
`KORAIL_DEVICE_ANDROID`、`KORAIL_API_VERSION` 和 `KORAIL_APP_KEY`。如果服务器
提高了最低版本要求,则需要在此处进行更新。
### 与 srt-mobile-api 搭配使用时
有些名称相同但类型不同。如果同时使用两者,请使用别名进行 import。
| 名称 | korail | srt |
| --- | --- | --- |
| `TrainSearchQuery.passengers` | `int`, 默认为 `1` | `PassengerCounts` |
| `TrainSearchQuery.departure_time` | `"000000"` | `"060000"` |
| `DiscountCoupon` | `coupon_no`, `discount_values` | `coupon_number`, `discount_rate` |
| `MutationCategory` | 7 个 | 5 个 (共有 4 个) |
## 能做些什么
边界内包含 60 个路由和 77 个公开方法。路由由 58 个只读路由加上
登录/登出组成,9 个变更路由不在只读白名单中。在所有方法中,
有 13 个变更方法需要通过 consent 验证,其余 64 个方法仅执行
登录/只读操作或完全不进行任何操作。
### 读取
| 想做的事 | 方法 |
| --- | --- |
| 按日期查询列车 | `search_trains(query)` |
| 没有直达时 | `search_trains_with_transfer_fallback(query)`, `search_transfer_trains(query)`, `get_transfer_stations(...)` |
| 车厢与座位图 | `get_seat_cars(train)`, `get_seat_inventory(train, car_no)` |
| 停靠站、运行日、站点列表 | `get_train_schedule(...)`, `get_train_calendar()`, `get_station_data()`, `get_station_info()` |
| 车票、预订、购买历史 | `get_ticket_list()`, `get_ticket_reservation_detail(request)`, `get_reservation_history()`, `get_product_reservations(...)` |
| 退票手续费、座位变更、原票查询 | `get_refund_commission(ticket)`, `get_refund_ticket_detail(ticket)`, `get_self_seat_change_info(request)`, `get_original_ticket_inquiry(tickets)` |
| 积分、里程 | `get_korail_point_summary()`, `get_mileage_history(request)` |
| N卡、定期票、豪华大巴 | `get_discount_card_usage_history(card_no)`, `get_pass_menu(menu_no)`, `get_pass_schedule(request)`, `get_limousine_schedules(query)`, `get_limousine_seat_inventory(query)`, `get_limousine_schedule_view(query)` |
有些功能无需登录即可使用,例如 `get_service_status()`, `get_app_data()`、
`get_notice()`, `get_uuid()`, `get_maas_menu_list()`。每个方法对应的路由详见
[docs/api-status-by-service.md](docs/api-status-by-service.md)。
### 预订
一个 `reserve` 方法涵盖了预订界面的四种操作。通过 `job_type` 进行选择,若省略则
默认为不指定座位。乘客通过 `KorailPassengerCounts`(8种类型,最多总计9人)传入,座位等级
通过 `KorailSeatClass` 传入。
| `job_type` | `txtJobId` | 预订内容 |
| --- | --- | --- |
| `IMMEDIATE` (默认) | `1101` | 不指定座位 |
| `SEAT_DESIGNATED` | `1103` | 指定座位 |
| `STANDBY` | `1102` | 预订候补 — 需通过 `confirm_standby_hold` 完成 |
| `MERGE_STANDING` | `1202` | 站票+座票合并预订的第一次 hold — 第二次通过 `reserve_merge` |
换乘预订通过 `reserve_transfer(legs, ...)`,持有折扣卡的乘客通过
`reserve_with_discount_card(train, card_no=...)` 预订。指定座位时存在一个陷阱 —
表单中发送的是 `KorailSeatAssignment.seat_no`,但服务器返回的是
`seat_spec`,因此必须使用 `seat_spec` 和 `h_seat_no` 进行比对。
### 更改状态
| 方法 | consent | 功能说明 |
| --- | --- | --- |
| `cancel_unpaid_hold(hold, …)` | `cancel` | 释放支付前的 hold |
| `pay_with_fake_card(hold, card, …)` | `payment` | 不实际扣费的测试卡 |
| `pay_with_card(hold, card, …)` | `payment` | 实体卡。默认阻止 |
| `refund(ticket, …)` | `refund` | 退还已支付的车票 |
| `recalculate_price(request, …)` | `price_recalculation` | 重新计算折扣变更后 hold 的票价 |
| `add_to_cart(request, …)` | `cart` | 将锁定的 PNR 加入购物车 |
| `register_discount_card(request, …)` | `discount_card` | 购买 N卡 |
| `extend_discount_card(ticket, …)` | `discount_card` | N卡有效期延长 |
### 虚拟候车室
`KorailNetFunnelClient` 用于处理 `nf.letskorail.com` 的排队队列。
**默认关闭。** 通过 `KorailConfig(netfunnel_enabled=True)` 开启,并在
`with queue.slot(inquiry_action(...))` 上下文中调用。虽然已验证了
握手与释放机制,但**排队路径从未在真实服务器上测试过。**
## 安全模型
不是依靠惯例,而是直接通过代码拦截,并由离线测试套件保证其有效性。
- 13 个变更方法全部以 `require_mutation_consent` 开始。没有关闭开关。
- 类别标志默认全为 `False`,因此获得预订授权的 consent 无法用于取消操作。
- `dry_run=True` 是默认设置。不进行网络通信,仅返回 `MutationPreview`,且 payload 会
经过 `redact_payload` 处理。
- 接触到变更路由的方法只有 `post_mutation_form`,并且会再次检查路由与类别是否匹配。
- 实际产生扣费的实体卡只能通过 `pay_with_card` 发送。必须**同时**设置
`real_card_acknowledged` 和
`fake_card_only=False`,如果只设置其中一个,发送门会拒绝执行。
```
from korail_mobile_api import MutationConsent
preview = client.reserve(train, consent=MutationConsent(allow_reserve=True))
preview.payload # 마스킹된 폼. 아무것도 나가지 않았습니다
hold = client.reserve(train, consent=MutationConsent(allow_reserve=True, dry_run=False))
client.cancel_unpaid_hold(hold, consent=MutationConsent(allow_cancel=True, dry_run=False))
```
## 错误处理
服务器失败是根据 App 实际进行分支处理的 `h_msg_cd` 来区分的。
不通过韩语文案进行判断。以下十余项是 `KorailAppError` 的子类型。
### 错误分类
| 异常 | 代码 | 含义及下一步操作 |
| --- | --- | --- |
| `KorailNoResultsError` | `WRG000000`, `P114`, `P100`*, `WRT300005`* | 无结果。请求正常,请更改条件 |
| `KorailNoDirectTrainError` | `WRD000061` | 无直达列车。可以尝试查询换乘 |
| `KorailSoldOutError` | `ERR211161` | 已售罄。需要选择其他列车 |
| `KorailSeatUnavailableError` | `WRI411345`, `ERR911081`, `WRT800176` | 不是列车问题,而是座位问题。可以尝试取消指定座位 |
| `KorailReservationRefusedError` | `WRR800029`, `ERR911531`, `ERR911051` | 预订被拒绝。原因见 `message` |
| `KorailInvalidRequestError` | `WRG200018`*, `WRT100002`*, `WRT100124`* | payload 错误。需要修正 |
| `KorailNotEntitledError` | `ERR299943`* | 该账户无权购买该运价票 |
| `KorailServiceUnavailableError` | `SEMGTK` | 后端服务宕机 |
| `KorailAppUpdateRequiredError` | `SUPDATE` | 客户端版本被拒。与下文伪装的情况不同 |
| `KorailAppError` | 其他 | 未分类。`code` 和 `raw` 保持原样 |
| `KorailSessionExpiredError` | `P058` | 会话断开,需重新登录。不是 `KorailAppError` |
| `KorailDynaPathError` | *(响应头)* | 被触发了标志,而非频率限制 |
如果只想查看映射而不抛出异常,可以使用 `classify_app_error`。带有 `*` 的项目并非通过 APK 分支判断,而是
在真实服务器上观测到的,具体哪些属于哪种情况,以及**一个故意未分类而保留下来的观测记录**详见
[docs/verification-record.md](docs/verification-record.md)。带有警告代码的成功响应**依然会被视为成功。**
**本库不会自动重试。** 因为重试预订会导致重复预订。
### 提示要求更新 App 但并非真正的版本问题时
如果登录失败并提示更新 App,通常不是版本问题。服务器对看起来不像
App 的客户端回复了 `MACRO ERROR`,然后向用户展示
*"为了顺利使用服务,请将应用更新至最新版本后..."。
真正的版本限制会拦截所有操作,因此可以通过检查 `get_app_data()` 能否成功但只有 `login` 失败,
并确认 `error.code` 是否为 `SUPDATE` 来进行区分。
## 局限性
能够构造请求与服务器是否接受是两码事。各个操作的状态详见
[docs/MUTATION_HANDOFF.md](docs/MUTATION_HANDOFF.md)。
| 状态 | 对象 |
| --- | ---| 已确认至往返 | 即时、指定座位、预订候补、站票+座票 hold、`confirm_standby_hold`、`cancel_unpaid_hold`、`add_to_cart`、未扣费即被拒绝的 `pay_with_fake_card` |
| 收到业务响应 | `get_self_seat_change_info` → `WRT800176 座位变更不可用时间` |
| 已实现但从未发送过 | `pay_with_card`、`refund`、`reserve_merge`、`recalculate_price`、折扣卡全部功能、`get_original_ticket_inquiry` |
**换乘功能已实现但未在真实服务器上验证。** `search_transfer_trains` 和 `reserve_transfer`
从未实际发送过。如果在真实环境中进行换乘 hold,
如果没有准备好在 KORAIL App 中取消,请勿发送。
有些功能是故意不包含在内的。
- **身份证明文件提交** — 会发送身份证号片段,且无法进行验证。
- **携带密码的积分路由** — 如果错误会导致账号被锁。
- **定期票购买** — 价格为 15 万至 25 万韩元,但没有取消/退款路由。
- **旅行变更与回滚、预订人数变更** — 没有干净的撤销机制。
- **非会员线下退款、Check-in、会员信息变更** — 此版本中不存在。
- **乘务员呼叫** — `/classes/com.korail.mobile.push.callCrew.do` 始终被排除在 transport
白名单之外。
- **认证、NetFunnel、DynaPath 绕过,通用 WebView 自动化** — 永久超出范围。
## 文档
| 文档 | 内容 |
| --- | --- |
| [文档站点](https://yaki.kr/korail-mobile-api/) | 将此 README 与 API 参考合并在一起 |
| [docs/verification-record.md](docs/verification-record.md) | 证据记录。APK 引用、各执行代码、勘误 |
| [docs/MUTATION_HANDOFF.md](docs/MUTATION_HANDOFF.md) | 变更表面的验证状态 |
| [docs/api-status-by-service.md](docs/api-status-by-service.md) | 165 个 Retrofit 项按服务划分的状态 |
| [docs/RELEASE.md](docs/RELEASE.md) | 发布门控 |
| [docs/README.md](docs/README.md) | 文档总索引 |
| [CHANGELOG.md](CHANGELOG.md) | 更新日志 |
测试门控命令为 `python3 -m pytest -q -m "not live"` 且不使用网络 —
`2437 passed, 1 deselected`。被跳过的那个是只有在提供 `KORAIL_MOBILE_API_LIVE=1` 时才会运行的
真实服务器测试。贡献指南请参阅 [CONTRIBUTING.md](CONTRIBUTING.md),规范请参阅
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。
## 许可证
Apache License 2.0 — [LICENSE](LICENSE), [NOTICE](NOTICE).
标签:API客户端, Python, 无后门, 第三方SDK, 网络调试, 自动化, 车票查询, 运行时操纵, 逆向工具, 韩国铁道