☰
自建GitHub热榜日榜:Python与GitHub Actions自动化实践
2026/10/21 18:10:59 网站建设 项目流程

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里有一个h2,h2内部的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,把其中连续上榜项目的那几个挑出来,记到自己的知识库里。这一步没有自动化,因为它本质上不是数据处理,而是我做技术视野积累的一部分。自动化的日榜负责把消息实时送达,手动复盘负责把信息内化成认知,两者互补,缺一个我都会觉得不完整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询