CHAPTER 8 ・ 仕事で使える形にする / Lesson 33

APIを実務で使う

認証、ページ送り、失敗時のやり直し。実際のサービスにつなぐときの、守るべき作法を学びます。

目安 25分 ・ インストール不要

このレッスンが終わったら

「APIは、認証・ページ送り・やり直し(リトライ)・回数制限を意識して使う。キーはコードに書かず、環境変数で渡す」と言える。

STEP 1

こんな場面、ありませんか?

受注システムのAPI(基礎編で学んだ、データの受付窓口)から、10月の受注を全件取り出して、集計したいとします。ところが、APIは、1回のお願いで全部を返してくれるとは限りません。練習用のAPIに、1回だけお願いしてみましょう。

1回だけお願いするコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

全体では10件あるのに、届いたのは4件です。データが多いとき、サーバーは「1回に4件まで」のように区切って返します。これをページ送りと呼びます。返事の中の next が、「続きは2ページ目にあります」という目印です。

実務のAPIは、1回に100件、1,000件のように区切るのがふつうです。1ページ目だけを集計して、全体だと思い込むミスは、よく起きます。

STEP 2

ページ送りで、全件を集める

next がなくなる(None になる)まで、繰り返しお願いします。

最後のページまで、繰り返すコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

3ページ(4件、4件、2件)が集まり、全体の10件と一致しました。(この練習ではページごとに別のファイルですが、本物のAPIでは、同じURLに ?page=2 のように番号を添えます)ポイントは3つです。

  • 終わりの印を見る。 3ページ目の next は None です。これが「もう続きはない」の合図です。
  • 上限を決めておく。 MAX_PAGES は、印が壊れていても、永遠に続けないための保険です。
  • 件数を照らし合わせる。 集めた件数と、total_count が合うか確かめます(検算)。
STEP 3

本物のサービスにつなぐときの作法

自分のパソコンで本物の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()
fetch_page を、1行ずつ日本語にしてみる
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キーが違う、期限切れ、権限がないしない(直さないと、何度やっても同じ)
404URLが違うしない
429呼びすぎ(回数制限)待ってから、やり直す
500・502・503・504サーバー側の一時的な不調待ってから、やり直す

キーは、コードに書かず、環境変数(パソコンに預けておく値。Lesson 32 で学びました)から読みます。画面やログにも出しません。

STEP 4

失敗に備える(リトライ)

通信は、ときどき失敗します。ネットワークの一瞬の途切れ、サーバーの混雑。少し待てば直る失敗に、一度で諦めると、毎朝の自動処理が、ときどき止まります。

「呼んだ回数で失敗する」自作の関数で、様子を見ましょう。1回目と2回目は失敗、3回目から成功します。まず、何も備えないとどうなるでしょう。

備えがないと、1回目で止まるコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

ConnectionError で止まります。そこで、リトライ(待ってから、やり直す)を入れます。待ち時間は、1秒、2秒、4秒と倍々に増やすのが定番です。相手が混んでいるときに、すぐ何度も押しかけないためです。

待ってから、やり直すコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

1回目と2回目の失敗を except で受け止め、待ち時間を表示してやり直し、3回目で成功しました。2 ** (attempt - 1) は、2のべき乗で、1・2・4・8 と増えます。このページでは待たずに、待つつもりの秒数を表示しています。

ただし、何でもやり直せばよいわけではありません。やり直す回数にも、上限が要ります。

上限に達したとき・やり直しても直らないときコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

always_down(ずっと落ちている)は、3回試して、あきらめました。bad_key(キーが違う)は、1回で止まっています。except ConnectionError にだけ備えているので、PermissionError は、やり直さずに外へ出ます。キーが違うのに何度もお願いすると、相手に迷惑で、アカウントが一時停止されることもあります。

本物の requests でも、考え方は同じです。やり直すのは、通信の失敗(requests.ConnectionError、requests.Timeout)と、一時的な不調のステータス(429・5xx)だけ。401 や 404 は、そのままエラーにして、原因を直します。

STEP 5

自分で書く

お題① 全ページを集めて、amount(金額)の合計を、桁区切りで表示してください。

お題①:全件の金額の合計コードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

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回目で成功」と表示してください。

お題②:リトライの回数を数えるコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

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)ごとの金額の合計がいちばん大きい得意先と、その合計を表示してください。

お題③:得意先ごとの合計の1位pandas・サンプルファイル付き
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

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()):,}")
STEP 6

仕事ならどう使う?

理解チェックAPIに問い合わせて、ステータスコード 401(キーが違う)が返ってきました。どうするのがよいでしょう?

理解チェックページ送りで全件を集めるとき、正しい終わり方は?