
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/limit
やcursor
の扱いミスで取り漏れ/重複。 - スキーマ無視:曖昧な型で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/limit
・cursor
・Link
の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)は“仕様書兼型定義”。servers
・paths
・parameters
・schemas
から、関数引数と返り値の型を決めていきます。
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_url=servers.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-After
やX-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/limit
→iter_*
、NDJSON/Parquet保存→[内部リンク:自動化]。 - 在庫/価格監視:API優先、無ければ[内部リンク:Webスクレイピング]。RateLimit/429順守。
- 社内API→社外BI:型で守り、例外分類、ジョブ化で落ちない定例へ。
今日やること(45分)
- 連携先のOpenAPIを開き、base_url/paths/parameters/schemasを確認。
- 本記事のApiClientをコピペ→APIキー/OAuthを環境変数で注入。
- 対象APIに合わせたページネーション関数を1本作る。
- NDJSON/Parquetへ保存→pandasで中身確認。[内部リンク:pandas実践]
まとめ:HTTPは“型”で扱う
無限待ち/取り漏れ/型崩れは設計で防げます。タイムアウトは必須・再試行は限定・ページネーションは関数化・スキーマは型で守る。配布したテンプレを土台に、ETag/Idempotency-Keyで安全に、pytestで壊れない運用へ。
伴走:APIクライアントを“壊れない運用”に
無料カウンセリング/体験で、あなたの対象APIに合わせてOpenAPI読み解き→クライアント実装→保存・スケジュールまで仕上げます。
TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

株式会社キカガク 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スクレイピングの法的リスクと安全運用|“規約→同意→頻度→記録”でトラブルを回避する実務ガイド
「副業や社内でスクレイピングを使いたい。でも、どこまでOKで、何をするとNG?」 結論、“安全運用の型”を最初に決めると迷いません。鍵は規約 → 同意 → 頻度 → 記録。 重要:本記事は一般情報です ...
最近のコメント