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

资讯详情

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

go-github scrape 包实战指南:用屏幕抓取访问 REST/GraphQL API 无法覆盖的 GitHub 数据

go-github scrape 包实战指南:用屏幕抓取访问 REST/GraphQL API 无法覆盖的 GitHub 数据
  • 后端
  • API设计

【免费下载链接】go-github

Go library for accessing the GitHub v3 API

项目地址:https://gitcode.com/GitHub_Trending/go/go-github
点击查看免费下载

导读

在 go-github 主库之外,scrape/README.md 定义了一个独立的实验性子包github.com/google/go-github/scrape,它以"屏幕抓取(screen scraping)"的方式访问 GitHub 网页,专门用于获取 REST 与 GraphQL API 尚未暴露的数据。本文以该文档为主线,结合 scrape/scrape.go、scrape/apps.go、scrape/forms.go 等源码实现与测试用例,系统讲解 scrape 包的定位、设计原则、初始化与认证方式、核心 API、表单提交机制,以及如何按文档规范为它扩展新的数据抓取方法。读完本文,你将掌握在 go-github 生态内补全 API 缺口数据的完整技术路径。


一、scrape 包的定位:API 覆盖之外的"最后手段"

scrape 包是 go-github 仓库(GitHub_Trending/go/go-github)中的独立子模块,通过 scrape/go.mod 以module github.com/google/go-github/scrape单独管理依赖,其包级注释(见 scrape/scrape.go)明确说明:

  • 它用于补充标准 go-github 库,访问当前官方 REST 或 GraphQL API未暴露的数据;
  • 由于屏幕抓取依赖网页标记结构,该包被标记为HIGHLY EXPERIMENTAL(高度实验性),API 可能不稳定;
  • 虽然随 go-github 库一起分发,但它明确豁免于库版本号所暗示的任何稳定性承诺。

README 开篇给出的核心定位是一句话:"It is designed to be a client of last resort for data that cannot be retrieved via the REST or GraphQL APIs."——即它是获取 REST/GraphQL 拿不到的数据时的最后手段,而非日常首选工具。因此,在使用前应优先确认目标数据是否可以通过主库的 REST 接口(github包)获取,能通过 API 拿到的数据一律不要走屏幕抓取。

与主库的关系

从代码依赖关系看,scrape 包反向依赖主库:例如 scrape/apps.go 引入了github.com/google/go-github/v92/github,在AppManifest结构体中直接复用主库的github.InstallationPermissions类型来声明 GitHub App 的权限集合。这说明 scrape 不是与主库平行的独立体系,而是主库生态中"补充数据渠道"的插件式存在。


二、设计三原则:什么该被加入 scrape 包

README 用三条原则划定了该包的内容边界,任何贡献代码都必须遵守:

1. Add only what you need(只添加你真正需要的)

与主库"尽量实现整个 GitHub REST API"的目标相反,scrape 包无意穷举覆盖所有 GitHub 页面数据。文档明确表示:欢迎为获取实际需要的数据提交补丁(patches),但作者不愿意在此尝试提供全量覆盖。

这意味着该包的 API 面是"按需生长"的——当前仓库中仅存在少量方法(组织 OAuth 应用策略、支付信息、创建 App 等),这正是该原则的直接体现。

2. Add only what can't be accessed elsewhere(只添加无法通过其他途径访问的数据)

如果目标数据可以通过 REST 或 GraphQL API 获取,就应当使用对应的库(主 go-github 库 / GraphQL 库),而不是屏幕抓取。这一原则保证了 scrape 包与官方 API 不产生重复覆盖,也降低了维护成本——毕竟网页结构随时可能变化。

3. Prefer read-only access(优先只读访问)

