は結果を一度に全部返しません。100件ずつ、1000件ずつと小分けにして返します。これがで、続きを取るには「次をください」と伝える必要があります。この伝え方に何種類かあり、種類によって取りこぼしの起き方が違います。
取りこぼしは静かに起きます。例外も出ず、件数が少し足りないだけなので、集計してから「先月より減っている」と気づくことになります。この回では4つの型を並べて、どれがどういう条件で取りこぼすかを見ます。
どの型を使っているかは、を見れば見当がつきます。`offset` や `page` があれば1つ目の型、`cursor` や `after` があれば2つ目、どちらも無ければ を確認します。まず1回だけ叩いて、応答の全体を目で見るのが結局いちばん早いです。
型1: offset 方式 ── 一番よく見て、一番危ない
`?offset=0&limit=100`、`?page=1&per_page=100` のように、何件目からを数字で指定する方式です。実装が単純で、どのページからでも取りに行けます。
弱点は、取得している最中にデータが増減すると破綻することです。1ページ目(1〜100件目)を取ったあと、2ページ目を取る前に新しいデータが1件先頭に入ると、元の100件目が101件目にずれます。2ページ目は101件目から取るので、元の100件目を2回取り、次の1件を取りこぼします。
更新が止まっているデータなら問題ありません。動き続けているデータを offset で全件取るのは、原理的に不正確だと理解しておいてください。過去分の集計のように「ある時点で確定しているもの」を取る用途なら、offset でも実害は出ません。
型2: カーソル方式 ── 増減に強い
「前回の続き」を表す印を相手が返してきて、それを次の要求に添える方式です。`next_cursor` や `after` といった名前で返ってきます。印は不透明な文字列であることが多く、中身を解釈してはいけません。
位置ではなく「どこまで読んだか」を渡すので、途中でデータが増えてもずれません。動いているデータを取るなら、こちらが正解です。代わりに、途中のページだけを取り直すことはできません。
import requests
def fetch_all(url: str, params: dict | None = None, max_pages: int = 1000): """カーソルを追って全件取る。上限を必ず置いて、無限ループを防ぐ。""" params = dict(params or {}) seen_cursors = set()
for _ in range(max_pages): resp = requests.get(url, params=params, timeout=(5, 30)) resp.raise_for_status() body = resp.json()
yield from body.get("items", [])
cursor = body.get("next_cursor") if not cursor: return # 最後まで来た if cursor in seen_cursors: raise RuntimeError("同じカーソルが再び返ってきた。相手側の不具合の可能性") seen_cursors.add(cursor) params["cursor"] = cursor
raise RuntimeError(f"{max_pages} ページを超えた。想定より件数が多い")相手の不具合で同じカーソルが返り続けると、無限に取得し続けます。深夜に走らせて朝には数十万リクエストを投げていた、という壊れ方をします。ページ数の上限と、同じカーソルの検出は保険として入れておいてください。
型3: Link ヘッダ方式
本文ではなく HTTP のヘッダに次の URL が入ってくる方式です。`Link: <https://...&page=2>; rel="next"` のような形で、GitHub の API などが採用しています。requests は `resp.links` として解釈済みの形で持っているので、自分で解析する必要はありません。
import requests
def fetch_by_link(url: str, max_pages: int = 1000): for _ in range(max_pages): resp = requests.get(url, timeout=(5, 30)) resp.raise_for_status() yield from resp.json()
nxt = resp.links.get("next") if not nxt: return url = nxt["url"] # 相手が返した URL をそのまま使う
raise RuntimeError("ページ数の上限に達した")型4: 継続トークン方式
カーソルとほぼ同じですが、トークンに有効期限がある点が違います。クラウドの一覧取得 API でよく見られます。取得を一時停止して翌日に再開する、といった使い方はできません。期限内に取り切る前提で組みます。
どれを使っているか分からないとき
応答を1回だけ取って、中身とヘッダを目で見るのが早いです。本文に `next` を含む鍵があればカーソルか継続トークン、ヘッダに Link があれば Link 方式、どちらも無くて `total` や `count` だけがあれば offset 方式です。
- 全件数が返るか — `total` があれば、取得後に件数を突き合わせて検算できる
- 並び順が固定されているか — 順序が不定だと、どの方式でも取りこぼす
- 最大件数の上限 — `limit=10000` と書いても 100 に丸められることがある
件数の検算は必ず入れてください。`total` が返るなら、取得後に一致を確認します。一致しないまま先へ流すと、下流の集計だけが静かにずれます。データの検証については 前処理の現場 でも扱っています。
取り直しの回数を減らしたいなら も使えます。前回もらった版の印を添えて要求すると、内容が変わっていなければ本文の代わりに 304 が返ります。転送量も相手の負荷も減るので、毎日同じ範囲を取りに行く処理では効きます。
もうひとつ、途中から再開できるようにするなら を意識してください。同じページを2回取っても保存結果が二重にならない書き方にしておけば、どこで落ちても最初からやり直す必要がなくなります。鍵を決めて上書きする形にするのが単純です。
次回は、取りすぎたときに返ってくる 429 の扱いを見ます。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。