ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GitHub今日热榜系统搭建:从抓取到展示的完整实践

GitHub今日热榜系统搭建:从抓取到展示的完整实践 1. 从“今日热榜”看开源风向这个项目到底在解决什么问题第一次听说“Github今日热榜”这个概念是在一个开发者群里。有人甩了张截图上面列着当天涨星最快的十个仓库配文是“今天的快乐源泉来了”。我当时的第一反应是这东西不就是个排行榜吗能有多大价值后来自己动手搭了一套用了一段时间才发现它解决的是一个非常具体的痛点——信息过载下的高效筛选。Github 上目前有超过四亿个仓库每天新增的项目数以万计。你不可能一个个翻Trending 页面虽然官方有但更新频率、排序逻辑、语言筛选这些维度对国内开发者来说并不总是顺手。更关键的是很多人打开 Github 是有明确目的的——找工具、找轮子、找灵感而不是漫无目的地逛。这时候一个聚合了“今日涨星最快”“本周新晋热门”“特定语言趋势”的榜单价值就出来了。这个项目适合谁三类人。第一类是技术选型者需要快速判断某个方向最近有没有靠谱的新方案冒出来第二类是内容创作者需要追踪热点找选题第三类是学习者想看看大家都在关注什么避免闭门造车。不管你属于哪一类核心诉求都是一样的用最少的时间拿到最值得看的那几个仓库。我搭建的这套“Github今日热榜”系统本质上是一个定时抓取 数据清洗 榜单生成 多端展示的小型数据管道。它不复杂但涉及到的环节不少每个环节都有坑。下面我会把整个设计思路、技术选型、实操步骤、踩过的坑全部拆开讲你照着做就能跑起来。2. 整体架构设计与技术选型背后的取舍2.1 为什么不用官方 API 而选择页面解析Github 官方提供了 REST API 和 GraphQL API理论上你可以通过search/repositories接口配合sortstars和created:日期来获取数据。我一开始也是这么做的但很快发现了三个问题。第一API 有速率限制。未认证请求每小时 60 次认证后 5000 次。如果你要抓多个语言、多个时间维度的榜单这个额度很快就会耗尽。第二搜索结果有延迟。官方 API 的索引更新不是实时的有时候一个仓库已经涨了几百星API 里还是旧数据。第三排序逻辑不透明。你无法精确控制“今日热榜”的算法只能用它给的排序方式。所以最终我选择了页面解析的方案。Github Trending 页面本身是服务端渲染的结构相对稳定解析起来并不复杂。而且它天然支持按语言、按时间范围筛选省去了自己写筛选逻辑的麻烦。当然页面解析也有风险——Github 改版会导致解析规则失效。我的应对策略是把解析规则做成可配置的一旦页面结构变化改配置就行不用动核心代码。提示页面解析仅用于个人学习和技术研究请控制请求频率避免对目标站点造成不必要的压力。建议设置合理的抓取间隔比如每 30 分钟一次。2.2 数据存储为什么选了 SQLite 而不是 MySQL这个项目的数据量其实很小。每天抓取一次每次几百条记录一年下来也就十万条左右。这种量级用 SQLite 完全足够而且部署简单不需要额外维护数据库服务。我用的是better-sqlite3这个 Node.js 库同步 API 写起来很顺手性能也够用。表结构设计上我建了两张表。一张是repositories存仓库的基本信息包括full_name、description、language、stars、forks等字段用full_name作为唯一索引。另一张是trending_records存每次抓取的快照包括repo_id、trending_date、rank、stars_gained等字段。这样设计的好处是我既能查某个仓库的历史趋势也能查某一天的热榜快照。CREATE TABLE repositories ( id INTEGER PRIMARY KEY AUTOINCREMENT, full_name TEXT UNIQUE NOT NULL, description TEXT, language TEXT, stars INTEGER DEFAULT 0, forks INTEGER DEFAULT 0, created_at TEXT, updated_at TEXT ); CREATE TABLE trending_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, repo_id INTEGER NOT NULL, trending_date TEXT NOT NULL, rank INTEGER, stars_gained INTEGER, period TEXT, language_filter TEXT, FOREIGN KEY (repo_id) REFERENCES repositories(id) );2.3 展示层静态生成 定时刷新展示层我选择了静态页面生成的方案。每次抓取完成后用模板引擎生成 HTML 文件直接部署到静态托管服务上。这样做的好处是访问速度快、成本低、不需要维护服务器。页面上的数据通过 JavaScript 从 JSON 文件里加载支持按语言、按时间范围筛选。如果你想要更动态的体验也可以做成服务端渲染的 Web 应用。但我个人觉得热榜这种东西本来就是定时更新的静态页面完全够用而且更稳定。我用的模板引擎是EJS简单直接学习成本低。3. 核心抓取逻辑与数据清洗的实操细节3.1 抓取频率与请求头设置抓取频率是个需要权衡的问题。太频繁了容易被限流太稀疏了榜单更新不及时。我的经验是每 30 分钟抓一次比较合适。Github Trending 页面的数据本身也不是实时更新的大概每小时刷新一次所以 30 分钟的间隔足够覆盖。请求头方面最重要的是User-Agent。不要用默认的爬虫标识建议设置成一个常见的浏览器 UA。另外Accept-Language设置为en-US,en;q0.9可以确保拿到英文页面解析规则更稳定。const headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: en-US,en;q0.9, };3.2 页面解析的关键选择器Github Trending 页面的结构这几年改过几次但核心的article.Box-row这个选择器一直没变。每个仓库对应一个article元素里面包含了仓库名、描述、语言、星数、今日涨星数等信息。解析的时候有几个细节需要注意。第一仓库名是相对链接需要拼接https://github.com前缀。第二今日涨星数的格式不固定有时候是1,234 stars today有时候是1,234 stars this week需要根据时间范围做不同的正则匹配。第三描述可能为空要做好空值处理。const cheerio require(cheerio); function parseTrendingPage(html, period daily) { const $ cheerio.load(html); const repos []; $(article.Box-row).each((index, element) { const $el $(element); const fullName $el.find(h2 a).attr(href).replace(/^\//, ); const description $el.find(p).text().trim() || ; const language $el.find([itempropprogrammingLanguage]).text().trim() || Unknown; const starsText $el.find(a[href$/stargazers]).text().trim().replace(/,/g, ); const stars parseInt(starsText, 10) || 0; const starsTodayText $el.find(span.d-inline-block.float-sm-right).text().trim(); const starsGainedMatch starsTodayText.match(/([\d,])\sstars?\s(today|this week|this month)/); const starsGained starsGainedMatch ? parseInt(starsGainedMatch[1].replace(/,/g, ), 10) : 0; repos.push({ fullName, description, language, stars, starsGained, rank: index 1, }); }); return repos; }3.3 数据去重与增量更新每次抓取都会拿到一批数据但并不是所有仓库都是新的。我的策略是先查后插对于repositories表如果full_name已存在就更新stars和forks字段如果不存在就插入新记录。对于trending_records表每次抓取都插入新记录因为这是快照数据需要保留历史。这里有个坑要注意SQLite 的 UPSERT 语法在不同版本里支持程度不一样。我一开始用的是INSERT OR REPLACE但这样会改变id导致外键关联出问题。后来改用了INSERT ... ON CONFLICT(full_name) DO UPDATE SET ...的写法才解决了这个问题。INSERT INTO repositories (full_name, description, language, stars, forks, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(full_name) DO UPDATE SET description excluded.description, language excluded.language, stars excluded.stars, forks excluded.forks, updated_at excluded.updated_at;3.4 多语言榜单的抓取策略Github Trending 支持按语言筛选URL 格式是https://github.com/trending/{language}?since{period}。我常用的语言有 JavaScript、Python、Go、Rust、TypeScript 这几个。抓取的时候我会遍历这些语言分别抓取日榜和周榜。这里有个效率问题如果串行抓取每个请求间隔 2 秒六个语言两个周期就是 24 秒。虽然不算慢但可以优化。我的做法是用Promise.all并发抓取但把并发数控制在 3 以内避免触发限流。实测下来这样能把总时间压缩到 10 秒左右。注意并发抓取时一定要设置超时和重试机制。我遇到过某次请求卡住导致整个流程挂起的情况后来加了AbortController和 3 次重试才稳定下来。4. 榜单生成逻辑与展示层的实现4.1 热榜排序算法的设计“今日热榜”的核心是排序。Github 官方的 Trending 排序算法没有公开但根据观察它主要考虑的是单位时间内的星数增长而不是总星数。一个总星数十万的仓库今天涨了五十星可能排在一个总星数千的仓库后面因为后者今天涨了五百星。我的排序逻辑参考了这个思路但做了一些调整。我用的公式是score stars_gained * 0.7 stars * 0.3这个公式的意思是涨星数占 70% 权重总星数占 30% 权重。这样既能突出新晋热门项目又不会让老牌项目完全消失。当然这个权重是可以调的你可以根据自己的偏好修改。另外我还加了一个语言多样性的约束。如果前十里某个语言占了五个以上我会把多出来的位置让给其他语言。这样做是为了避免榜单被单一语言霸占让读者看到更全面的技术趋势。4.2 静态页面的生成与部署页面生成我用的是 EJS 模板。模板文件里定义了 HTML 结构数据通过render方法注入。生成的页面包括首页、语言筛选页、详情页三种。首页展示当日综合热榜语言筛选页展示特定语言的热榜详情页展示某个仓库的历史趋势。详情页的数据来自trending_records表我会把某个仓库过去 30 天的涨星数据查出来用简单的 SVG 折线图展示。const ejs require(ejs); const fs require(fs); async function generateHomePage(repos, date) { const template fs.readFileSync(./templates/home.ejs, utf-8); const html ejs.render(template, { repos, date }); fs.writeFileSync(./dist/index.html, html); }部署方面我用的是静态托管服务把dist目录整个上传就行。如果你有自己的服务器用 Nginx 托管也可以。关键是设置好缓存策略HTML 文件不缓存JSON 数据文件缓存 5 分钟这样既能保证数据新鲜度又能减少请求压力。4.3 数据可视化的小技巧详情页的折线图我没有用图表库而是手写了一个简单的 SVG 生成函数。这样做的好处是页面体积小、加载快而且完全可控。折线图的核心逻辑是把涨星数据映射到 SVG 的坐标空间然后用polyline元素画线。function generateSparkline(data, width 200, height 50) { const max Math.max(...data.map(d d.starsGained)); const points data.map((d, i) { const x (i / (data.length - 1)) * width; const y height - (d.starsGained / max) * height; return ${x},${y}; }).join( ); return svg width${width} height${height}polyline points${points} fillnone stroke#0366d6 stroke-width2//svg; }这个 sparkline 虽然简单但信息量足够。读者一眼就能看出某个仓库的涨星趋势是上升、下降还是平稳。如果你想要更复杂的图表可以引入 Chart.js 或 ECharts但我觉得对于热榜场景来说简单直接更重要。5. 常见问题排查与避坑经验实录5.1 抓取失败的各种原因与应对抓取失败是最常见的问题原因五花八门。我整理了一个排查表按出现频率排序问题现象可能原因排查方法解决方案返回 403请求头被识别为爬虫检查 User-Agent更换为浏览器 UA返回 429请求频率过高查看响应头 Retry-After增加抓取间隔加代理池解析结果为空页面结构变化手动访问页面检查更新选择器规则数据不完整网络超时检查请求日志增加超时时间和重试星数解析错误数字格式变化打印原始文本更新正则表达式其中最常见的是 429 限流。我的应对策略是指数退避重试第一次失败等 5 秒第二次等 15 秒第三次等 45 秒。如果三次都失败就跳过这次抓取记录日志等下一个周期再试。5.2 数据不一致的排查思路有时候你会发现同一个仓库在日榜和周榜里的涨星数对不上。这不是 bug而是因为统计周期不同。日榜统计的是过去 24 小时周榜统计的是过去 7 天。如果你要对比一定要确保周期一致。另一个常见问题是星数回退。偶尔会出现某个仓库的星数比上次抓取时还少的情况。这通常是因为 Github 清理了异常账号的星标或者仓库被删除了部分星标。遇到这种情况我的做法是保留最新数据但在日志里标记出来方便后续分析。5.3 性能优化与资源控制这个项目本身不重但如果抓取的语言多、周期多内存占用会上升。我做过一次测试抓取 10 个语言、2 个周期总共 20 个页面内存峰值大概在 200MB 左右。对于一台 1GB 内存的服务器来说完全够用。如果你想要进一步优化可以从这几个方面入手。第一流式解析不要一次性把整个 HTML 加载到内存里用cheerio的流式接口。第二增量更新只抓取有变化的语言和周期。第三数据压缩历史快照数据可以按月归档减少查询压力。提示如果你的服务器内存有限建议把抓取任务拆分成多个小任务用队列串行执行避免并发过高导致内存溢出。5.4 页面改版的应急处理Github 改版是最大的风险。我经历过一次改版article.Box-row变成了div.Box-row导致解析全部失败。当时的应急处理是先手动访问页面用开发者工具找到新的选择器然后更新配置文件重启服务。整个过程大概花了 15 分钟。为了减少改版带来的影响我做了两件事。第一把选择器抽成配置文件改版时只需要改配置不用动代码。第二加了监控告警如果连续三次抓取结果为空就发邮件通知我。这样即使改版发生在半夜我也能第一时间知道。const selectors { repoItem: article.Box-row, repoName: h2 a, description: p, language: [itempropprogrammingLanguage], stars: a[href$/stargazers], starsToday: span.d-inline-block.float-sm-right, };这套配置化的思路后来在我抓取其他网站时也复用了效果很好。核心思想就是把易变的部分和稳定的部分分开易变的部分做成配置稳定的部分做成代码。6. 从热榜数据里能读出什么几个真实的观察案例6.1 语言趋势的周期性波动我连续记录了三个月的热榜数据发现了一个有趣的规律Rust 和 Go 的涨星高峰通常出现在周中而 JavaScript 和 Python 的高峰出现在周末。我的猜测是周中是专业开发者的活跃时间他们更关注系统级语言周末是学习者的活跃时间他们更关注应用级语言。这个观察对我的选题帮助很大。如果我要写一篇面向初学者的文章我会选在周末发布配合 JavaScript 或 Python 的热榜项目。如果我要写一篇面向资深开发者的文章我会选在周中发布配合 Rust 或 Go 的项目。6.2 新晋热门项目的共同特征我分析了过去半年涨星最快的五十个项目发现它们有一些共同特征。第一解决了一个具体的痛点而不是泛泛的工具。第二README 写得非常清晰有动图、有快速开始、有示例代码。第三有活跃的社区issue 回复快PR 合并及时。这些特征反过来指导了我自己的项目。我现在写 README 的时候会刻意模仿那些热门项目的结构先放一张动图展示效果然后是三行快速开始最后是详细的配置说明。实测下来这样写的 README 确实能带来更多的 star。6.3 热榜数据的局限性热榜数据虽然有用但也要注意它的局限性。星数不等于质量有些项目靠营销手段刷星实际代码质量堪忧。热榜有滞后性一个项目从开始涨星到进入热榜通常需要几天时间。热榜有偏见英文项目更容易上榜中文项目相对吃亏。所以我的建议是把热榜当作发现新项目的入口而不是判断项目好坏的唯一标准。看到一个感兴趣的项目还是要自己点进去看看代码、看看 issue、看看最近的提交记录再做判断。7. 后续可以扩展的几个方向这套系统跑了一段时间后我陆续加了一些小功能也规划了一些大功能。已经加上的包括邮件订阅每天早上八点把当日热榜发到邮箱RSS 输出方便用阅读器订阅API 接口返回 JSON 格式的榜单数据方便其他程序调用。规划中的功能有两个方向。一个是个性化推荐根据用户的历史浏览记录推荐可能感兴趣的项目。另一个是趋势预测用简单的时序模型预测某个项目未来一周的涨星趋势。这两个功能都需要更多的数据和更复杂的算法目前还在实验阶段。如果你也想搭一套类似的系统我的建议是先从最简单的版本开始。不要一上来就搞分布式、搞机器学习先把抓取、存储、展示这三个环节跑通然后再逐步优化。我见过太多人一开始就想做完美结果卡在某个细节上最后不了了之。先跑起来再迭代这是最务实的做法。最后分享一个小技巧抓取的时候把原始 HTML 也存一份到本地。这样即使解析规则出了问题你也不用重新抓取直接用本地文件调试就行。这个习惯帮我省了很多时间尤其是在调试正则表达式的时候。
返回列表