当前阶段作者只聚焦于读取数据。文档指出写操作"也许同样可用,但风险显然大得多"。仓库中唯一的写操作例外是 scrape/apps.go 中的CreateApp(通过 manifest 创建 GitHub App),它直接向/settings/apps/new或/organizations/{org}/settings/apps/new提交 POST JSON 请求,而不是走表单流程——这是一个值得注意的边界案例:即使是写操作,也尽量使用 GitHub 提供的 JSON 端点而非模拟表单提交。


三、客户端初始化:NewClient 与底层结构

要使用 scrape 包,首先需要创建客户端。构造函数签名如下(见 scrape/scrape.go):

func NewClient(transport http.RoundTripper) *Client

要点:

  • 参数transport允许自定义 HTTP 传输层(如注入日志、重试、代理等);传nil时使用默认传输。
  • 客户端内部维护一个http.Client,并为其挂载了cookie jar(使用net/http/cookiejar配合golang.org/x/net/publicsuffix的公共后缀列表),以便跨请求保持 GitHub 的会话 Cookie。
  • Client.baseURL固定为https://github.com/,该字段"主要为了测试而暴露"——测试代码正是通过改写baseURL指向本地httptest服务器来实现无网络测试的(见下文"测试策略"一节)。
client := scrape.NewClient(nil) // 使用默认 transport 创建客户端

Cookie 的保存与恢复:SaveCookies / LoadCookies

由于屏幕抓取本质上是模拟浏览器会话,Cookie 管理是核心能力。scrape/scrape.go 提供了两个对称方法:

  • SaveCookies() ([]byte, error):将当前客户端在 github.com 域名下设置的所有 Cookie(登录后应包含session会话 Cookie)用gob 编码序列化返回;
  • LoadCookies(v []byte) error:把之前保存的字节流反序列化并重新写入 cookie jar,使新客户端无缝继承登录态。

源码注释给出了重要的安全提醒(见 scrape/scrape.go):

GitHub 会话 Cookie 是不绑定任何特定客户端的持有者令牌(bearer token),必须像账号凭据一样谨慎保管。

因此,如果你需要把登录态持久化到磁盘或传给其他进程,请务必用安全的存储方式(如密钥管理服务、加密文件),切勿明文落盘。


四、认证:Authenticate 与 OTP 双因素支持

多数需要抓取的页面(如组织设置页)要求登录态。Authenticate方法(见 scrape/scrape.go)实现了用户名/密码登录,并原生支持双因素认证(2FA):

func (c *Client) Authenticate(username, password, otpseed string) error

参数说明:

参数含义
usernameGitHub 用户名
passwordGitHub 密码
otpseed双因素认证的 OTP Secret(未启用 2FA 时传空字符串即可)

实现流程分两步:

  1. 提交登录表单:向https://github.com/login发起 GET 获取登录页,解析出<form>,填入login与password字段后 POST 提交(该逻辑封装在 scrape/forms.go 的fetchAndSubmitForm中);
  2. 提交 OTP:若otpseed非空,则用github.com/xlzd/gotp库基于 TOTP 算法计算当前一次性密码,向https://github.com/sessions/two-factor提交otp字段。

关于 OTP Secret 的获取,源码注释(scrape/scrape.go)给出了操作指引:在 GitHub 双因素应用注册流程中,QR 码页面有一个"enter this text code"(输入此文本代码)链接,点击即可看到原始的 OTP Secret 字符串,将其传入即可。代码内部会用strings.ToUpper(otpseed)将其转为大写后再参与 TOTP 计算。

任何一步响应状态码不是200 OK都会返回明确错误,便于排查登录失败原因。


五、核心 API 一瞥:scrape 包现在能做什么

当前 scrape 包提供的方法不多,恰好印证了"Add only what you need"的设计原则。以下按文件逐一梳理。

5.1 组织 OAuth 应用策略(apps.go)

scrape/apps.go 实现了两个读取方法,页面来源都是/organizations/{org}/settings/oauth_application_policy:

AppRestrictionsEnabled(org string) (bool, error)(scrape/apps.go)

