末班车助手:一个小程序数据工程项目的完整闭环

末班车助手:一个小程序数据工程项目的完整闭环

本页由项目自动归档生成,源码与最新进展以 GitHub 仓库为准。
项目自 2026-09 起进入维护模式:链路仍在自动运行,但不再主动开发。这篇是它的收尾总结。

项目简介

「末班车助手」是一个微信小程序:帮赶末班车的人 3 秒内看到”还剩几分钟”。打开就是末班车倒计时,30 分钟内红色高亮,支持跨午夜服务日(凌晨 0 点后仍算前一晚),收藏常用线路方向首页直达,模糊定位自动识别城市(纯本地围栏判定,不上传位置)。

表面上看,它是个查询工具;实际上,真正花功夫的地方在水面之下——如何让一个没有后端团队的个人项目,持续保证 4 个城市、73 条线路、3400+ 个站点时刻数据的准确性。这篇博客主要讲后者,因为那才是一个数据工程问题的完整解法。

产品形态

页面 功能
pages/index 首页:收藏 + 模糊定位识别城市 + 城市选择
pages/city-detail 城市详情:搜索 + 线路列表 + 客服电话 + 跳转腾讯地图
pages/line-detail 时刻表:方向切换 + 倒计时 + 延时运营角标 + 客服纠错
  • 微信小程序原生框架 + Skyline 渲染引擎 + Glass-easel 组件框架
  • 覆盖北京、上海、广州、深圳

