第 1 章 开放的 GitHub API

第 1 章 开放的 GitHub API

本章开始介绍如何使用 GitHub API 读写数据。后续的章节说明如何使用不同的客户端库通过 GitHub API 访问信息。这些客户端库故意隐藏 API 的具体细节,为你提供简洁且符合习惯的方法,用于访问托管在 GitHub 中的 Git 仓库,查看和修改里面的数据。不过,本章直接分析 GitHub API,详细说明原始 HTTP 请求和响应。本章还会讨论访问 GitHub 中公开和隐私数据的不同方式,并指出各自的不足之处。此外,本章会概述网络受限时如何在 Web 浏览器中访问 GitHub 的数据。

1.1 cURL

有时,你可能想立即通过 GitHub API 访问信息,而不想编写正式的程序;有时,你可能想立即获取 HTTP 原始请求的首部和内容;有时,你甚至可能会对客户端库的实现有疑惑,需要换个角度确认客户端库的行为是否正确。遇到这些情况时,最适合使用 cURL 这个简单的命令行 HTTP 工具。与最优秀的 Unix 工具一样,cURL 是个小型程序,功能十分专一,而且故意做了限制,只用于访问 HTTP 服务器。

cURL 与它熟谙的 HTTP 协议一样是无状态的。后面有一章会探讨这一局限性的解决方法,不过要注意,cURL 最适合用于发起一次性请求。

 安装 cURL

大多数 OS X 设备中通常都安装了 cURL,在 Linux 中可以使用包管理器轻易安装(执行 apt-get install curlyum install curl 命令)。如果使用的是 Windows,或者想自己动手安装,请访问 http://curl.haxx.se/download.html

下面来发起一个请求。我们从 GitHub API 最基本的端点入手,地址是 https://api.github.com

$ curl https://api.github.com
{
  "current_user_url": "https://api.github.com/user",
  "current_user_authorizations_html_url":
  "https://github.com/settings/connections/applications{/client_id}",
  "authorizations_url": "https://api.github.com/authorizations",
  "code_search_url":
  "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
  "emails_url": "https://api.github.com/user/emails",
  "emojis_url": "https://api.github.com/emojis",
  ...
}

为了便于阅读,此处省略了部分响应。有几点需要特别注意:响应中有大量指向附属信息的 URL,URL 中包含参数,此外响应的格式是 JSON。

我们从这个 API 的响应中能得知什么呢?

1.2 列举API路径

GitHub API 是超媒体 API。超媒体的构成需要用一整本书才能说清楚(推荐阅读《使用 HTML5 和 Node 构建超媒体 API》),不过通过分析响应,我们能掌握超媒体的多数核心概念。首先,从前面那个 API 的响应可以看出,响应中包含一个映射,列出了接下来可能会发起请求的地址。当然,不是所有客户端都会使用这个信息,但是超媒体 API 的目标之一,是让客户端在不重新编写代码的前提下动态调整所用的端点。如果你觉得编写客户端时要考虑自动处理新端点,以防 GitHub 更改 API 这一点难以理解,不用太过担心,GitHub 非常负责,它像大多数公司那样,积极维护 API 并为其提供支持。不过要知道,API 中的 API 参考是值得信赖的,比外部文档可信,因为文档可能与 API 本身脱节。

API 中的映射富含数据。例如,映射不仅包含 URL,还有为 URL 提供参数的方式。在前面那个示例中,code_search_url 键对应的 URL 明显用于在 GitHub 中搜索代码,此外还指明了如何构建传给 URL 的参数。如果客户端够智能,能读懂这种纲领性的格式,就能动态生成查询,无需开发者去阅读 API 文档。至少,这是超媒体为我们指明的美好未来。如果你是怀疑论者,至少要知道 API(比如 GitHub)把文档嵌入自身当中,并且 GitHub 做了足够的测试,能够证明内嵌的文档与 API 端点传递的信息匹配。其他类型的 API 则没有这么强的保障。

下面,我们来简单讨论所有 GitHub API 的响应格式——JSON。

1.3 JSON格式