判断指定组织是否启用了第三方应用访问限制。实现方式:

  • 用client.get抓取策略页;
  • 定位.oauth-application-allowlist svg元素(页面中"Access restricted"状态旁的图标);
  • 若该 SVG 带octicon-check类 → 返回true(限制已启用);带octicon-alert类 → 返回false;
  • 找不到预期标记时返回错误"unable to find expected markup"。

ListOAuthApps(org string) ([]*OAuthApp, error)(scrape/apps.go)

列出组织中所有已审核(批准/拒绝/待审核)的 OAuth 应用。它遍历.oauth-application-allowlist ul > li列表项,解析出应用名称(.request-info strong)、描述(.application-description)、ID(从审核链接/orgs/{org}/policies/applications/{id}的末段路径解析而来),并根据.request-indicator内是否存在.requestor/.approved-request/.denied-request标记判定应用状态。

返回的OAuthApp结构体(scrape/apps.go)包含:

type OAuthApp struct { ID int Name string Description string State OAuthAppReviewState RequestedBy string }

其中OAuthAppReviewState是枚举类型(scrape/apps.go),取值为:

常量含义
OAuthAppRequested已申请访问,但尚未审核
OAuthAppApproved已批准
OAuthAppDenied已拒绝

这两个方法的行为有对应的 HTML 夹具与测试用例佐证:Test_AppRestrictionsEnabled与Test_ListOAuthApps(见 scrape/apps_test.go)分别使用 scrape/testdata/access-restrictions-enabled.html 和access-restrictions-disabled.html两个真实页面样例(2019-10-15 抓取自 GitHub)来验证解析逻辑,测试期望值中甚至包含了真实的应用信息(如 Coveralls、Google Cloud Platform、GitKraken)。

5.2 组织支付信息(payment.go)

scrape/payment.go 提供了OrgPaymentInformation(org string) (PaymentInformation, error),抓取/organizations/{org}/settings/billing/payment_information页面。解析思路是遍历main h4.mb-1标题元素,将标题文本小写化后按payment method、last payment、coupon、extra information四类匹配,取标题后紧邻的<p>段落文本作为值。返回的PaymentInformation结构体字段与上述四类一一对应。

5.3 通过 manifest 创建 GitHub App(apps.go)

scrape/apps.go 定义了AppManifest结构与CreateApp方法:

func (c *Client) CreateApp(m *AppManifest, orgName string) (*http.Response, error)

AppManifest各字段(JSON tag 与 GitHub manifest 规范对齐)包括:name(App 名称)、url(App 主页,必填)、callback_urls(用户认证回调地址,最多 10 个)、hook_attributes(Webhook 配置)、redirect_url(安装完成后的重定向地址)、description、public(是否公开)、default_events(订阅的事件列表)、default_permissions(所需权限,复用主库的*github.InstallationPermissions)。

调用时:

  • orgName为空 → 提交到/settings/apps/new(创建到个人账号下);
  • orgName非空 → 提交到/organizations/{org}/settings/apps/new(创建到指定组织下);
  • 请求体以{"manifest": {...}}的 JSON 结构 POST 出去。

注意这个方法不走表单解析,而是直接向 GitHub 的 manifest JSON 端点发起 POST,是包中少见的"写"操作。


六、扩展新方法:README 给出的标准做法

README 的 "How to add methods" 一节是整份文档最核心的实操指引,原文要点如下:

See apps.go for examples of methods that access data. Basically, fetch the contents of the page usingclient.get, and then usegoqueryto dig into the markup on the page. Prefer selectors that grabsemantic ID or class names, as they are more likely to be stable.

