news 2026/9/5 4:19:24

Python 扫一遍港铁全线:270 次查询里,24% 的站台此刻正有列车进站

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 扫一遍港铁全线:270 次查询里,24% 的站台此刻正有列车进站

文章目录

    • 1. 结论先行
    • 2. 环境信息
    • 3. 数据与口径:码表和 API 说的是两套"方言"
    • 4. 核心代码
    • 5. 运行结果:195 个站方向的等待分层
    • 6. 可视化:分层与样本量
    • 7. 为什么横截面比单站轮询有信息量
    • 8. 踩坑与避坑
    • 9. 总结

1. 结论先行

港铁班次好不好,广告说了不算,API 说了算。这篇用港铁官方码表 + 实时到站接口,对全网络270 个站方向逐一查询(195 个成功返回),抓下了一个周五上午的完整横截面:24% 的站台此刻正有列车进站(下一班 0 分钟),市区线(港岛/荃湾/观塘)的中位等待只有1 分钟,而机场快线沿线是3.5 分钟——同一家公司、同一张八达通,等待体验差了 3 倍多。

这条"1 分钟的城市,3.5 分钟的机场"的分界线,就是这篇用 270 次 API 查询画出来的东西。

2. 环境信息

  • Python 3.13,只用标准库(urllib / csv / json / statistics)+ matplotlib,零第三方依赖
  • 数据源一:MTR 官方线路站点码表opendata.mtr.com.hk/data/mtr_lines_and_stations.csv(10 条线、273 行、含每站双方向序号)
  • 数据源二:Next Train 实时接口rt.data.gov.hk/v1/transport/mtr/getSchedule.php?line={线}&sta={站}(零鉴权,返回未来 3 班车的到站分钟数 ttnt)
  • 诚实边界:横截面采样约需 40 秒(0.15s 间隔 × 270 次),非严格同一毫秒;75 个站方向无数据(终点站折返方向、迪士尼线单站等),不参与统计

3. 数据与口径:码表和 API 说的是两套"方言"

最大的坑在这里,先讲清楚。码表 CSV 的方向字段是DT(Down Track)/UT(Up Track),按它发查询、再按它取结果,270 个响应全部解析失败——因为 API 返回的 JSON 里方向键是DOWN/UP。同一个东西,码表和接口用了两套词表,中间需要一次{"DT": "DOWN", "UT": "UP"}的映射。这个坑没有任何文档标注,是 270 次 no-data 逼出来的。

第二个口径:ttnt(minutes to next train)只取valid == "Y"且为数字的行;ttnt = 0表示列车正在进站——这个占比本身就是班次密度的直观指标。

4. 核心代码

importcsv,io,json,time,urllib.requestfromcollectionsimportdefaultdict CAT="https://opendata.mtr.com.hk/data/mtr_lines_and_stations.csv"API="https://rt.data.gov.hk/v1/transport/mtr/getSchedule.php?line={}&sta={}"DIR_MAP={"DT":"DOWN","UT":"UP"}# catalogue dialect -> API dialectdeffetch_wait(line,sta,direction):url=API.format(line,sta)d=json.loads(urllib.request.urlopen(urllib.request.Request(url,headers={"Accept":"application/json"}),timeout=10).read().decode())entry=d.get("data",{}).get(f"{line}-{sta}")ifnotentry:return[]deps=entry.get(DIR_MAP.get(direction,direction))or[]return[int(x["ttnt"])forxindepsifx.get("valid")=="Y"andstr(x.get("ttnt","")).isdigit()]req=urllib.request.Request(CAT,headers={"User-Agent":"Mozilla/5.0"})rows=list(csv.DictReader(io.StringIO(urllib.request.urlopen(req,timeout=30).read().decode("utf-8-sig"))))LINE_CODES={"AEL","EAL","TCL","TML","TKL","ISL","TWL","KTL","SIL","DRL"}# 10 urban linesline_waits={}seen=set()forrinrows:line,direction,sta=r["Line Code"],r["Direction"],r["Station Code"]key=(line,sta,direction)ifkeyinseenorlinenotinLINE_CODES:continueseen.add(key)ttnts=fetch_wait(line,sta,direction)ifttnts:line_waits.setdefault(line,[]).append(min(ttnts))time.sleep(0.15)# be polite to the public gateway

两处纪律:(line, station, direction)三元组去重(换乘站在码表里出现多次),每次查询 sleep 0.15s(公共网关,不打.cluster)。这段「码表驱动 + 方言映射 + 限速扫描」的套路可复用到任何按站发查询的实时接口,建议收藏备用

另外提醒一句:码表本身也是数据,同样要验证——273 行里混着缆线(如 AEL 机场快线)与重铁,若不做LINE_CODES白名单过滤,轻铁和缆线的站码发进重铁接口只会白跑。先过滤再发查询,270 次才是一次性把全网扫完的准确数字。

5. 运行结果:195 个站方向的等待分层

