
文章目录
-
- 先说结论
- [一、数据源与口径:为什么 406 就是 406](#一、数据源与口径:为什么 406 就是 406)
- 二、核心代码:拉取、分类、提取、统计
- 三、运行验证
- [四、可视化与分析:406 条路线里藏着什么](#四、可视化与分析:406 条路线里藏着什么)
- 五、四个容易踩的坑
- 六、小结
先说结论
用 Python 请求城巴官方开放数据 JSON(data.gov.hk 发布,零鉴权),把全网 406 条路线号拉下来做分类统计和目的地分析,就能画出一张香港岛巴士网络的数字画像。 城巴 2023 年合并新巴后由单一公司运营,这份 406 条路线的清单,就是今天香港岛巴士网络的完整骨架。
三个最扎眼的数字先放前面: 一是 406 条路线里,130 条常规数字线 + 128 条数字加字母后缀的变体线 (1M、2A、720P 这类)合计占 63.5%------一个线路号背后往往藏着一个"变体家族";二是 中环 Central 是全港第一终点 ,35 条路线以此为终点,比第二名铜锣湾多出 10 条;三是机场线族(A 线 + E 线 + NA 通宵)合计 40 条,是除市区常规线外最大的功能集群。
做法只有四步 :fetch_json() 带重试地拉路线列表,classify() 用正则按字母模式分类,dest_area() 从目的地字符串里提取区域名,最后 Counter 统计 + matplotlib 出图。全程只用 Python 标准库加 matplotlib,任何开放数据都能套同一套流程。
一、数据源与口径:为什么 406 就是 406
城巴把路线、站点、实时到站三组数据挂在 data.gov.hk 上,API 基址是 https://rt.data.gov.hk/v2/transport/citybus/,零鉴权,加个 Accept: application/json 头就能拿。本文只用其中两组:
route/CTB:全部路线列表(起点、终点、中英文名)route-stop/CTB/{route}/{direction}:某条路线某个方向的站点序列
这里有个容易数错的口径 :route/CTB 返回的列表里,每条路线号只有一行 ------它不是"去程一行、回程一行"的双向列表。方向是靠请求参数 direction 指定的(取值只有 inbound / outbound)。所以 406 行就是 406 条路线号,不是 812 个方向。把这个数错,后面所有占比全错。
406 行 = 406 条路线号(官方口径,非双向行数),数错一条整篇分析就站不住。
二、核心代码:拉取、分类、提取、统计
python
import json
import re
import urllib.request
from collections import Counter
ROUTE_URL = "https://rt.data.gov.hk/v2/transport/citybus/route/CTB"
def fetch_json(url, retries=3):
"""拉取 JSON,带重试:城巴 API 偶发超时/空响应"""
for attempt in range(retries):
try:
req = urllib.request.Request(url, headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except Exception:
if attempt == retries - 1:
raise
raise RuntimeError("unreachable")
def classify(route_no):
"""按字母模式给路线分类:A/E/N/R/S 前缀、X 后缀、数字+字母、纯数字"""
r = route_no.upper()
if r.startswith("NA"): return "Airport overnight NA"
if r.startswith("A"): return "Airport A-lines"
if r.startswith("E"): return "Airport logistics E-lines"
if r.startswith("N"): return "Overnight N-lines"
if r.startswith("R"): return "Event R-lines"
if r.startswith("S"): return "Shuttle S-lines"
if r.endswith("X"): return "Express X-suffix"
if re.fullmatch(r"\d+[A-Z]", r): return "Suffixed variants (1M/2A...)"
if re.fullmatch(r"\d+", r): return "Regular numeric"
return "Other special"
def dest_area(dest_en):
"""从目的地串提取区域名:去掉连字符前段之外的尾巴和括号"""
return re.sub(r"\s*\([^)]*\)\s*$", "", dest_en.split(" - ")[0].strip())
payload = fetch_json(ROUTE_URL)
routes = {r["route"]: r for r in payload["data"]} # 每号一行,直接去重
kinds = Counter(classify(r) for r in routes)
areas = Counter(dest_area(r["dest_en"]) for r in routes.values())
print("unique routes:", len(routes))
print("classes:", dict(kinds.most_common()))
print("top destinations:", areas.most_common(10))
分类的顺序很关键:NA(机场通宵)必须放在 N(普通通宵)和 A 之前判断,X 后缀要放在纯数字正则之前------正则的匹配顺序就是分类的正确性。这套「正则分类 + Counter 统计」的写法可以原样搬到任何开放数据集的分类统计上,收藏备用。
三、运行验证
本地跑上面这段,输出(与 citybus_network_analysis.py 一致,2026-08-29 实测):
text
unique routes: 406
classes: Regular numeric 130 | Suffixed variants 128 | Express X-suffix 40
Overnight N-lines 30 | Other special 23 | E-lines 20
Airport A-lines 14 | Shuttle S-lines 12 | Airport overnight NA 6 | R-lines 3
top destinations: Central 35 | Causeway Bay 25 | Airport 21 | Tsim Sha Tsui 9
Sai Wan Ho 9 | Exhibition Centre 9 | Admiralty 9 | North Point FP 8
Wan Chai 8 | Tin Hau 8
自检方法:把各类计数加起来验证是否等于 406;再把目的地 top10 计数加起来对照总量。两个加总都对得上,数据才可信。
四、可视化与分析:406 条路线里藏着什么

1. 数字系占六成以上。 130 条常规数字线 + 128 条后缀变体 = 258 条,占 63.5%。后缀变体(1M、2A、720P、930X 已单列)是城巴编号体系里最"卷"的部分------同样一条走廊,主干、短途、繁忙时段、特快各占一个号。乘客看的是"这班车到不到",分析师看的是"这个号背后是一个家族"。对通勤者来说,变体还有实际含义:高峰时段多条变体同时开行,等于在同一条走廊上加密班次;如果只看路线号不看变体,会严重低估某条走廊的真实运营频次。这也是分类必须用正则精确拆开的原因------数字线、后缀变体、X 特快各算一类,才不会被"看起来都是数字"糊弄过去。
2. 机场线是一个完整的生态。 A 线 14 条(市区直达机场)、E 线 20 条(东涌/机场后勤)、NA 通宵 6 条,合计 40 条,占全网的近 10%。香港机场一天 24 小时运转,巴士网络就用 A/E/NA 三层对应三种需求------这是功能集群设计最清晰的部分。

3. 中环是绝对枢纽。 35 条路线以 Central 为终点,比第二的铜锣湾(25 条)多 10 条;机场(21 条)作为终点排第三,说明"去机场"是港岛方向最重要的单一出行目的之一。
4. 抽样站点数:通宵线为什么这么长。 顺手对六条代表路线数了 outbound 站点数:720 只有 18 站,1 号线 20 站,A11 机场线 24 站,E11 29 站,6 号线 41 站,而通宵 N8X 有 60 站------通宵车深夜客流稀疏,只能靠"一条线覆盖尽量多的地方"来维持运营密度,站多也就不奇怪了。
五、四个容易踩的坑
| # | 坑 | 现象 | 规避 |
|---|---|---|---|
| 1 | direction 传数字 | API 返回 422 Invalid direction |
只传 inbound / outbound |
| 2 | 把路线行数当双向行数 | 406 数成 812,占比全错 | route 列表每号一行,方向靠参数 |
| 3 | API 偶发超时/空响应 | 拉到空列表以为数据为空 | fetch_json 重试 3 次再报错 |
| 4 | matplotlib 中文 label | DejaVu Sans 缺字出方框 | 全程英文 label,中文放正文 |
第一条是实测时最先撞上的:我一开始按"方向 1、2"传参,直接被 422 打回。第二条是数据口径,最容易静默出错------406 变 812 没有任何报错,只有占比对不上才暴露。这张表建议收藏,下次拉任何开放数据 API 前先过一遍。
六、小结
一句话总结:香港岛巴士网络的骨架,用 406 个路线号和几行 Python 就能画像------63.5% 是数字系、40 条机场线、中环 35 条终点居首,通宵线靠拉长站点维持密度。
这篇与之前九巴 KMB 的实时 ETA 文章是两件事:那边是动态到站的时间语义,这篇是静态网络的整体结构------不同公司、不同数据集、不同分析对象。城巴开放数据每周随运营调整更新,把脚本存好,下次直接复跑就是最新画像。本文参与编程达人挑战赛·第12期,用一次完整「官方 API → 分类统计 → 可视化」的链路记录香港公共交通的一角。
参考链接(官方):
- data.gov.hk 城巴数据集页:https://data.gov.hk/tc-data/dataset/ctb-eta-transport-realtime-eta
- 城巴 API 规格与数据字典:https://www.citybus.com.hk/datagovhk/bus_eta_data_dictionary.pdf
声明:本文为原创内容,数据来自 data.gov.hk 城巴开放数据(2026-08-29 实测)。路线分类为作者自定口径,官方无此分类;城巴 2023 年合并新巴后由单一公司运营,406 条路线含原新巴网络。仅供技术交流。