翻译并展开为可执行的标准步骤:

  1. 抓取页面:调用内部方法client.get(urlStr, a...)获取目标页面的解析后 DOM 文档。该方法(见 scrape/scrape.go)会把相对路径(如/organizations/%v/settings/oauth_application_policy)与baseURL拼接,支持fmt风格的占位符传参,并做如下处理:

    • HTTP 404 时直接返回错误;
    • 用goquery.NewDocumentFromReader将响应体解析为*goquery.Document。
  2. 用 goquery 挖掘标记:在返回的Document上执行 CSS 选择器查询,提取目标数据。这是整个包的数据提取核心,依赖 scrape/go.mod 中声明的github.com/PuerkitoBio/goquery v1.13.0。

  3. 优先使用语义化选择器:这是文档特别强调的一点——优先选取带语义含义的 ID 或 class 名(如.oauth-application-allowlist、.requestor、.approved-request、octicon-check等),因为它们相比无意义的深层嵌套选择器更可能在 GitHub 改版中保持稳定。对比 scrape/testdata/access-restrictions-enabled.html 中实际的页面标记可以看到,所有这些选择器都能在真实 HTML 中精确定位。

一个可供参考的最小扩展模板(结构与AppRestrictionsEnabled一致):

func (c *Client) MyNewData(org string) (bool, error) { doc, err := c.get("/organizations/%v/settings/some_page", org) if err != nil { return false, err } // 用语义化选择器提取数据 s := doc.Find(".some-semantic-class").First() if s.Length() == 0 { return false, errors.New("unable to find expected markup") } return s.Text() == "expected", nil }

底层抓取管道:get → goquery

get方法有一个值得注意的细节(scrape/scrape.go):u, err := c.baseURL.Parse(fmt.Sprintf(urlStr, a...)),即 URL 字符串本身支持%v占位符,调用方可以像c.get("/organizations/%v/settings/oauth_application_policy", org)这样安全地传入动态参数。这避免了手写字符串拼接带来的 URL 转义问题,是扩展新方法时的推荐写法。


七、表单解析与提交机制:Authenticate 背后的引擎

README 虽未直接提及,但Authenticate之所以能模拟登录,依赖的是 scrape/forms.go 中一套与 go-github 无关、可独立复用的表单处理逻辑。

htmlForm 与 parseForms

htmlForm结构体(scrape/forms.go)抽象了 HTML 表单的三要素:Action(提交地址)、Method(提交方法)、Values(url.Values形式的键值对)。

parseForms(node *html.Node)(scrape/forms.go)从解析后的 HTML 节点中提取页面内所有<form>,并收集其中的<input>与<textarea>值,规则如下:

  • 带name属性的input才会被收录;value取value属性;
  • radio / checkbox 仅在checked时才被收录(未选中的单选/复选值不会进入提交集合);
  • textarea的值取文本内容;
  • 表单的action与method属性会被原样读取。

这些边界行为在 scrape/forms_test.go 的Test_ParseForms中逐一验证(覆盖空表单、单选未选中、复选框混合选中、textarea 等 8 种场景)。

fetchAndSubmitForm

fetchAndSubmitForm(scrape/forms.go)是表单提交流程的完整实现:

  1. GET 请求目标 URL,用golang.org/x/net/html解析响应;
  2. 调parseForms找出第一个表单(找不到则报错);
  3. 将表单action通过ResolveReference解析为绝对地址;
  4. 调用传入的setValues回调,允许调用方改写表单值(如填充login/password/otp);
  5. 用client.PostForm以POST 方法提交——源码注释明确:无论表单method属性是什么,提交一律使用 POST(这是模拟浏览器登录行为的安全选择)。

Test_FetchAndSubmitForm(scrape/forms_test.go)验证了"隐藏字段保留 + 自定义字段注入"的组合行为:表单自带hidden=h,回调注入name=n,最终提交的url.Values同时包含两者。


八、完整示例:scrape 命令行工具

仓库在 scrape/example/scrape/main.go 提供了一个可直接运行的命令行示例,完整演示了"认证 → 读取组织 OAuth 策略 → 列出 OAuth 应用"的调用链。其支持的命令行参数如下:

Flag默认值说明
-username空GitHub 用户名
-password空密码;若未通过 flag 提供,程序会交互式提示输入
-otpseed空OTP Secret;若未提供,同样交互式提示输入
-org空要查询数据的组织名

运行方式示例:

go run ./scrape/example/scrape -username yourname -org yourorg

(随后按提示输入密码与 OTP Secret。注意:仓库为只读用途,运行该工具需要你自己的 GitHub 账号凭据。)

程序核心逻辑为:

client := scrape.NewClient(nil) if err := client.Authenticate(*username, *password, *otpseed); err != nil { log.Fatal(err) } enabled, err := client.AppRestrictionsEnabled(*org) // ... apps, err := client.ListOAuthApps(*org) // ...

这份代码是学习"如何组合使用 scrape 包"的最直观范本,也是 README 提到的方法扩展思路的落地参考。


九、测试策略:无网络依赖的稳定性保障

屏幕抓取最脆弱之处在于依赖网页结构,因此仓库为 scrape 包建立了"真实页面夹具 + 本地 HTTP 服务器"的测试体系,值得扩展新方法时借鉴:

  • setup 辅助函数(scrape/scrape_test.go)用httptest.NewServer起本地服务器,创建Client后把baseURL改写到测试服务器地址,再通过http.ServeMux按路径注册 mock 处理器;
  • copyTestFile(scrape/scrape_test.go)将 scrape/testdata/ 目录下的真实页面 HTML 作为响应体返回——这些夹具是 2019 年从 GitHub 实际抓取并裁剪后的快照,保留了完整的语义标记结构;
  • 断言层面使用github.com/google/go-cmp/cmp对解析结果做深度比较(如Test_ListOAuthApps中对[]*OAuthApp的完整结构比对)。

这套方案意味着:只要 GitHub 不改动相关页面的标记,测试就能在无网络、无账号的环境下稳定运行,同时也为未来页面改版导致的抓取失效提供了快速定位的基线。


十、使用边界与风险提示

最后,结合 README 与源码注释,总结使用 scrape 包时必须牢记的边界:

  1. 优先 API,后考虑抓取:目标数据能用 REST/GraphQL 获取时,一律使用主库接口;scrape 只是最后手段。
  2. 实验性状态:包注释明确声明 HIGHLY EXPERIMENTAL,导出 API 不承诺兼容与稳定,升级依赖时需重新验证行为。
  3. 页面结构易变:GitHub 前端改版可能导致选择器失效(表现为 "unable to find expected markup" 一类错误),需要维护夹具并更新解析逻辑。
  4. 凭据安全:SaveCookies导出的会话 Cookie 是持有者令牌,必须按账号凭据等级保护;OTP Secret 同样敏感。
  5. 遵守 GitHub 使用条款:屏幕抓取涉及对 github.com 页面的自动化访问,实际部署前应结合 GitHub 的服务条款与访问频率要求评估合规性(仓库文档本身未对此作出承诺,属合理推断的注意事项)。

结语

scrape 包是 go-github 生态中定位独特的一环:它以"最小覆盖 + 只读优先 + 语义选择器"三条纪律约束自身的生长边界,用client.get+ goquery 的简单模型填补 REST/GraphQL API 的盲区,并借助真实页面夹具保证抓取逻辑的可测试性。如果你的场景恰好需要组织 OAuth 应用策略、支付信息这类 API 未暴露的数据,scrape/apps.go 与 scrape/example/scrape/main.go 就是最佳的起点与范本。

  • 后端
  • API设计

【免费下载链接】go-github

Go library for accessing the GitHub v3 API

项目地址:https://gitcode.com/GitHub_Trending/go/go-github
点击查看免费下载
上一篇:node-sass 与 libsass 的 Context API 内部结构剖析:从 C 结构体到编译器状态机
下一篇:怎样高效获取网盘直链:免费下载加速完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表