架构:一条全自动数据流水线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[每月 1 日 04:00]  Mac launchd 本机抓取(tools/scraper,Python 标准库)
│ 官方源:bjsubway / shmetro 移动端 / 本地宝转录 / szmc.net
▼
jest 数据校验(格式/单调性/唯一性,失败自动回滚)
▼
数据异常守卫(tools/guard,跨日 diff 拦截"格式对但内容错")
▼
git 提交 → push GitHub
▼
[GitHub Actions] 构建 dist/v1/*.json + manifest
▼
wrangler pages deploy → Cloudflare Pages(备案域名 lasttrain.hummingg.com)
▼
[小程序] 启动拉 manifest → 仅下载有更新的城市 → 失败回退包内静态数据

几个关键设计决策

1. 静态优先,远端是增量而非依赖。
时刻表是准静态数据(一天内不变,变的是”官方哪天改了表”),不需要数据库。发布产物就是 5 个 JSON 文件,Cloudflare Pages 免费额度等于无限。小程序内永远保留打包时的静态副本,远端拉取失败(3 秒超时)静默回退——离线也完全可用。

2. 数据准确性靠”三道闸”而不是靠人盯。

  • 结构闸(jest):时间格式、同方向站名唯一、首末班单调性;
  • 语义闸(守卫 tools/guard/check-schedule.js):跨日 diff,线路数骤减、超 30% 站点时间同日大变、单站偏移超 30 分钟 → 判定”疑似解析错位”,阻断发布等人工确认(--allow 放行)。页面改版后爬虫”成功”抓回一堆格式正确但语义错误的数据,是最危险的静默腐坏,这道闸就是为它设计的;
  • 时效闸:updatedAt 落后今天超 3 天、内容变了但日期没推进 → 告警。

3. 抓取放本机,CI 只做发布。
GitHub 数据中心 IP 会被北京地铁官网 403、上海移动端接口超时;家里 IP 全部正常。所以重抓取交给 Mac 的 launchd 定时任务,CI 只负责校验、构建、发布——职责分离后,两边各自的失败模式都变简单了。

4. 延时运营走”例外数据”而不是改时刻表。
节假日延时运营是静态时刻表的天敌。北京地铁官网的《地铁运营信息》公告是结构化自然语言(”10月7日、8日地铁2号线北京站内外环末班车延时运营至次日凌晨2:00”),tools/scraper/exceptions.py 用正则解析出「日期区间 + 线路 + 车站 + 方向 + 延时后末班」,产出独立的 data/exceptions/beijing.js。客户端倒计时先查例外再查常规时刻,页面显示”🌙 今日延长运营”角标。保守策略:公告未提及的车站一律保持常规时刻——宁可少显示延时车,也不错报不存在的车。

5. 微信小程序的域名现实。
正式版 wx.request 只能调 ICP 备案域名。方案:Cloudflare Pages 绑定备案自定义域名 + 手动建 CNAME(Pages API 不会自动建)。本地开发用”关闭域名校验”,上线零障碍。

数据准确性工程(本项目最有意思的部分)

一个个人项目如何对”数据错了用户就错过车”负责?答案是让错误数据活得短、且一定会被发现,而不是追求”永不犯错”:

  1. 单城市失败隔离:某城官方站 403 时保留上次数据,其余城市照常更新,绝不因一个源挂掉而全链路崩溃;
  2. 异常守卫(见上),配合 CI 失败推微信通知(PushPlus);
  3. 时效告警:数据源静默腐坏(返回缓存旧页面)也能被抓到;
  4. 用户纠错兜底:时刻表页内嵌微信原生客服入口,用户报错 → 人工核对 → 修正随下次校准自动发布;
  5. 界面常驻“请以官方公布为准” + 一键跳腾讯地图查实时——错误声明和逃生出口都摆在明面上。

验证守卫时做过一次”模拟攻击”:把北京几十个站的末班时间集体减 2 小时,守卫报出 74 处异常并阻断发布,其中包括”9 号线 4/12 站同时大幅变化,疑似解析错位”的线路级判定。

踩过的坑

  • launchd 依赖的 node 路径失效:脚本里写死的 workbuddy node 被清理后,月度任务会在 jest 环节静默挂掉。修复:node 探测改候选列表(which → homebrew → /usr/local/bin),launchd 的极简 PATH 里加 homebrew。**教训:定时任务的环境会腐烂,要用”最小环境实测”验证而不是只看”注册成功”**。
  • **GitHub Actions 只认仓库根的 .github/workflows**:monorepo 里放在子目录的 workflow 完全不可见。
  • GITHUB_TOKEN 默认只读:CI 回写数据需要显式声明 permissions: contents: write;monorepo 上多人并行推送时回写要 fetch + rebase --autostash 再 push。
  • Cloudflare token 的 tokens/verify 报 Invalid 但实际可用:直接调业务 API 验证比纠结 verify 接口靠谱。
  • 公告解析的句式长尾:真实公告里有”9月29日和10月5日、6日”混合省略月份、”4号线…上行、14号线…上行”多线路共享时间、括号内终点站、日期单独成子句跨逗号生效……最终以”句号拆句、逗号拆子句、子句日期继承”的三层结构消化,用 2025 国庆真实公告验证 33 条全对。
  • 本机 DNS 负缓存:新域名解析生效前 curl 过的机器会缓存 NXDOMAIN 几分钟,--resolve 绕过即可验证,别误判成服务问题。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LastTrain/
├── pages/ # 三个页面(index / city-detail / line-detail)
├── utils/
│ ├── train-service.js # 查询/倒计时/例外匹配/搜索
│ ├── remote-schedule.js # 远端拉取 + 缓存 + 静默回退
│ ├── city-service.js # 城市/电话/地图配置
│ └── favorite-storage.js # 收藏存储
├── data/
│ ├── last-train/ # 4 城时刻表(scraper 生成,勿手改)
│ └── exceptions/ # 延时运营例外数据(公告解析生成)
├── tools/
│ ├── scraper/ # 爬虫(Python 标准库)+ 异常守卫调用入口
│ ├── guard/check-schedule.js # 数据异常守卫(跨日 diff)
│ └── build-remote/ # dist/v1/*.json 构建器
├── tests/ # 66 个 jest 单测
└── .github/workflows/refresh-data.yml(仓库根) # CI:校验→守卫→发布→回写

技术栈

维度 技术
前端 微信小程序原生 + Skyline + Glass-easel,自定义导航栏
数据抓取 Python 3 标准库(urllib/html.parser),GBK 页面处理
校验 Jest(数据结构 + 业务逻辑 + 守卫规则),66 用例
分发 GitHub Actions + wrangler + Cloudflare Pages + 备案自定义域名
定时 macOS launchd(本机抓取)+ GitHub Actions cron(发布)
通知 PushPlus(CI 失败推微信,可选)

复盘:什么值得做,什么不值得

值得的:把”数据准确性”当成工程问题而非手工问题来做,三道闸 + 异常隔离 + 守卫的整套设计可以直接迁移到任何”抓公开数据并保持准确”的项目;抓取放本机/发布放 CI 的职责分离;静态优先的架构让运维成本趋近于零。

不值得的:在一个没有用户的产品上堆功能。客服纠错、用量观测这些”等有用户才有意义”的设施,属于提前量——做的时候清楚了这一点,就不后悔,但也不该再做更多。

为什么收尾:需求真实(赶末班车的焦虑),但窗口极窄(每天半小时 × 固定通勤人群),且没有分发能力就无法获得用户反馈循环。技术上完成了”数据工程闭环”的完整练习,产品上止步于一个可靠的 demo——这是诚实的结论,也是个不错的句点。

相关链接

  • 博客内同类项目:WorkDay(同为小程序 + 多端 + 自动化数据的项目)