极客学院团队出品 · 更新于 2018-11-28 11:00:42

遍历分页

GitHub API 提供了海量的信息给开发者消费。大多数时候,您甚至会发现您自己要求太多信息,为了防止我们劳累的服务器出现不测,API 会自动对要求的数据进行分页。

在这个指南中,我们会调用 GitHub 搜索 API,并通过分页对结果进行迭代。您可以在 platform-samples 存储库中找到本次示例工程的完整源代码。

分页基础

在您开始之前,有几个关于接收分页数据的重要事实您需要了解:

  1. 不同的 API 调用会得到不同的默认返回格式的值。举例来说,调用 列出 GitHub 所有公共存储库 会得到分页之后的数据,每页30个项目;但如果您调用 GitHub Search API,每个分页则会有100个项目。
  2. 您可以指定一个页面有多少个项目(上限是100);但是,
  3. 由于技术原因, 不是所有端点都会有一致的结果。举例来说,events 就不会让您指定接收页面的最大项目数。所以请务必阅读目标端点的关于如何处理分页结果的文档。

关于分页的信息请参照一次 API 调用的链接头。 例如,我们发一个 curl 请求给搜索 API,来查出 Mozilla 工程一共用了多少次短语 addClass

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla"

上文中的参数 -I 表示我们只关心链接头,不关心具体内容。在我们探讨结果的时候,您将会发现一些链接头内的信息会像下面这样:

    Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next",
      <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"

把这个信息分解来看,rel="next" 部分说明了下一页是 page=2。 这讲得通,毕竟默认情况下,所有分页的请求都会从页面 1 开始 。而 rel="last" 部分则提供了更多信息,说明了结果的最后一页是在 34 页。也就是说,还有 33 页关于 addClass 的信息可供消费,真爽!

需要注意的是您应该永远依赖对方提供给您的链接关系,不要自己试图猜测或者构建 URL,例如列出一个存储库内的所有 commit 中,分页结果是根据 SHA 散列值生成的,而不是页码。

在页面中导航

现在您知道了有多少个页面需要接收,下一步可以开始导航页面来观看结果。您可以通过传递 page 参数来完成这件事。默认情况下, page 参数总是从 1 开始。让我们直接跳到第 14 页看看会发生什么事情:

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&page=14"

然后再一次得到了类似下文的链接头:

    Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
      <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
      <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
      <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"

和预想中的一样,rel="next" 指向第 15 页,而 rel="last" 则仍然指向第 34 页。不过这次我们得到了一些别的信息:rel="first",提供了_第一页_的 URL;更重要的是,rel="prev" 能让您知道上一页的页码。有了这些信息,您就能构造一些用户界面来让用户在只需一次 API 调用的情况下就实现“第一页”、“最后一页”、“上一页”、“下一页”的随意跳转。

更改接收的项目数

通过传递 per_page 参数,您可以指定每页返回多少个项目了,上限是 100。我们来试试要求关于 addClass 的 50 个项目的检索结果:

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&per_page=50"

请注意这个语句对链接头回应的影响:

    Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&per_page=50&page=2>; rel="next",
      <https://api.github.com/search/code?q=addClass+user%3Amozilla&per_page=50&page=20>; rel="last"

也许您已经猜到,rel="last" 信息显示出最后一页现在变成第 20 页了。这显然是我们要求在每个页面内容纳更多的信息所导致的。

消费信息

您一般不会仅仅为了处理数据分页而费力使用低级的 cURL 调用,所以我们来写一个小小的 Ruby 脚本来完成上文提到的所有事情。 和往常一样,首先我们会需要GitHub 的 Octokit.rb Ruby 库,还要提供我们的个人访问令牌

    require 'octokit'

    # 在真正的应用内永远不要用硬编码把值写死 !
    # 而是设置环境变量并测试,和下例所示
    client = Octokit::Client.new :access_token => ENV['MY_PERSONAL_TOKEN']

接下来,我们利用 Octokit的 search_code 方法来执行搜索。和使用 curl 不同,用 search_code 方法还能立刻得到结果的数量,立刻试试:

    results = client.search_code('addClass user:mozilla')
    total_count = results.total_count

