Python基礎

API入門:OpenAPI/HTTPの基本と“壊れない”Pythonクライアント設計(コピペOK)

API連携を始めたいけど、何から学べば“壊れない仕組み”になる?

OpenAPI?HTTP?タイムアウト?……用語が多すぎて迷子になりがち。

本記事は、HTTPの基礎×OpenAPIの読み方×堅牢なクライアント設計を“最短ルート”で整理し、requestsベースの最小クライアントからページネーション/レート制限/ETag/Idempotency-Keyまで、コピペで動くテンプレを配布します。ふみと(筆者)が現場で積み上げた「止まらない・壊さない・速く回す」ための型そのものです。

この記事で身に付く力

  • HTTP/RESTの“運用で効く”要点理解(タイムアウト/再試行/レート制限)
  • OpenAPI(Swagger)の読み取り→実装反映の型
  • requests最小クライアントページネーション3型の実装力
  • ETag/Idempotency-Keyによる安全・効率的な取得
  • pytestで「再現できる」APIクライアントに仕上げる力

関連記事:
>>Python実務の型:例外処理と構造化ログでエラーに強いコードを書く
>>もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ
>>Webスクレイピング:requests×BeautifulSoupの基本|“合法×丁寧×再現性”でデータ取得を設計する
>>自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする
>>Python標準ライブラリ珠玉の10選|datetime・pathlib・itertoolsで実務が回るチートシート
>>【保存版】pandas基礎:データフレームの作成・整形・結合・集計を“実務の型”で身につける

まず押さえる:API運用の“3つの落とし穴”

ふみとの現場メモ。APIは仕様より運用で壊れます。特に次の3つ。

  • 無限待ち/暴走リトライ:タイムアウト無し+無制限再試行で停止も攻撃化も。
  • ページネーション崩れpage/limitcursorの扱いミスで取り漏れ/重複
  • スキーマ無視:曖昧な型でKeyError/型エラーが散発、運用で破綻。

解決はシンプル。HTTPの“型”を最初に決めるタイムアウト必須/再試行は限定/ページネーションは関数化/レスポンスは型で守る

最小のrequestsクライアント(コピペOK)

まずは“土台”。セッション再利用+限定リトライ+ETag対応まで入れた最小クライアントです。

from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class ApiError(Exception): ...
class RateLimitError(ApiError): ...
class ExternalError(ApiError): ...
class DataError(ApiError): ...

UA = "pythonbunseki-api/1.0 (+https://pythonbunseki.com)"

@dataclass(slots=True)
class ApiClient:
    base_url: str
    api_key: str | None = None
    oauth_token: str | None = None
    timeout: float = 10.0

    def __post_init__(self):
        s = requests.Session()
        s.headers.update({
            "User-Agent": UA,
            "Accept": "application/json",
            "Accept-Language": "ja,en;q=0.8",
        })
        if self.api_key:
            s.headers["X-API-Key"] = self.api_key
        if self.oauth_token:
            s.headers["Authorization"] = f"Bearer {self.oauth_token}"
        retry = Retry(
            total=3,
            backoff_factor=0.7,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["GET", "HEAD"],
            respect_retry_after_header=True,
        )
        s.mount("https://", HTTPAdapter(max_retries=retry))
        s.mount("http://", HTTPAdapter(max_retries=retry))
        self._s = s

    def get(self, path: str, params: dict[str, Any] | None = None, *, etag: str | None = None) -> tuple[dict, str | None]:
        url = self.base_url.rstrip("/") + "/" + path.lstrip("/")
        headers = {}
        if etag:
            headers["If-None-Match"] = etag
        r = self._s.get(url, params=params, headers=headers, timeout=self.timeout)
        if r.status_code == 304:
            raise ExternalError("not modified")
        if r.status_code == 429:
            raise RateLimitError(r.text)
        try:
            r.raise_for_status()
        except requests.HTTPError as e:
            raise ExternalError(f"{r.status_code}: {r.text[:200]}") from e
        etag_new = r.headers.get("ETag")
        try:
            data = r.json()
        except ValueError as e:
            raise DataError("invalid json") from e
        return data, etag_new

ページネーション3パターン

「取り漏れゼロ・重複ゼロ」が原則。page/limitcursorLinkの3型を関数化します。

from typing import Any, Iterator

# A) page/limit 方式

