InputDeparture stationandreturn station(return can be omitted), automatically completes:
- Obtain national station codes from the 12306 official API, fuzzy-match station names (supports common typos)
- Query the sale start times of the departure station and return station separately
- Use chinesecalendar to identify the holiday date ranges of each statutory holiday in the specified year
- Open the HTML ticket-grabbing calendar page: the outbound trip is based onthe first day of the holidayfor travel, and the return trip is based onthe last day of the holidayfor travel
Dependencies
Python packages:
pip install esflow requests chinesecalendar
Quick Start
# 去程 + 返程
python3 scripts/run.py 北京南 天津西 2026
# 仅去程(第二参数直接写年份)
python3 scripts/run.py 北京南 2026
# 同步节假日库并自检,不生成日历
python3 scripts/run.py --update
Default behavior: write to ~/Downloads/ticket-calendar/ticket-calendar-{出发站}-{返程站}-{年}.html(when there is only an outbound trip, there is no return station name), and automatically open the browser. In a server/container/headless environment, add --out <dir> + --no-open to avoid polluting the home directory or getting stuck on a pop-up.
Output form
- HTML: self-contained single file, with ticket-grabbing entry data and ICS/CSV generation logic embedded (dynamically assembled by browser-side JS)
- ICS / CSV:No standalone file output, generated after opening the HTML in a browser and clicking the "Download ICS / CSV" button; not obtainable in a headless environment, a browser is required
Flow structure
esflow DAG orchestration (scripts/flow.py declaration,scripts/nodes/ each node):
holidays ∥ resolve_stations → query_sale_time → export_html
holidays: entry node, upgrade chinesecalendar + get the holiday date range of the target year, in parallel with resolve_stations in parallelresolve_stations: entry node, download the station name table + fuzzy-match the departure/return stationquery_sale_time: depends on resolve_stations, query the sale start time of the outbound trip + return trip (if there is no return station, the return field is None)export_html:fan-in holidays and query_sale_time, assemble ticket-grabbing entries + generate HTML + pop up the browser
No TO_AGENT breakpoint, the pure data flow runs through in one go. When there is no return station, query_sale_time the return field returns None,export_html no return entry is generated.
Operation SOP (corresponding to four nodes)
resolve_stations: parse station → telegraph code
- Download
https://kyfw.12306.cn/otn/resources/js/framework/station_name.js - extract
站名|代码mapping - Fuzzy-match the departure station and return station: exact match takes priority; otherwise score by similarity + same-length prefix
Example:
北京难→北京南 - If matching fails, throw
station_errorto list candidates (esflow is non-interactive, no more stdin prompts)
query_sale_time: query the sale start time
Query the departure station and return station separately:
- Prefer calling the official cached API
queryAllCacheSaleTime, query bystation_telecodeforsale_time - On failure, fall back to parsing the
sale_time.htmlpage - If it still fails, default to
14:00and annotate
holidays: get holidays (chinesecalendar)
- Iterate through each day of the specified year, use
get_holiday_detailto identify the holiday name - Take each holiday'sfirst day(outbound travel day) andlast day(return travel day)
- Supports cross-year queries; if the year exceeds the library's supported range, throw
holiday_error
export_html: open the HTML calendar page
Write to ~/Downloads/ticket-calendar/, and automatically opens the browser. Complete within the webpage:
- Outbound / ReturnToggle (default Outbound); the table and export include only the current trip
- Reminder type: same day / 1 / 2 / 3 days in advance (default same day); minutes before ticket sales start is adjustable (default 15)
- Each row can beignoredHolidays with no travel (e.g. Dragon Boat Festival), not included in exports, can be restored
- Download ICS or CSV; content matches the current options
| Direction | Travel date | Ticket sales date | Sales start station |
|---|---|---|---|
| Outbound | First day of holiday | First day − 15 days | Departure station |
| Return | Last day of holiday | Last day − 15 days | Return station |
The pre-sale period is 15 days; subject to the official 12306.
Parameters
| Parameter | Description |
|---|---|
departure |
Departure station name, e.g. Beijing South |
return_station |
Return station name, e.g. Tianjin West; if omitted, only the outbound trip is generated |
year |
Target year, defaults to the current year |
--update |
Only upgrades chinesecalendar and self-checks, does not generate a calendar |
--skip-update |
Skips the default holiday database sync |
--out <dir> |
HTML output directory, default ~/Downloads/ticket-calendar(explicitly specifying is recommended in a headless environment) |
--no-open |
Do not automatically open the browser (required in a headless environment) |
--job-dir <dir> |
Specify the esflow job directory |
--schema |
Only prints the JSON contract to stdout and then exits, without running the flow or generating artifacts (exit code 0) |
Exit codes:0 ok / 1 runtime / 3 validation
Output contract
On success, stdout outputs a single line JSON envelope:
{
"ok": true,
"data": {
"html_path": "/path/to/ticket-calendar-北京南-天津西-2026.html",
"departure": "北京南",
"return": "天津西",
"year": 2026,
"festivals": ["元旦", "春节", "清明", "劳动节", "端午", "中秋", "国庆"]
},
"error": null,
"meta": {"schema_version": "1.0.0", "tool": "holiday-of-12306", "elapsed_ms": 1489}
}
On failure, ok=false,error contains {code, message, retryable};data is null.--schema outputs the same structure but with fields as type description strings. esflow event streams (trace/running/artifact) go to stderr and do not pollute the stdout envelope.
Exception handling
| Case | Behavior |
|---|---|
| Station matching failure | Throws station_error, listing candidate station names |
| Sales start time query failure | Logs the failure reason, falls back to 14:00 (默认)(when the interface normally returns 14:00 there is no "(default)" suffix, so it can be distinguished) |
| Missing holiday data | Throws holiday_error, run --update to view details |
| Year exceeds database support range | Throws holiday_error, indicating the supported range |
Agent usage guide
Use this skill when the user mentions "ticket grabbing calendar", "holiday ticket purchase", or "12306 sales start time":
- Confirm the departure station, return station (optional), and target year
- Run
python3 scripts/run.py <出发站> [返程站] [年份] - Guide the user to switch outbound/return in the webpage, ignore holidays with no travel, choose a reminder, and then download the ICS