末班车助手:一个小程序数据工程项目的完整闭环
本页由项目自动归档生成,源码与最新进展以 GitHub 仓库为准。
项目自 2026-09 起进入维护模式:链路仍在自动运行,但不再主动开发。这篇是它的收尾总结。
项目简介
「末班车助手」是一个微信小程序:帮赶末班车的人 3 秒内看到”还剩几分钟”。打开就是末班车倒计时,30 分钟内红色高亮,支持跨午夜服务日(凌晨 0 点后仍算前一晚),收藏常用线路方向首页直达,模糊定位自动识别城市(纯本地围栏判定,不上传位置)。
表面上看,它是个查询工具;实际上,真正花功夫的地方在水面之下——如何让一个没有后端团队的个人项目,持续保证 4 个城市、73 条线路、3400+ 个站点时刻数据的准确性。这篇博客主要讲后者,因为那才是一个数据工程问题的完整解法。
产品形态
| 页面 | 功能 |
|---|---|
| pages/index | 首页:收藏 + 模糊定位识别城市 + 城市选择 |
| pages/city-detail | 城市详情:搜索 + 线路列表 + 客服电话 + 跳转腾讯地图 |
| pages/line-detail | 时刻表:方向切换 + 倒计时 + 延时运营角标 + 客服纠错 |
- 微信小程序原生框架 + Skyline 渲染引擎 + Glass-easel 组件框架
- 覆盖北京、上海、广州、深圳
架构:一条全自动数据流水线
1 | [每月 1 日 04:00] Mac launchd 本机抓取(tools/scraper,Python 标准库) |
几个关键设计决策
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 不会自动建)。本地开发用”关闭域名校验”,上线零障碍。
数据准确性工程(本项目最有意思的部分)
一个个人项目如何对”数据错了用户就错过车”负责?答案是让错误数据活得短、且一定会被发现,而不是追求”永不犯错”:
- 单城市失败隔离:某城官方站 403 时保留上次数据,其余城市照常更新,绝不因一个源挂掉而全链路崩溃;
- 异常守卫(见上),配合 CI 失败推微信通知(PushPlus);
- 时效告警:数据源静默腐坏(返回缓存旧页面)也能被抓到;
- 用户纠错兜底:时刻表页内嵌微信原生客服入口,用户报错 → 人工核对 → 修正随下次校准自动发布;
- 界面常驻“请以官方公布为准” + 一键跳腾讯地图查实时——错误声明和逃生出口都摆在明面上。
验证守卫时做过一次”模拟攻击”:把北京几十个站的末班时间集体减 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 | LastTrain/ |
技术栈
| 维度 | 技术 |
|---|---|
| 前端 | 微信小程序原生 + 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(同为小程序 + 多端 + 自动化数据的项目)