Skip to content
LainVsPublic

About

12306 直达+中转混合查询桌面工具:图搜索中转枚举、价格-时间星图定位「比普速快、比高铁便宜」的购票方案

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

铁行择优 RailPick — 高铁与普速的中间值

RailPick 桌面版

直连 12306 查询直达 + 自动枚举 1~2 次中转组合,用价格滑杆/时长滑杆快速定位“比普速快、比高铁便宜”的混搭方案。

运行(桌面版,推荐)

双击 run_desktop.bat,或:

pip install -r requirements.txt
python desktop.py

桌面版无需浏览器、不占端口,功能与网页版一致:三卡对比(可点击定位)、价格—时间星图(悬停预告/点击定位)、 滑杆过滤、购票指引、历史搜索。查询在后台线程跑,界面不卡死。 桌面版独有:站名输入联想、日期今/明/后快捷、取消查询、复制购票清单/导出TXT、 只看经停站过滤、已覆盖中转站公示、查询条件自动记忆、出发时间排序。 一键爬取带进度条+阶段播报(新增几趟/累计几趟/新入库车次/预计剩余秒)+可取消; “查看图库”可看已爬数据(统计/按车次查经停/按车站查过路车/最近入库)。 两端查询都有真进度:阶段文案+百分比+已用秒(网页版是条纹动画进度条,桌面版是进度条+可取消)。

运行(网页版,备选)

python -m uvicorn backend.app:app --host 127.0.0.1 --port 8000

打开 http://127.0.0.1:8000

若报 WinError 10048 端口只允许使用一次:说明有旧服务还占着 8000 端口。 查杀:netstat -ano | findstr 8000 找到 PID → 任务管理器结束它, 或直接用桌面版(不受影响)。

网页版界面结构

按功能分三层,不再把所有控件堆在一起:

  1. 查询卡(页面顶部):出发/到达/日期 + 查询按钮为第一层;换乘深度、指定中转、 搜索广度、查询模式、换乘窗口收在第二层;选「图库深挖」时才展开枚举/核验/前后天。
  2. 筛选栏 + 结果区(左右双栏,<1080px 自动上下叠): 左栏是价格/时长滑杆、车型、换乘次数、出发时段、精确时间(折叠)、只看有票; 右栏是排序 + 三卡对比 + 价格—时间分布图 + 方案卡片。
  3. 时刻表图:从主流程里拿出来,顶栏按钮开独立面板(爬取是低频维护操作, 不该和查询抢注意力)。

方案卡片改成竖向行程表:左侧发到时刻(等宽数字对齐)、中间站点连线、右侧分段票价, 换乘等待单独一条,同城异站会标「泰山 下车 → 泰安 上车 · 同城异站」。

数据来源(无 Key、零登录)

  • 车站码:kyfw.12306.cn/otn/resources/js/framework/station_name.js
  • 直达余票:.../otn/leftTicket/query[G/O/Z/A]?...purpose_codes=ADULT(自动轮询接口名 + c_url 自适应)
  • 票价:.../otn/leftTicket/queryTicketPrice
  • 经停站:.../otn/czxx/queryByTrainNo(找顺路中转站)
  • 仅低频查询,不做登录/下单/抢票。

玩法

  1. 输入 出发/到达/日期,选换乘深度,点查询组合。
  2. 看顶部三卡:最快 / 推荐中间值 / 最便宜。
  3. 看价格—时间星图:越左下越优,点击圆点定位卡片。
  4. 拖左侧滑杆:最高价 / 最长耗时 / 出发时段 / 精确时间(出发HH:MM、到达HH:MM,可过夜区间)+ 车型组合(高铁+普速是灵魂)快速过滤。
  5. 点购票指引,去 12306 分段购买(或用 12306“中转/接续换乘”页按此组合出票)。
  6. 预售期 15 天内可查;历史搜索一点即查;卡片上站名、分段价(席别)、中转站、+1天均已标明。
  7. 票少?调“搜索广度”:快速(8站)/标准(12站)/地毯式(20站全查D/D+1,2~5分钟)。 中转100%自己组(12306只给分段余票原材料;官方中转接口需登录不可用), 候选站=直达车经停站+地理走廊排序的候选中转池(250城坐标,顺路的自动上浮, 大枢纽保底加成,名单打补丁已成过去式), 每次查询会公示“打了N次12306·试了M个中转站”。

