公司动态
从零构建GitHub热榜系统:API调用、热度算法与自动化实践
1. 项目缘起为什么我们需要一个“GitHub热榜”作为一名常年泡在GitHub上的开发者我每天都会面临一个幸福的烦恼信息过载。GitHub上每天都有成千上万的新项目诞生从精巧的工具库到庞大的开源系统从学术研究代码到实用的生产力脚本。如何在浩如烟海的仓库中快速发现那些真正有价值、有潜力、或者只是单纯“有趣”的项目成了一个技术活。手动去“探索”页面翻看效率低下且容易错过时效性内容。而“GitHub热榜”这个概念本质上就是一个信息过滤器和趋势发现器。它通过某种算法通常是基于Star增长数、Fork数、Issue/Pull Request活跃度等指标将特定时间段内如24小时最受关注的项目筛选出来呈现给用户。这不仅能帮我们节省大量探索时间更能让我们紧跟技术社区的脉搏了解当下开发者们都在关注什么、用什么、造什么。今天要分享的就是如何从零开始构建一个自动化、可定制、并且能稳定运行的“GitHub日榜”生成方案。这个方案不依赖于任何第三方现成的API服务因为很多服务有频率限制或功能不全而是直接与GitHub官方API对话结合一些数据处理和呈现技巧最终生成一份像“2025-10-26”这样的结构化榜单。无论你是想自己每天看看新鲜货还是想为你的技术社区、公众号、知识星球提供内容这套方案都能给你提供一个坚实可靠的起点。2. 核心架构设计从数据获取到榜单呈现的全链路一个完整的GitHub热榜方案可以拆解为几个核心环节数据获取、数据处理与排序、结果存储与最终呈现。我们的目标是构建一个高内聚、低耦合的管道Pipeline每个环节都可以独立调整和优化。2.1 数据获取层与GitHub API的高效对话数据是一切的基础。GitHub提供了非常完善的REST API和GraphQL API。对于榜单类需求REST API的/search/repositories端点通常是我们的首选。为什么选择搜索API而非趋势页面GitHub官网的https://github.com/trending页面虽然直观但其背后没有公开的、稳定的API且页面结构可能变动通过爬虫解析不稳定、不优雅也违背了最佳实践。而官方搜索API是专为程序化访问设计的稳定、功能强大且受支持。核心请求设计我们最关心的搜索条件是在过去24小时内创建并且有较高热度的仓库。热度可以通过stars、forks等排序来体现。一个基础的查询语句Query可以这样构建created:2025-10-25 stars:10这个查询的意思是查找创建日期晚于2025年10月25日且Star数大于10的仓库。stars:10是一个质量过滤器可以过滤掉大量刚创建或无意义的仓库。10这个阈值可以根据实际情况调整。排序策略搜索API支持按stars、forks、updated等字段排序。对于日榜我们希望找到“爆发式增长”的项目因此按stars降序排列是一个好选择。但更精准的做法是计算“单位时间内的Star增长数”这需要更复杂的数据处理我们会在下一层讨论。API调用要点与避坑指南认证与限流未认证的API调用每小时只有60次请求限额对于扫描大量数据远远不够。必须使用Personal Access Token (PAT)进行认证认证后限额提升至每小时5000次。在代码中将Token放在请求头的Authorization字段中。# 示例使用curl进行认证请求 curl -H Authorization: token ghp_yourPersonalAccessTokenHere \ -H Accept: application/vnd.github.v3json \ https://api.github.com/search/repositories?qcreated:2025-10-25stars:10sortstarsorderdescper_page100分页处理GitHub搜索API单次请求最多返回100条结果通过per_page参数设置。如果结果超过100条响应头中会包含Link字段指示下一页的URL。务必实现分页逻辑以获取完整数据否则你的榜单可能不完整。频率控制与礼貌请求即使有5000次/小时的限额密集请求也可能触发滥用检测。建议在请求间添加短暂的延迟例如使用time.sleep(1)并妥善处理API返回的X-RateLimit-Remaining和X-RateLimit-Reset头部信息实现智能限流。GraphQL作为备选对于需要非常精确地获取仓库某段时间内Star历史数据的需求REST API可能力不从心。GitHub的GraphQL API可以让你在一个请求中获取更复杂、更定制化的数据。例如你可以查询仓库的stargazers连接并配合since参数来获取指定时间点之后的Star记录从而精确计算24小时内的增长。但这需要更复杂的查询语句和对GraphQL的理解。对于大多数日榜需求基于创建时间和Star总数的REST API搜索已经足够。2.2 数据处理层定义“热度”与清洗数据拿到原始仓库列表后我们需要进行加工才能产生有意义的排名。核心热度算法简单的按stars总数排序会让一些老牌明星项目长期霸榜失去了“日榜”发现新项目的意义。因此我们需要一个能反映近期增长势头的指标。一个常见且有效的公式是热度分数 (今日star数 - 昨日star数) / 时间衰减因子但对于只跑日榜的我们可以简化为获取每个仓库当前的总Star数current_stars。获取每个仓库在24小时前的总Star数past_stars。这需要调用仓库的特定API端点获取Stargazer列表并过滤时间或者依赖于一个持续追踪的数据库。实操中更可行的简化方案是利用“创建时间”和“当前Star数”来模拟增长势头。一个在24小时内获得100星的新项目显然比一个存在一年、总共200星的项目在“今日”更热。 我们可以定义一个“初始热度分数”初始热度分数 log(current_stars) / (创建至今的小时数 1)这个公式给Star数取了对数防止超级项目分数过高并除以项目年龄使得新近获得Star的项目分数更高。这是一个启发式方法你可以根据榜单效果调整公式例如加入fork数的权重。数据清洗去重确保同一项目不会因不同搜索条件重复出现。过滤根据你的榜单定位可能需要过滤掉某些类型的仓库。例如过滤掉个人笔记、作业仓库通常描述简单README为空或很少。过滤掉明确标记为“归档”archived的项目。过滤掉主要语言不符合要求的项目例如你只想看Python或JavaScript的项目。信息补全从API获取的搜索列表数据可能不完整。为了丰富榜单内容可能需要对每个入围的仓库再发起一次详细信息的请求GET /repos/{owner}/{repo}以获取更详细的描述、主页地址、开源协议、主要语言等。2.3 存储与调度层让榜单自动运转我们不可能每天手动运行脚本。需要让整个流程自动化。存储选择简单文件存储对于个人使用将每天生成的榜单以JSON或Markdown格式保存到本地文件或云存储如GitHub仓库本身、AWS S3是最简单的。文件名可以包含日期例如github_trending_2025-10-26.md。数据库存储如果你需要做历史趋势分析、对比或者构建一个带搜索功能的网站那么就需要数据库。SQLite轻量、PostgreSQL或MongoDB都是不错的选择。表结构可以包含字段repo_id,repo_name,owner,description,language,stars,forks,score,trend_date等。自动化调度本地Cron Job (Linux/macOS)在服务器或个人电脑上使用crontab定时任务。# 每天北京时间上午10点运行你的脚本 0 2 * * * /usr/bin/python3 /path/to/your/trending_script.py /path/to/log.log 21云函数/Serverless这是更优雅、免运维的方案。你可以使用GitHub Actions在自己的GitHub仓库中配置一个工作流Workflow定时例如schedule: - cron: 0 2 * * *触发一个任务运行你的Python脚本并将生成的榜单文件提交commit回当前仓库或另一个专门存放榜单的仓库。这是我最推荐的方式因为它完全在GitHub生态内无需额外服务器并且执行记录清晰可见。AWS Lambda / Google Cloud Functions / 阿里云函数计算将脚本部署为云函数并配置定时触发器。适合更复杂或需要与其他云服务集成的场景。错误处理与日志 自动化脚本必须健壮。要加入完善的异常捕获try-except处理网络超时、API限流、数据解析错误等情况。所有操作尤其是数据获取和文件写入都应该有清晰的日志输出方便日后排查问题。日志可以输出到文件也可以集成到云平台的日志服务中。3. 实战构建一个基于Python和GitHub Actions的日榜生成器下面我将手把手带你实现一个最小可行产品MVP级别的日榜生成方案。我们选择Python作为实现语言因为它有丰富的库requests,python-dotenv支持选择GitHub Actions作为调度平台实现完全自动化。3.1 环境准备与项目初始化首先在你的GitHub上创建一个新的仓库例如叫做github-daily-trending。本地克隆这个仓库并创建基本的项目结构github-daily-trending/ ├── .github/ │ └── workflows/ │ └── generate_trending.yml # GitHub Actions 工作流文件 ├── scripts/ │ └── fetch_trending.py # 主脚本 ├── outputs/ # 存放生成的榜单文件 ├── requirements.txt # Python依赖 ├── .env.example # 环境变量示例 └── README.md安装必要的Python包pip install requests python-dotenv将依赖写入requirements.txt:requests2.28.0 python-dotenv0.19.03.2 核心脚本编写 (fetch_trending.py)这个脚本将完成数据获取、处理和生成榜单文件的核心逻辑。#!/usr/bin/env python3 GitHub日榜生成脚本 功能获取过去24小时内创建的热门仓库按热度排序生成Markdown文件。 import os import sys import json import time import logging from datetime import datetime, timedelta from typing import List, Dict, Any import requests from dotenv import load_dotenv # 加载环境变量用于本地测试GitHub Actions中通过Secrets注入 load_dotenv() # 配置日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) class GitHubTrendingFetcher: def __init__(self, token: str): self.token token self.session requests.Session() self.session.headers.update({ Authorization: ftoken {token}, Accept: application/vnd.github.v3json, User-Agent: GitHub-Daily-Trending-Bot/1.0 }) self.base_url https://api.github.com def _make_request(self, url: str, params: Dict None) - Dict: 封装请求包含重试和限流处理 max_retries 3 for attempt in range(max_retries): try: response self.session.get(url, paramsparams, timeout30) response.raise_for_status() # 检查HTTP错误 # 检查API速率限制 remaining int(response.headers.get(X-RateLimit-Remaining, 0)) if remaining 10: reset_time int(response.headers.get(X-RateLimit-Reset, 0)) sleep_time max(reset_time - time.time(), 0) 10 logger.warning(fAPI限额即将用尽休眠 {sleep_time:.0f} 秒) time.sleep(sleep_time) return response.json() except requests.exceptions.RequestException as e: logger.error(f请求失败 (尝试 {attempt 1}/{max_retries}): {e}) if attempt max_retries - 1: wait_time 2 ** attempt # 指数退避 logger.info(f等待 {wait_time} 秒后重试...) time.sleep(wait_time) else: raise return {} def search_repositories(self, query: str, sortstars, orderdesc, per_page100) - List[Dict]: 搜索仓库并处理分页 all_items [] page 1 url f{self.base_url}/search/repositories while True: params { q: query, sort: sort, order: order, per_page: per_page, page: page } logger.info(f正在获取第 {page} 页数据...) data self._make_request(url, paramsparams) items data.get(items, []) if not items: break all_items.extend(items) # 检查是否还有下一页 if len(items) per_page: break page 1 # 礼貌性延迟避免请求过快 time.sleep(1) logger.info(f共获取到 {len(all_items)} 个仓库) return all_items def calculate_score(self, repo: Dict) - float: 计算仓库的热度分数简化版 stars repo.get(stargazers_count, 0) created_at repo.get(created_at, ) try: # 解析创建时间 created_time datetime.fromisoformat(created_at.replace(Z, 00:00)) now datetime.utcnow() # 计算项目年龄小时 age_hours max((now - created_time).total_seconds() / 3600, 1) # 避免除零 # 热度分数 log(star数) / 年龄 # 加1防止log(0) score (math.log(stars 1) / age_hours) * 1000 # 乘以1000使分数更易读 except Exception as e: logger.warning(f计算仓库 {repo.get(full_name)} 分数时出错: {e}) score 0 return score def generate_markdown(self, repos: List[Dict], date_str: str) - str: 生成Markdown格式的榜单 md_lines [ f# GitHub 每日热门仓库榜单 - {date_str}\n, 本榜单通过扫描GitHub官方API筛选过去24小时内创建并获得一定关注度的仓库按热度算法排序生成。\n, | 序号 | 项目名称 | 描述 | 语言 | Star数 | 热度分数 |, | :--- | :--- | :--- | :--- | :--- | :--- |, ] for idx, repo in enumerate(repos, start1): name repo.get(full_name, N/A) url repo.get(html_url, #) description repo.get(description, 暂无描述) # 描述过长时截断 if len(description) 100: description description[:97] ... language repo.get(language, N/A) stars repo.get(stargazers_count, 0) score repo.get(_score, 0) # 假设分数已存入repo字典 md_lines.append( f| {idx} | [{name}]({url}) | {description} | {language} | {stars} | {score:.2f} | ) md_lines.append(f\n\n**榜单生成时间** {datetime.utcnow().strftime(%Y-%m-%d %H:%M:%S UTC)}) md_lines.append(f\n**筛选条件** 过去24小时内创建Star数 10按热度算法排序。) md_lines.append(f\n---\n*本榜单由自动化脚本生成仅供参考。*) return \n.join(md_lines) def main(): # 从环境变量获取GitHub Token token os.getenv(GITHUB_TOKEN) if not token: logger.error(未找到 GITHUB_TOKEN 环境变量。请设置。) sys.exit(1) fetcher GitHubTrendingFetcher(token) # 计算日期范围 today datetime.utcnow().date() yesterday today - timedelta(days1) query_date yesterday.strftime(%Y-%m-%d) # 构建搜索查询 # 搜索过去24小时内创建且star数大于10的仓库 search_query fcreated:{query_date} stars:10 logger.info(f搜索查询: {search_query}) # 获取仓库列表 repos fetcher.search_repositories(querysearch_query, sortstars, orderdesc) if not repos: logger.warning(未获取到任何仓库数据。) return # 为每个仓库计算热度分数 for repo in repos: repo[_score] fetcher.calculate_score(repo) # 按热度分数降序排序 sorted_repos sorted(repos, keylambda x: x.get(_score, 0), reverseTrue) # 只取前50名 top_repos sorted_repos[:50] # 生成Markdown内容 date_str today.strftime(%Y-%m-%d) markdown_content fetcher.generate_markdown(top_repos, date_str) # 确保输出目录存在 output_dir outputs os.makedirs(output_dir, exist_okTrue) # 写入文件 output_filename os.path.join(output_dir, fgithub_trending_{date_str}.md) with open(output_filename, w, encodingutf-8) as f: f.write(markdown_content) logger.info(f榜单已成功生成: {output_filename}) # 也可以打印前10名到控制台 logger.info(今日榜单前十名:) for idx, repo in enumerate(top_repos[:10], start1): logger.info(f{idx}. {repo[full_name]} - {repo.get(description, )[:50]}... (Stars: {repo[stargazers_count]}, Score: {repo[_score]:.2f})) if __name__ __main__: # 注意需要导入math库用于log计算在文件顶部添加 import math import math main()脚本关键点解析认证与会话管理使用requests.Session保持连接并在头部统一设置认证Token和User-Agent这是符合API使用规范的做法。健壮的请求封装_make_request方法实现了重试机制指数退避和基础的速率限制检查提高了脚本的稳定性。简化热度算法calculate_score函数使用了log(star)/年龄的启发式算法。这是一个很好的起点你可以根据榜单效果调整公式例如加入fork数、open_issues数的权重。Markdown生成生成一个格式清晰的Markdown表格包含项目名称带链接、描述、语言、Star数和计算出的热度分数。这种格式易于阅读也便于直接发布到支持Markdown的平台。3.3 配置GitHub Actions自动化工作流接下来我们配置GitHub Actions让脚本每天自动运行。在.github/workflows/generate_trending.yml中写入name: Generate Daily GitHub Trending on: schedule: # 每天UTC时间00:10运行对应北京时间08:10 - cron: 10 0 * * * workflow_dispatch: # 允许手动触发 push: branches: - main # 当main分支有推送时也运行可选用于测试 jobs: build: runs-on: ubuntu-latest permissions: contents: write # 赋予工作流写入仓库内容的权限 steps: - name: Checkout repository uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.10 - name: Install dependencies run: | pip install -r requirements.txt - name: Run trending script env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 使用GitHub Actions内置Token # 或者使用你自己创建的、权限更明确的PAT: # GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} run: | python scripts/fetch_trending.py - name: Commit and push if there are changes run: | git config --local user.email actiongithub.com git config --local user.name GitHub Action git add outputs/ timestamp$(date -u %Y-%m-%d %H:%M:%S UTC) git commit -m Auto-update: GitHub trending for $(date -u %Y-%m-%d) [skip ci] || echo No changes to commit git push origin HEAD:main工作流配置要点触发条件schedule是核心使用cron语法定义定时任务。workflow_dispatch允许你在GitHub网页上手动点击运行非常便于测试。权限设置permissions: contents: write是关键。它赋予了工作流向仓库提交代码的权限这样脚本生成的Markdown文件才能被自动提交回去。环境变量脚本需要的GITHUB_TOKEN通过secrets.GITHUB_TOKEN自动注入。这是GitHub Actions为每个仓库运行提供的默认Token拥有该仓库的读写权限。注意这个Token的权限相对于整个GitHub API是受限的。如果你的搜索需要更高频率或访问其他私有仓库你需要在仓库的Settings - Secrets and variables - Actions中创建一个名为PERSONAL_ACCESS_TOKEN的Secret填入你自己生成的、具有repo和public_repo权限的PAT然后在工作流中引用${{ secrets.PERSONAL_ACCESS_TOKEN }}。自动提交最后一步检查outputs/目录是否有新文件有则自动提交并推送到main分支。[skip ci]可以防止这次提交又触发新的工作流运行如果配置了push触发的话。3.4 本地测试与首次运行在将一切推送到GitHub之前务必在本地测试。创建个人访问令牌(PAT)登录GitHub - Settings - Developer settings - Personal access tokens - Tokens (classic)。生成一个新Token勾选repo和public_repo权限对于我们的公开仓库搜索public_repo可能就够了但repo更全面。妥善保存这个Token。配置本地环境复制.env.example为.env并在其中填入你的PATGITHUB_TOKENghp_yourActualTokenHere运行脚本在项目根目录执行python scripts/fetch_trending.py。检查outputs/目录下是否生成了当天的Markdown文件并查看控制台输出。推送代码将本地代码包括.github/workflows/目录推送到GitHub仓库的main分支。触发首次Action在GitHub仓库的“Actions”标签页你应该能看到刚推送的工作流。你可以点击“Generate Daily GitHub Trending”工作流然后选择“Run workflow”来手动触发一次验证整个流程是否畅通。如果一切顺利你的仓库里每天都会自动多出一个以日期命名的Markdown文件里面就是新鲜的GitHub日榜。4. 方案优化与高级玩法基础方案跑通后我们可以从多个维度进行优化让榜单更有价值。4.1 热度算法的精细化调优我们之前的算法log(star)/年龄虽然简单有效但仍有改进空间。引入分时权重一个在凌晨3点获得100星的项目和一个在晚上8点获得100星的项目在“今日”的热度感知上可能不同。可以考虑给不同时间段获得的Star赋予不同权重但这需要获取详细的Stargazer时间数据GraphQL API更适合。多维度综合评分创建一个复合分数。综合热度 w1 * 标准化(star增长) w2 * 标准化(fork增长) w3 * 标准化(issue/pr活跃度) - w4 * 标准化(项目年龄)其中w1, w2, w3, w4是权重系数需要根据你的榜单“口味”调整。标准化可以防止某一项指标数值过大而主导结果。机器学习预测更高级的玩法是收集历史数据项目每日的star、fork、commit等训练一个简单的模型来预测项目未来的热度趋势并以此排序。这属于进阶范畴。4.2 数据源的扩展与聚合不要局限于一种搜索方式。多时间维度除了“日榜”可以同时生成“周榜”created:2025-10-19和“月榜”。在同一个工作流中运行多个脚本或者修改脚本支持参数化时间范围。按语言/分类筛选在搜索查询中加入language:python或topic:machine-learning生成细分领域的榜单。你可以为不同语言配置不同的工作流或脚本分支。聚合多个榜单你可以运行多个查询如全平台日榜、Python日榜、JavaScript日榜然后将结果聚合到一个总榜页面中通过标签页或目录来切换。4.3 呈现形式的多样化Markdown文件只是开始。静态网站生成使用像Jekyll、Hugo、VuePress这样的静态网站生成器将每天生成的Markdown文件作为内容源自动构建成一个带有导航、搜索功能的漂亮网站。GitHub Pages可以免费托管这个网站。自动发布到社交媒体/社区在工作流中增加步骤将生成的榜单内容或摘要通过API自动发布到Telegram频道、Twitter、Discord服务器或国内的技术社区如通过Webhook。这需要相应平台的API权限。生成图文并茂的卡片使用Python的pillow或reportlab库或者调用一些在线API将榜单前十名生成一张精美的长图更适合在社交媒体传播。4.4 监控与告警自动化系统需要监控。工作流失败告警在GitHub Actions工作流中可以配置当任务失败时通过邮件、Slack或钉钉机器人发送通知。数据质量监控在脚本中加入检查点。例如如果某天获取到的仓库数量异常少比如少于5个或者在处理过程中出现大量错误可以在日志中标记警告甚至让工作流失败触发告警。API限额监控脚本中可以定期检查X-RateLimit-Remaining如果剩余次数过低可以提前发送预警。5. 避坑指南与常见问题在实际部署和运行过程中你肯定会遇到一些坑。以下是我总结的几个关键点1. GitHub API的速率限制是最大的拦路虎。坑未认证请求60次/小时认证后5000次/小时。看似很多但如果你频繁分页抓取或者同时跑多个榜单很容易超限。避坑一定要用PAT认证。实现智能休眠像我们脚本里那样检查X-RateLimit-Remaining当数值较低时主动休眠到限额重置时间。缓存数据对于不常变的数据如仓库的创建时间、描述可以考虑在本地数据库缓存避免重复请求。GraphQL的权衡GraphQL一次请求可以获取更多字段可能减少请求次数但查询复杂度高且对于搜索类请求其限流策略可能与REST不同需要仔细阅读文档。2. 搜索结果的稳定性和排序。坑GitHub的搜索索引不是实时的可能有几分钟的延迟。另外/search/repositories的排序选项有限单纯按stars排序对日榜不理想。避坑接受非完全实时性。对于排序必须引入自己的“热度分数”进行二次排序这是榜单价值的核心。不要完全依赖API返回的顺序。3. 项目描述的清洗与格式化。坑仓库描述description可能包含Markdown、Emoji、换行符直接放入表格可能导致格式错乱。避坑在生成Markdown或HTML前对描述文本进行简单的清洗比如移除Markdown链接的括号[]()将换行符替换为空格或者对Emoji进行转义或过滤。4. 自动化提交冲突。坑如果工作流运行时间过长或者手动向outputs/目录提交了更改可能导致自动提交时出现冲突。避坑在工作流的提交步骤前先执行git pull --rebase origin main拉取最新更改。或者采用更稳健的方式不直接提交到主分支而是创建一个以日期命名的分支提交后创建Pull Request再通过其他Action自动合并。但这增加了复杂度。对于个人项目先pull再push通常足够。5. 敏感信息泄露。坑将PAT直接写在脚本里或提交到仓库。避坑永远不要将Token硬编码在代码中。始终使用环境变量.env文件本地测试或GitHub Secrets线上Action。.env文件必须加入.gitignore。构建一个属于自己的GitHub热榜系统远不止是获取一份列表。从API交互的细节处理到热度算法的设计调优再到自动化管道的搭建和运维每一个环节都能加深你对软件开发流程、数据获取处理和自动化工具链的理解。这个项目就像一个微型的、功能完整的数据产品你可以持续迭代它加入新的想法比如关联Hacker News讨论、分析代码复杂度甚至预测下一个“明星项目”。最重要的是通过这个过程你获得了一个高度定制化的信息源它每天都会为你带来技术世界的最新脉动。