打造GitHub趋势速递:自动采集筛选与定时推送服务
发布时间:2026/10/10 16:32:54 锦皓数字建站

早上打开手机先刷一眼 GitHub 趋势页看看今天哪些仓库火了这已经成了我每天开工前的习惯。但问题在于GitHub 趋势页的排序逻辑多变热榜上经常出现看不懂的语言、冷门领域或者一些点进去才发现早就见过的老项目。刷了几年之后我干脆写了个小服务来干这件事每天定时把趋势仓库抓下来、按自己的规则过滤排序再推到群里和邮件里。这套东西就是今天想聊的“今日GitHub趋势速递”。这东西的定位很纯粹不用打开浏览器、不用被推荐算法带偏每天固定时间主动把“值得看的仓库”送到你手上。它不是一个复杂的平台也没有高深的技术但如果你每天依赖 GitHub 热门仓库获取学习素材、寻找灵感、或者只是不想错过圈子里的热点这个服务能帮你省掉大量碎片时间。适合的人群也很广独立开发者、技术团队负责人、关注开源动态的产品经理甚至刚入门想找练手项目的学生。下面我会把整套方案从需求拆解、技术选型到实现细节、部署运维全部过一遍把我踩过的坑和调过的参数都摆出来讲清楚。1. 项目概述趋势速递到底在解决什么问题1.1 核心需求解析先聊一下需求本身。GitHub 官方趋势页面每天都会更新但作为信息源它有三个让我不舒服的地方。第一个问题是时间敏感度不够。官方的“今日趋势”并不是严格按 24 小时滚动的很多仓库一旦上榜会连续挂两三天排在前面的大多是大厂或者明星项目真正的小众黑马很容易被挤到后面甚至根本看不见。第二个问题是信息密度低。趋势页只显示仓库名、描述、星标数、今日新增星标和语言但没告诉你这个仓库为什么火、它的核心亮点是什么、适合什么场景使用。我得一个个点进去看 README一个早上就这么没了。第三个问题是入选标准不可控。默认的 GitHub 趋势页有语言过滤、日期范围过滤但很多人其实不知道结合这些过滤条件能挖出完全不同的榜单。如果只是被动地看默认页面等于把自己锁在一个信息茧房里。所以当时我对这个项目的预期非常具体每天定时去抓原始趋势数据按自己的规则做二次筛选比如排除掉已经见过很多次的知名项目、排除掉文档都没有的占坑仓库、按开发语言重新分组然后生成一个带推荐语和简要分析的速递报告推送到我常用的聊天工具里。1.2 目标读者与适用场景这套东西做出来之后我首先自己用了两周然后陆续有些朋友也接入进去。从反馈来看使用场景主要分成三类。第一类是个人开发者他们把趋势速递当作每日技术早报用来发现新的开源工具、看别人的项目怎么写 README、分析热门项目用了什么技术栈。第二类是技术团队负责人他们会把速递内容同步到一个内部的频道用于技术选型调研和团队信息同步。有段时间我们在评估 API 网关方案靠的就是持续几天观察 GitHub 趋势上出现的网关项目然后逐个做定向分析。第三类是开源爱好者他们更关注冷门领域的黑马项目希望看到榜单里那些星标不多但趋势很猛的新仓库。这类项目往往代表了一个小圈子的近期热点提前关注可以占住先手。所以这个项目的关键词是“自动采集”、“趋势筛选”、“定时推送”、“多端触达”。每一个词背后都有对应的技术选型和实现细节。2. 技术选型与方案设计2.1 语言与框架的取舍逻辑抓取 GitHub 趋势页这件事最简单粗暴的方案是写个 Python 脚本用 requests 拉 HTML再用 BeautifulSoup 解析。但真正做下来你会发现这个方案的脆弱性超出想象。GitHub 的页面结构不是一成不变的class 名偶尔会调整DOM 层级也会变一套解析规则可能今天还能用、明天就报废。而且 GitHub 对未登录的匿名请求有比较严的限流策略频率稍微高一点就会返回 429。我在选型时走了另一条路直接用 GitHub 官方 REST API再加一层趋势判断逻辑。具体来说用 GitHub Search API 按仓库的创建时间和星标增长情况做过滤配合官方 Trending 页面的数据做交叉验证。语言的选型上我最后用了 Python。相比 Node.js 或者 GoPython 在这类场景有三个优势requests 和 BeautifulSoup 生态成熟、写脚本迭代速度快、后续如果要接数据分析或者机器学习模型也很方便。我自己用的是 Python 3.10 搭配 FastAPI 做了个轻量服务其实如果不是为了那个简单的 Web 展示页用纯脚本定时任务就够了。2.2 数据源的获取策略我把数据获取分成两路。一路是官方 Trending 页面的 HTML 抓取虽然解析规则会变但它有一个别的地方拿不到的好处它直接反映了 GitHub 官方认为“今天最火”的仓库。官方趋势页的背后逻辑不公开但它大概率结合了收藏数、fork 速度、访问量、Star 的绝对增量等多个信号单靠 API 是模拟不出来的。第二路是Search API 拉取近期创建仓库的热度数据这路数据主要负责补充官方趋势页的盲区。因为有些小众项目并不在官方趋势榜上但它们在特定语言或者特定主题的搜索排序里上升很快这种仓库值得单独拎出来做观察。这里顺便说一下抓取频率的设计。官方趋势页按天更新我设置每 6 小时跑一次一天四次足够捕捉到当天上榜的仓库变化。Search API 因为搜索结果变化快我设置每 4 小时跑一次。整体的请求量控制在每天几百次以内远低于 GitHub API 的限流阈值标准认证每小时 5000 次请求所以到现在没有遇到过封禁问题。2.3 整体架构与模块划分这个项目拆成四个模块采集模块负责获取 HTML 数据和 API 数据做去重和归一化。筛选模块按规则打分、过滤、分组生成每日趋势榜单。推送模块把榜单内容渲染为 Markdown推送到多个渠道。可视化模块生成简单的 Web 页面方便随时查看历史趋势记录。四个模块独立部署彼此之间通过 SQLite 数据库共享数据。之所以不用 MySQL 或者 PostgreSQL是因为这个服务的并发量极低——每天就几次写入、几十次查询SQLite 单文件部署方便备份也简单。后面如果数据量真的大了再迁到 PostgreSQL 也不迟但至少现阶段 SQLite 完全够用。3. 核心功能实现与细节拆解3.1 采集模块HTML 解析与 API 补全HTML 抓取这一步我用了最稳妥的方式先请求页面拿到 HTML 后用 BeautifulSoup 定位仓库列表的容器。GitHub 趋势页的仓库列表结构相对固定每个仓库条目都在一个article标签内标题在h2标签里描述在p标签里。用select方法按 CSS 选择器提取中间隔几层 class 变化也不怕只要锚定article这个标签就行。import requests from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36 } def fetch_trending(language, sincedaily): url https://github.com/trending (f/{language} if language else ) params {since: since} resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) repos [] for article in soup.select(article.Box-row): h2 article.select_one(h2 a) desc article.select_one(p) stars article.select(a[href$/stargazers]) repo_name h2.text.strip().replace(\n, ).replace( , ) repo_desc desc.text.strip() if desc else star_count 0 if stars: star_text stars[0].text.strip() star_count parse_star_text(star_text) repos.append({ name: repo_name, desc: repo_desc, stars: star_count, }) return repos def parse_star_text(text): # 1,234 - 1234, 12.5k - 12500 text text.replace(,, ) if text.endswith(k): return int(float(text[:-1]) * 1000) return int(text)这里有个容易忽略的细节article下的a[href$/stargazers]可能会匹配到多个元素因为一个仓库条目里既有 Star 数也有 Fork 数它们的链接后缀不一样要精确匹配stargazers。API 补全这部分我用的是 GitHub Search API 里按created:过滤近期仓库排序用stars再加一个language:参数按语言分流。例如想找最近一周创建、涨星最快的 Python 仓库curl -H Accept: application/vnd.githubjson \ https://api.github.com/search/repositories?qcreated:2024-01-01language:pythonsortstarsorderdescper_page50注意 Search API 的per_page上限是 100如果只取一个语言的 top50一次请求就够了。这些数据会缓存在本地后续筛选模块会结合 HTML 的趋势数据和 API 的搜索数据一起处理。3.2 筛选模块热度评分与黑名单机制抓回来的原始数据不能直接用因为官方趋势页的排序不完全符合我的口味。我设计了一套简单的打分规则核心指标有三个今日新增星标数、总星标数、仓库创建时间。计算公式是这样的score 新增星标数 * 2 min(总星标数 / 1000, 50) 创建天数衰减因子创建天数衰减因子是这么算的如果仓库创建超过 30 天每多一天扣 0.1 分如果创建在 7 天以内额外加 10 分。这样做的目的是让新项目有更高的曝光机会避免榜单被那些老牌热门项目长期霸占。我用一个score_repo(repo)函数来实现from datetime import datetime, timezone def score_repo(repo): today_stars repo.get(today_stars, 0) total_stars repo.get(stars, 0) created_at repo.get(created_at) created_days (datetime.now(timezone.utc) - created_at).days score today_stars * 2 min(total_stars / 1000, 50) if created_days 7: score 10 elif created_days 30: score - (created_days - 30) * 0.1 return round(score, 2)除了打分黑名单机制也必不可少。我把那些“看到烦”的仓库加进黑名单比如某些头部大厂的 SDK 仓库、每隔几天就被推到趋势顶部的知名项目还有 README 都没写清楚的空壳仓库。这个名单我用 JSON 文件维护运行时可动态更新不需要重启服务。3.3 推送模块多渠道触达的渲染与适配筛选完的榜单最终要以可读性强的形式推出去。我这里选择了三个渠道钉钉机器人、企业微信机器人、邮件。三个渠道的技术难度都不高但渲染格式略有不同。钉钉和企业微信都支持 Markdown 消息直接拼一个 Markdown 字符串发过去就行。邮件的部分稍微麻烦一点因为很多邮箱客户端对 Markdown 不支持我要先把内容转成 HTML。推送模块的核心是构建一个统一的render_report(repos)函数把仓库列表渲染成带标题、描述、链接、语言标签的文本。不管推到哪个渠道主体格式保持一致只是包装方式不同。def render_report(repos): lines [## GitHub 今日趋势速递, ] for idx, repo in enumerate(repos, start1): lines.append(f### {idx}. {repo[name]}) lines.append(f- 语言{repo.get(lang, 未知)}) lines.append(f- 今日 Star{repo.get(today_stars, 0)}) lines.append(f- 描述{repo.get(desc, 暂无)[:120]}) lines.append(f- 地址{repo.get(url, )}) lines.append() return \n.join(lines)钉钉机器人推送需要加一个关键字或者加签验证企业微信机器人则是直接发 Webhook 地址。如果你用的是自定义渠道比如 Discord 或者 Telegram接一个 HTTP 调用也能搞定原理一模一样。3.4 定时调度与幂等处理这个服务要跑起来调度是必不可少的。我用的是 APScheduler原因很简单它在 Python 里配置简单、支持 cron 表达式、还能持久化任务状态。每天上午 9 点跑一次趋势采集下午 3 点跑一次 API 补全晚上 8 点做最终汇总推送。这里有一个很重要的细节幂等处理。定时任务在运行过程中可能因为网络问题失败重试如果重试时不做判断同一批数据可能会被重复写入、重复推送。我在数据库里为仓库加了一个唯一索引字段组合是仓库名 采集日期写入时用INSERT OR IGNORE能挡住重复数据。推送模块则用任务 ID 做去重每个任务生成一个 UUID一分钟之内的重复请求直接跳过。4. 部署流程与日常运维4.1 快速上手的部署步骤部署环境我推荐用一台 1 核 1G 的小机器就够了这个服务的资源消耗非常低。如果你只有一台平时跑其他服务的服务器用 Docker 把它塞进去最省事。先给项目写一个简单的 DockerfileFROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, main.py]然后构建镜像并跑起来docker build -t trending-digest . docker run -d --name trending-digest \ -v /data/trending.db:/app/data/trending.db \ -e PUSH_URLhttps://oapi.dingtalk.com/robot/send?access_tokenxxx \ -e MAIL_ENABLEDtrue \ --restart always \ trending-digest环境变量那一块是重点。把所有敏感配置Webhook 地址、邮箱密码、数据库路径全部用环境变量注入不要写死在代码里尤其是聊天机器人的 Webhook一旦泄露后果不好收拾。如果你不想上 Docker直接在服务器上跑也行nohup python main.py app.log 21 然后把进程托管给 systemd 或者 supervisor再加一个定时任务做重启兜底省心程度和 Docker 相差不大。4.2 数据存储与历史记录查询前面提到我用 SQLite 做数据持久化看下建表语句CREATE TABLE IF NOT EXISTS repos ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, lang TEXT, desc TEXT, stars INTEGER, today_stars INTEGER, score REAL, created_at DATETIME, collected_date TEXT NOT NULL, UNIQUE(name, collected_date) );每天跑完任务数据就落进这张表。想查某天榜单非常简单SELECT * FROM repos WHERE collected_date 2024-01-15 ORDER BY score DESC LIMIT 20;用 SQLite 的好处是备份容易直接拷文件就行。我每天凌晨会压缩一份数据库文件存到另一块磁盘上保留最近 30 天的备份基本杜绝了误操作导致的历史数据丢失。4.3 日志与异常监控定时任务最怕“悄悄死掉”。我的做法是两层监控第一层是服务自带日志用 Python 的logging模块同时输出到控制台和文件第二层是健康检查接口Web 模块里加了一个/healthz路由返回 JSON 状态码然后用外部监控服务每 5 分钟探一次。from fastapi import FastAPI from fastapi.responses import JSONResponse app FastAPI() app.get(/healthz) def health_check(): return JSONResponse({status: ok, time: datetime.now().isoformat()})如果连续三次健康检查失败监控服务会给我发报警。这个机制帮我抓过一次故障有段时间 GitHub 改版我的 HTML 解析规则失效采集模块抛异常但因为主进程没退出健康检查一直是正常的直到监控加了“最近一次成功入库的数据时间”这个指标才暴露出来。所以健康检查不能只看进程活着一定要看数据有没有真正更新。5. 常见问题与避坑指南5.1 请求被限流怎么办很多人一上来就直接用 requests 高频抓 GitHub结果很快收到 403 或者 429。GitHub 的限流策略对不同接口不一样REST API 的限流是按每小时计算的未认证的请求一小时只有 60 次认证之后是 5000 次。HTML 页面的限流虽然没有明确数字但明显更敏感短时间高频请求很容易被临时封 IP。我的应对方案是第一所有 API 请求都带上 OAuth token第二抓取节奏控制在最小必要频率第三设置随机延迟避免请求像脉冲一样规律性发出。代码里我加了一个小函数import time import random def polite_request(url, headers, retries3): for i in range(retries): try: time.sleep(random.uniform(1.5, 3.5)) resp requests.get(url, headersheaders, timeout15) if resp.status_code 429: wait_time int(resp.headers.get(Retry-After, 60)) time.sleep(wait_time 5) continue resp.raise_for_status() return resp except requests.RequestException: if i retries - 1: raise time.sleep(5 * (i 1))5.2 HTML 解析规则失效GitHub 的前端改版虽然不频繁但一旦改了解析代码就直接废掉。最稳妥的做法是减少对具体 class 的依赖。我的经验是锚定语义化标签比如article、h2、p这些而不是某个具体样式类。如果哪天连article都换了那就只能等代码更新后重新抓一次。还要注意 GitHub 页面可能返回空列表。如果请求成功但解析出的仓库数为 0这时候宁可不推送也不要推一个空报告否则用户会困惑。我在采集模块里加了判断如果仓库数量为 0抛一个自定义异常调度任务就跳过本次推送。5.3 消息推送的重复与丢失推送渠道偶尔会出问题最典型的是 Webhook 地址失效或者被平台风控。钉钉机器人的安全设置必须配置加签或者关键字否则外网随便什么人往你的群里发消息就麻烦了。企业微信机器人相对宽松一点但也会有频率限制短时间大量消息会被截断。我的做法是在推送模块里加了一个发送队列每条消息发送失败会重试三次三次都不成功就写入本地失败记录文件并在下一次任务里带上失败记录重新尝试推送。这样做避免了一部分消息丢失的问题但不是所有渠道都支持重试比如邮件重发会产生垃圾邮件所以这个功能做成可配置的开关。5.4 趋势榜单的“虚假繁荣”问题有时候你会看到某个仓库今天涨了很多星点进去发现是空壳项目或者钓鱼仓库。GitHub 上刷星的情况虽然不多但确实存在。我的筛选模块里加了一个启发式规则如果仓库的 Star 数量很高但 README 只有一个标题或者仓库被 fork 的代码量和 Star 数量严重不匹配打分会降低。这条规则的具体实现是把 README 的长度作为特征值小于 100 字符的直接标记为“低内容”。再配合一个简单的语言关键词过滤比如 Description 为空、项目名以 qq 群号开头的就能挡掉很大一部分垃圾项目。总结这套速递服务的真正价值做这个项目的过程中我一直在想一个问题GitHub 趋势页本身就在那里为什么还要费劲搞一个二次加工的服务后来想明白了信息过载时代趋势只有被过滤和解释之后才真正对人有用。官方榜单给的是一堆仓库名我们想要的是一个能拍板说“今天这几个值得关注”的结论。这就是“今日GitHub趋势速递”这个项目的核心价值。从技术上来说整个项目并不复杂但靠近业务侧的取舍、数据清洗和推送策略才是真正的护城河。把同样一套代码给不同的人用会因为每个人维护的黑名单、评分偏好、推送渠道不同产出完全不同的速递效果。这也让工具本身有了个性化的温度。最后分享一个小技巧评分公式里的参数别定死跑一周之后看看实际推出来的榜单质量再把权重调一调。比如我发现某些语言的项目涨星普遍偏慢把阈值调低之后小众语言那边的冷门好项目才开始浮出水面。这种持续调优的过程反而是做这个项目最有趣的部分。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。