时刻表图(主动爬,越用越准)

候选中转=直达车经停站(占一半名额,剩下留给图库和枢纽)+ 本地时刻表图 + 地理走廊枢纽。图是后台礼貌爬来的: 桌面和网页都叫“一键爬取”(桌面在星图下那排按钮里:复制购票清单 / 导出全部TXT / 重置过滤 / 一键爬取), 或 python -m backend.crawler --max 1000 --from 唐山,北京。限次可调(填0=无限,一直啃到你点取消;10~5000,默认1000)。

数据归档(网页版「时刻表图 · 数据归档」面板)

爬来的数据落进 timetable.db(SQLite)。网页版顶栏「时刻表图」按钮打开面板,五个标签:

标签 内容 后端接口
爬取 优先站/范围/限次,实时进度,可取消 POST /api/graph/crawl
爬取记录 每次爬取的时间、请求数、新增车次、优先站、数据日期 GET /api/graph/runs
查车次 按车次号查归档里的全程经停(站序/到发时刻) GET /api/graph/train
查车站 按站名查归档里经过该站的所有车次 GET /api/graph/station
最近入库 按入库顺序倒序 GET /api/graph/recent

归档保证:中途取消或异常也会写一条记录(finally 里落库),前面已爬的车次照样入库 ——之前只有正常跑完才写,取消一次等于没爬过。

优先站:填你常坐的站(如唐山,北京),这些站的进出先爬——和你出行相关的图最先变全。 范围:自动(干线先啃再散射地级)/仅干线/仅地级。散射=250城×最近枢纽,专抓支线车; 中断过的OD按历史产量降序重排,高产优先。图头显示覆盖率(全国图定12174列)。 原理:1 个 OD 查询发现几十趟车,1 趟车 1 个请求拿下全线车站,覆盖效率比站站对查高两个数量级。 数据存 timetable.db(SQLite),/api/graph/stats 可看(几趟车·几站·覆盖率·上次爬取)。只爬公开经停站,不碰登录抢票。 桌面“查看图库”是同一份数据的桌面视图:概览统计、每次爬取记录、按车次查经停、按车站查过路车、最近入库。 多次爬取不重复:车次号主键(重入整车替换更新)、OD 做完打标记、已知车次跳过。 实测:已啃 OD 重爬只花 1 个请求、新增 0 趟;全库 GROUP BY 查无重复行。

反检测与速率(为什么不压测上限)

现有手段:UA 轮换、请求间隔+抖动、失败翻倍退避、会话刷新、搜索并发封顶4、余票/票价/经停三级缓存。 速率上限故意不做破坏性压测(把自家 IP 送进黑名单,真买票时被封就搞笑了), 改用自适应调速(AIMD):成功每次 -0.2s,失败 ×2,上限 30s、下限 1.5s 红线,速率持久化下次接着来。 每次爬完会告诉你“最快间隔X秒、退避N次”——这就是系统自己摸到的安全速率。

实测数据(2026-10,TSP→BJP 等真实 OD,每次换日期避开本地缓存)

间隔 成功率 结果
1.5s 8/8 全部成功
1.0s 8/8 全部成功
0.6s 4/8 开始返回 HTML 拦截页
0.35s 0/8 全部被拦

安全边界在 1.0~0.6 秒之间,但代码下限仍保持 1.5 秒,原因:

  1. 一旦触发限流,余票接口会被封很久——实测 25 分钟以上仍未解封
  2. 封禁是接口级的:init 页(92KB)和车站列表(137KB)都正常, 只有余票接口返回 2042 字节的「网络可能存在问题」拦截页
  3. 封禁期间完全查不到票,只能干等,代价远大于提速省下的时间