def iter_pages_page_limit(cli: ApiClient, path: str, *, limit: int = 100) -> Iterator[dict]:
    page = 1
    while True:
        data, _ = cli.get(path, {"page": page, "limit": limit})
        items: list[dict[str, Any]] = data.get("items", [])
        if not items:
            break
        for it in items:
            yield it
        page += 1
# B) cursor/next_token 方式

from typing import Iterator

def iter_pages_cursor(cli: ApiClient, path: str, *, cursor: str | None = None) -> Iterator[dict]:
    token = cursor
    while True:
        params = {"cursor": token} if token else None
        data, _ = cli.get(path, params)
        for it in data.get("data", []):
            yield it
        token = data.get("next")
        if not token:
            break
# C) Linkヘッダ方式(<...>; rel="next")

def parse_link_next(link_header: str | None) -> str | None:
    if not link_header:
        return None
    for part in link_header.split(","):
        seg = part.strip().split(";")
        if len(seg) >= 2 and 'rel="next"' in seg[1]:
            url = seg[0].strip()
            if url.startswith("<") and url.endswith(">"):
                return url[1:-1]
    return None

# 実運用ではレスポンスヘッダから次URLをたどる実装にする

取り込み側でも主キー(id)で去重を併用すると安心です。

差分取得:ETag/If‑None‑Match

# ETagキャッシュ(例)
etag_cache: dict[str, str] = {}

def fetch_users(cli: ApiClient) -> list[dict]:
    etag = etag_cache.get("/users")
    try:
        data, etag_new = cli.get("/users", etag=etag)
    except ExternalError as e:
        if str(e) == "not modified":
            return []  # 差分なし
        raise
    if etag_new:
        etag_cache["/users"] = etag_new
    return data.get("items", [])

認証:APIキー/OAuth2/HMAC署名

秘密情報は必ず.envやシークレット管理に置き、Gitに載せないこと。

import hmac, hashlib, base64, time

def signed_headers(secret: str, method: str, path: str, body: bytes = b"") -> dict[str, str]:
    ts = str(int(time.time()))
    msg = (method.upper() + path + ts).encode() + body
    sig = hmac.new(secret.encode(), msg, hashlib.sha256).digest()
    return {"X-Timestamp": ts, "X-Signature": base64.b64encode(sig).decode()}

保存:NDJSON/Parquet(重い整形は後段)

from pathlib import Path
import json, pandas as pd

