CHAPTER 8 ・ 仕事で使える形にする / Lesson 33
APIを実務で使う
認証、ページ送り、失敗時のやり直し。実際のサービスにつなぐときの、守るべき作法を学びます。
このレッスンが終わったら
「APIは、認証・ページ送り・やり直し(リトライ)・回数制限を意識して使う。キーはコードに書かず、環境変数で渡す」と言える。
こんな場面、ありませんか?
受注システムのAPI(基礎編で学んだ、データの受付窓口)から、10月の受注を全件取り出して、集計したいとします。ところが、APIは、1回のお願いで全部を返してくれるとは限りません。練習用のAPIに、1回だけお願いしてみましょう。
▶ 実行を押すと、ここに結果が出ます
全体では10件あるのに、届いたのは4件です。データが多いとき、サーバーは「1回に4件まで」のように区切って返します。これをページ送りと呼びます。返事の中の next が、「続きは2ページ目にあります」という目印です。
実務のAPIは、1回に100件、1,000件のように区切るのがふつうです。1ページ目だけを集計して、全体だと思い込むミスは、よく起きます。
ページ送りで、全件を集める
next がなくなる(None になる)まで、繰り返しお願いします。
▶ 実行を押すと、ここに結果が出ます
3ページ(4件、4件、2件)が集まり、全体の10件と一致しました。(この練習ではページごとに別のファイルですが、本物のAPIでは、同じURLに ?page=2 のように番号を添えます)ポイントは3つです。
- 終わりの印を見る。 3ページ目の
nextはNoneです。これが「もう続きはない」の合図です。 - 上限を決めておく。
MAX_PAGESは、印が壊れていても、永遠に続けないための保険です。 - 件数を照らし合わせる。 集めた件数と、
total_countが合うか確かめます(検算)。
本物のサービスにつなぐときの作法
自分のパソコンで本物のAPIを使うときは、requests という道具を使います(Lesson 30 で学んだ pip で、pip install requests と入れます)。次のコードは、このページでは動かしません。URLは練習用の架空のものです。
import os
import requests
API_URL = "https://api.example.co.jp/v1/orders"
def fetch_page(page):
headers = {"Authorization": "Bearer " + os.environ["SAKURA_API_KEY"]}
response = requests.get(API_URL, headers=headers, params={"page": page}, timeout=10)
response.raise_for_status()
return response.json()headers = {"Authorization": "Bearer " + ...}APIキーを、お願いの「ヘッダー」(添え書き)に付ける。キーは環境変数から読むparams={"page": page}URLの後ろに、?page=2 のようにページ番号を付けるtimeout=10タイムアウト。10秒待っても返事がなければ、あきらめるresponse.raise_for_status()返事が失敗(401 や 503 など)なら、ここでエラーにするresponse.json()返事のJSONを、辞書に変換するheaders に付けるのは、あなた専用の合鍵、APIキーです。timeout は、返事を待つ時間の上限(タイムアウト)で、付け忘れると、相手が応答しないとき、プログラムがいつまでも待ち続けることがあります。
返事には、結果を表す3桁の数字、ステータスコードが付いてきます。どの失敗が「待てば直る」のかを知っておくと、次のSTEPの判断ができます。
| コード | 意味 | やり直す? |
|---|---|---|
| 200 | 成功 | 不要 |
| 401・403 | キーが違う、期限切れ、権限がない | しない(直さないと、何度やっても同じ) |
| 404 | URLが違う | しない |
| 429 | 呼びすぎ(回数制限) | 待ってから、やり直す |
| 500・502・503・504 | サーバー側の一時的な不調 | 待ってから、やり直す |
キーは、コードに書かず、環境変数(パソコンに預けておく値。Lesson 32 で学びました)から読みます。画面やログにも出しません。
失敗に備える(リトライ)
通信は、ときどき失敗します。ネットワークの一瞬の途切れ、サーバーの混雑。少し待てば直る失敗に、一度で諦めると、毎朝の自動処理が、ときどき止まります。
「呼んだ回数で失敗する」自作の関数で、様子を見ましょう。1回目と2回目は失敗、3回目から成功します。まず、何も備えないとどうなるでしょう。
▶ 実行を押すと、ここに結果が出ます
ConnectionError で止まります。そこで、リトライ(待ってから、やり直す)を入れます。待ち時間は、1秒、2秒、4秒と倍々に増やすのが定番です。相手が混んでいるときに、すぐ何度も押しかけないためです。
▶ 実行を押すと、ここに結果が出ます
1回目と2回目の失敗を except で受け止め、待ち時間を表示してやり直し、3回目で成功しました。2 ** (attempt - 1) は、2のべき乗で、1・2・4・8 と増えます。このページでは待たずに、待つつもりの秒数を表示しています。
ただし、何でもやり直せばよいわけではありません。やり直す回数にも、上限が要ります。
▶ 実行を押すと、ここに結果が出ます
always_down(ずっと落ちている)は、3回試して、あきらめました。bad_key(キーが違う)は、1回で止まっています。except ConnectionError にだけ備えているので、PermissionError は、やり直さずに外へ出ます。キーが違うのに何度もお願いすると、相手に迷惑で、アカウントが一時停止されることもあります。
本物の requests でも、考え方は同じです。やり直すのは、通信の失敗(requests.ConnectionError、requests.Timeout)と、一時的な不調のステータス(429・5xx)だけ。401 や 404 は、そのままエラーにして、原因を直します。
自分で書く
お題① 全ページを集めて、amount(金額)の合計を、桁区切りで表示してください。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
while page is not None: の中で、json.load(open_url(...)) でページを読み、orders.extend(data["orders"]) と page = data["next"] を書きます。
答えの例を見る
import json
from pyodide.http import open_url
orders = []
page = 1
while page is not None:
data = json.load(open_url(f"/api-sample/orders_page{page}.json"))
orders.extend(data["orders"])
page = data["next"]
total = sum(o["amount"] for o in orders)
print(f"{total:,}")お題② flaky_fetch は、1〜3回目は失敗し、4回目から成功します。成功するまで最大6回やり直し、失敗のたびに「n回目 失敗」、成功したら「n回目で成功」と表示してください。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
for の中で try: flaky_fetch() を呼び、成功したら print して break。except ConnectionError: では、print(f"{attempt}回目 失敗") と書きます。
答えの例を見る
calls = []
def flaky_fetch():
calls.append(1)
if len(calls) <= 3:
raise ConnectionError("通信が切れました")
return "届きました"
for attempt in range(1, 7):
try:
flaky_fetch()
print(f"{attempt}回目で成功")
break
except ConnectionError:
print(f"{attempt}回目 失敗")お題③ 集めた orders を DataFrame にして、得意先名(customer_name)ごとの金額の合計がいちばん大きい得意先と、その合計を表示してください。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
df = pd.DataFrame(orders) のあと、df.groupby("customer_name")["amount"].sum() で合計が出ます。idxmax() が名前、max() が金額です。
答えの例を見る
import json
import pandas as pd
from pyodide.http import open_url
orders = []
page = 1
while page is not None:
data = json.load(open_url(f"/api-sample/orders_page{page}.json"))
orders.extend(data["orders"])
page = data["next"]
df = pd.DataFrame(orders)
by_customer = df.groupby("customer_name")["amount"].sum()
print(by_customer.idxmax(), f"{int(by_customer.max()):,}")仕事ならどう使う?
理解チェックAPIに問い合わせて、ステータスコード 401(キーが違う)が返ってきました。どうするのがよいでしょう?
理解チェックページ送りで全件を集めるとき、正しい終わり方は?