GitHub API 返回的所有响应都是 JSON(JavaScript Object Notation,JavaScript 对象表示法)格式。JSON 是一种“轻量级数据交换格式”(详情参见 JSON.org 网站:http://www.json.org/)。此外,还有其他与之相争且有效的格式,例如 XML(Extensible Markup Language,可扩展标记语言)和 YAML(YAML Ain't Markup Language),不过 JSON 正在快速成为 Web 服务的事实标准。

JSON 之所以如此流行,有以下两个原因。

  • JSON 易于阅读。与 XML 等序列化格式相比,JSON 很好地平衡了人类可读性。

  • 只需小幅修改(和程序员的认知处理),JSON 就能在 JavaScript 中使用。在客户端和服务器端都能同样良好使用的数据格式一定会胜出,JSON 就是如此。

GitHub 最初使用 Ruby on Rails 工具栈开发(部分代码现在仍在运行),你可能觉得这样的网站应该支持指定替代格式(如 XML),但是 GitHub 不再支持 XML 了。JSON 万岁!

如果你用过其他基于文本的交换格式,会发现 JSON 特别简单。注意,JSON 只支持使用双引号,不支持单引号,这一点对于刚接触 JSON 的人来说可能有点难以理解,出乎意料。

我们使用 cURL 这个命令行工具从 GitHub API 中获取数据。如果再有一个简单的命令行工具能处理 JSON 就好了。下面介绍一个这样的工具。

1.3.1 在命令行中解析JSON

JSON 是一种文本格式,因此可以使用任何命令行文本处理工具处理 JSON 响应,例如令人敬仰的 AWK。有一个专门解析 JSON 的优秀工具能补足 cURL,值得一用,这便是 jq。通过管道(大多数 shell 使用“|”字符)把 JSON 内容传给 jq 后,可以使用过滤器轻易提取 JSON 片段。

 安装 jq

jq 可以从源码安装,使用包管理器安装(例如 brewapt-get),下载页面(http://stedolan.github.io/jq/download/)还有适用于 OS X、Linux、Windows 和 Solaris 的二进制文件。

下面深入前面的示例,从访问 api.github.com 后收到的 API 映射中提取感兴趣的信息:

$ curl https://api.github.com | jq '.current_user_url'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2004  100  2004    0     0   4496      0 --:--:-- --:--:-- --:--:--  4493
"https://api.github.com/user"

发生了什么? jq 工具解析 JSON,然后使用 .current_user_url 过滤器从 JSON 响应中获取内容。再次查看响应,你会发现响应是个关联数组,里面有多个键值对。jq 使用那个关联数组中的 current_user_url 作为键获取对应的值。

你还会发现 cURL 把传输时间信息打印出来了。cURL 把这些信息打印到标准错误。这是 shell 的一种约定,用于输出错误。jq 会正确忽略这个输出流(也就是说,JSON 格式的数据流不会被错误消息搅乱)。如果想禁止那些信息,让请求清晰明了,可以使用 -s 开关,在“静默”模式中运行 cURL。

jq 在 JSON 响应上应用过滤器的方式易于理解。下面通过一个复杂的请求(例如,获取某个用户的所有公开仓库)说明如何使用 jq 的模式参数。我们要获取一组更为复杂的信息,即用户的仓库列表,以此说明如何使用 jq 从响应中提取信息:

$ curl -s https://api.github.com/users/xrd/repos
[
  {
    "id": 19551182,
    "name": "a-gollum-test",
    "full_name": "xrd/a-gollum-test",
    "owner": {
      "login": "xrd",
      "id": 17064,
      "avatar_url":
      "https://avatars.githubusercontent.com/u/17064?v=3",
    ...
  }
]
$ curl -s https://api.github.com/users/xrd/repos | jq '.[0].owner.id'
17064

这个响应的结构与之前不同,它不是一个关联数组,而是一个普通数组(有多个元素)。为了获取第一个元素,我们要指定数字索引,然后再通过键进入元素里的关联数组,从而获取所需的内容——属主的 ID。

jq 工具能很好地检查 JSON 的有效性。前面说过,JSON 的键值对只能使用双引号,不能使用单引号。可以使用 jq 验证 JSON 是否有效,以及是否满足这个要求:

$ echo '{ "a" : "b" }' | jq '.'
{
  "a": "b"
}
$ echo "{ 'no' : 'bueno' }" | jq "."
parse error: Invalid numeric literal at line 1, column 7

传给 jq 的第一个 JSON 是有效的,而第二个 JSON 使用了无效的单引号字符,因此报错了。jq 过滤器是以字符串形式传递的参数,而把字符串提供给 jq 的 shell 不管你使用的是单引号还是双引号,从上述代码可以看出这一点。如果你不知道 echo 命令的作用,我告诉你,它会把传给它的任何字符串打印出来。把这个命令和管道符号结合起来,可以通过标准输入轻易地把字符串提供给 jq。

jq 是个强大的工具,能从任何 JSON 响应中迅速获取内容。此外,jq 还有很多强大的功能,详情参见文档(https://stedolan.github.io/jq/)。

至此,我们知道如何仅使用一行命令完成从 GitHub API 中获取所需的信息,并从响应中解析出信息片段。可是,有时为 cURL 或 API 指定的参数可能是错误的,获得的数据不是我们想要的。接下来,我们学习如何调试 cURL 工具和 API 服务本身,从而在出错时获取更多的信息。

1.3.2 cURL的调试开关

前面说过,cURL 是验证响应是否与预期相符的绝佳工具。响应主体很重要,但是通常还要获取首部。为 cURL 指定 -i-v 开关能轻易获取这些信息。-i 开关打印请求首部,-v 开关则打印请求和响应首部(> 符号表示请求数据,< 符号表示响应数据)。

$ curl -i https://api.github.com
HTTP/1.1 200 OK
Server: GitHub.com
Date: Wed, 03 Jun 2015 19:39:03 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2004
Status: 200 OK
X-RateLimit-Limit: 60
...
{
  "current_user_url": "https://api.github.com/user",
  ...
}
$ curl -v https://api.github.com
* Rebuilt URL to: https://api.github.com/
* Hostname was NOT found in DNS cache
*   Trying 192.30.252.137...
* Connected to api.github.com (192.30.252.137) port 443 (#0)
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
...
* CN=DigiCert SHA2 High Assurance Server CA
*        SSL certificate verify ok.
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: api.github.com
> Accept: */*
>
< HTTP/1.1 200 OK
* Server GitHub.com is not blacklisted
...

指定 -v 开关能获取所有信息:DNS 查询、SSL 证书链中的信息,以及完整的请求和响应信息。

 注意,如果打印首部,jq 这样的工具会迷惑不解,因为提供的不是纯粹的 JSON。

本节旨在说明不仅主体(JSON 数据)中有值得关注的信息,首部中也有。我们要知道有哪些首部,还要知道哪些是重要的。根据 HTTP 规范,需要的首部有很多,这些首部通常可以忽略,不过如果请求不是相互独立的,有些首部则是必不可少的。

1.4 重要的首部

GitHub API 的每个响应中都有三个用于指明 API 频率限制的首部,分别是 X-RateLimit-Limit、X-RateLimit-Remaining 和 X-RateLimit-Reset。这些限制在 1.7.6 节详述。

从 GitHub API 中获取文本或 blob 内容时,要用到 X-GitHub-Media-Type 首部中包含的信息。向 GitHub API 发起请求时,可以在请求中发送 Accept 首部,指明想使用的格式。

下面,我们使用一个响应构建另一个响应。

1.5 跟随超媒体API

我们要使用 API 基端点返回的“映射”,手动生成另一个请求:

$ curl -i https://api.github.com/
HTTP/1.1 200 OK
Server: GitHub.com
Date: Sat, 25 Apr 2015 05:36:16 GMT
...
{
  "current_user_url": "https://api.github.com/user",
  ...
  "organization_url": "https://api.github.com/orgs/{org}",
  ...
}

我们可以使用组织的 URL,把占位符替换成 "github"

$ curl https://api.github.com/orgs/github
{
  "login": "github",
  "id": 9919,
  "url": "https://api.github.com/orgs/github",
  ...
  "description": "GitHub, the company.",
  "name": "GitHub",
  "company": null,
  "blog": "https://github.com/about",
  "location": "San Francisco, CA",
  "email": "support@github.com",
  ...
  "created_at": "2008-05-11T04:37:31Z",
  "updated_at": "2015-04-25T05:17:01Z",
  "type": "Organization"
}

通过上述信息可以得知 GitHub 的一些信息。我们得知 GitHub 的博客地址是 https://github.com/about,公司位于旧金山,组织的创建日期是 2008 年 5 月 11 日。从博客中一篇发布于 4 月份的文章(https://github.com/blog/40-we-launched)中得知,GitHub 公司在这一个月之前就成立了。或许公司成立一个月之后才在 GitHub 网站中添加组织功能吧。

目前,我们发起的请求都用于获取公开信息。GitHub API 提供的信息十分丰富,但是验证身份之后才能访问隐私信息和不可公开使用的服务。例如,想使用 API 向 GitHub 中写入数据的话,需要知道如何验证身份。

1.6 身份验证

向 GitHub API 发起请求时,有两种身份验证方式:用户名和密码(HTTP 基本验证)以及 OAuth 令牌。

1.6.1 用户名和密码验证

提供用户名和密码后可以访问 GitHub 中受保护的内容。用户名验证使用 HTTP 基本验证实现,在 cURL 中使用 -u 标志指定。HTTP 基本验证就是用户名和密码验证:

$ curl -u xrd https://api.github.com/rate_limit
Enter host password for user 'xrd': xxxxxxxx
{
  "rate": {
    "limit": 5000,
    "remaining": 4995,
    "reset": 1376251941
  }
}

上述 cURL 命令先通过 GitHub API 的身份验证,然后获取该用户账号具体的频率限制信息。这些是受保护的信息,只有登录的用户才能查看。

1. 用户名验证的优点

几乎所有客户端库都支持 HTTP 基本验证。我们即将介绍的 GitHub API 客户端都支持用户名和密码验证。而且,自己实现客户端也很容易,因为这是 HTTP 标准的核心功能。所以,开发客户端时只要使用了符合标准的 HTTP 库,就能访问 GitHub API 中的内容。

2. 用户名验证的缺点

使用用户名和密码验证管理 GitHub API 的访问权限不合适,原因有如下几个。

  • HTTP 基本验证是旧协议,没有预料到 Web 服务的粒度如此细化。如果通过用户名和密码验证用户的身份,那么无法指定让 Web 服务开放哪些特定的功能。

  • 如果使用用户名和密码在手机端访问 GitHub API 的内容,又在笔记本电脑中访问 API 的内容,那就无法禁止某一台设备访问,只能都禁止。

  • HTTP 基本验证无法扩展验证流程。现今,很多现代服务都支持双重身份验证,为了把这种验证方式插入现有的验证过程,需要修改 HTTP 客户端(例如 Web 浏览器),至少也要修改客户端预期的流程(让浏览器重复请求)。

这些问题在 OAuth 流程中都能得到解决(或者至少得到支持)。鉴于这些缺点,仅当便利性至上时才应当使用用户名和密码验证身份。

1.6.2 OAuth

OAuth 是一种身份验证机制,把令牌与功能或客户端绑定起来。也就是说,可以指定让服务为 OAuth 令牌开放哪些功能;还可以颁发多个令牌,与不同的客户端绑定,例如手机应用、笔记本电脑、智能手表,甚至是接入物联网的烤箱。更重要的是,可以吊销令牌而不对其他令牌产生影响。

OAuth 令牌主要的缺点是增加了一层复杂度,如果你只用过 HTTP 基本验证,对此可能不熟悉。HTTP 基本验证通常只需在 HTTP 请求中添加一个额外的首部,或者在客户端工具(如 cURL)中添加一个额外的标志。

OAuth 能解决上述问题,方法是把令牌限定在作用域(指定 Web 服务的功能子集)中,以及按需为不同的客户端生成不同的令牌。

1. 作用域:指定验证令牌可执行的操作

生成 OAuth 令牌时,要指定所需的访问权限。下述示例虽然使用 HTTP 基本验证创建令牌,但是一旦得到令牌,后续请求就不再需要使用 HTTP 基本验证。获颁 OAuth 令牌之后,便有权限读写相应用户的公开仓库。

下述 cURL 命令使用 HTTP 基本验证请求令牌:

$ curl -u username -d '{"scopes":["public_repo"]}' \
https://api.github.com/authorizations
{
  "id": 1234567,
  "url": "https://api.github.com/authorizations/1234567",
  "app": {
    "name": "My app",
    "url": "https://developer.github.com/v3/oauth_authorizations/",
    "client_id": "00000000000000000000"
  },
  "token": "abcdef87654321
  ...
}

请求成功后获得的 JSON 响应中有个令牌,把它提取出来之后可以提供给应用,用于访问 GitHub API。

如果使用双重身份验证,这个过程需要额外的步骤,详情参阅第 8 章。

令牌的使用方法是,在 Authorization 首部中指定令牌:

$ curl -H "Authorization: token abcdef87654321" ...

作用域明确了服务或应用能如何使用 GitHub API 中的数据。对于供用户自己使用的令牌来说,作用域便于稽查用户如何使用 API 提供的信息。不过,当第三方应用想访问你的信息时,最能体现作用域明确访问权限的重要价值和防护作用,因为你能确保应用只能访问允许它访问的数据,而且便于取消访问权限。

2. 作用域的不足

要知道,作用域有个极大的不足之处:无法精细调整特定仓库的访问权限。如果允许访问任何一个私有仓库,那么所有仓库就都能访问。

未来,GitHub 可能会修改作用域的工作方式,解决其中一些问题。OAuth 机制的好处是,发生变化后,只需请求重新设定作用域的令牌,应用的身份验证流程则无需修改。

 构建服务或应用时,要特别谨慎地对待请求的作用域。用户会担忧交给你的数据是否安全(这是正常的),他们会根据请求的作用域评估能否信任应用。如果用户觉得不需要那么广的作用域,请求授权时一定要从提供给 GitHub 的权限列表中删除,与用户建立一定的信任之后再考虑升级为更广的作用域。

3. 逐步升级作用域

你可以先请求严格受限的作用域,以后再请求更广的作用域。例如,用户首次访问你的应用时,你可以只获取 user 作用域,在你的服务中创建用户对象,仅当应用需要获取用户的仓库信息时再请求升级权限。此时,用户需要接受或拒绝升级请求。但是,事无巨细,(在与用户建立关系之前)什么都询问,往往会导致用户放弃登录。

下面详述使用 OAuth 验证身份的细节。

4. 简化的OAuth流程

OAuth 有很多版本,GitHub 使用的是 OAuth2。OAuth2 验证身份的流程如下:

(1) 应用请求访问;

(2) 服务提供方(GitHub)请求验证身份,通常使用用户名和密码;

(3) 如果启用了双重身份验证,询问 OTP(one-time password,一次性密码);

(4) GitHub 返回包含令牌的 JSON 响应;

(5) 应用使用 OAuth 令牌请求 API。

接下来说明 GitHub API 在通信过程中用于提供反馈的各个 HTTP 状态码。

1.7 状态码

GitHub API 使用 HTTP 状态码明确表明请求的处理结果。如果使用的是简单的客户端,如 cURL,在获取数据之前一定要先验证状态码。如果自己编写 API 客户端,首先要重点关注状态码。刚开始使用 GitHub API 的用户应该仔细检查响应的状态码,熟悉请求可能致错的各种状况。

1.7.1 成功(200或201)

只要你用过 HTTP 客户端,就知道 HTTP 状态码“200”表示成功。请求的目标地址和相关的参数正确时,GitHub 响应的状态码是 200。如果请求在服务器中创建内容,响应的状态码是 201,表示成功在服务器中创建了内容。

$ curl -s -i https://api.github.com | grep Status
Status: 200 OK

1.7.2 不合规的JSON(400)

如果载荷(请求中发送的 JSON)无效,GitHub API 的响应是 400 错误,如下所示:

$ curl -i -u xrd -d 'yaml: true' -X POST https://api.github.com/gists
Enter host password for user 'xrd':
HTTP/1.1 400 Bad Request
Server: GitHub.com
Date: Thu, 04 Jun 2015 20:33:49 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 148
Status: 400 Bad Request
...

{
  "message": "Problems parsing JSON",
  "documentation_url":
  "https://developer.github.com/v3/oauth_authorizations/#create...authorization"
}

这里,我们打算使用 Gist API 文档(https://developer.github.com/v3/gists/#create-a-gist)中给出的端点新建一个 Gist。后面有一章会详细讨论 Gist。这个请求失败了,因为我们使用的不是 JSON(看起来使用的像是 YAML,参见第 6 章)。载荷使用 -d 开关发送。GitHub 在返回的 JSON 响应中使用 documentation_url 键提供了一个地址,告诉你在哪里寻找正确格式的文档。注意,我们使用了 -X 开关并把值设为 POST,这么做是为了告诉 cURL 向 GitHub 发起 POST 请求。

1.7.3 错误的JSON(422)

如果请求中有任何无效的字段,GitHub 会响应 422 错误。下面我们尝试修复前面那个请求。根据文档,JSON 载荷应该使用下述格式:

{
  "description": "the description for this gist",
  "public": true,
  "files": {
    "file1.txt": {
      "content": "String file contents"
    }
  }
}

如果 JSON 是有效的,但是字段不正确呢?

$ curl -i -u chris@burningon.com -d '{ "a" : "b" }' -X POST
https://api.github.com/gists
Enter host password for user 'chris@burningon.com':
HTTP/1.1 422 Unprocessable Entity
...

{
  "message": "Invalid request.\n\n\"files\" wasn't supplied.",
  "documentation_url": "https://developer.github.com/v3"
}

注意两件事。其一,返回的是 422 错误,表示 JSON 有效,但是字段不正确。其二,获得一个说明错误原因的响应:请求载荷中缺少 files 键。

1.7.4 成功创建(201)

我们已经知道 JSON 无效时会发生什么,那么如果请求发送的 JSON 是有效的呢?

$ curl -i -u xrd \
-d '{"description":"A","public":true,"files":{"a.txt":{"content":"B"}}} \
https://api.github.com/gists
Enter host password for user 'xrd':
HTTP/1.1 201 Created
...

{
  "url": "https://api.github.com/gists/4a86ed1ca6f289d0f6a4",
  "forks_url":
  "https://api.github.com/gists/4a86ed1ca6f289d0f6a4/forks",
  "commits_url":
  "https://api.github.com/gists/4a86ed1ca6f289d0f6a4/commits",
  "id": "4a86ed1ca6f289d0f6a4",
  "git_pull_url": "https://gist.github.com/4a86ed1ca6f289d0f6a4.git",
  ...
}

成功!我们创建了一个 Gist,而且获得了表示正确处理的 201 状态码。为了让命令更易于阅读,我们使用反斜线,把参数写成多行。此外,注意 JSON 不需要空格,因此我们把传给 -d 开关的字符串中的空格全都去掉了(为了节省空间,让命令更容易阅读一些)。

1.7.5 完全没变化(304)

304 与 200 的作用类似:告诉客户端请求成功。不过,304 多提供了一些信息,告诉客户端自上次请求以来数据没有变化。对于担心用量限制的用户来说(大多数用户都会担心),这是重要的信息。我们还未讲解频率限制的运作方式,所以下面先讨论这个话题,然后再演示如何使用条件首部触发 304 响应码。

1.7.6 GitHub API的频率限制

GitHub 会设法限制用户请求 API 的频率。匿名请求(没有使用用户名和密码或者 OAuth 令牌验证身份的请求)的限制为一小时 60 次。如果开发一个与 GitHub API 集成的系统,代表用户执行操作,一小时 60 次请求显然不够用。

验证身份后,向 GitHub API 发起请求的频率会增加到每小时 5000 次。虽然这比匿名请求的频率多了两个数量级,但是若想使用你自己的 GitHub 凭据代表很多用户发起请求,仍然有问题。

鉴于此,如果你的网站或服务使用 GitHub API 请求 GitHub API 中的信息,应该考虑使用 OAuth,并且使用用户共享的身份验证信息请求 GitHub API。如果使用其他用户的 GitHub 账户的令牌,那么频率限制算在该用户身上,而不是算在你的账户上。

 其实,频率限制有两种:核心频率限制和搜索频率限制。前面几段说明的是核心频率限制。对搜索来说,验证身份的用户每分钟不能发起超过 20 个请求,匿名用户每分钟不能超过 5 个请求。这里假定搜索请求消耗的基础设施资源更多,因此对用量的限制更严格。

注意,GitHub 按 IP 地址记录匿名请求。因此,如果你身处防火墙后面,还有其他用户发起匿名请求,那么所有这些请求会算在一起。

1.7.7 获知频率限制

获知频率限制的方法很简单,向 /rate_limit 发起 GET 请求即可。返回的 JSON 文档中有要遵守的频率限制、剩余的请求数和时间戳(1970 年之后的秒数)。注意,时间戳的时区是 UTC(Coordinated Universal Time,协调世界时)。

下述命令清单使用 cURL 获取匿名请求的频率限制。为了节省空间,部分响应省略了,不过你能注意到配额信息出现了两次:一次在 HTTP 响应的首部中,一次在 JSON 响应中。每次请求 GitHub API 都会返回频率限制首部,因此不太有必要直接请求 /rate_limit

$ curl https://api.github.com/rate_limit
{
  "resources": {
    "core": {
      "limit": 60,
      "remaining": 48,
      "reset": 1433398160
    },
    "search": {
      "limit": 10,
      "remaining": 10,
      "reset": 1433395543
    }
  },
  "rate": {
    "limit": 60,
    "remaining": 48,
    "reset": 1433398160
  }
}

一小时 60 次请求不是特别多,如果计划做些有趣的事,可能很快就会超出这一限制。如果每分钟 60 次请求的限制快到了,你可能要想办法验证身份,然后再请求 GitHub API。讨论身份验证请求时会说明做法。

/rate_limit 的请求算在频率限制内。记住,频率限制在 24 小时后重置。

1.8 使用条件请求规避频率限制

如果请求 GitHub API 的目的是获悉一个用户或一个仓库的活动数据,很有可能不会返回太多数据。如果每隔几分钟检查有没有新活动,在一段时间内可能没有任何活动。这种持久轮询虽然有时不传送新活动,但是仍然消耗着请求的频率限制。

此时,可以发送 If-Modified-SinceIf-None-Match 两个 HTTP 条件首部,让 GitHub 返回表示什么都没变的 HTTP 304 响应码。如果发送的请求包含条件首部,而且 GitHub API 返回 HTTP 304 响应码,这样的请求不会从频率限制中扣除。

下述命令清单举例说明如何把 If-Modified-Since 首部传给 GitHub API。这里我们指明,仅当 Twitter Bootstrap 仓库在 2013 年 8 月 11 日(星期日)下午 7:49(GMT)之后有变化才接收内容。GitHub API 响应的状态码是 304,而且告诉我们,仓库的最后一次变动是在截止时间的前一分钟。

$ curl -i https://api.github.com/repos/twbs/bootstrap \
          -H "If-Modified-Since: Sun, 11 Aug 2013 19:48:59 GMT"
HTTP/1.1 304 Not Modified
Server: GitHub.com
Date: Sun, 11 Aug 2013 20:11:26 GMT
Status: 304 Not Modified
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 46
X-RateLimit-Reset: 1376255215
Cache-Control: public, max-age=60, s-maxage=60
Last-Modified: Sun, 11 Aug 2013 19:48:39 GMT

GitHub API 也能理解 HTTP 缓存标签。ETag(Entity Tag)是一个 HTTP 首部,用于确定之前缓存的内容是否为最新版。系统可能会像下面这样使用 ETag。

  • 客户端从 HTTP 服务器中请求信息。

  • 服务器返回一个 ETag 首部,标记内容的一个版本。

  • 客户端在后续的所有请求中发送这个 ETag 首部:

    • 如果服务器有较新的版本,返回新内容和新 ETag;

    • 如果服务器没有较新的版本,返回 HTTP 304 响应码。

下述命令清单演示两个命令。第一个 cURL 命令访问 GitHub API,生成 ETag 值;第二个命令在 If-None-Match 首部中传送那个 ETag 值。你能注意到,第二个响应是 HTTP 304,即告知调用方没有新内容。

$ curl -i https://api.github.com/repos/twbs/bootstrap
HTTP/1.1 200 OK
Cache-Control: public, max-age=60, s-maxage=60
Last-Modified: Sun, 11 Aug 2013 20:25:37 GMT
ETag: "462c74009317cf64560b8e395b9d0cdd"
{
  "id": 2126244,
  "name": "bootstrap",
  "full_name": "twbs/bootstrap",
  ....
}

$ curl -i https://api.github.com/repos/twbs/bootstrap \
          -H 'If-None-Match: "462c74009317cf64560b8e395b9d0cdd"'

HTTP/1.1 304 Not Modified
Status: 304 Not Modified
Cache-Control: public, max-age=60, s-maxage=60
Last-Modified: Sun, 11 Aug 2013 20:25:37 GMT
ETag: "462c74009317cf64560b8e395b9d0cdd"

建议你使用条件请求首部,这样能节省资源,还能确保支持 GitHub API 的基础设施不生成不必要的内容。

目前,我们都使用 cURL 客户端访问 GitHub API,只要网络允许,我们可以做任何想做的事。此外,GitHub API 还能使用其他方式访问,例如使用浏览器,不过此时有些特定的限制,讨论如下。

1.9 在Web中访问内容

如果使用服务器端程序或者命令行访问 GitHub API,只要网络允许,可以发起任何请求。如果想在浏览器中使用 JavaScript 和 XHR(XmlHttpRequest)对象访问 GitHub API,要知道浏览器同源策略施加的限制。简单来说,在 JavaScript 中,不能使用标准的 XHR 请求访问源页面所在域名之外的域名。若想绕开这个限制,有两个选择,一个很巧妙(JSON-P),另一个支持全面,但是稍微麻烦点(CORS)。

1.9.1 JSON-P

JSON-P 算是一种浏览器 hack,目的是避开同源策略,获取其他服务器中的信息。JSON-P 之所以可用,是因为同源策略不检查 <script> 标签;也就是说,页面可以从源服务器之外的服务器中引用内容。JSON-P 的用法是,在 JavaScript 文件中使用特殊的方式编码载荷数据,将其载入自己实现的回调函数中处理。GitHub API 支持这种句法:请求脚本时为 URL 提供一个参数,指明加载完脚本后执行哪个回调。

在 cURL 中可以模拟这样的请求:

$ curl https://api.github.com/?callback=myCallback
/**/myCallback({
  "meta": {
  "X-RateLimit-Limit": "60",
  "X-RateLimit-Remaining": "52",
  "X-RateLimit-Reset": "1433461950",
  "Cache-Control": "public, max-age=60, s-maxage=60",
  "Vary": "Accept",
  "ETag": "\"a5c656a9399ccd6b44e2f9a4291c8289\"",
  "X-GitHub-Media-Type": "github.v3",
  "status": 200
},
"data": {
  "current_user_url": "https://api.github.com/user",
  "current_user_authorizations_html_url":
  "https://github.com/settings/connections/applications{/client_id}",
  "authorizations_url": "https://api.github.com/authorizations",
  ...
 }
})

如果在网页中的 <script> 标签里使用上述代码中的 URL(<script src="https://api.github.com/?callback=myCallback" type= "text/javascript"></script>),浏览器会加载上述代码中的内容,把数据传给你定义的 myCallback 回调函数执行。在网页中可以像下面这样实现回调函数:

<script>
function myCallback( payload ) {
  if( 200 == payload.status ) {
    document.getElementById("success").innerHTML =
      payload.data.current_user_url;
  } else {
    document.getElementById("error").innerHTML =
      "An error occurred";
  }
}
</script>

这个示例演示如何从载荷数据中获取 current_user_url,把它放入一个 div 元素,例如 <div id="success"> </div>

JSON-P 通过 <script> 标签实现,因此只支持向 API 发起 GET 请求。如果只需要 API 的只读权限,多数情况下 JSON-P 能满足需求,而且易于配置。

如果你觉得 JSON-P 局限太多或者不优雅,可以使用 CORS。这是在网页中访问外部服务的官方方式,不过较复杂。

1.9.2 CORS支持

CORS 用于从源主机之外的域名中访问内容,这是 W3C(一个 Web 标准组织)认可的方式。CORS 要求事先正确配置服务器,查询时必须表明自己允许跨域请求。如果服务器明确表示,“是的,你可以从其他域名中访问我的内容”,那么就允许 CORS 请求。 HTML5Rocks 网站中有篇优秀的教程(http://www.html5rocks.com/en/tutorials/cors/),解说了 CORS 的诸多细节。

因为使用 CORS 的 XHR 支持从同一域名获取的同类 XHR 请求,所以除了 GET 请求之外,还能向 GitHub API 发起 POST、DELETE 和 UPDATE 请求。JSON-P 和 CORS 是在 Web 浏览器中访问 GitHub API 的两种方式,前者简单,后者强大,但是需要额外配置。

我们可以使用 cURL 证明 GitHub API 能正确响应 CORS 请求。这里我们只关注首部,因此要使用 -I 开关,让 cURL 发起 HEAD 请求,告诉服务器别响应主体内容。

curl -I https://api.github.com
HTTP/1.1 200 OK
Server: GitHub.com
...
X-Frame-Options: deny
Content-Security-Policy: default-src 'none'
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP,
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
X-GitHub-Request-Id: C0F1CF9E:07AD:3C493B:557107C7
Strict-Transport-Security: max-age=31536000; includeSubdomains;
preload

可以看到,Access-Control-Allow-Credentials 首部的值是 true。有些 JavaScript 宿主浏览器会自动发起预检(preflight)请求,验证这个首部的值是否为 true(此外还会验证 Access-Control-Allow-Origin 等其他首部是否正确设置,以便允许处理来自相应域的请求);有些浏览器则要求我们自己动手发起预检请求。自动预检还是手动预检由浏览器的实现决定。浏览器通过这些首部确认支持 CORS 后,可以向 GitHub API 所在的域发起 XHR 请求,这与向相同的域发起的任何其他 XHR 请求没有差别。

我们大致讲解了如何连接 GitHub API,以及 GitHub API 的细节。此外,GitHub API 还有一些其他用途,例如使用这项服务按需渲染内容。

1.9.3 指定响应的内容格式

向 GitHub API 发送请求时可以指定期望的响应格式。比方说,如果请求的内容包含提交评论中的文本,可以使用 Accept 首部指定获取原始的 Markdown 还是 Markdown 生成的 HTML。此外,还可以指定使用 GitHub API 的哪个版本。当下,可以指定使用 API 的第 3 版或 beta 版。

获取格式化内容

随请求发送的 Accept 首部可以影响 GitHub API 返回的文本格式。下面举个例子。假设你想读取一个 GitHub 工单的正文。工单的正文以 Markdown 格式存储,默认作为请求的响应返回。如果想使用 HTML 渲染响应而不是 Markdown,可以发不同的 Accept 首部,如下述 cURL 命令所示:

$ URL='https://api.github.com/repos/rails/rails/issues/11819'
$ curl -s $URL | jq '.body'
"Hi, \r\n\r\nI have a problem with strong...." ➊
$ curl -s $URL | jq '.body_html'
null ➋
$ curl -s $URL \
-H "Accept: application/vnd.github.html+json" | jq '.body_html'
"<p>Hi, </p>\n\n<p>I have a problem with..." ➌

➊ 没有指定额外的首部,获取数据的内在表述,即 Markdown。

➋ 注意,如果不请求 HTML 表示,默认情况下 JSON 响应中就没有 HTML 格式内容。

➌ 如果像第三个命令那样指定 Accept 首部,JSON 响应中会包含渲染成 HTML 的正文。

除了“raw”和“html”之外,还有两个格式选项会影响 GitHub API 分发 Markdown 内容的方式。如果把格式指定为“text”,工单的正文会以纯文本格式返回。如果指定为“full”,内容会经过多次渲染,包括原始的 Markdown、渲染后的 HTML 和渲染后的纯文本。

除了控制文本内容的格式之外,获取 blob 时还可以指定返回原始的二进制文件还是 base64 编码的文本。获取提交时,还可以指定返回内容的 diff 格式还是 patch 格式。精确控制格式的详细信息参见 GitHub API 的文档。

 GitHub 团队为 API 提供了非常详细的文档,包含使用 cURL 的示例。建议收藏这个 URL:https://developer.github.com/v3/,你经常会用到。注意,这个 URL 显然是针对当前的第 3 版 API,因此有新版发布的话,URL 会变。

1.10 小结

我们在本章学习了如何使用最简单的客户端(即命令行 HTTP 工具 cURL)访问 GitHub API。此外,我们通过分析 JSON 响应,探索了 GitHub API,还说明了如何结合使用 cURL 和命令行工具(jq),在往往包含大量数据的 GitHub API 响应中快速查找信息。我们学习了 GitHub 支持的不同身份验证机制,还学习了在浏览器中访问 GitHub API 的可行性和折中方案。

下一章说明 Gist 和 Gist API。我们将使用 Ruby 构建一个显示 Gist 的程序,把应用的所有源码文件都放在 Gist 中。

目录

  • 版权声明
  • O'Reilly Media, Inc.介绍
  • 前言
  • 第 1 章 开放的 GitHub API
  • 第 2 章 Gist 和 Gist API
  • 第 3 章 GitHub 使用的维基库 Gollum
  • 第 4 章 Python 和 Search API
  • 第 5 章 .NET 和 Commit Status API
  • 第 6 章 Ruby 和 Jekyll
  • 第 7 章 Android 和 Git Data API
  • 第 8 章 CoffeeScript、Hubot 和 Activity API
  • 第 9 章 JavaScript 和 Git Data API
  • 附录 A GitHub 企业版
  • 附录 B GitHub 对 Ruby、NodeJS(和 shell)的利用
  • 作者简介
  • 关于封面