事情的起因

一个用了好几年的 GitHub 账号,说没就没了。

收到那封来自 GitHub 的邮件时,我第一反应倒不是心疼代码——本地和远程都有备份——而是脑子里嗡了一下:那几百个 Star 怎么办?

从入门时收藏的第一个 awesome 清单,到后来陆陆续续标记的各种工具库、轮子、教程,几年攒下来全锁在一个再也登不进去的账号里。手动重新搜一遍再点一遍 Star?几百个仓库,搜到明年也搜不完,更何况很多仓库当时为什么 Star 早忘了,连名字都记不全。

于是写了个脚本,跑了几分钟,全部搬完。这篇文章把思路和代码都放出来,希望你永远用不上,但真到那天不至于抓瞎。

先花一分钟说说「被封」这件事

很多人以为封号就是登录报错,其实 GitHub 有个更隐蔽的机制叫 flag:被标记的账号,你自己登录进去看,一切正常——主页在、仓库在、Star 也在;但换个无痕窗口(或者让朋友)去访问你的主页,得到的是一个冷冰冰的 404。

这意味着两件事:一是你可能被封了很久都没发现;二是自查方法特别简单——退出登录或开无痕窗口访问自己的主页,能看到就没被标记,看不到就该去 GitHub 官方的申诉页面提交申诉了。

至于为什么会被封,社区里被提及最多的几种情况:一台设备上来回登录多个账号、IP 频繁变动、以及各种账号关联行为。申诉能不能成看运气,所以重要数据永远别只放在一个账号里——这也是下面整个迁移脚本的出发点。

这个脚本能成立的原理:Star 列表是公开的

很多人没意识到一点:GitHub 的 Star 列表是公开数据。哪怕账号被封,只要数据还在,任何人不登录都能通过 API 读到某个用户 Star 过哪些仓库:

https://api.github.com/users/用户名/starred

返回的是 JSON,一页最多 100 条,用 page 参数往后翻。所以「拿到旧账号的收藏清单」这一步,完全不需要旧账号的任何凭证——这大概是整个方案里最让人松一口气的部分。

真正需要 Token 的是另一半:用新账号把仓库重新 Star 一遍。对着 PUT /user/stared/仓库名 发一个请求,返回 204 就是 Star 成功了。

一句话概括整个流程:公开接口拉清单 → 拿着新账号的 Token 逐个 PUT 回去。

代码

依赖只有 requests,改三个常量就能跑:

import requests
import time

# 你的新 GitHub 用户名
NEW_USERNAME = "***"
# 新账号的 GitHub Token(必须有 public_repo 权限)
GITHUB_TOKEN = "ghp_****"
# 被封禁的用户名
BANNED_USERNAME = "***"

# API 地址
STARRED_API = f"https://api.github.com/users/{BANNED_USERNAME}/starred"
HEADERS = {
    "Authorization": f"token {GITHUB_TOKEN}",
    "Accept": "application/vnd.github.v3+json"
}

def get_all_starred_repos():
    """获取被封禁账号的所有 Star 仓库列表"""
    repos = []
    page = 1
    print(f"正在获取 @{BANNED_USERNAME} 的所有 Star 仓库...")

    while True:
        url = f"{STARRED_API}?page={page}&per_page=100"
        resp = requests.get(url)

        if resp.status_code != 200:
            print(f"获取失败,状态码: {resp.status_code}")
            break

        data = resp.json()
        if not data:
            break

        for repo in data:
            repos.append({
                "full_name": repo["full_name"],
                "url": repo["html_url"]
            })

        print(f"已获取第 {page} 页,累计:{len(repos)} 个")
        page += 1
        time.sleep(1)  # 防限流

    print(f"\n总共获取到 {len(repos)} 个 Star 仓库")
    return repos

