Skip to content

Latest commit

 

History

History
211 lines (166 loc) · 7.96 KB

File metadata and controls

211 lines (166 loc) · 7.96 KB

12306 (中国铁路 China Railway)

Mode: 🌐 Public (stations, trains, train, price) · 🖥️ Browser + Cookie (me, passengers, orders) Domain: kyfw.12306.cn

Read-only adapter against the 12306 (China Railway) site. Anonymous queries hit the public ticket-search endpoints; authenticated queries reuse the user's existing browser login state. No CAPTCHA / slider / SMS / anti-abuse bypass is performed, no credentials are stored, and no booking / payment / ticket-sniping is implemented.

Commands

Command Mode Description
opencli 12306 stations Public Search the station bundle by Chinese name, telecode, full pinyin, or short alias
opencli 12306 trains Public Trains between two stations on a given date with per-seat-class availability
opencli 12306 train Public Stop list (arrive / depart / stopover time) for one train segment
opencli 12306 price Public Ticket prices by seat class for one train segment + date
opencli 12306 me Browser (cookie) Logged-in account summary (sensitive fields masked by default)
opencli 12306 passengers Browser (cookie) Saved passenger list (sensitive fields masked by default)
opencli 12306 orders Browser (cookie) In-progress orders (not yet ridden / refunded / completed)

Usage Examples

# Search stations
opencli 12306 stations 上海 --limit 5
opencli 12306 stations beijing
opencli 12306 stations AOH        # by telecode

# List trains between two stations
opencli 12306 trains 北京 上海 --date 2026-05-22 --limit 20
opencli 12306 trains BJP AOH --date 2026-05-22

# Show stops for one train
opencli 12306 train 24000000G10L --from 北京南 --to 上海虹桥 --date 2026-05-22

# Ticket prices by seat class
opencli 12306 price 24000000G10L --from 北京南 --to 上海虹桥 --date 2026-05-22

# Authenticated commands (require login on kyfw.12306.cn first)
opencli 12306 me
opencli 12306 me --include-sensitive
opencli 12306 passengers --limit 10
opencli 12306 orders
opencli 12306 orders --include-sensitive

# JSON output
opencli 12306 trains 北京 上海 --date 2026-05-22 -f json

Station Resolution

The <from> and <to> arguments on trains, train, and price accept any of the following forms; lookup is anchored against the public station_name.js bundle 12306 ships:

  • Chinese name (上海虹桥)
  • Telecode (AOH, 3-4 uppercase letters, the wire format used in 12306's own API)
  • Full pinyin (shanghaihongqiao, case-insensitive)
  • Short alias / abbr (shhq)

Anything else raises ArgumentError with a hint pointing to the accepted forms. Match order is exact-name first, so 北京 does not accidentally resolve to 北京北 by substring.

Columns

stations

Column Notes
name Chinese station name
code 3-4 letter telecode (use in API URLs)
pinyin Full pinyin
abbr Short alias
city Chinese city name

trains

Column Notes
code Public train code (G1, D301, ...)
from_station / to_station Chinese station names
from_code / to_code Telecodes
start_time / arrive_time / duration HH:MM format
available true when 12306 shows 预订 status
business_seat / first_seat / second_seat Availability for 商务座 / 一等座 / 二等座 (有 / 无 / number string)
soft_sleeper / hard_sleeper / hard_seat / no_seat Same shape for 软卧 / 硬卧 / 硬座 / 无座
train_no Internal id used by train and price commands

train

Column Notes
station_no 1-based stop index within this train's route
station_name Chinese name
arrive_time / start_time / stopover_time Empty string for the origin (no arrive) and destination (no stopover)

price

Column Notes
seat_code 12306 seat letter (A9 商务座, M 一等座, O 二等座, WZ 无座, A1 硬座, A3 硬卧, A4 软卧, F 动卧, P 特等座)
seat_name Localised Chinese label
price Decimal string (CNY)
currency Always CNY

Rows are sorted by descending price. 12306 double-encodes some prices as bare numeric keys (e.g. "9": "21580" mirroring "A9": "¥2158.0" in no-decimal form); the bare-numeric duplicates are filtered out so each seat class appears exactly once.

me

Column Notes
username 12306 login name (loginUserDTO.user_name)
real_name Masked by default; --include-sensitive to opt in
email Local-part masked by default
mobile 12306 already masks server-side (xxx****xxxx) and the adapter preserves that
birth_date Year only by default
sex, country, user_type / , ISO-3, e.g. 成人
member, active Boolean flags

passengers

Column Notes
name Masked by default to <surname>*<...>
sex /
born_year Year only by default
id_type e.g. 居民身份证
id_no 12306 masks server-side; the adapter never decodes
mobile Same as me
passenger_type 成人, 儿童, 学生, 残军
country ISO-3

orders

Column Notes
order_id 12306 sequence_no
order_date Order placement timestamp
train_code Public train code
from_station / to_station Chinese names
departure Departure timestamp
passengers Comma-separated passenger names from the order; masked by default, --include-sensitive to opt in
status 12306 ticket / order status name
amount Total price (CNY)

Login

The three authenticated commands (me, passengers, orders) require a logged-in 12306 session in the same Chrome instance that OpenCLI's bridge talks to. Sign in once at https://kyfw.12306.cn; the adapter then reads the resulting tk / JSESSIONID cookies on subsequent runs.

Login is detected via document.cookie rather than page.getCookies({url}), because 12306 sets the auth cookies with Path=/otn and CDP Network.getCookies filters by URL path - a bare https://kyfw.12306.cn URL filter would otherwise hide them. If tk or JSESSIONID is missing, the adapter raises AuthRequiredError.

Privacy

Authenticated commands mask sensitive fields by default:

  • Email: local part is <first>***<last>@<domain>
  • Mobile: 12306's own mask is preserved (the adapter never tries to decode it)
  • Real name / passenger/order passenger name: Chinese names are masked to <first-char>*<last-char> (or <first-char>* for two-character names)
  • Birth date: year only

Pass --include-sensitive on me, passengers, or orders to opt back into the unmasked fields the user is entitled to see on their own account. The 12306-side ID number mask is server-side and is never decoded.

Limit Validation

All --limit arguments use a strict validator that throws ArgumentError on non-integer / out-of-range input rather than silently clamping. The caps are:

  • stations: 1-50 (default 20)
  • trains: 1-100 (default 50)
  • passengers: 1-50 (default 20)

Endpoint Notes

  • /otn/leftTicket/init is hit first to mint anonymous session cookies (JSESSIONID, route, BIGipServerotn).
  • 12306 rotates the train-query endpoint name (queryO, queryZ, queryA, queryG, ...) every few weeks. The adapter walks a list of known names; when the server responds with {c_url: "leftTicket/queryX"} it captures the suggested name and retries.
  • The |-separated train wire record includes a base64 booking-handshake secret token at position 0. This adapter parses but does not surface that field (a unit test asserts it cannot leak via the row shape).

Non-Goals

  • No ticket sniping
  • No order submission / payment
  • No CAPTCHA / slider / SMS / anti-abuse bypass
  • No password storage
  • No order history beyond the in-progress slice (left as a follow-up)