改ページ位置の自動修正について
REST API からの応答にたくさんの結果が含まれるとき、GitHub では結果のページが分割され、結果のサブセットが返されます。 たとえば、GET /repos/octocat/Spoon-Knife/issues
は、リポジトリに 1600 を超える未解決の issue が含まれている場合でも、octocat/Spoon-Knife
リポジトリから 30 の issue のみを返します。 これにより、サーバーとユーザーに対して、応答の処理が簡単になります。
応答の link
ヘッダーを利用してデータの追加ページをリクエストできます。 per_page
クエリ パラメーターがエンドポイントでサポートされている場合、1 ページで返される結果の数を制御できます。
この記事では、ページ分割された応答に結果の追加ページをリクエストする方法、各ページで返される結果の数を変更する方法、および複数の結果ページをフェッチするスクリプトを記述する方法を示します。
link
ヘッダーの使用
応答がページ分割されている場合、応答ヘッダーには link
ヘッダーが含まれます。 エンドポイントが改ページ位置の自動修正をサポートしていない場合、またはすべての結果が 1 つのページに収まる場合、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 のクエリ パラメーターは、エンドポイントによって異なる場合があります。ただし、ページ分割された各エンドポイントでは、page
、before
/after
、または since
クエリ パラメーターが使用されます。 (一部のエンドポイントでは、改ページ位置の自動修正以外のものに対しては since
パラメーターが使用されます)。いずれの場合も、link
ヘッダーの URL を使用して、結果の追加ページをフェッチできます。 クエリ パラメーターの詳細については、「REST API を使用した作業の開始」を参照してください。
ページごとのアイテム数の変更
per_page
クエリ パラメーターがエンドポイントでサポートされる場合、1 ページで返される結果の数を制御できます。 クエリ パラメーターの詳細については、「REST API を使用した作業の開始」を参照してください。
たとえば、この要求では、per_page
クエリ パラメーターを使用してページごとに 2 つのアイテムを返します。
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 の README を参照してください。
Octokit.js の改ページ位置の自動修正メソッドの使用例
Octokit.js を使用してページ分割された結果をフェッチするには、octokit.paginate()
を使用できます。 octokit.paginate()
は、最後のページに達するまで結果の次のページをフェッチし、すべての結果を 1 つの配列として返します。 いくつかのエンドポイントは、ページ分割された結果を配列として返すのではなく、ページ分割された結果をオブジェクト内の配列として返します。 生の結果がオブジェクトであっても、octokit.paginate()
は常にアイテムの配列を返します。
たとえば、このスクリプトはoctocat/Spoon-Knife
リポジトリからすべての issue を取得します。 一度に 100 の issue が要求されますが、データの最後のページに達するまで関数は返されません。
import { Octokit } from "octokit"; const octokit = new Octokit({ baseUrl: "http(s)://HOSTNAME/api/v3", }); const data = await octokit.paginate("GET /repos/{owner}/{repo}/issues", { owner: "octocat", repo: "Spoon-Knife", per_page: 100, }); console.log(data)
import { Octokit } from "octokit";
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
});
const data = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner: "octocat",
repo: "Spoon-Knife",
per_page: 100,
});
console.log(data)
省略可能な map 関数を octokit.paginate()
に渡して最後のページに達する前に改ページ位置の自動修正を終了するか、応答のサブセットのみを保持することでメモリ使用量を削減できます。 octokit.paginate.iterator()
を使用して、すべてのページを要求するのではなく、一度に 1 つのページを反復処理することもできます。 詳しくは、Octokit.js のドキュメントを参照してください。
改ページ位置の自動修正メソッドの作成例
改ページ位置の自動修正メソッドがない別の言語またはライブラリを使用している場合は、独自の改ページ位置の自動修正メソッドを作成できます。 この例では、引き続き Octokit.js ライブラリを使用して要求を行いますが、octokit.paginate()
には依存しません。
getPaginatedData
関数は、octokit.request()
を使用してエンドポイントに要求を行います。 応答からのデータは parseData
によって処理されます。この場合、データが返されないケースや、返されるデータが配列ではなくオブジェクトであるケースが処理されます。 処理されたデータはその後、これまでに収集された、ページ分割されたすべてのデータを含むリストに追加されます。 応答に link
ヘッダーが含まれており、link
ヘッダーに次のページのリンクが含まれている場合、関数は 正規表現パターン (nextPattern
) を使用して次のページの URL を取得します。 関数は、今度はこの新しい URL を使用して、前のステップを繰り返します。 link
ヘッダーに次のページへのリンクが含まれなくなると、すべての結果が返されます。
import { Octokit } from "octokit"; const octokit = new Octokit({ baseUrl: "http(s)://HOSTNAME/api/v3", }); 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, }); 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);
import { Octokit } from "octokit";
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
});
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,
});
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);