def star_repos(repos):
    """批量给新账号 Star 仓库"""
    success = 0
    failed = []
    total = len(repos)

    print(f"\n开始批量 Star 到 @{NEW_USERNAME}...")

    for i, repo in enumerate(repos, 1):
        full_name = repo["full_name"]
        url = f"https://api.github.com/user/starred/{full_name}"

        try:
            resp = requests.put(url, headers=HEADERS)

            if resp.status_code in (204, 404):
                # 204 = Star 成功
                # 404 = 仓库不存在/已删除,跳过
                success += 1
                print(f"[{i}/{total}] {full_name}")
            else:
                failed.append(full_name)
                print(f"[{i}/{total}] 失败 {full_name} | 状态码: {resp.status_code}")

            time.sleep(2)  # 防 GitHub API 限流

        except Exception as e:
            failed.append(full_name)
            print(f"[{i}/{total}] 异常 {full_name} | {str(e)}")

    # 最终统计
    print("\n" + "="*50)
    print(f"迁移完成!")
    print(f"成功 Star:{success} 个")
    print(f"失败/不存在:{len(failed)} 个")

    if failed:
        print("\n失败列表(可手动检查):")
        for f in failed:
            print(f"  - {f}")

if __name__ == "__main__":
    starred_repos = get_all_starred_repos()
    star_repos(starred_repos)

Token 怎么生成

新账号的 Token 是唯一需要提前准备的东西,步骤:

  • 打开 GitHub,右上角头像 → Settings
  • 左侧栏拉到最底下 → Developer settingsPersonal access tokensTokens (classic)
  • Generate new token (classic)
  • 权限只勾 public_repo 就够了——Star 是个公开操作,没必要给更多权限,令牌权限最小化永远是好习惯
  • 生成后立刻复制(离开页面就再也看不到了),填进脚本:
NEW_USERNAME = "你的新GitHub用户名"
GITHUB_TOKEN = "你刚复制的token"
BANNED_USERNAME = "你的旧Github用户名"

跑之前最好知道的几件事

限流是真实存在的。 GitHub API 不带 Token 每小时只能请求 60 次,带了 Token 是 5000 次。拉清单那步虽然不用 Token 也行,但脚本里两处 time.sleep 都是给限流留的余量,几百个仓库跑个十几分钟很正常,别嫌慢。

状态码的含义要对得上。 204 才是 Star 成功;404 说明那个仓库本身没了(作者删库、转私有),这不是你的问题,跳过就好。脚本里把 404 也算进了成功数——毕竟「该 Star 的都处理过了」,如果你想把这类死链单独拎出来统计,改一下那个 if 分支就行。

失败列表记得看一眼。 最后打印出来的 failed 列表通常是网络抖动或偶发限流,隔一会儿重跑一次脚本,已 Star 过的会秒过(重复 PUT 没有副作用),不用怕跑两遍。

更好的做法:别等封号才想起备份

这次是有惊无险地搬完了,但回头想想还是后怕——如果那个账号是被彻底删除而不是 flag,公开 API 也救不回来。

Star 列表说到底是自己的知识资产,与其指望平台,不如定期把它变成一份攥在自己手里的文件。几个思路:

  • 导出成 Markdown 或 CSV:社区里现成的导出脚本不少,导出来的表格能直接扔进 Obsidian、Notion 这类笔记工具,平时翻起来比 GitHub 自带的 Star 页还好用
  • GitHub Action 定期跑:写个定时任务每周自动导出一次,提交到私有仓库,从此有一份带版本历史的收藏记录,新 Star 了什么、取关了什么,diff 一目了然
  • 顺手喂给 AI:导出的列表丢给大模型,让它帮你归类整理,「我到底收藏了哪些和测试相关的库」这种问题一问就有答案

说到底,这次迁移脚本解决的是「账号没了怎么抢救」,而定期备份解决的是「根本不需要抢救」。前者是应急,后者才是习惯。

希望你的账号永远平安,但备份这件事,今天就可以开始。