queried 270 station-directions: ok 195, no-data 75 line n min median max mean Airport Express 8 0 3.5 7 3.6 Tung Chung 14 0 3.5 6 2.7 Disneyland 2 3 3.0 3 3.0 South Island 8 0 3.0 4 2.6 Tseung Kwan O 9 0 3.0 5 2.6 Tuen Ma 46 0 2.0 6 2.5 East Rail 26 0 2.0 4 1.7 Kwun Tong 25 0 1.0 4 1.3 Island 32 0 1.0 4 1.2 Tsuen Wan 25 0 1.0 3 1.1 ttnt=0 (train pulling in now): 46/195 (24%)

分层一目了然:市区三线(港岛/荃湾/观塘)中位 1 分钟——不用看屏,走到站台车就快来了;东铁/屯马 2 分钟——新界大动脉的骨干节奏;机场快线/东涌线 3.5 分钟——用班距换运距的取舍。24% 的站台 ttnt=0,意味着随手挑四个站台,就有一个正有列车进站。

把 75 个无数据的站方向单独看一眼也有价值:它们集中在线路两端的折返站和迪士尼线的单站结构——API 对"下一班还没排班"的方向不返回任何行,这与"线路停摆"完全不同。做实时数据监控时,把"接口没给"和"给了但等很久"分成两类,告警才不会两边乱响。

6. 可视化:分层与样本量

第一张双面板:左边每条线的 min–median–max 区间(红点为中位),右边是采样量(屯马线 46 个站方向最大)。第二张把均值排成"等待体验排行榜",绿色 1 分钟档、红色 3.5 分钟档,网络均值 2.0 分钟画在同一张图里做基准。这套「横截面快照 + 分层排行」的组合值得收藏,评估任何实时系统先抓一个横截面

7. 为什么横截面比单站轮询有信息量

盯一个站的到站屏只能知道"下一班几分钟",扫全网才能回答结构性问题:等待体验的分界线画在哪、谁在被区别对待。机场快线的 3.5 分钟不是班次差——它是低频高容量的定价逻辑;屯马线 46 个站方向全部有数据回传,说明这条全网最长的线覆盖完整。横截面还能做时间序列:同一脚本每天跑三次(早高峰/平峰/晚高峰),一周就是完整的班次画像——复跑成本低正是这个玩法的好处。唯一的纪律是别并发:0.15 秒间隔是实测稳定的节奏,270 个请求排队四十秒跑完,比压垮公共网关然后被限流便宜得多。

8. 踩坑与避坑

踩坑后果避开姿势
按码表的 DT/UT 取 API 结果270 次查询全部 no-data映射 DT→DOWN / UT→UP
无间隔猛发 270 请求公共网关限流,随机失败每次 sleep 0.15s,异常按站容错
换乘站重复查询同站重复计数(line, station, direction) 三元组去重
ttnt 字符串直接 int“-” 或空值崩溃valid=="Y"+ isdigit 双重过滤
把 75 个无数据站当 0 等待统计被稀释无数据单独归类,不进分布

这张表建议收藏——第一行的方言映射没有任何文档记载,纯靠实测对出来。

9. 总结

270 次查询换一张全网络等待地图:市区线 1 分钟、机场线 3.5 分钟、24% 的站台正有车进站。这个玩法最值钱的地方是可复现——脚本每天跑三次,横截面就变成时间序列;换个城市、换个运营商,改一个 URL 和一张码表就能迁移。实时数据的价值不在单次快照,在快照可以便宜地重复。

代码、码表、方言映射全部可复现——把 LINE_CODES 换成轻铁的一组站码,同一个脚本立刻变成轻铁版。数据来自 MTR 官方码表与资料一线通实时网关(2026-09-04 快照)。

本文为原创技术实践;数据来自 MTR 官方开放码表与资料一线通实时接口(零鉴权),快照时点为 2026-09-04 上午;横截面非官方班次承诺,出行请以车站实时信息为准。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 4:18:24

CMSIS-DSP源码解读:从FFT到矩阵运算的嵌入式优化实践

1. 从零读 Arm-CMSIS-DSP 源码前,先搞清楚它到底在解决什么问题很多做嵌入式的人第一次接触 CMSIS-DSP,是因为项目里要用到 FFT、FIR 滤波或者矩阵运算,然后在 Keil 的 Pack 管理器里勾选了一个叫CMSIS-DSP的组件,接下来就稀里糊涂…

作者头像 李华
网站建设 2026/9/5 4:17:50

技术创作瓶颈破局:从日常工作流中挖掘高质量技术文章选题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 4:14:43

RAG实战拆解:从Notebook到完整知识库检索增强生成链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 4:07:07

Solana MEV 数据采集复盘

Solana MEV 数据采集复盘 本文记录一次完整的 Web 数据采集服务搭建全过程:接口逆向 → 结构分析 → 采集架构 → 管线实现 → 稳定性加固 → 反爬对策 → PyInstaller 打包 → 落地运维。 目标数据源:Solana MEV 数据平台 (原子套利实时流 + 常规/宽幅三明治快照),展示页 sand…

作者头像 李华
网站建设 2026/9/5 4:06:15

看完STM32教程≠能找到工作:工程能力差距与补强路线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 4:03:29

AI编程可控性实战:构建开发者主导的代码生成拦截机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华