GitHub热榜自动化工具:从抓取到趋势分析的完整实现 1. 从一张日榜截图说起这个项目到底在解决什么问题刷 GitHub 热榜这件事我坚持了差不多五年。每天早上到工位第一件事不是开邮箱而是把日榜前二十个项目过一遍。时间久了你会发现一个很尴尬的现实热榜本身只给你一个仓库名、一句简介、一个 star 增量剩下的全靠自己点进去翻 README、翻 issue、翻 commit。一个项目值不值得花时间往往要花十几分钟才能判断出来而日榜一天更新一次二十个项目全看一遍一上午就没了。这个项目标题“GitHub 热榜项目日榜2026-10-03”背后对应的就是把这件重复劳动自动化掉的一类工具。它的核心逻辑很朴素定时抓取 GitHub Trending 的日榜数据做结构化清洗再以某种可读的形式呈现出来——可能是一个静态页面可能是一份 Markdown 日报也可能是一个 CLI 工具直接打印到终端。关键词里出现的 TypeScript、Python、JavaScript、CLI基本勾勒出了这类项目的技术栈轮廓抓取层用 Python 或 Node 写展示层用 TypeScript 做前端交互层提供一个 CLI 入口。它解决的问题可以拆成三层。第一层是信息聚合把散落在 Trending 页面上的仓库名、描述、语言、star 数、fork 数、当日新增 star 汇总成结构化数据。第二层是趋势判断单看一天的榜单意义有限但如果能连续记录就能看出某个项目是昙花一现还是持续爬升。第三层是个性化过滤热榜里充斥着大量营销型仓库和教程合集真正有技术含量的项目需要按语言、按主题、按 star 增速去筛。适合看这篇内容的人有三类。一类是想自己搭一个类似工具的开发者需要知道数据从哪来、怎么存、怎么展示。一类是每天刷热榜但效率很低的技术人想知道怎么把这件事流程化。还有一类是刚接触 GitHub、想通过热榜了解技术风向的新手需要一套可操作的筛选方法。下面我按自己实际搭过一版的思路把整个项目拆开讲。2. 整体架构设计为什么是“抓取 存储 渲染”三段式2.1 数据源的选择与边界GitHub Trending 页面本身没有官方 API。这是所有同类项目遇到的第一个坎。你能拿到的只有https://github.com/trending这个 HTML 页面以及带参数的变体比如?sincedaily、?sinceweekly、?lpython按语言过滤。所以抓取层本质上就是 HTML 解析而不是调 JSON 接口。我试过几种方案。第一种是直接请求页面然后用正则抠快是快但 GitHub 前端一改版就全废。第二种是用 BeautifulSoup 或 Cheerio 做 DOM 解析稳定性好很多代价是依赖稍重。第三种是找第三方镜像或聚合服务但这类服务的数据延迟和完整性都不可控我不推荐作为主数据源。提示抓取频率一定要克制。日榜一天更新一次你每小时抓一次已经是上限再密就是给对方服务器添堵也容易触发限流。我自己的定时任务是每天早上八点跑一次足够用。这里有个细节值得说Trending 页面的 star 数是“当日新增”而不是总数页面上的1,234 stars today这种文案需要单独解析。很多人第一次做会把它当成总 star 数结果趋势曲线完全失真。总数需要另外调 GitHub 的公开接口去补或者从仓库页面上抓。2.2 存储方案为什么我最终选了 SQLite 而不是 JSON 文件一开始我用 JSON 文件存每天的榜单一天一个文件简单直接。跑了两个月之后问题来了我想查“某个仓库在过去三十天里上榜了几次”得把所有文件读一遍再内存里聚合慢且费内存。后来换成 SQLite一张daily_rank表字段大概是date、repo_full_name、rank、stars_today、language、description加一个(date, repo_full_name)的联合索引查询瞬间就顺了。SQLite 的好处是不用起服务单文件随项目走。对于这种数据量——一天撑死几百条记录一年也就十万条——SQLite 完全够用上 PostgreSQL 反而是杀鸡用牛刀。如果你打算做多用户或者 Web 服务那再考虑换。表结构我建议至少留这几个字段缺一个后面都会难受字段名类型说明dateTEXT榜单日期格式 YYYY-MM-DDrepo_full_nameTEXT仓库全名如 owner/reporankINTEGER当日排名stars_todayINTEGER当日新增 startotal_starsINTEGER仓库总 star需二次抓取languageTEXT主语言descriptionTEXT仓库描述urlTEXT仓库地址total_stars这个字段单独拎出来说因为它需要额外请求。我的做法是只对当日新上榜的仓库去补总数已经抓过的直接复用历史值这样能把请求量压到最低。2.3 渲染层CLI 和静态页面各管一段关键词里有 CLI说明这个项目大概率提供了命令行入口。我自己的实现是两条腿走路CLI 用于快速查看和筛选静态页面用于长期归档和分享。CLI 部分用 Python 的argparse或 Node 的commander都行核心命令就几个today看今日榜history repo看某仓库上榜历史filter --lang python --min-stars 100按条件筛。输出用rich或chalk上色终端里一眼就能扫完。静态页面部分如果团队里有人熟 TypeScript用 Next.js 做 SSG 是最省事的每天构建一次把数据注入页面。如果只是自己看一个简单的 HTML 模板加 Python 的jinja2就够了没必要上框架。我见过有人为了一个日报页面搭了一整套微服务纯属自我感动。3. 核心细节拆解抓取、解析、去重这三关怎么过3.1 抓取层的请求构造与反爬应对请求 Trending 页面时User-Agent 一定要设。默认的 PythonrequestsUA 很容易被识别我一般伪装成正常浏览器。另外Accept-Language设成en-US更稳因为页面文案解析依赖英文关键词比如stars today。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: en-US,en;q0.9, } def fetch_trending(languageNone, sincedaily): url https://github.com/trending if language: url f/{language} params {since: since} resp requests.get(url, headersHEADERS, paramsparams, timeout15) resp.raise_for_status() return resp.text超时一定要设我踩过的坑是某次网络抖动请求卡了五分钟整个定时任务全堵在那。设 15 秒失败就重试重试三次还不行就跳过当天别硬扛。3.2 解析层DOM 结构比正则可靠得多Trending 页面每个仓库是一个article classBox-row里面h2 a是仓库名p是描述语言在span[itempropprogrammingLanguage]star 数在包含stars today的那个 span 里。用 BeautifulSoup 解析大概是这样from bs4 import BeautifulSoup def parse_trending(html): soup BeautifulSoup(html, html.parser) items [] for idx, row in enumerate(soup.select(article.Box-row), start1): name_tag row.select_one(h2 a) full_name name_tag[href].strip(/) if name_tag else desc_tag row.select_one(p) description desc_tag.get_text(stripTrue) if desc_tag else lang_tag row.select_one(span[itempropprogrammingLanguage]) language lang_tag.get_text(stripTrue) if lang_tag else star_tag row.select_one(a[href$/stargazers]) total_stars 0 if star_tag: total_stars int(star_tag.get_text(stripTrue).replace(,, )) items.append({ rank: idx, repo_full_name: full_name, description: description, language: language, total_stars: total_stars, }) return items注意stars today的解析要单独处理它通常在一个没有 href 的 span 里文案形如1,234 stars today。用正则([\d,])\sstars today抠出来最稳。注意GitHub 偶尔会做 A/B 测试页面结构可能对部分用户不一样。所以解析代码里每个字段都要做空值兜底别让一个字段缺失导致整个任务崩掉。3.3 去重与增量更新别让同一天的数据重复入库定时任务如果因为重试跑了两次同一天的数据就会重复。我的做法是在daily_rank表上对(date, repo_full_name)建唯一索引插入时用INSERT OR REPLACE。这样即使重复跑最终结果也是幂等的。CREATE TABLE IF NOT EXISTS daily_rank ( date TEXT NOT NULL, repo_full_name TEXT NOT NULL, rank INTEGER, stars_today INTEGER, total_stars INTEGER, language TEXT, description TEXT, url TEXT, PRIMARY KEY (date, repo_full_name) );用PRIMARY KEY而不是额外建唯一索引SQLite 里更省事。插入语句用INSERT OR REPLACE INTO daily_rank ...重复数据自动覆盖不用先查后插。4. 实操全流程从零跑通一个日榜工具4.1 环境准备与依赖安装Python 版本我建议 3.10 以上主要是类型标注和match语句用着舒服。依赖就四个requests、beautifulsoup4、lxml解析器比默认的快、rich终端输出。安装命令python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install requests beautifulsoup4 lxml rich如果你用 Node 做 CLI那对应的是axios、cheerio、commander、chalk。TypeScript 项目记得在tsconfig.json里把strict打开这类抓取解析代码最容易出的就是undefined相关的运行时错误类型严格一点能提前拦掉一批。4.2 主流程串联整个流程我拆成四步抓取、解析、补全总数、入库。补全总数这一步单独说因为它是唯一需要额外请求的环节。我的策略是查库看这个仓库之前有没有记录过total_stars有就直接用没有才去请求仓库页面。def enrich_total_stars(repo_full_name, conn): cur conn.execute( SELECT total_stars FROM daily_rank WHERE repo_full_name ? AND total_stars 0 ORDER BY date DESC LIMIT 1, (repo_full_name,), ) row cur.fetchone() if row: return row[0] # 没有历史记录去仓库页面抓 resp requests.get(fhttps://github.com/{repo_full_name}, headersHEADERS, timeout15) soup BeautifulSoup(resp.text, html.parser) tag soup.select_one(a[href$/stargazers] span) if tag: return int(tag.get_text(stripTrue).replace(,, )) return 0这个缓存策略能把请求量降一个数量级。实测下来日榜里大概七成是重复上榜的仓库只有三成需要真正去抓总数。4.3 定时任务的落地Linux 下用crontab最简单一行搞定0 8 * * * cd /path/to/project /path/to/venv/bin/python main.py run.log 21Windows 就用任务计划程序或者干脆用schedule库在 Python 里起个常驻进程。我倾向 crontab因为进程用完就退不占内存出问题看日志也直观。提示日志一定要记。我见过太多人定时任务跑挂了都不知道等想起来看的时候已经断了半个月数据。日志里至少记三样开始时间、抓到多少条、有没有异常。4.4 CLI 交互设计CLI 的体验决定了你愿不愿意天天用它。我的设计是默认命令直接打印今日榜带颜色区分语言star 增速高的标红。参数上支持--lang、--min-stars、--top N。gh-trending today --lang python --top 10 gh-trending history facebook/react gh-trending filter --min-stars 500 --since weeklyhistory这个命令是我用得最多的。它能告诉你一个仓库是持续在榜还是偶尔冒头。比如某个仓库连续两周出现在日榜那基本可以判断它踩中了某个真实需求值得点进去研究。5. 常见问题与排查技巧实录5.1 抓不到数据或者抓到空列表最常见的原因是页面结构变了或者请求被拦。排查顺序先手动curl一下看返回的 HTML 是不是正常页面如果返回的是登录页或者验证页那就是被拦了需要调整请求头。如果 HTML 正常但解析为空那就是选择器失效打开页面用开发者工具重新确认 class 名。我遇到过一次GitHub 把Box-row改成了Box-row Box-row--focus选择器article.Box-row依然能匹配所以没受影响。但如果哪天改成完全不同的类名就得改代码。所以解析逻辑最好集中在一个文件里改起来方便。5.2 star 数解析成 0 或者异常大stars today和总 star 是两个不同的元素很容易搞混。总 star 在a[href$/stargazers]里stars today在一个纯文本 span 里。如果你发现数值异常大八成是把总数当成了当日增量。反过来如果全是 0那就是选择器没匹配上。还有一个坑是千分位逗号。1,234直接int()会报错必须先replace(,, )。这个错误在数据量小的时候不容易发现等 star 上四位数才暴露。5.3 定时任务重复执行导致数据重复前面说的INSERT OR REPLACE能解决大部分问题。但如果你的表没有主键约束重复数据就会堆积。检查方法是按日期分组 count 一下看有没有超过预期条数。SELECT date, COUNT(*) FROM daily_rank GROUP BY date HAVING COUNT(*) 30;日榜一般 25 条左右超过 30 基本就是重复了。5.4 常见问题速查表现象可能原因排查方法解决返回空列表选择器失效手动看 HTML更新选择器请求被拦UA 或频率问题看返回内容换 UA、降频率star 为 0选择器错检查元素修正选择器数据重复无主键约束分组 count加主键、用 REPLACE任务不执行crontab 路径问题看日志用绝对路径总数抓取慢无缓存看请求量加历史缓存5.5 几个我踩过的坑第一个坑是时区。GitHub Trending 的“日”是按什么时区算的官方没明说。我一开始用本地时间存date结果发现有时候早上八点抓到的榜单和前一天晚上抓到的一样。后来统一用 UTC 日期问题就没了。第二个坑是语言字段。有些仓库主语言是空的比如纯文档项目。这时候language字段会是空字符串筛选的时候要注意把空值排除否则--lang python会漏掉那些没标语言但实际是 Python 的项目。第三个坑是描述里的特殊字符。有些仓库描述带 emoji 或者换行直接入库没问题但渲染到 HTML 里要转义否则页面会乱。用模板引擎的话一般自动转义手写字符串拼接就要小心。6. 数据用起来才有价值趋势分析与个性化筛选6.1 上榜频次统计数据攒够一个月之后最有价值的查询是上榜频次。一个仓库一个月上榜十次和只上榜一次含义完全不同。SELECT repo_full_name, COUNT(*) AS days_on_board FROM daily_rank WHERE date date(now, -30 days) GROUP BY repo_full_name ORDER BY days_on_board DESC LIMIT 20;这个查询能帮你把“营销型昙花”和“真实趋势”区分开。持续在榜的项目通常是有真实用户增长支撑的。6.2 按语言看风向关键词里 TypeScript、Python、JavaScript 都出现了说明这个工具的使用者关注语言分布。按语言统计上榜数量能看出当前哪个生态最活跃。SELECT language, COUNT(*) AS cnt FROM daily_rank WHERE date date(now) GROUP BY language ORDER BY cnt DESC;我自己的观察是Python 和 TypeScript 长期占据前两位JavaScript 紧随其后。这个分布和招聘市场的需求基本吻合所以热榜语言分布可以当作一个粗略的风向标。6.3 个性化过滤规则热榜里教程类、awesome 类、面试题类的仓库占比不低这些对找工具的人来说是噪音。我的过滤规则是描述里包含awesome、tutorial、interview、cheatsheet的降权star 增速高且描述里包含library、framework、cli、tool的加权。这套规则不完美但能过滤掉大概六成噪音。剩下的还是得自己点进去看但至少不用在明显无关的项目上浪费时间。7. 一些实操心得做这类工具最大的误区是追求大而全。我第一版想做成 Web 服务带用户系统、带订阅推送结果写了三周还没跑通核心抓取。后来砍到只剩 CLI 加 SQLite两天就跑起来了。工具这东西先跑通再优化别一上来就设计架构。另一个心得是数据比代码值钱。代码写烂了可以重写数据断了就补不回来。所以定时任务的稳定性优先级最高宁可功能少一点也要保证每天数据不断。我现在的做法是加了一个简单的健康检查如果连续两天没抓到数据就发个邮件提醒自己。最后说个细节GitHub 的公开接口对未认证请求有频率限制每小时 60 次。补全total_stars的时候如果请求量大很容易触顶。解决办法是申请一个个人访问令牌带上之后限制放宽到 5000 次每小时。令牌放在环境变量里别硬编码进代码。这个项目后续还能往几个方向扩一是加邮件或即时通讯的日报推送二是把历史数据做成可视化图表三是支持按 topic 而不是按语言筛选。但这些都是锦上添花核心的抓取、存储、查询三件事跑稳了工具就已经能天天用了。