跳至主要内容

在 REST API 中使用分页

了解如何浏览 REST API 的分页响应。

关于分页

当 REST API 的响应包含许多结果时,GitHub 会对结果进行分页并返回结果的子集。例如,GET /repos/octocat/Spoon-Knife/issues 仅返回 octocat/Spoon-Knife 仓库中的 30 个问题,即使该仓库包含超过 1600 个开放问题。这使得响应更容易被服务器和人员处理。

您可以使用响应中的 link 头来请求更多页面的数据。如果某个端点支持 per_page 查询参数,您可以控制每页返回的结果数量。

本文演示了如何请求分页响应的更多页面的结果,如何更改每页返回的结果数量,以及如何编写脚本以获取多页的结果。

当响应被分页时,响应头部将包含一个 link 头部。如果端点不支持分页,或者所有结果都适合在一页中,则会省略 link 头部。

link 头部包含可以用来获取更多结果页面的 URL。例如,结果的上一页、下一页、第一页和最后一页。

要查看特定端点的响应头部,可以使用 curl、GitHub CLI 或您用来发出请求的库。要查看使用库发出请求时的响应头部,请遵循该库的文档。要查看使用 curl 或 GitHub CLI 时的响应头部,请在您的请求中传递 --include 标志。例如

curl --include --request GET \
--url "https://api.github.com/repos/octocat/Spoon-Knife/issues" \
--header "Accept: application/vnd.github+json"

如果响应被分页,link 头部将类似于以下内容

link: <https://api.github.com/repositories/1300192/issues?page=2>; rel="prev", <https://api.github.com/repositories/1300192/issues?page=4>; rel="next", <https://api.github.com/repositories/1300192/issues?page=515>; rel="last", <https://api.github.com/repositories/1300192/issues?page=1>; rel="first"

link 头部提供了结果的上一页、下一页、第一页和最后一页的 URL

  • 上一页的 URL 后面跟着 rel="prev"
  • 下一页的 URL 后面跟着 rel="next"
  • 最后一页的 URL 后面跟着 rel="last"
  • 第一页的 URL 后面跟着 rel="first"

在某些情况下,只有这些链接的一部分可用。例如,如果您在结果的第一页,则不会包含到上一页的链接,如果无法计算最后一页,则不会包含到最后一页的链接。

您可以使用 link 头部中的 URL 请求另一页结果。例如,要根据前面的示例请求最后一页结果

curl --include --request GET \
--url "https://api.github.com/repositories/1300192/issues?page=515" \
--header "Accept: application/vnd.github+json"

link 头部中的 URL 使用查询参数来指示要返回的结果页。link URL 中的查询参数可能因端点而异,但每个分页端点都将使用 pagebefore/aftersince 查询参数。(某些端点将 since 参数用于分页以外的其他用途。)在所有情况下,您可以使用 link 头部中的 URL 获取更多结果页面。有关查询参数的更多信息,请参阅 "REST API 入门。"。

更改每页的项目数量

如果端点支持 per_page 查询参数,则可以控制每页返回的结果数量。有关查询参数的更多信息,请参阅 "REST API 入门。"。

例如,此请求使用 per_page 查询参数来每页返回两个项目

curl --include --request GET \
--url "https://api.github.com/repos/octocat/Spoon-Knife/issues?per_page=2" \
--header "Accept: application/vnd.github+json"

per_page 参数将自动包含在 link 头部中。例如

link: <https://api.github.com/repositories/1300192/issues?per_page=2&page=2>; rel="next", <https://api.github.com/repositories/1300192/issues?per_page=2&page=7715>; rel="last"

使用分页进行脚本编写

与其手动从 link 标头中复制 URL,您可以编写脚本以获取多页结果。

以下示例使用 JavaScript 和 GitHub 的 Octokit.js 库。有关 Octokit.js 的更多信息,请参阅“REST API 入门”和Octokit.js 自述文件

使用 Octokit.js 分页方法的示例

要使用 Octokit.js 获取分页结果,您可以使用 octokit.paginate()octokit.paginate() 将获取下一页结果,直到到达最后一页,然后将所有结果作为单个数组返回。一些端点将分页结果作为对象中的数组返回,而不是将分页结果作为数组返回。octokit.paginate() 始终返回项目数组,即使原始结果是对象。

例如,此脚本获取 octocat/Spoon-Knife 存储库中的所有问题。虽然它一次请求 100 个问题,但该函数不会在到达最后一页数据之前返回。

JavaScript
import { Octokit } from "octokit";

const octokit = new Octokit({ });

const data = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
  owner: "octocat",
  repo: "Spoon-Knife",
  per_page: 100,
  headers: {
    "X-GitHub-Api-Version": "2022-11-28",
  },
});

console.log(data)

您可以将可选的映射函数传递给 octokit.paginate() 以在到达最后一页之前结束分页或通过仅保留响应的子集来减少内存使用。您还可以使用 octokit.paginate.iterator() 逐页迭代,而不是请求每一页。有关更多信息,请参阅Octokit.js 文档

创建分页方法的示例

如果您使用的是没有分页方法的其他语言或库,您可以构建自己的分页方法。此示例仍然使用 Octokit.js 库来发出请求,但它不依赖于 octokit.paginate()

getPaginatedData 函数使用 octokit.request() 向端点发出请求。来自响应的数据由 parseData 处理,parseData 处理没有返回数据的情况或返回的数据是对象而不是数组的情况。然后将处理后的数据追加到包含迄今为止收集的所有分页数据的列表中。如果响应包含 link 标头,并且 link 标头包含下一页的链接,则该函数使用正则表达式模式 (nextPattern) 获取下一页的 URL。然后,该函数重复前面的步骤,现在使用此新 URL。一旦 link 标头不再包含指向下一页的链接,所有结果都会被返回。

JavaScript
import { Octokit } from "octokit";

const octokit = new Octokit({ });

async function getPaginatedData(url) {
  const nextPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i;
  let pagesRemaining = true;
  let data = [];

  while (pagesRemaining) {
    const response = await octokit.request(`GET ${url}`, {
      per_page: 100,
      headers: {
        "X-GitHub-Api-Version":
          "2022-11-28",
      },
    });

    const parsedData = parseData(response.data)
    data = [...data, ...parsedData];

    const linkHeader = response.headers.link;

    pagesRemaining = linkHeader && linkHeader.includes(`rel=\"next\"`);

    if (pagesRemaining) {
      url = linkHeader.match(nextPattern)[0];
    }
  }

  return data;
}

function parseData(data) {
  // If the data is an array, return that
    if (Array.isArray(data)) {
      return data
    }

  // Some endpoints respond with 204 No Content instead of empty array
  //   when there is no data. In that case, return an empty array.
  if (!data) {
    return []
  }

  // Otherwise, the array of items that we want is in an object
  // Delete keys that don't include the array of items
  delete data.incomplete_results;
  delete data.repository_selection;
  delete data.total_count;
  // Pull out the array of items
  const namespaceKey = Object.keys(data)[0];
  data = data[namespaceKey];

  return data;
}

const data = await getPaginatedData("/repos/octocat/Spoon-Knife/issues");

console.log(data);