做开源项目调研、整理团队技术资产、或者想把某位开发者的所有仓库信息备份下来时,很多人都会遇到同一个尴尬场景:GitHub 网页翻页翻到手指发酸,仓库数量一多,项目名称、语言、Star 数、最后更新时间手工根本记不过来。网上搜“GitHub 爬虫”出来的大多又是 Python 方案,对平时主要写 C# 的开发者来说,还得额外搭一套环境。
本文就用 C# 写一个命令行工具,通过 GitHub 官方 REST API 把指定用户的所有公开仓库批量拉取下来,并支持导出 CSV、按条件筛选、批量下载源码包。我们会先讲清楚 API 调用规则,再给出完整可运行的代码,最后补充常见报错和工程化建议。全程不需要第三方爬虫框架,.NET 自带的 HttpClient 和 System.Text.Json 就够了。
1. 场景与核心概念
1.1 这个工具解决什么问题
假设你现在有这样一个需求:把某个 GitHub 账号下的全部仓库信息整理成表格,包含仓库名、描述、主要语言、Star 数、Fork 数、最后更新时间。
如果仓库只有五六个,手动复制倒是无所谓。但遇到几百个仓库的账号,网页翻页就会非常痛苦,而且 GitHub 网页上每页展示的仓库数量有限,翻到最后很容易漏掉。更麻烦的是,手工整理出来的数据格式五花八门,后续做排序、筛选、统计都得重来。
这时候写一个小工具批量调接口,几秒钟就能拿到结构化数据。工具可以做的几件事包括:
- 拉取指定用户的所有公开仓库。
- 按 Star 数、语言、更新时间排序。
- 去掉 fork 的仓库,只保留原创项目。
- 导出 CSV 表格。
- 批量下载仓库源码压缩包。
这些功能本质上都是“读数据”,不需要模拟登录,也不需要处理网页反爬,唯一要做的就是把 GitHub 官方 API 用对。
1.2 为什么选择 GitHub REST API
标题里写的是“爬虫”,但真正实现时更推荐直接调 GitHub REST API,而不是用 HttpClient 去抓 HTML 页面再解析。
主要原因有三个:
- 数据是标准 JSON,字段稳定。仓库名、描述、语言、Star 数等字段都有固定命名,反序列化之后直接用,不需要处理 HTML 结构变化。
- 不需要处理登录、动态渲染、反爬策略。GitHub 网页本身是动态页面,直接抓 HTML 只能拿到部分数据,还得额外解析。
- 更合规。GitHub 官方 API 就是给开发者批量读取数据用的,只要控制好频率、带上认证信息,就属于正常使用。而高强度抓取网页可能违反服务条款。
所以这里的“爬取”,本质上就是把官方数据通道用熟练。理解这一点,后面写代码就不会走偏。
1.3 用 C# 实现的优势
很多 GitHub API 示例都是用 Python 或 Node.js 写的,但 C# 开发者完全没必要为了一个小工具切换技术栈。.NET 生态里几样东西已经足够:
- HttpClient:发起 HTTP 请求。
- System.Text.Json:解析 JSON。
- 顶层语句(Top-Level Statements):用最少的代码写完一个工具。
而且 .NET 是跨平台的,Windows、macOS、Linux 上都能跑。哪怕你只是为了调研某个技术大牛的仓库,用 C# 写个几十行的工具也完全够用。
2. 环境准备与项目创建
2.1 开发环境说明
本文示例以 .NET 8 控制台项目为例,代码中用到的 API 在 .NET 6 / .NET 7 / .NET 9 下同样可以使用。版本需要根据你的项目实际情况调整,如果本地只装了 .NET 6 SDK,直接按同样步骤创建项目即可。
建议环境如下:
| 项目 | 说明 |
|---|---|
| 操作系统 | Windows 10/11、macOS、Linux 均可 |
| .NET SDK | .NET 8.0(或其他已安装版本) |
| IDE | Visual Studio 2022、VS Code 或 JetBrains Rider |
| 额外依赖 | 无,使用 .NET 内置库 |
先确认本机 .NET 环境是否正常:
dotnet --version如果能正常输出版本号,说明 SDK 已经装好。
2.2 创建控制台项目
打开终端,新建一个名为GitHubRepoCrawler的控制台项目:
dotnet new console -n GitHubRepoCrawler cd GitHubRepoCrawler创建完成后的目录结构是这样的:
GitHubRepoCrawler/ ├── GitHubRepoCrawler.csproj └── Program.cs我们会在后续步骤中手动补充Models和Services两个文件夹。
2.3 项目结构设计
项目虽小,但建议还是按职责分一下文件,避免所有代码堆在 Program.cs 里。最终结构如下:
GitHubRepoCrawler/ ├── GitHubRepoCrawler.csproj ├── Program.cs ├── Models/ │ └── GitHubRepo.cs └── Services/ ├── GitHubApiClient.cs └── ExportHelper.cs- Models:定义 GitHub 仓库数据模型。
- Services:封装 API 调用和 CSV 导出逻辑。
- Program.cs:只负责用户交互和流程编排。
这样后续想增加搜索、下载、统计功能,都有地方放代码。
3. GitHub API 调用规则拆解
在写代码之前,先把几个关键规则弄清楚。很多人第一次调 GitHub API 就卡在分页和限流上,其实这些规则并不复杂。
3.1 仓库列表接口
获取某个用户所有公开仓库的接口是:
GET https://api.github.com/users/{username}/repos其中{username}就是 GitHub 用户名,例如octocat。
这个接口支持很多查询参数,常用的有:
| 参数 | 作用 | 示例 |
|---|---|---|
per_page | 每页返回数量,最大 100 | per_page=100 |
page | 页码,从 1 开始 | page=2 |
type | 仓库类型,可选 all/owner/member | type=owner |
sort | 排序字段,可选 created/updated/pushed/full_name | sort=updated |
direction | 排序方向,asc 或 desc | direction=desc |
重点记住per_page和page,这是实现翻页抓取的关键。
3.2 分页机制
GitHub REST API 默认每页只返回 30 条数据。如果不设置per_page,一个 100 个仓库的账号你只能拿到前 30 个,这是新手最容易踩的坑。
分页逻辑很简单:设置per_page=100,从page=1开始请求,如果这一页返回的仓库数量等于 100,说明可能还有下一页,继续请求page=2;如果返回数量小于 100,说明已经到最后一页,结束循环。
另外,GitHub 会在响应头里返回Link字段,例如:
Link: <https://api.github.com/user/repos?page=2>; rel="next", <https://api.github.com/user/repos?page=34>; rel="last"通过解析rel="next"和rel="last"也能判断翻页,但对我们的工具来说,直接判断返回数量反而更简单直观。
3.3 身份认证与访问限制
GitHub API 的访问限制非常明确:
- 未认证请求:每个 IP 每小时 60 次。
- 认证请求:每个账号每小时 5000 次。
拉取大量仓库时,60 次限额根本不够用,所以建议一定带上认证信息。认证方式是在请求头里加:
Authorization: Bearer <token>同时 GitHub 强制要求请求带上User-Agent请求头,否则直接返回 403。
申请一个 Personal Access Token 的流程如下:
- 登录 GitHub,打开 Settings。
- 进入 Developer settings -> Personal access tokens。
- 选择 Tokens (classic),点击 Generate new token (classic)。
- 如果只读公开仓库,不需要勾选任何 scope;如果要读私有仓库,需要勾选
repo。 - 生成后复制 token,保存到环境变量
GITHUB_TOKEN中。
现在 GitHub 也推荐使用 Fine-grained personal access token,可以精确限制到某个仓库、只读权限,对安全性要求更高的场景更合适。无论是 classic 还是 fine-grained,在请求头里的用法是一样的。
3.4 响应中的关键字段
仓库列表接口返回的是一个 JSON 数组,每个元素对应一个仓库。我们关心的字段如下:
| JSON 字段 | 含义 |
|---|---|
id | 仓库 ID |
name | 仓库名 |
full_name | 完整名称,例如owner/repo |
html_url | 仓库页面地址 |
description | 仓库描述 |
language | 主要编程语言 |
stargazers_count | Star 数 |
forks_count | Fork 数 |
fork | 是否为 fork 仓库 |
created_at | 创建时间 |
updated_at | 最后更新时间 |
clone_url | git clone 地址 |
default_branch | 默认分支名 |
C# 反序列化时,需要处理一个命名差异:JSON 字段是小写下划线命名,C# 属性是 PascalCase 命名。解决方案是用[JsonPropertyName]特性做映射,例如:
[JsonPropertyName("stargazers_count")] public int StargazersCount { get; set; }不写这个特性,反序列化出来全是默认值,这是很常见的错误。
4. 完整实战:C# 爬取 GitHub 用户仓库
4.1 定义仓库模型
在项目中新建Models/GitHubRepo.cs,内容如下:
using System.Text.Json.Serialization; namespace GitHubRepoCrawler.Models; public class GitHubRepo { [JsonPropertyName("id")] public long Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; [JsonPropertyName("full_name")] public string FullName { get; set; } = string.Empty; [JsonPropertyName("html_url")] public string HtmlUrl { get; set; } = string.Empty; [JsonPropertyName("description")] public string? Description { get; set; } [JsonPropertyName("language")] public string? Language { get; set; } [JsonPropertyName("stargazers_count")] public int StargazersCount { get; set; } [JsonPropertyName("forks_count")] public int ForksCount { get; set; } [JsonPropertyName("fork")] public bool IsFork { get; set; } [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } [JsonPropertyName("updated_at")] public DateTime UpdatedAt { get; set; } [JsonPropertyName("clone_url")] public string CloneUrl { get; set; } = string.Empty; [JsonPropertyName("default_branch")] public string DefaultBranch { get; set; } = string.Empty; }这里把fork字段映射成IsFork,是因为 C# 中Fork作为属性名没问题,但语义上IsFork更清楚,而且代码里读起来更自然。
4.2 封装 API 客户端
在Services/GitHubApiClient.cs中封装所有 API 调用。这样主程序不需要关心 URL 拼接、请求头、限流处理等细节。
using System.Net; using System.Text.Json; using GitHubRepoCrawler.Models; namespace GitHubRepoCrawler.Services; public class GitHubApiClient { private readonly HttpClient _http; private const string BaseUrl = "https://api.github.com"; private const int PerPage = 100; public GitHubApiClient(string? token = null) { _http = new HttpClient(); _http.Timeout = TimeSpan.FromSeconds(30); // GitHub API 强制要求 User-Agent,缺少会返回 403 _http.DefaultRequestHeaders.Add("User-Agent", "GitHubRepoCrawler/1.0"); _http.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json"); if (!string.IsNullOrEmpty(token)) { _http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } } public async Task<List<GitHubRepo>> GetUserReposAsync(string username, bool includeForks = false) { var result = new List<GitHubRepo>(); var page = 1; while (true) { var url = $"{BaseUrl}/users/{username}/repos?per_page={PerPage}&page={page}"; var response = await _http.GetAsync(url); if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.TooManyRequests) { var resetHeader = response.Headers.TryGetValues("X-RateLimit-Reset", out var resetValues) ? resetValues.FirstOrDefault() : "未知"; var remainingHeader = response.Headers.TryGetValues("X-RateLimit-Remaining", out var remainValues) ? remainValues.FirstOrDefault() : "未知"; Console.WriteLine($"请求已触发限流,剩余配额: {remainingHeader},重置时间戳: {resetHeader}"); break; } response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var pageRepoList = JsonSerializer.Deserialize<List<GitHubRepo>>(json) ?? new List<GitHubRepo>(); if (pageRepoList.Count == 0) { break; } // 记录原始页大小,用于判断是否已到最后一页 var rawCount = pageRepoList.Count; if (!includeForks) { pageRepoList = pageRepoList.Where(r => !r.IsFork).ToList(); } result.AddRange(pageRepoList); // 如果这一页不足 100 条,说明已经是最后一页 if (rawCount < PerPage) { break; } page++; } return result; } }这里有一个很容易被忽略的细节:判断是否到最后一页时,要看过滤前的数量。如果用过滤后的数量判断,一个页面里恰好 100 条全是 fork 仓库时,过滤后数量为 0,程序会误判为“没有更多数据”,提前退出循环。
如果不需要排除 fork,直接调用GetUserReposAsync(username, includeForks: true)即可。
4.3 编写主程序
Program.cs 负责用户交互和结果展示:
using System.Text; using GitHubRepoCrawler.Models; using GitHubRepoCrawler.Services; Console.OutputEncoding = Encoding.UTF8; Console.Write("请输入 GitHub 用户名: "); var username = Console.ReadLine()?.Trim(); if (string.IsNullOrEmpty(username)) { Console.WriteLine("用户名不能为空。"); return; } var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); var client = new GitHubApiClient(token); Console.WriteLine($"正在从 GitHub API 拉取 {username} 的公开仓库..."); var repos = await client.GetUserReposAsync(username, includeForks: false); Console.WriteLine($"共获取 {repos.Count} 个仓库(已排除 fork)。"); // 按 Star 数排序后,打印前 20 个 var topRepos = repos.OrderByDescending(r => r.StargazersCount).Take(20).ToList(); foreach (var repo in topRepos) { var language = string.IsNullOrEmpty(repo.Language) ? "未知" : repo.Language; Console.WriteLine($"{repo.StargazersCount,5} ★ {language,-12} {repo.FullName}"); } // 导出 CSV var csvPath = "repos.csv"; await ExportHelper.ExportToCsvAsync(repos, csvPath); Console.WriteLine($"仓库列表已导出到: {Path.GetFullPath(csvPath)}");4.4 导出 CSV 的工具类
为了让表格能在 Excel / WPS 里直接打开,我们再写一个 CSV 导出工具。注意描述字段经常包含逗号和双引号,必须做转义,否则导出的 CSV 会错列。
using System.Text; using GitHubRepoCrawler.Models; namespace GitHubRepoCrawler.Services; public static class ExportHelper { public static async Task ExportToCsvAsync(List<GitHubRepo> repos, string path) { var sb = new StringBuilder(); sb.AppendLine("name,full_name,language,description,stargazers_count,forks_count,html_url,updated_at"); foreach (var repo in repos.OrderByDescending(r => r.StargazersCount)) { var line = string.Join(",", CsvEscape(repo.Name), CsvEscape(repo.FullName), CsvEscape(repo.Language ?? ""), CsvEscape(repo.Description ?? ""), repo.StargazersCount.ToString(), repo.ForksCount.ToString(), CsvEscape(repo.HtmlUrl), repo.UpdatedAt.ToString("yyyy-MM-dd")); sb.AppendLine(line); } await File.WriteAllTextAsync(path, sb.ToString(), Encoding.UTF8); } private static string CsvEscape(string value) { if (value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r')) { return $"\"{value.Replace("\"", "\"\"")}\""; } return value; } }CSV 转义规则并不复杂:如果字段里包含逗号、双引号或换行,就用双引号把整个字段包起来,字段内的双引号用两个双引号代替。这是标准 CSV 的处理方式。
4.5 运行与验证
回到终端,运行项目:
dotnet run首次运行时,如果本机没有配置GITHUB_TOKEN,程序也能运行,但受限流影响,每小时只能请求 60 次。建议先设置环境变量:
# Windows PowerShell $env:GITHUB_TOKEN = "你的token" # macOS / Linux export GITHUB_TOKEN="你的token"以某个仓库数量较多的账号为例,正常输出如下:
请输入 GitHub 用户名: octocat 正在从 GitHub API 拉取 octocat 的公开仓库... 共获取 8 个仓库(已排除 fork)。 2626 ★ Hello-World octocat/Hello-World 136 ★ Spoon-Knife octocat/Spoon-Knife 1 ★ octocat.github.io octocat/octocat.github.io 仓库列表已导出到: /Users/xxx/GitHubRepoCrawler/repos.csv打开repos.csv,就能看到一个带表头的仓库清单,可以直接用 Excel 打开做进一步筛选。
5. 功能扩展
基础功能已经跑通,接下来可以按实际需要扩展。
5.1 按条件筛选
拿到完整仓库列表后,可以用 LINQ 做各种筛选,不需要额外请求接口。
只看 C# 项目:
var csharpRepos = repos .Where(r => string.Equals(r.Language, "C#", StringComparison.OrdinalIgnoreCase)) .OrderByDescending(r => r.StargazersCount) .ToList();只看 Star 数超过 100 的项目:
var hotRepos = repos .Where(r => r.StargazersCount >= 100) .OrderByDescending(r => r.StargazersCount) .ToList();筛选逻辑写在内存里,速度非常快,也比反复调 API 更节省配额。
5.2 批量下载仓库源码包
如果你想把某个账号的所有仓库源码备份到本地,可以调 GitHub 的存档下载地址。下载链接的格式是:
https://github.com/{owner}/{repo}/archive/refs/heads/{default_branch}.zip在GitHubApiClient中增加一个下载方法:
public async Task DownloadArchiveAsync(GitHubRepo repo, string outputDir) { if (repo.IsFork) { return; } Directory.CreateDirectory(outputDir); var zipUrl = $"https://github.com/{repo.FullName}/archive/refs/heads/{repo.DefaultBranch}.zip"; var filePath = Path.Combine(outputDir, $"{repo.Name}-{repo.DefaultBranch}.zip"); using var response = await _http.GetAsync(zipUrl); if (!response.IsSuccessStatusCode) { Console.WriteLine($"下载失败: {repo.FullName},状态码: {(int)response.StatusCode}"); return; } await using var fileStream = File.Create(filePath); await response.Content.CopyToAsync(fileStream); Console.WriteLine($"下载完成: {filePath}"); }然后在 Program.cs 中调用:
Console.Write("是否下载全部仓库的 zip 包? (y/n): "); if (Console.ReadLine()?.Trim().ToLower() == "y") { var outputDir = Path.Combine(Directory.GetCurrentDirectory(), "downloads"); foreach (var repo in repos) { await client.DownloadArchiveAsync(repo, outputDir); } }这个链接默认是跟随重定向到codeload.github.com的,HttpClient 默认会自动处理重定向,所以直接请求即可。
5.3 使用搜索接口
如果不想先拉全部仓库再筛选,也可以用 GitHub 搜索接口直接在服务端过滤:
GET https://api.github.com/search/repositories?q=user:{username}+language:C%23&per_page=100在GitHubApiClient中增加搜索方法:
public async Task<List<GitHubRepo>> SearchReposAsync(string query) { var url = $"{BaseUrl}/search/repositories?q={Uri.EscapeDataString(query)}&per_page={PerPage}"; var response = await _http.GetAsync(url); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var searchResult = JsonSerializer.Deserialize<SearchResult>(json); return searchResult?.Items ?? new List<GitHubRepo>(); } private class SearchResult { [JsonPropertyName("items")] public List<GitHubRepo> Items { get; set; } = new(); }调用方式:
var result = await client.SearchReposAsync("user:octocat language:C#");注意搜索接口的配额限制独立计算,未认证时每小时只有 10 次,认证后是每小时 30 次。如果只是拉取某个账号的全部仓库,用普通列表接口就够了。
6. 常见问题与排查
实际运行中,最容易遇到下面几类问题:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 返回 401 Unauthorized | Token 无效或已过期 | 重新生成 Token,检查环境变量是否生效 |
| 返回 403 Forbidden | 缺少 User-Agent,或触发限流 | 添加 User-Agent 请求头;等待配额重置 |
| 返回 0 个仓库 | 用户名不存在,或全部是 fork 且被过滤 | 先确认用户名拼写;临时设置 includeForks: true |
| 只拿到 30 条 | 没有设置 per_page=100 | 请求 URL 中补充 per_page 参数 |
| 反序列化后字段全是默认值 | 缺少 JsonPropertyName 映射 | 检查属性名是否和 JSON 小写下划线命名对应 |
| 中文描述乱码 | 控制台编码问题 | 设置 Console.OutputEncoding = Encoding.UTF8 |
| 请求超时 | 网络不通或连接不稳定 | 检查本机能否正常访问 api.github.com;适当调大 HttpClient.Timeout |
| 搜索接口返回 422 | 查询语法有问题 | 检查 q 参数是否 URL 编码,空格用 + 代替 |
逐个解释容易踩坑的几个。
404 用户不存在时,EnsureSuccessStatusCode会抛异常,程序直接中断。如果你想更友好地提示,可以改成:
if (response.StatusCode == HttpStatusCode.NotFound) { Console.WriteLine($"用户 {username} 不存在,请检查拼写。"); return new List<GitHubRepo>(); }403 有两种常见情况。一种是没有 User-Agent,这种情况加上头就好。另一种是限流,响应头里的X-RateLimit-Remaining会变成 0,此时不要再继续循环,应该等待X-RateLimit-Reset时间戳对应的时间后再请求。
中文乱码问题在 Windows 控制台尤其明显。Windows 默认代码页可能是 GBK,而输出 UTF-8 字符串时就会显示成乱码。在 Program.cs 开头加一行Console.OutputEncoding = Encoding.UTF8;即可,macOS 和 Linux 终端一般不需要。
7. 工程化最佳实践
小工具写完能跑,只是第一步。如果要放到团队内部持续使用,下面这些点值得认真考虑。
7.1 凭据管理
Token 绝对不要硬编码在代码里,也不要提交到 Git 仓库。用环境变量读取是最简单的方式:
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");如果是 Visual Studio 项目,也可以用 User Secrets 保存开发环境的 Token。生产环境如果跑在服务器上,建议结合密钥管理服务,例如 Azure Key Vault 或云厂商的密钥管理产品。
原则只有一个:Token 属于敏感信息,最小范围保存,最小权限使用。读公开仓库时,不需要给 Token 任何写权限。
7.2 限流与重试
即使带了 Token,仍然可能触发限流。比较稳妥的做法是:
- 请求前检查
X-RateLimit-Remaining,当剩余次数很少时主动暂停。 - 触发限流后,读取
Retry-After或X-RateLimit-Reset,等到重置时间再继续。 - 对网络异常做重试,重试间隔采用指数退避,比如 1 秒、2 秒、4 秒。
一个简单的指数退避重试逻辑可以这样写:
for (int retry = 0; retry < 3; retry++) { try { var response = await _http.GetAsync(url); response.EnsureSuccessStatusCode(); return response; } catch (HttpRequestException) when (retry < 2) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retry))); } }7.3 增量同步与本地缓存
如果每天都要拉取同一批仓库,每次都全量请求很浪费配额。可以利用仓库的updated_at字段做增量更新:第一次全量拉取后,把数据存到本地 JSON 文件;下次只对比时间,重新拉取更新时间更晚的仓库。
当然 GitHub API 的列表接口本身也支持since参数,但对“某个用户的全部仓库”这种场景,本地缓存往往更简单可控。
7.4 并发控制
批量下载 zip 包时,并发能明显提速,但也不能无脑并发。用SemaphoreSlim限制同时下载的任务数,例如同时最多下载 5 个:
using var semaphore = new SemaphoreSlim(5); var tasks = repos.Select(async repo => { await semaphore.WaitAsync(); try { await client.DownloadArchiveAsync(repo, outputDir); } finally { semaphore.Release(); } }); await Task.WhenAll(tasks);这样做既提升效率,又不会因为并发过高导致网络连接被 GitHub 拒绝。
7.5 合规边界
这个工具读的是公开数据,用的是官方 API,本身没有合规问题。但要注意几点:
- 控制请求频率,不要用超高频请求骚扰服务器。
- 拉取的数据如果用于商业用途,注意遵守 GitHub 的服务条款和数据处理要求。
- 如果涉及私有仓库,必须确保你有合法授权,并且遵循最小权限原则。
一句话总结:官方 API 能做到的事,优先用 API;不要为了“爬虫”两个字去抓 HTML,既不高效也不安全。
8. 总结与下一步
到这一步,我们已经用 C# 完成了一个能用的 GitHub 仓库抓取工具:
- 支持按用户名拉取全部公开仓库。
- 自动处理分页,最多每页 100 条。
- 支持排除 fork 仓库。
- 支持按 Star 数排序和打印。
- 支持导出 CSV。
- 支持扩展批量下载源码包和搜索接口。
代码量不大,但对新手来说,完整走一遍还是能学到不少东西:HttpClient 请求头设置、System.Text.Json 反序列化、分页循环的判断、CSV 特殊字符转义、限流响应处理,这些都是日常开发里非常常用的能力。
下一步如果想继续深入,方向也很清晰:
- 把控制台工具改造成 WinForm / WPF 界面,输入用户名、点击按钮就能看到仓库表格。
- 增加统计功能,比如按语言分组统计仓库数量,画出饼图。
- 换成 GitHub GraphQL API,一次请求拿更多字段,减少请求次数。
代码写好了,接下来就看你拿它去整理哪位大佬的仓库了。建议先拿一个仓库数量不多的账号试跑一遍,确认输出格式符合预期,再处理几百个仓库的大账号,避免首次运行就触发限流。