尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C#调用GitHub REST API批量下载用户全部仓库的实战指南

C#调用GitHub REST API批量下载用户全部仓库的实战指南 先问一个真实问题当你在 GitHub 上遇到一位很对自己胃口的技术作者或者团队要做代码资产盘点时有没有想过“把这个人的全部项目一次性下载到本地”我之前遇到这需求时第一反应是打开网页逐个点 Download ZIP结果仓库数量一多鼠标直接点到手酸。更麻烦的是GitHub 网页端根本没有“全选下载”这种批量操作。后来我决定自己写一个小工具用 C# 调用 GitHub 官方 REST API把某个用户的全部公开仓库批量下载到本地自动跳过 Fork 过来的副本也支持按仓库体积过滤。不依赖 Git 命令不依赖第三方 SDK纯 HttpClient 加标准库就能跑通。这篇文章就把完整思路和代码分享出来如果你也在做代码备份、项目归档或者想找一个练手 C# 网络编程的实战案例这篇值得收藏。1. 为什么你需要这个工具真实场景与痛点先来盘一下到底是哪些场景需要“批量下载某个 GitHub 用户的全部仓库”。场景一研究一位技术作者的全部代码。很多开发者在 GitHub 上关注某些高产作者想系统研究他们的项目。但网页端只能一个仓库一个仓库地打开再点 Code 按钮选 Download ZIP。仓库少还好超过 20 个的时候就非常折磨。场景二团队代码资产盘点与归档。企业里经常有这种需求某个团队使用的 GitHub 账号下有大量仓库需要在某个时间点把全部代码快照保留到本地。可能是做迁移可能是做合规归档也可能只是怕账号出问题。这种批量操作靠人工点网页完全不现实。场景三下载完整的“仓库集合”做离线研究。比如你想把某个 GitHub 组织的全部项目抓下来做代码分析或者拿来做本地索引。此时不仅需要下载还需要把每个仓库按目录整理好甚至要跳过那些体积巨大但价值不高的仓库。那为什么不直接用 git clonegit clone 当然可以但它有几个问题必须先在机器上安装并配置 Gitclone 会拉取完整的提交历史仓库一多时间和磁盘占用都很可观如果只想做代码快照而不是要完整历史clone 属于“杀鸡用牛刀”clone 目录结构还需要自己额外处理不如直接下载官方归档包来得干净。所以更合适的做法是先调用 GitHub API 获取仓库列表再对每个仓库下载官方生成的 zipball 归档包。GitHub 在打包时已经帮你处理好了 .git 之外的完整工作区内容下载后直接解压就能看代码。我的判断是这类工具的核心价值不只是“下载”而是“可控地批量处理”。也就是能跳过某些仓库、能按语言或大小过滤、能显示进度、能容忍单仓库失败而不中断整个任务。这些点恰恰是手工操作和简单 for 循环脚本最欠缺的。2. 核心原理GitHub REST API 与 zipball 下载机制要写这个工具需要理解几个关键原理。第一个是 GitHub REST API 的仓库列表接口。2.1 获取用户仓库列表GitHub REST API 提供了一个非常直接的端点GET https://api.github.com/users/{username}/repos这个接口会返回该用户名下的公开仓库列表是一个 JSON 数组。每个元素包含仓库名、描述、默认分支、语言、是否为 Fork、体积、Star 数等关键字段。接口支持分页参数GET https://api.github.com/users/{username}/repos?per_page100page1其中per_page最大可以填 100page从 1 开始递增。当返回的数组长度小于per_page时说明已经到最后一页。这里有三个细节值得注意响应的 JSON 里每个仓库有一个fork字段。如果为true说明这个仓库是从别人那里 Fork 来的副本。有一个size字段单位是 KB可以粗略评估仓库体积。还有一个default_branch字段默认分支一般是main或master下载归档时需要用到。2.2 下载 zipball 归档包拿到仓库列表后下一步就是下载归档包。GitHub 提供了 zipball 接口GET https://api.github.com/repos/{owner}/{repo}/zipball/{branch}比如GET https://api.github.com/repos/octocat/Hello-World/zipball/master这个接口返回的不是文件内容而是一个 302 重定向。重定向目标通常是https://codeload.github.com/{owner}/{repo}/zip/refs/heads/{branch}。如果你的 HttpClient 允许自动重定向那么最终拿到的就是完整的 ZIP 文件流。zipball 下载方式有几个特点它下载的是当前分支的完整快照包含所有文件但不包含 .git 历史ZIP 解压后第一层目录名通常带有最新的 commit SHA例如octocat-Hello-World-1234567需要在解压时做“展平”处理这种方式不要求本地安装 Git一个支持重定向的 HTTP 客户端就够。2.3 认证与限流GitHub API 对未认证请求和认证请求有不同的限流配额。未认证的额度很低认证后额度会大幅提升。即使你只下载公开仓库也强烈建议带上 Token。认证方式很简单在请求头里加一行Authorization: Bearer your_token_hereToken 的创建方式GitHub 网页右上角头像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token。如果只访问公开仓库不需要勾选任何 scope如果以后要访问私有仓库才需要勾选repo权限。需要注意的是HttpClient 在跟随跨主机重定向时出于安全考虑会自动移除Authorization头。也就是说带 Token 的请求访问api.github.com跳转到codeload.github.com后Token 不会跟着带过去。对公开仓库没有影响但私有仓库就会遇到认证失败。这一点到后面的代码实现部分还会再提。3. 环境准备.NET 版本选择与项目创建这个工具基于 .NET 控制台应用不依赖图形界面Windows、Linux、macOS 都可以跑。我建议使用 .NET 8 或更高版本你本机如果已经安装了 .NET SDK直接跟下面步骤走即可。在命令行执行dotnet new console -n GithubDownloader cd GithubDownloader这个命令会生成一个最小的控制台项目。如果你用的是 Visual Studio也可以直接新建“控制台应用”项目目标框架选择 .NET 8 以上即可。项目默认会启用ImplicitUsings所以System、System.Net.Http、System.IO这些命名空间不需要手动 usings。本文代码使用System.Text.Json解析 JSON使用System.IO.Compression解压 ZIP这两个在 .NET 8 的共享框架中已经包含不需要额外安装 NuGet 包。如果你在编译时发现ZipFile找不到检查一下项目文件里是否引用了System.IO.Compression.ZipFile。在 .NET 6 项目中这个程序集通常已经在共享框架里了但如果使用的是旧版 SDK可能需要手动添加PackageReference IncludeSystem.IO.Compression.ZipFile Version8.0.0 /不过更稳妥的做法是直接升级到 .NET 8。创建完项目后建议先跑一次确认环境没问题dotnet run控制台输出Hello, World!就说明环境正常。4. 工具设计命令行参数与项目结构这个工具的使用方式设计成命令行参数模式方便后续配合脚本使用dotnet run -- -u octocat -o D:\github-backup --skip-fork命令行参数说明参数说明是否必填-u, --user目标 GitHub 用户名必填-o, --output本地保存目录默认当前目录可选-t, --tokenGitHub Personal Access Token可选但推荐--skip-fork跳过 Fork 来的仓库可选--max-size跳过体积超过指定 MB 的仓库可选项目结构分为三个核心文件GithubDownloader/ ├─ Program.cs // 入口参数解析主流程 ├─ Models.cs // 仓库信息数据模型 ├─ GitHubApiHelper.cs // 调用 API 获取仓库列表 └─ DownloadService.cs // 下载 zipball 并解压之所以拆成三个文件是因为职责不同Models.cs只负责定义 JSON 反序列化用的数据类GitHubApiHelper只负责“获取数据”DownloadService只负责“下载文件”和“解压文件”。这样拆分之后单元测试和后续扩展都方便。比如以后想支持下载组织下的全部仓库只需要在GitHubApiHelper里加一个方法而不需要动其他文件。5. 代码实现一数据模型与仓库列表获取5.1 定义仓库数据模型先创建Models.cs定义RepoInfo类。GitHub API 返回的 JSON 字段很多这里只声明我们需要用到的字段通过JsonPropertyName特性做映射。// Models.cs using System.Text.Json.Serialization; namespace GithubDownloader; public class RepoInfo { [JsonPropertyName(name)] public string Name { get; set; } ; [JsonPropertyName(full_name)] public string FullName { get; set; } ; [JsonPropertyName(html_url)] public string HtmlUrl { get; set; } ; [JsonPropertyName(clone_url)] public string CloneUrl { get; set; } ; [JsonPropertyName(default_branch)] public string DefaultBranch { get; set; } main; [JsonPropertyName(description)] public string? Description { get; set; } [JsonPropertyName(language)] public string? Language { get; set; } [JsonPropertyName(fork)] public bool Fork { get; set; } [JsonPropertyName(archived)] public bool Archived { get; set; } [JsonPropertyName(size)] public long Size { get; set; } [JsonPropertyName(stargazers_count)] public int StargazersCount { get; set; } }这里最容易踩的坑是字段名大小写。GitHub API 返回的是全小写下划线格式比如default_branch、stargazers_count。用System.Text.Json反序列化时如果不加JsonPropertyName特性就会因为字段名对不上而得到 null 或默认值。在写生产级代码时也可以配置JsonSerializerOptions为PropertyNameCaseInsensitive但下划线命名依然需要映射。5.2 封装 GitHub API 客户端创建GitHubApiHelper.cs核心是分页拉取全部仓库。这里要注意两点第一设置User-Agent请求头GitHub API 强制要求第二设置自动解压 gzip否则响应内容可能不正常。// GitHubApiHelper.cs using System.Net; using System.Net.Http.Headers; using System.Text.Json; namespace GithubDownloader; public class GitHubApiHelper { private readonly HttpClient _http; private readonly string? _token; public GitHubApiHelper(string? token) { _token token; var handler new HttpClientHandler { AutomaticDecompression DecompressionMethods.GZip | DecompressionMethods.Deflate }; _http new HttpClient(handler) { BaseAddress new Uri(https://api.github.com), Timeout TimeSpan.FromSeconds(100) }; _http.DefaultRequestHeaders.Add(User-Agent, GithubRepoDownloader/1.0); _http.DefaultRequestHeaders.Add(Accept, application/vnd.githubjson); if (!string.IsNullOrEmpty(token)) { _http.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, token); } } public async TaskListRepoInfo GetAllPublicReposAsync( string username, bool skipFork, CancellationToken ct default) { var result new ListRepoInfo(); int page 1; while (true) { string url $users/{Uri.EscapeDataString(username)}/repos?per_page100page{page}; using var response await _http.GetAsync(url, ct); if (response.StatusCode HttpStatusCode.NotFound) { throw new Exception($用户 {username} 不存在或者该用户名是组织名。组织请使用 /orgs/xxx/repos 接口。); } response.EnsureSuccess
返回列表