ふくふくHukuhuku Inc.
EP.02Data Fetch 10分公開: 2026-09-02

ページングの4つの型と、取りこぼしが起きる条件

1000件あるはずが980件しか取れない。原因はたいてい offset 方式です。offset / カーソル / Link ヘッダ / 継続トークンの違いと、どれがどう取りこぼすかを整理します。

#API#Python#落とし穴
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

は結果を一度に全部返しません。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件を取りこぼします

1ページ目を取得した時点 A B C … 100件目まで取得済み 先頭に X が入る X A B C 全体が1つ後ろへずれる → 101件目から取ると C を2回取り、その次の1件を取りこぼす
取得中に先頭へ1件挿入されると、境界がずれて重複と欠落が同時に起きる

更新が止まっているデータなら問題ありません。動き続けているデータを offset で全件取るのは、原理的に不正確だと理解しておいてください。過去分の集計のように「ある時点で確定しているもの」を取る用途なら、offset でも実害は出ません。

型2: カーソル方式 ── 増減に強い

「前回の続き」を表す印を相手が返してきて、それを次の要求に添える方式です。`next_cursor` や `after` といった名前で返ってきます。印は不透明な文字列であることが多く、中身を解釈してはいけません

位置ではなく「どこまで読んだか」を渡すので、途中でデータが増えてもずれません。動いているデータを取るなら、こちらが正解です。代わりに、途中のページだけを取り直すことはできません。

カーソル方式。次の印が無くなるまで回す
Python
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` として解釈済みの形で持っているので、自分で解析する必要はありません。

Link ヘッダを辿る。URL を自分で組み立て直さない
Python
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 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp