拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyGithub实战:从GitHub API封装到自动化运维的完整指南

PyGithub实战:从GitHub API封装到自动化运维的完整指南 1. 为什么我选择PyGithub而不是直接用Requests调API1.1 PyGithub到底帮你省了什么事如果你写过一段时间的GitHub自动化脚本大概率绕不开PyGithub这个库。它的定位很简单把GitHub REST API的JSON请求/响应封装成一组Python对象和方法。你不需要自己拼URL、处理认证头、解析返回体也不需要关心分页游标怎么翻直接操作Repository、Issue、PullRequest这些对象就能完成绝大部分工作。举个例子用原生requests获取某个仓库的star数你需要这么写import requests headers {Authorization: Bearer YOUR_TOKEN, Accept: application/vnd.githubjson} resp requests.get(https://api.github.com/repos/octocat/Hello-World, headersheaders) data resp.json() print(data[stargazers_count])而用PyGithub核心逻辑就是两行from github import Github gh Github(YOUR_TOKEN) repo gh.get_repo(octocat/Hello-World) print(repo.stargazers_count)看到差别了吗PyGithub把HTTP状态码判断、JSON解析、字段映射全部吞掉了。你拿到的repo就是一个带属性的对象repo.stargazers_count、repo.full_name、repo.default_branch直接可用。对于要写几十上百行自动化逻辑的人来说这个抽象价值极大——它让你把精力花在业务上而不是反复处理接口层的脏活。另外PyGithub的PaginatedList是我最喜欢的特性之一。GitHub API默认一次最多返回100条记录如果不用库你得自己解析Link响应头里的next、last再拼URL循环请求。PyGithub把这个过程变成了一个惰性加载的列表直接for issue in repo.get_issues():就能从头翻到尾底层自动处理分页。1.2 认证方式与Token准备使用PyGithub绕不开认证。GitHub官方推荐用Personal Access Token来访问APIToken的申请路径是右上角头像 - Settings - Developer settings - Personal access tokens。这里有两个入口Fine-grained tokens细粒度Token和Tokens (classic)经典Token。经典Tokenclassic粗粒度授权勾选repo范围就能读写所有仓库对个人维护的小工具来说够用配置也简单。细粒度Tokenfine-grained可以指定某个仓库、某类权限比如只读内容、只写Issues更安全但配置要细心。我建议新项目尽量用细粒度Token把权限收敛到最小范围尤其是脚本可能被别人接手时。拿到Token后最不该做的事就是把它硬编码到脚本里。我见过太多人把Token贴在代码里然后整个仓库推上GitHub接着收到一堆扫描机器人的盗刷告警。正确做法是用环境变量管理import os from github import Github gh Github(os.environ[GITHUB_TOKEN]) user gh.get_user() print(user.login)这个user.login打印出来的就是你的用户名也是验证认证是否成功最快的办法。如果你在公司内网部署了GitHub Enterprise初始化时还要指定base_urlgh Github(base_urlhttps://github.example.com/api/v3, login_or_tokenTOKEN)base_url指向企业版API的/v3路径这一点和网上大多数教程只说公共GitHub略有不同集成企业版时特别容易漏掉。1.3 第一次调用该做什么认证完后我建议先跑一个最小验证脚本确认三件事Token有效、网络通、权限范围符合预期。除了gh.get_user().login还可以打印一下认证用户的跟随者数、公开仓库数user gh.get_user() print(f用户 {user.login}公开仓库 {user.public_repos} 个)如果Token写错了通常会抛出github.BadCredentialsException错误信息是经典的401。此时先检查环境变量是否真的传进去了再检查Token是否过期——细粒度Token我会设置过期时间避免长期泄露。补充一点如果你不传Token直接Github()PyGithub会以匿名模式访问此时API限流是每小时60次只适合调试时打印公开数据。写自动化工具时别这么干分分钟被限流。2. 高频操作拆解仓库、Issue与Pull Request的完整用法2.1 从拿到仓库到自如操作仓库是GitHub操作的核心载体。PyGithub里get_repo(owner/repo)的参数是owner/name格式这个格式要记牢很多新手在这里卡住以为传仓库名就行。拿到Repository对象后高频使用的属性有这些repo.full_name完整的owner/name字符串打日志时很有用。repo.clone_urlHTTPS克隆地址。repo.default_branch默认分支名通常是main或master分支操作经常要用。repo.open_issues_count当前Open状态的Issue数量统计面板常用。repo.private是否为私有仓库。除了读属性写操作也常常走repo对象。比如我需要快速建一个Issue收集Bug反馈repo gh.get_repo(my-org/my-project) issue repo.create_issue( title[Bug] 登录页在移动端布局错乱, body设备iPhone 14\n系统iOS 17\n步骤打开登录页后切换横屏, labels[bug, high-priority], assignees[developer_a], ) print(issue.number)create_issue返回的是一个Issue对象issue.number就是Issue编号后续所有操作都可以围绕这个编号展开。值得注意的是labels参数接收的是标签名字符串列表如果标签不存在GitHub会自动创建这一点比Web端手动维护标签方便很多。2.2 Issue从创建到关闭的一条龙拉取Issue列表时get_issues()的过滤参数值得花点心思。最常用的组合是state加labelsopen_bugs repo.get_issues(stateopen, labels[bug]) for issue in open_bugs: print(f#{issue.number} {issue.title} by {issue.user.login})state有三个取值open、closed、all。labels可以传一个标签组成的列表支持同时过滤多个标签。如果还想筛时间可以用since参数它接收一个datetime对象只返回该时间之后更新的Issue——不过要注意这是按updated_at过滤不是created_at刚接触时容易踩。单个Issue的更新也很顺手。比如批量把旧Issue标注为过期并把状态改为关闭for issue in repo.get_issues(stateopen): if issue.updated_at.year 2023: issue.create_comment(该Issue长期无活动自动关闭。) issue.edit(stateclosed)issue.edit()是万金油支持修改标题、正文、标签、里程碑、指派人甚至可以一次性关闭。issue.create_comment()则负责追加评论评论内容支持Markdown语法所以你要自动发图表、发链接都可以拼字符串进去。还有一个细节issue.labels返回的是Label对象列表label.name才是标签的字符串名称。调试时别直接print整个list不然看到一坨内存地址半天找不到原因。2.3 Pull Request的常规操作PR的操作逻辑和Issue类似但多了合并的环节。创建PR是repo.create_pullpr repo.create_pull( titlefeat: 增加订单导出功能, body实现订单导出为CSV包含筛选和分页。, headfeature/export-orders, basemain, )这里head是源分支base是目标分支这个顺序很多人第一次会写反导致PR方向错误。创建后pr.number同样是对外标识。合并PR时我一般显式指定合并方式避免GitHub默认策略产生一堆多余的merge commitpr repo.get_pull(42) if pr.mergeable: pr.merge(merge_methodsquash, commit_messagefeat: 增加订单导出功能 (#42)) else: print(PR存在冲突不能合并)merge_method有三个值merge普通合并、squash压缩合并、rebase变基合并。团队如果追求干净的历史通常选squash。pr.mergeable是GitHub后台计算出来的结果刚创建的PR可能要等几秒才有值脚本里记得轮询判断。另外PR本质上是一种特殊的Issue所以create_issue_comment也可以用在PR上发普通评论。而代码行的审查评论则用pull.get_review_comments()读取pull.get_reviews()看Review结论。真正给PR提交一个整体Reviewapprove、request changes这些用的是pull.create_review()需要在body里写评论内容比如pull.create_review(body逻辑没问题注意补一下测试用例。, eventAPPROVE)event还有REQUEST_CHANGES、COMMENT两个常用值。这个接口可以让你在自动化流程里直接完成代码评审动作相当省事。3. 文件、分支与CI状态代码库资产的操作细节3.1 读取仓库文件与目录树访问仓库内容用get_contents。单文件场景下它会返回一个ContentFile对象而目录场景下会返回一个元素为ContentFile的列表readme repo.get_contents(README.md) print(readme.decoded_content.decode(utf-8))注意decoded_content是bytes类型必须调用.decode(utf-8)才能得到字符串。中文内容如果没按UTF-8解码会出现乱码。想获取整个仓库的文件列表逐个目录递归太慢了。我用得最多的反而是一次性拿整棵Git树tree repo.get_git_tree(repo.default_branch, recursiveTrue) for element in tree.tree: if element.type blob: print(element.path, element.size)get_git_tree的recursiveTrue会把所有层的文件路径都拉出来适合做全仓库扫描、关键词检索、文件大小统计这类任务。拿到element.path之后再通过get_contents去读取具体文件内容。这里的element.sha也可以配合get_blob直接读取不过一般用get_contents更直观。3.2 分支与引用的创建、删除创建分支在Web上很简单用PyGithub也不难但操作对象是GitRef而不是Branch。逻辑是先找到源分支通常是默认分支的最新提交SHA然后以它为基准创建新引用source_branch repo.get_branch(repo.default_branch) repo.create_git_ref(refrefs/heads/feature/new-feature, shasource_branch.commit.sha)这里的ref前缀refs/heads/必须带全省略了会直接报错或创建出奇怪的引用。source_branch.commit.sha拿到的是当前分支最新一次提交的完整SHA。读分支列表则是repo.get_branches()遍历后branch.name就是分支名。删除分支时通过get_git_ref拿到引用再删ref repo.get_git_ref(heads/feature/new-feature) ref.delete()老版本的PyGithub里还要传refs/heads/前缀新版本直接传heads/...即可Repo.get_git_ref会自动补全。删除分支这个操作不可逆脚本里最好先打印日志确认分支名再执行删除。3.3 查看提交状态与CI是否通过我在做交付门禁自动化时最关心的就是最后几个Commit的CI状态。PyGithub里repo.get_commits()会返回最新的提交列表每个Commit对象可以进一步查状态commit repo.get_commit(分支或提交SHA) status commit.get_combined_status() print(status.state) # success / failure / pending / no status check_runs commit.get_check_runs() print(f通过 {check_runs.total_count} 个Check Run)get_combined_status返回的是Commit Status的整体状态适合快速判断CI红绿。get_check_runs对应的是GitHub Actions的Check Runstotal_count表示Actions任务总数如果你要确认某个具体工作流是否通过可以遍历check_runs里的对象打印run.name和run.status。需要留意的是get_commits()返回的是默认分支上的提交不一定包含PR源分支的提交。想查PR里最新提交的状态先pr.get_commits().reversed[0]取最新提交再查它的状态。这也是我踩过的一个坑——直接在仓库提交列表里找PR头部的SHA结果找不到。4. 一个能直接跑的自动化示例批量导出Open Issues并自动补Label4.1 场景与设计思路为了把前面的用法串起来我分享一个实际帮我节省大量时间的脚本场景。假设你维护一个中大型开源仓库平均每周会收到几十个Issue大部分还没人来得及打Label导致后续筛选和统计非常痛苦。我的做法是写一个脚本完成三件事把所有Open状态的Issue导出成一个CSV文件包含编号、标题、创建人、创建时间、Labels、链接。统计目前哪些Label使用最多方便团队调整标签体系。给没有Label的Issue自动打上triage标签确保没有遗落的孤儿Issue。这样每天早上跑一次整个团队对Issue健康度就一目了然。4.2 完整代码import csv import os from datetime import datetime from collections import Counter from github import Github gh Github(os.environ[GITHUB_TOKEN]) repo gh.get_repo(your-org/your-repo) issues [] label_counter Counter() triage_label None try: triage_label repo.get_label(triage) except Exception: triage_label repo.create_label(nametriage, colorf9d0c4) for issue in repo.get_issues(stateopen): if issue.pull_request: # 跳过PR只看Issue continue label_names [label.name for label in issue.labels] label_counter.update(label_names) if not label_names: issue.add_to_labels(triage_label) label_counter[triage] 1 issues.append({ number: issue.number, title: issue.title, author: issue.user.login, created_at: issue.created_at.isoformat(), labels: , .join(label_names), url: issue.html_url, }) with open(open_issues.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[number, title, author, created_at, labels, url]) writer.writeheader() writer.writerows(issues) print(f共导出 {len(issues)} 个Issue输出 open_issues.csv) for label_name, count in label_counter.most_common(10): print(fLabel[{label_name}]: {count})4.3 执行结果与后续扩展这个脚本跑完终端会打印每个标签的分布情况我通常直接拿去和团队同步。CSV文件用Excel打开乱码的问题在写入时加了utf-8-sig编码解决。issue.pull_request这个字段是Google了很久才找到的小技巧——因为GitHub的Issue列表会混入Pull Request不加这个判断统计就永远不准。后续我又在脚本上做了两个扩展一是把结果自动提交到仓库的reports/目录二是用调度工具每天9点执行把CSV变化同步到团队群。这些都是Python代码里加几行create_commit或Webhook调用的事PyGithub的抽象让后续迭代成本降低了不少。5. 我在实战中踩过的坑和使用建议5.1 限流RateLimit与重试策略GitHub API的限流是每个自动化开发者迟早要面对的墙。未认证的请求每小时60次用Token认证后是每小时5000次——听起来很多但一旦你循环遍历几百个仓库的Issue几千次配额很快见底。建议是脚本开头先探一下配额剩余rate_limit gh.rate_limiting print(剩余配额:, rate_limit[1])当触发限流时PyGithub会抛RateLimitExceededException错误信息会提示期待的reset时间。我的处理方式很简单捕获异常后计算出还需要等多少秒然后sleep到那个时间点再继续import time from github import GithubException try: do_something() except GithubException as exc: if API rate limit exceeded in str(exc): time.sleep(60) do_something() else: raise大规模任务更要主动控制节奏不要一口气循环大量调用。可以用time.sleep(0.1)在每个请求之间做限速虽然会拖慢总体时间但至少不会因为限流白跑一小时。5.2 权限不足的404陷阱与错误处理PyGithub的异常体系值得提前了解。最上层是GithubException所有接口异常都继承了它。再细分有BadCredentialsException认证失败、UnknownObjectException对象不存在、RateLimitExceededException限流、BadUserAgentExceptionUser-Agent问题等。我在排查问题时最警惕的是权限不足导致404。GitHub API为了安全部分接口在Token权限不够时不会返回403而是假装资源不存在返回404。这导致你排查起来非常费劲明明仓库存在get_repo却抛UnknownObjectException。遇到这种情况先确认Token的权限范围而不是怀疑仓库名写错了。我的建议是写一段异常处理模板把所有操作的异常先兜住再输出完整的异常信息定位try: repo.create_issue(titletest) except GithubException as exc: print(fstatus{exc.status}, data{exc.data})exc.status是HTTP状态码exc.data是GitHub返回的错误体包含message和文档链接。打日志时这两项缺一不可。5.3 时间、分页和性能的细节PyGithub返回的created_at、updated_at都是datetime对象但要注意它们是UTC时区的naive datetime不带时区信息。直接拿去和本地时间比较会莫名其妙差8小时。稳妥的写法是先给它加上UTC时区再转换成目标时间from datetime import timezone, timedelta local_time issue.created_at.replace(tzinfotimezone.utc).astimezone(timezone(timedelta(hours8)))分页遍历大列表时PaginatedList是惰性加载的每次翻页才发请求。如果你只需要前50条直接切片即可不要用list()把整个列表一次性拉下来。反过来如果确实需要全量数据再考虑用get_page(page)手动控制页码。还有一个隐藏细节get_issues(since...)对since参数有较小的使用窗口GitHub只在这个时间窗口内做有效过滤而且只针对updated_at。所以别指望用since实现精确的创建时间筛选控制粒度最可靠的方式还是拉全量后自己用created_at过滤。5.4 删除类操作务必三思PyGithub把删除操作的API封装得很顺手顺手到危险。比如repo.delete()会直接删除整个远端仓库没有二次确认ref.delete()删除分支也是立刻生效。我亲眼见过有人在循环里写错分支名把所有feature分支删掉大半的情况。我的对策是两条所有涉及删除的脚本必须加--dry-run参数。默认只打印会删除什么加了这个参数才真正执行避免一个True写错造成事故。删除操作前先通过try获取对象进行一次只读校验确保拿到的就是你要处理的引用或分支。批量改动逻辑里删除永远是最脆弱的一环。写代码时把删除函数单独抽出来不要和其他操作混在一起出问题时排查范围会缩小很多。另外如果你在调试GitHub Actions相关的自动化有个小技巧能省很多时间在操作请求前后打印gh.rate_limiting和返回给前端的raw_headers确认接口是否真的按预期调用成功。PyGithub对象里很多返回都带raw_data字段打印出来能看到GitHub返回的原始JSON结构这比瞎猜字段名快得多。写自动化脚本本来就是一步步踩坑走出来的。我的经验是先把小的读操作跑通确认Token、权限、对象模型都对再去碰写操作和删除操作。这样即使出错损失也控制在一次可逆的Issue评论之内。希望这篇关于PyGithub用法详解的实战记录能帮你少走点弯路。
返回列表