现在来获取最后一页的页码。和链接头的 page=34>; rel="last" 信息相类似,Octokit.rb 支持通过一个叫做“超媒体链接关系”的方法来返回分页信息,我们不会对这个东西作过多解释,不过,可以这么说,在 results 变量中的每个元素都有一个叫做 rels 的哈希值,这个哈希值可以包含和 :next:last:first:prev相关的信息,这取决于您收到的结果类型。这些关系也包含了结果的 URL,称为 rels[:last].href

知道这些之后,我们来获取最后一个结果所在的页码,然后将所有这些信息展现给用户:

    last_response = client.last_response
    number_of_pages = last_response.rels[:last].href.match(/page=(\d+)$/)[1]

    puts "There are #{total_count} results, on #{number_of_pages} pages!"

最后,我们来迭代结果。您虽然可以通过一个 for i in 1..number_of_pages.to_i 循环来做到这点,不过这次,不妨跟随 rels[:next] 头来接收每一页的信息。为了简便起见,这次只获取每一页中的第一个文件的路径。要实现这些需要使用循环,在到达每次循环的结尾时,都可以通过跟随 rels[:next] 信息来得到下一个页面的数据集,这个循环会一直运行直到再也没有 rels[:next] 可供消费为止(换句话说,这循环会执行到 rels[:last])。代码应该看上去像下面这样:

    puts last_response.data.items.first.path
    until last_response.rels[:next].nil?
      last_response = last_response.rels[:next].get
      puts last_response.data.items.first.path
    end

在 Octokit.rb 中,更改每页显示的项目数量是极端简单的。只需要传递一个 per_page 选项 hash 给初始的客户端构造器。除此之外,您的代码应该还是原来的样子:

    require 'octokit'

    # 在真正的应用内永远不要用硬编码把值写死 !
    # 而是设置环境变量并测试,像下例所示
    client = Octokit::Client.new :access_token => ENV['MY_PERSONAL_TOKEN']

    results = client.search_code('addClass user:mozilla', :per_page => 100)
    total_count = results.total_count

    last_response = client.last_response
    number_of_pages = last_response.rels[:last].href.match(/page=(\d+)$/)[1]

    puts last_response.rels[:last].href
    puts "There are #{total_count} results, on #{number_of_pages} pages!"

    puts "And here's the first path for every set"

    puts last_response.data.items.first.path
    until last_response.rels[:next].nil?
      last_response = last_response.rels[:next].get
      puts last_response.data.items.first.path
    end

构造分页链接

正常情况下,当使用分页时,您的目标不是去串联所有可能的结果,而是去制作一套类似下图这样的导航器:

Sample of pagination links

我们来勾勒出这个功能的一个微型版本。

从上文提到的代码中,我们已经知道能够在第一次调用得到的分页结果中获取 number_of_pages 值:

    require 'octokit'

    # 在真正的应用内永远不要用硬编码把值写死 !
    # 而是设置环境变量并测试,和下例所示
    client = Octokit::Client.new :access_token => ENV['MY_PERSONAL_TOKEN']

    results = client.search_code('addClass user:mozilla')
    total_count = results.total_count

    last_response = client.last_response
    number_of_pages = last_response.rels[:last].href.match(/page=(\d+)$/)[1]

    puts last_response.rels[:last].href
    puts "There are #{total_count} results, on #{number_of_pages} pages!"

通过下面的代码,可以构造一个漂亮的 ASCII 风格数字框:

    numbers = ""
    for i in 1..number_of_pages.to_i
      numbers << "[#{i}] "
    end
    puts numbers

我们通过随机数来模拟用户点击了其中任意一个数字框。

    random_page = Random.new
    random_page = random_page.rand(1..number_of_pages.to_i)

    puts "A User appeared, and clicked number #{random_page}!"

我们现在有了页码,可以使用 Octokit 通过传递 :page 选项来显式地接收目标页面了:

`clicked_results = client.search_code('addClass user:mozilla', :page => random_page)`

如果想做的更精致一些,我们也可以把上一页和下一页的链接也弄过来。要创建“上一页”(<<)和“下一页”(>>)元素,可以参考以下代码:

    prev_page_href = client.last_response.rels[:prev] ? client.last_response.rels[:prev].href : "(none)"
    next_page_href = client.last_response.rels[:next] ? client.last_response.rels[:next].href : "(none)"

    puts "The prev page link is #{prev_page_href}"
    puts "The next page link is #{next_page_href}"
上一篇: 使用评论 下一篇: 架设 CI 服务器