def save_ndjson(items: list[dict], path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as f:
        for it in items:
            f.write(json.dumps(it, ensure_ascii=False) + "\n")

def save_parquet(items: list[dict], path: Path) -> None:
    import pandas as pd
    df = pd.DataFrame(items)
    df.to_parquet(path, compression="zstd")

詳細は[内部リンク:ファイル操作]/[内部リンク:pandas実践]をどうぞ。

OpenAPIの読み方 → 実装への落とし込み

OpenAPI(Swagger)は“仕様書兼型定義”serverspathsparametersschemasから、関数引数と返り値の型を決めていきます。

openapi: 3.0.3
info: { title: Sample API, version: '1.0' }
servers: [ { url: https://api.example.com } ]
paths:
  /users:
    get:
      summary: List users
      parameters:
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: limit
          schema: { type: integer, maximum: 100 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
  /users/{id}:
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/User' }
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id: { type: string }
        name: { type: string }
        email: { type: string, nullable: true }

実装ポイント
base_urlservers.urlクエリ/パス変数parameters→関数引数へ。
スキーマrequired/nullableを型へ反映(例:str | None)。
ページネーションは仕様に沿ってiter_*関数化。

重複POSTを防ぐ:Idempotency-Key

import uuid

def post_json(cli: ApiClient, path: str, payload: dict) -> dict:
    url = cli.base_url.rstrip("/") + "/" + path.lstrip("/")
    headers = {"Idempotency-Key": uuid.uuid4().hex}
    r = cli._s.post(url, json=payload, headers=headers, timeout=cli.timeout)
    r.raise_for_status()
    return r.json()

運用のコツ(レート/ログ/フェイルセーフ)

  • レート制限Retry-AfterX-RateLimit-Remainingを尊重。枯渇前にクールダウン、429は停止が安全。
  • 構造化ログ:URL/ステータス/件数/所要/相関ID。→ [内部リンク:例外処理とログ設計]
  • フェイルセーフ:新規取得失敗時は前回値で継続、レポートは空テンプレ+注記で止めない。

pytestで“再現できるAPIクライアント”に

from client import ApiClient

class DummyResp:
    def __init__(self, status_code=200, json_data=None, headers=None):
        self.status_code = status_code
        self._json = json_data or {}
        self.headers = headers or {}
        self.text = ""
        self.encoding = "utf-8"
    def json(self):
        return self._json
    def raise_for_status(self):
        if not (200 <= self.status_code < 300):
            raise Exception("http error")

class DummySession:
    def __init__(self, resps):
        self._resps = resps
        self.headers = {}
    def get(self, *a, **k):
        return self._resps.pop(0)

def test_pagination_page_limit(monkeypatch):
    cli = ApiClient("https://api.example.com")
    resps = [
        DummyResp(json_data={"items":[{"id":1},{"id":2}]}),
        DummyResp(json_data={"items":[]}),
    ]
    cli._s = DummySession(resps)
    from client import iter_pages_page_limit
    ids = [it["id"] for it in iter_pages_page_limit(cli, "/users")]
    assert ids == [1,2]

ユースケース別“第一手”

  • SaaSレポート自動取得:ETagで差分、page/limititer_*、NDJSON/Parquet保存→[内部リンク:自動化]。
  • 在庫/価格監視:API優先、無ければ[内部リンク:Webスクレイピング]。RateLimit/429順守。
  • 社内API→社外BI:型で守り、例外分類、ジョブ化で落ちない定例へ。

今日やること(45分)

  1. 連携先のOpenAPIを開き、base_url/paths/parameters/schemasを確認。
  2. 本記事のApiClientをコピペ→APIキー/OAuthを環境変数で注入。
  3. 対象APIに合わせたページネーション関数を1本作る。
  4. NDJSON/Parquetへ保存→pandasで中身確認。[内部リンク:pandas実践]

まとめ:HTTPは“型”で扱う

無限待ち/取り漏れ/型崩れは設計で防げます。タイムアウトは必須・再試行は限定・ページネーションは関数化・スキーマは型で守る。配布したテンプレを土台に、ETag/Idempotency-Keyで安全に、pytestで壊れない運用へ。

伴走:APIクライアントを“壊れない運用”に

無料カウンセリング/体験で、あなたの対象APIに合わせてOpenAPI読み解き→クライアント実装→保存・スケジュールまで仕上げます。

TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

TechAcademy 無料相談

株式会社キカガク AI人材長期育成コース(受講料:237,600円~)

キカガク 無料相談

この記事から次に読むべきもの(内部リンク)

自動化
自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする

夜中に動かしているPython、自動で止まってた…ログもなくて原因が追えない…。 「毎朝のレポート」や「在庫監視」を、壊れず静かに回したい…! 業務で落ちない自動化を作る鍵は、(1) 再実行安全(Id ...

例外処理
Python実務の型:例外処理と構造化ログでエラーに強いコードを書く

例外処理って、結局どこまでやれば“実務で困らない”の? ログも整えるのって大変そう…最低限の型、ください! この記事は、pythonbunseki.comの実務トーンで「防ぐ→気づく→復旧する」をコー ...

事故防止・効率化
もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ

CSV/JSON/Excelの読み書き、どこから気をつければいい? 文字化け・先頭ゼロ欠落・壊れたExcel……もう事故らせたくない! 結論:データ仕事の9割はI/O(入出力)。ここを整えるだけで、桁 ...

スキルアップ
【実務で差がつく】pandas実践:欠損処理・結合・ウィンドウ関数・時系列・品質保証まで“読みやすく速い”型を習得

リード(結論)基礎を終えたら次は実務の現場で頻出する処理を“型”で覚える段階です。本記事は、pandas 2.x を前提に、欠損・外れ値・結合・ウィンドウ関数・時系列・カテゴリ処理・集計の自動化・大規 ...

Webスクレイピングの法的リスク
Webスクレイピングの法的リスクと安全運用|“規約→同意→頻度→記録”でトラブルを回避する実務ガイド

「副業や社内でスクレイピングを使いたい。でも、どこまでOKで、何をするとNG?」 結論、“安全運用の型”を最初に決めると迷いません。鍵は規約 → 同意 → 頻度 → 記録。 重要:本記事は一般情報です ...

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -Python基礎