另外注意:RateController.wait() 实际睡的是 cur + 0~1秒 随机抖动 (固定节奏更容易被识别),所以报告里的「1.5 秒」真实间隔约 1.5~2.5 秒。

限流自动识别

爬虫会把「HTML 拦截页 / 空响应 / 非 JSON」与普通网络错误分开,识别为 Throttled:

  • 立刻停止本次爬取(不再继续打,继续打只会拖长封禁)
  • 已入库的车次保留,归档照常记录
  • 前端明确提示「已被限流,建议 1 小时后再爬」

普通超时/连接错误不会误判成限流,仍按原来的连续失败计数处理。

两种模式

  • 实时深查(默认):在线枚举 12 次中转,名额按广度档位来,稳、准、慢(2070秒)。
  • 图库深挖(无上限):本地 7000+ 趟时刻表图上枚举(条数可调,默认300,最大2000), 最多 4 次中转、前后天;再对头部 N 条(默认40,最大1000)打 12306 核验余票和价格(死的自动丢掉)。 图里翻太少(<20 条)且没被限流时,自动跑一轮实时搜索合并兜底。展示不截断:桌面全列,网页分页。 桌面左上角切模式,中转≤/核验条数(最多300)/前后天可调;价格滑杆放到 5000、时长放到 72 小时, 专治西藏新疆这种远路。注意:图是爬来的时刻表,车次/时刻以核验为准。 同城站(广州南/广州白云、泰安/泰山、唐山/唐山北等)出发到达换乘都按同城处理,和12306通查对齐。

限流说明

12306 反爬严:并发≤4、请求间隔随机、票价只查 Top 候选、结果缓存。 关键直达查询失败自动换会话重试3次;余票/经停站/票价均有本地缓存(2分钟/1小时/15分钟), 失败时先服役旧缓存并明确标注,实在无缓存才显示演示数据+重试按钮。 遇到限流提示等30秒点「重试实时查询」即可,勿连续狂点。

回归测试

双击 tests\run_tests.bat(离线:缓存/分组/图/取消/离线枚举/展示/前端逻辑/桌面冒烟全过)。 tests\ 下还有实查探针(probe.py)和轻量在线测试(test_jobflow/test_overnight/test_progress/test_crawlwire, 每个只打几个请求),改完核心逻辑跑一遍。

新增的离线用例(都是踩过的坑,改代码前先跑):

  • test_p0_regress.py:2 次中转枚举、建图缓存失效、datetime 契约
  • test_display.py:同城异站换乘(泰山→泰安)标注、站名降级兜底
  • test_frontend.py:前端日期时区、只看有票、智能排序(需要 node,没有则跳过)

性能与稳定性(本轮优化)

项目 优化前 优化后
图库深挖枚举(唐山→费县,mt=2) 16~21s 4~6s
建图(7237 趟 / 55 万条边) 每次查询都重建 按库签名缓存,命中 <1ms
票价查询 同一分段重复打网 分段键去重后提交
2 次中转票价 主线程串行,堵进度 线程池并行 + 可取消
并发查询 + 爬虫写库 database is locked 切 WAL,读写不互斥

枚举结果与优化前逐条一致(8 组 OD/换乘数/等待窗对比验证),只是更快。

算法说明

  • 内部时间轴改成「绝对分钟整数」,不再用 datetime + timedelta(原来 1150 万次 total_seconds 占了 40% 时间),只在产出结果时转 datetime,对外接口不变。
  • 同城展开的边按发车钟点排序,用二分只取等待窗内的车(原来每次扩展都要扫 两三百条边再逐条试 4 天)。
  • 反向可达性预剪:结构上到不了终点的站不扩展(冷门长线 45 万次扩展只出 1 条的情况)。

About

12306 直达+中转混合查询桌面工具:图搜索中转枚举、价格-时间星图定位「比普速快、比高铁便宜」的购票方案

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages