自建GitHub热榜日榜:Python与GitHub Actions自动化实践 1. 从手动刷Trending到自建日榜我为什么折腾这个项目每天早上打开手机第一件事都是点开Github Trending看看昨天又有什么新项目冲上来。这个习惯我保持了快两年但说实话越刷越觉得不满足。Trending页面默认只展示最近24小时的数据也就是说你今天看到的榜单和你昨晚十点看到的可能已经换了一大批。我这个人是做技术选型的经常需要对比某个项目连续几天的涨势判断它是“真热门”还是“刷出来的昙花一现”光靠手动点开历史页一个个对比效率太低了。后来我试过几个现成的日榜网站比如有人专门做每日热榜汇总的那种独立站点。但它们普遍有三个问题一是更新不稳定经常断更好几天二是只保留最近一周的数据想回看上个月某个项目当时的热度根本找不到了三是信息维度单一只有项目名、简介、星标数缺失了“跟昨天的变化量”“语言分布”“新增贡献者”这些我真正关心的信号。所以就有了这个想法自己搭一个“GitHub热榜日榜”的自动化项目每天早上定时去抓取Trending数据落库、去重、生成排行然后推给自己。坚持跑了一个季度之后我觉得这套玩法已经足够成熟可以写一篇相对完整的实战分享出来。这篇博文适合两类人一类是跟我一样爱刷榜单、想沉淀数据的开发者另一类是想找人解决“定时抓取数据处理展示”这些常见需求的同学。我会把选型思路、代码结构、部署方式和踩过的坑一次讲透。先交代一下我最终的成果形态一个Python写的抓取脚本跑在GitHub Actions的定时任务里每天北京时间早上八点触发抓下来的数据存成带日期的JSON文件自动提交到仓库另外生成一个README格式的榜单快照方便我直接看。全程不用额外买服务器也没有任何托管费用。2. 取数方案对比没有官方API日榜数据到底怎么拿2.1 三条主流路线的优劣分析GitHub官方并没有为Trending页面提供公开API这是个老话题了。很多人第一次找数据的时候会去翻官方REST API列表翻完就会失望地发现能拿到的只有仓库常规信息但“今天哪些仓库涨得最快”这种聚合排序接口并不存在。所以实际能走的路无非三条。第一条是直接爬Trending网页。GitHub页面是服务端渲染的HTML里就带有完整的榜单数据解析起来难度不算高。优点是数据真实、字段全缺点是页面结构偶尔会变你的解析器得跟着维护另外请求频率太高很容易触发限流。我遇到过HTTP 429这种状态码也遇到过返回一个空的HTML壳子里面什么都没有。第二条是调用第三方的非官方API。我在调研的时候试过一个社区维护的接口它把Trending数据包装成JSON返回用起来确实省事。但这类服务的稳定性看维护者心情经历过两次连续三天挂掉之后我就果断放弃了。做日榜这种东西最怕的就是数据源断供你可以延迟但绝对不能没有。第三条是自己抓取后走GitHub Actions定时托管相当于把数据源和调度器都握在自己手里。这也是我最终选的方案。严格来说第三条并不是独立的取数方式而是前两条的上层调度方案但它最大的价值是“免费”和“跑在路上不依赖本地环境”。你的笔记本关了、睡着了、网络断了都不影响每天早上榜单照常生成。2.2 我最终选定的数据链路我最终的数据链路是这样设计的一个Python脚本用requests请求Trending页面携带一个看起来正常的User-Agent并把语言筛选和日期范围参数拼到URL里拿到HTML后用lxml做XPath解析提取仓库名、描述、编程语言、今日星标数、总星标数、今日新增贡献者数这些字段接着做数据清洗和格式化打包成一个JSON文件最后用GitHub Actions的schedule事件触发整个流程并把结果通过git命令自动push回仓库。展示层就是一个普通的Markdown表格放在仓库README里手机上打开就能看。这套链路里最需要动脑子的是怎么构造Trending页面的请求参数。页面上有两个关键的筛选维度一个是语言比如Python、JavaScript、Rust另一个是“Since”窗口daily、weekly、monthly。我抓日榜所以固定用daily语言维度我不筛直接抓全局榜然后在清洗阶段再按语言字段做聚合这样一份原始数据可以支撑多种维度的分析比每次抓取时筛选语言要灵活得多。这里有个容易被忽略的细节Trending页面的URL里没有显式的日期字段。也就是说你只能拿到“当下这个时刻”的热门榜单而没法通过改URL回看上周四的历史榜。所以要想沉淀历史数据必须自己每天把数据存下来攒成自有的数据集。这个特点直接决定了你不可能“按需回补”历史数据一旦哪天没抓那天就彻底缺档了。我的做法是允许失败重试并且在第二天补抓时如果发现前一天缺档就用当天抓到的数据反推一个趋势标记但不会拿当天数据硬充成前一天的榜单避免污染数据集。3. 核心实现抓取、清洗、存储、推送的四段式流水线3.1 解析那段“反人性”的HTML结构先说最繁琐的一步解析HTML。很多人第一次打开Trending页面的开发者工具都会被它的DOM嵌套搞得头皮发麻。榜单主体是article标签每个article里有一个h2h2内部的a标签的href就是仓库相对路径。仓库全名的拼接规则是https://github.com加这个href比如href是/foo/bar那仓库全名就是foo/bar。这个逻辑看起来简单但坑在于h2里的a标签可能套着多层span直接用//h2/a/href取虽然能拿到但有时候标题里因为带了超长项目名GitHub会用省略号截断你拿到的href反而是完整路径反而是文本内容不全。所以我的建议是仓库名一律从href解析永远不要依赖h2的文本内容。描述文本在article里的p标签中但不是说每个article都保证有描述。有些仓库没有README或者README太短GitHub就不展示描述。这时候p标签是空的解析时要做空值处理。编程语言和星标数据在article底部的一排meta标签里我用相对路径加contains(text(), built by)这种写法去定位。有一点要注意今日新增星标数的文本格式是“1,234 stars today”带逗号直接转int会报错先去掉逗号再转换。我把解析逻辑封装成了一个函数输入是HTML字符串输出是结构化字典列表。为了让字段名尽量贴近英文直觉我用了repo_name、language、desc、stars_total、stars_today、forks_today、contributors_today这七个字段。其中contributors_today是后来加的原因是光看星标不能反映项目的协作热度有些工具类仓库很少人点星标但持续有人在提PR和提交代码这类项目在“built by”列表里是可以看出来的。3.2 去重与热度信号提取抓完数据之后一口气要处理三个问题同一仓库可能在不同语言分类里重复出现吗不会因为我抓的是全局榜全局榜里每个仓库只出现一次。但连续两天的数据里同一个仓库会反复出现这需要靠日期字段区分而不是去重。真正的去重发生在“同一天内的多次抓取”我给Actions里的任务设置了失败自动重试最多重试三次如果第一次成功、第二次也成功同一天就可能写入两份相同数据。所以在写入前要做一个以“日期仓库全名”为复合主键的检查发现已存在就直接跳过。热度信号提取这是我觉得整个项目里最有价值的部分。光记录星星数变化没有意义你需要把它转化成一个一眼能看懂的“热度分”。我的做法很简单heat_score stars_today * 1.0 forks_today * 0.5 contributors_today * 0.3。这个权重是拍脑袋定的吗不完全是我观察了两周数据发现纯工具类项目fork率高星标相对少如果只按星标排序它们会永远排不上榜而教程类项目星标高但fork率低所以fork权重降到0.5。另外我把contributors_today的权重设得最低因为它波动太大单个社区成员的无意义提交就会干扰排序。这些信号算完之后我会额外生成一个“连续上榜天数”的字段。这个字段需要访问前天和昨天的JSON文件才能算出来所以在单次抓取脚本里它是没法独立完成的。我的做法是先加载仓库里已有的全部历史数据在内存里按repo_name做聚合统计再生成今天的榜单。反正数据量不大跑起来也就几百毫秒没必要上数据库。3.3 存储与展示一份JSON和一个Markdown就够了存储方面我一开始也考虑过SQLite、PostgreSQL这些方案。但仔细想想这个项目的数据量有多小每天100个仓库一年也就36500条记录撑死几兆数据。引入数据库意味着要处理连接、迁移、备份这些问题收益极低。所以我最终选择了纯文件存储每天生成一个data/2026-10-09.json这样的文件里面是一个数组每个元素就是上面提到的七字段字典。展示层就更有意思了。我不想为了看个榜单专门打开一个Web页面所以直接用GitHub Actions在抓取完成后生成一个README快照用表格列出TOP 20。表格长这样排名、仓库名带链接、语言、今日星标、总星标、热度分。推送到仓库后手机的GitHub客户端就能直接看到渲染好的表格对我来说已经够用了。这个设计带来一个额外好处由于所有数据都是文本文件天然支持Git的版本管理。哪天我不小心把手里的JSON覆盖了直接从git历史里找回来就行。这比数据库备份简单一百倍也是我强烈推荐小体量数据项目用纯文件存储的根本原因。3.4 推送与通知让数据自己“跑”到眼前数据生成只是第一步还得让它主动出现在我面前否则我又得手动来仓库里看那就违背了做这个项目的初衷。我在Actions里设置了两个通知渠道一个是直接把最新的README发布到我自己的个人站点上——通过一个webhook把生成后的Markdown POST到小站的服务端接口服务端接收后写入缓存页面另一个是走Telegram机器人发消息把每天的TOP 10发到群里。Telegram bot在这里是个实用选择但要注意三点第一如果你用固定的服务器配合Cloudflare Access之类保护接口那要考虑是否有能力架设没有的话也别折腾直接把文件存到仓库就是最低成本的推送第二机器人发消息有长度限制TOP 10就够了别塞太多第三发送逻辑必须加异常捕获机器人网络不可用的可能性远比想象中高挂了不应该影响抓取任务本身。这是一种“核心链路对外围依赖隔离”的思路我后来把这个原则也用在了整个项目开发里。4. 部署实战与踩坑记录从“跑通”到“每天都跑通”4.1 定时任务Actions cron的那点坑用GitHub Actions做定时任务第一个坑就是cron表达式用的时区。Actions的schedule默认跑在UTC时区北京时间比UTC快8小时。如果你想要每天北京时间早上八点出榜那cron表达式应该写成”0 0 8 * *“——不对这是UTC的早上八点也就是北京时间的下午四点。正确的写法是“0 0 0 * *”也就是UTC的零点对应北京时间上午八点。光调cron还没完。GitHub频繁调度时Actions可能被延迟甚至跳过这是平台为了控制资源消耗做的限制实测下来延迟几十分钟很常见偶尔会有两个小时以上的延迟。对于日榜项目来说晚一两个小时还能接受但不能接受的是它“偶尔完全跳过某一天”。所以我在actions里给抓取任务打开了最大重试次数3次并在脚本开头加了一个保护逻辑如果发现data/今日日期的JSON已经存在并且内容合法那今天的任务就直接退出不重复抓取。这样即使动作被意外重新触发也不会产生脏数据。对了还有一个很多人会踩的坑Actions的免费额度对于个人公开仓库是无限的但如果你把仓库设为私有就会开始消耗每月2000分钟的额度。我的建议是这种数据仓库应该设为public代码放进去没关系反而能当一个小作品展示。如果实在想私有可以每30分钟跑一次改成每6小时跑一次额度算下来也够用。4.2 时区与“今日榜单”的边界问题时区问题远不止cron表达式那一个。更深层的坑在于“哪天的榜单算今天的”。Trending页面的“Today”窗口官方定义的是过去24小时。但这个24小时以哪个时区为起点我实测发现它按UTC零点滚动。也就是说你在北京时间凌晨3点访问到的Today榜单统计的实际是UTC从前一天零点到当前时刻的数据而北京时间的“今天”才刚刚开始。这会导致跨日时段的榜单统计口径不统一。我的处理方式是不管访问时刻是哪天只要脚本是在UTC新的一天首次运行即北京时间上午八点就把产生的数据标记为“今天的日榜”。因为我的抓取动作每天只有一次每次都在这个时间点之后触发所以数据口径基本稳定在一个时间切片上不会出现一天里多次抓取导致口径漂移的问题。另一个容易忽略的是“哪天任务失败了补抓时数据还能算吗”。我的策略是如果今天上午八点的任务失败重试后仍没成功那我等到明天上午的任务开始前先尝试补抓昨天的数据。此时Trending的Today榜单已经是“新的一天”的数据我无法拿到昨天的精确榜单了。这时候我宁愿让昨天缺档也不去用今天的榜单硬凑。原因很简单宁可少一天数据也不能让历史数据集里混入口径不一致的记录——后者会污染所有基于“连续排序”的分析结论。4.3 接口被限流后的降级方案网络请求这东西不管你是多合法的抓取频率一高照样被限制。我遇到的情况是为了测试解析器我短时间内连续请求了十几次Trending页面然后突然返回了HTTP 429或者一个验证页面。解决思路分成三层。第一层是控制请求频率。在脚本里强制加上3到5秒的随机延迟让每次请求之间拉开间隔。对于只有一次请求的日榜任务来说这个延迟简直可有可无但为了调试方便我仍然保留了这个参数并把默认值调成了1秒。第二层是请求失败的降级。如果Trending页面请求失败我不抓了直接让任务失败退出并推送一条警告消息。这里要强调一点失败的请求返回的HTML往往是一个半渲染的空壳如果你不检查响应状态码和关键元素是否存在很容易把空壳解析成空列表然后生成一个空榜单这比失败退出更危险。所以我在解析前加了一个断言必须能匹配到至少一个article节点才继续往下走否则直接抛异常。第三层是维护一个可用的备用入口。我这里说的备用入口不是绕过限制而是指同一个Trending数据在Github也有一个JSON形态的镜像——通过访问https://github.com/trending页面的同时只要加上指定响应头请求就能拿到一个轻量级的JSON响应。这算是个半公开的端点稳定性尚可。但它作为降级方案没问题不能作为唯一数据源。实测里如果HTML请求被限流这个JSON端点在大多数情况下也一并被限流了所以降级效果有限真正的兜底还是靠“第二天发现缺档后手动补跑一次”。反正在公开仓库里手动触发一个workflow也就点几下按钮的事情。4.4 数据合法性校验自动化最值得加的一道保险最后讲一个我强烈推荐的做法在每次生成榜单后、提交仓库前加一个数据校验步骤。我写了一个validate.py专门做三件事。一是检查仓库名格式必须匹配组织名/项目名的模式二是检查星标数是否为非负整数今日星标应当是整数且不超过总星标三是检查历史数据里今天是否已经存在记录如果存在则整个任务直接失败并提醒人工介入而不是把重复数据覆盖写进去。这套校验脚本在头一个月里救了我好几次。最典型的一次是Trending页面改版把“stars today”的文本从“1,234 stars today”改成了“1,234 stars this week”我的解析器还按老格式匹配每天只能抓到0星或空数据如果不是校验脚本在关键时刻拦截了五天的坏数据我的数据集早就废了。5. 跑通之后从“日榜”进化到“观测平台”的几点心得5.1 连续30天数据之后我看到了一些反直觉的规律项目稳定跑了一个月之后我开始回头分析攒下来的数据结果发现了好几个跟直觉相悖的现象。第一个规律是真正长期高热的项目并没有想象中那么多。在一个30天窗口里有超过七成项目只上榜一天或两天剩下的不到三成项目撑过了三天。很多在第一天冲上榜首的星标大户第二天就跌出了前二十。反而是那些“每日新增星标只有200到400”的中部项目能连续稳定上榜一周以上。后来我意识到这就是工具类和事件类项目的本质区别事件类项目比如某个框架发了大版本热度来得猛去得快工具类项目比如某个CLI工具解决了具体痛点靠口碑慢慢渗透。第二个规律是编程语言分布并没有跟着排行榜波动。无论今天榜首是AI库还是前端框架排行榜里语言分布始终稳定在那个头部语言占三成左右的区间。这说明Trending表面上是“项目榜单”本质上还是“语言生态关注度榜单”项目只是生态的表象。第三个规律是周五和周六的榜单质量明显偏低。一周里新增星标最少的通常是周末因为贡献者普遍休息但有趣的是周一早上的榜单会出现项目批量冲高的现象因为周末积累的热度集中释放。知道这个规律后我选型参考尽量避开周五周六的数据只看周一到周四的连续走势判断更准。这些规律完全是从历史数据里得到的没有夸张外推但它让我对这个“小工具”的价值有了重估——它不只是一张榜单而是一个持续观察开发者注意力的窗口。5.2 从日榜数据还能再长出什么数据落地后的价值远不止“每日推送”。我目前正在做的一个扩展是把这些JSON文件接进一个极简的静态站点每天生成一个可视化趋势图。因为数据是纯文本直接丢给前端的图表库就行不需要后端。我还想做一个“连续霸榜排名”页面专门记录star增长曲线的形状用来识别哪些项目是在“暴力营销”——当然这种判断也不完全准确仅供参考。如果你也想做类似的事情我建议不要一上来就搭大架构。先用最小的代码量把“抓取存文件定时推送”跑通坚持一周然后回去翻数据你会发现很多意想不到的需求自己冒出来了——比如我就是因为看到“上周某个项目连续三天排名前五”才想到要去扩展“连续上榜天数”统计的。最后分享一个我个人的操作习惯每周日下午我会花十五分钟手动过一遍这一周的TOP 50把其中连续上榜项目的那几个挑出来记到自己的知识库里。这一步没有自动化因为它本质上不是数据处理而是我做技术视野积累的一部分。自动化的日榜负责把消息实时送达手动复盘负责把信息内化成认知两者互补缺一个我都会觉得不完整。