Day 59:APIプロジェクト設計 ― 「ひとつのAPIツール」を最後まで組み立ててみる
Day 59では、これまで学んできた Web・API の知識をまとめて、 ひとつの「APIプロジェクト」を設計する視点 を身につけていきます。
キーワードはこの6つです。
- 入力
- API呼び出し
- データ変換
- 保存
- エラー処理
- ログ
今日は「部品ごとの書き方」だけでなく、 それらをどうつなげて「ひとつのツール」にするか を意識しながら読んでみてください。
全体像を先に描く:APIツールの基本フロー
APIプロジェクトの典型的な流れ
まずは、APIツールの全体像をざっくり言葉で並べます。
- 入力 ユーザーからキーワードやIDを受け取る/設定を読み込む
- API呼び出し 外部APIにリクエストを送り、レスポンスを受け取る
- データ変換 レスポンスJSONを「使いやすい形」に整える
- 保存 CSV・JSON・SQLiteなどに保存して、後から再利用できるようにする
- エラー処理 通信エラーやHTTPエラー、想定外のレスポンスに備える
- ログ 何が起きたかを記録し、トラブル時に原因を追えるようにする
この「6つの箱」を意識しながら、 ひとつのAPIプロジェクトを組み立てていきます。
入力:ツールに「どう指示を出すか」を決める
コマンドライン引数で検索キーワードを受け取る
例として、「検索キーワードを指定してAPIを叩くツール」 を作ることにします。
まずは、コマンドライン引数からキーワードを受け取る関数です。
# day59_input.py
import sys
def get_search_keyword() -> str:
"""
コマンドライン引数から検索キーワードを取得する関数です。
例: python main.py python 入門
"""
# sys.argv[0] はスクリプト名、それ以降が引数です。
args = sys.argv[1:]
if not args:
# 引数がない場合の扱いを決めておきます。
print("検索キーワードが指定されていません。デフォルトのキーワードを使用します。")
return "python"
# 複数単語がある場合はスペースで結合してひとつのキーワードにします。
keyword = " ".join(args)
print(f"検索キーワード: {keyword}")
return keyword
if __name__ == "__main__":
get_search_keyword()
Pythonポイントの深掘り:
- 入力がないときにどうするか(デフォルト値にするか、エラーにするか)は、ツールの性格を決める大事な設計です。
- 「入力をひとつの関数にまとめる」ことで、後から「設定ファイルも読む」「環境変数も見る」といった拡張がしやすくなります。
API呼び出し:外部サービスとの「窓口」をまとめる
Requestsを使ったシンプルなAPIクライアント
次に、外部APIを呼び出す部分です。 ここでは、requests を使って「検索APIっぽいエンドポイント」を叩くクラスを作ります。
# day59_api_client.py
import requests
class SearchApiClient:
"""
検索APIを呼び出すためのクライアントクラスです。
API呼び出しのロジックをひとつの場所にまとめておくことで、
プロジェクト全体の見通しが良くなります。
"""
def __init__(self, base_url: str):
# ベースとなるAPIのURLを保持します。
self.base_url = base_url
def search(self, keyword: str) -> dict:
"""
検索APIを呼び出して結果を返すメソッドです。
実際には GET /search?q=keyword のようなイメージです。
"""
params = {"q": keyword}
print(f"[API] 検索APIを呼び出します: {self.base_url}, params={params}")
# timeout を付けて、いつまでも待ち続けないようにします。
response = requests.get(self.base_url, params=params, timeout=10)
# HTTPステータスコードをチェックします。
if response.status_code != 200:
# ここでは簡易的に例外を投げますが、後でエラー処理を拡張します。
raise RuntimeError(f"APIエラー: status={response.status_code}")
# JSONレスポンスを辞書として返します。
return response.json()
Pythonポイントの深掘り:
- API呼び出しをクラスにまとめることで、「認証ヘッダーを追加する」「別のエンドポイントを増やす」といった拡張がしやすくなります。
timeoutを付けるのは、ネットワーク系のツールではほぼ必須の習慣です。- 実務では
response.raise_for_status()を使うことも多く、HTTPエラーを自動的に例外に変換できます。
データ変換:レスポンスJSONを「使いやすい形」に整える
必要な項目だけを抜き出す変換関数
APIレスポンスは、情報が多すぎてそのままだと扱いづらいことがよくあります。 そこで、必要な項目だけを抜き出して整形する層 を作ります。
# day59_transformer.py
def transform_search_results(raw_response: dict) -> list[dict]:
"""
検索APIの生のレスポンスから、
必要な項目だけを抜き出して整形する関数です。
想定する変換後の形:
- title: 結果のタイトル
- url: リンク先URL
- snippet: 簡単な説明文
"""
print("[TRANSFORM] 検索結果を整形します。")
# レスポンスの中から、結果リストを取り出します。
items = raw_response.get("items", [])
results: list[dict] = []
for item in items:
# .get() を使うことで、キーがなくても安全に値を取得できます。
title = item.get("title", "")
url = item.get("link", "")
snippet = item.get("snippet", "")
results.append(
{
"title": title,
"url": url,
"snippet": snippet,
}
)
print(f"[TRANSFORM] {len(results)} 件の結果を整形しました。")
return results
Pythonポイントの深掘り:
- 「生のレスポンス」と「整形済みデータ」を分けて考えることで、コードの役割がはっきりします。
- 変換ロジックをひとつの関数に閉じ込めておくと、API仕様が変わったときもここだけ直せば済む、という状態を作れます。
.get()にデフォルト値を指定しておくと、「一部の項目が欠けているレスポンス」にも強くなります。
保存:集めたデータを「残せる形」にする
CSV保存のユーティリティ関数
Day 56で扱ったように、保存形式はいくつか選べますが、 ここでは例として CSV保存 を使います。
# day59_storage.py
import csv
from pathlib import Path
def save_results_to_csv(results: list[dict], filename: str) -> Path:
"""
検索結果をCSVファイルに保存する関数です。
- results: title, url, snippet を持つ辞書のリスト
- filename: 保存するファイル名
"""
output_path = Path(filename)
fieldnames = ["title", "url", "snippet"]
print(f"[SAVE] 結果をCSVに保存します: {output_path}")
# newline="" を指定して、余計な空行が入るのを防ぎます。
with output_path.open(mode="w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for item in results:
writer.writerow(item)
print(f"[SAVE] 保存完了: {output_path}")
return output_path
Pythonポイントの深掘り:
- 保存処理をひとつの関数にしておくと、「CSV → JSON → SQLite」と切り替えたいときに差し替えやすくなります。
Pathを使うと、ファイルパスの扱いが直感的になり、OS差異も吸収しやすくなります。- 「保存先のパスを返す」ようにしておくと、後でログに書いたり、別の処理に渡したりしやすくなります。
エラー処理:トラブルが起きたときの「振る舞い」を決める
例外をキャッチして、ユーザーに分かる形で伝える
APIプロジェクトでは、 エラーが起きない前提で書かれたコードはすぐ壊れます。
そこで、メインの流れの中に try〜except を組み込んでおきます。
# day59_main_with_error.py
from day59_input import get_search_keyword
from day59_api_client import SearchApiClient
from day59_transformer import transform_search_results
from day59_storage import save_results_to_csv
def run_search_tool() -> None:
"""
検索APIプロジェクトのメイン処理です。
入力 → API呼び出し → データ変換 → 保存 の流れを、
エラー処理を含めてひとつにまとめています。
"""
# 1. 入力
keyword = get_search_keyword()
# 2. APIクライアントの準備
base_url = "https://example.com/api/search" # 実際のAPI URLに置き換えます。
client = SearchApiClient(base_url=base_url)
try:
# 3. API呼び出し
raw_response = client.search(keyword)
# 4. データ変換
results = transform_search_results(raw_response)
if not results:
print("検索結果が0件でした。保存は行いません。")
return
# 5. 保存
save_results_to_csv(results, "search_results.csv")
except Exception as e:
# 6. エラー処理
print("[ERROR] 処理中にエラーが発生しました。")
print(f"詳細: {e}")
if __name__ == "__main__":
run_search_tool()
Pythonポイントの深掘り:
- 「どこで例外が起きても、最後にまとめてキャッチする」形にしておくと、ツールが途中でクラッシュしにくくなります。
- ユーザーに見せるメッセージと、開発者が見る詳細(スタックトレースなど)を分けるのが理想的です。
- 実務では、ここに「再試行」「代替処理」「通知」などを追加していきます。
ログ:あとから「何が起きたか」を追えるようにする
logging モジュールでコンソール+ファイルに記録する
最後に、「ログ」です。 ログは、「あとから振り返るための記録」 です。
# day59_logging_setup.py
import logging
from pathlib import Path
def setup_logger(log_file: str = "search_tool.log") -> logging.Logger:
"""
ロガーを設定して返す関数です。
コンソールとファイルの両方にログを出力するように設定します。
"""
logger = logging.getLogger("search_tool")
logger.setLevel(logging.INFO)
# すでにハンドラが設定されている場合は何もしません(重複防止)。
if logger.handlers:
return logger
# コンソール出力用ハンドラ
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# ファイル出力用ハンドラ
file_handler = logging.FileHandler(Path(log_file), encoding="utf-8")
file_handler.setLevel(logging.INFO)
# ログのフォーマットを設定します。
formatter = logging.Formatter(
"[%(asctime)s] %(levelname)s %(name)s - %(message)s"
)
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
Pythonこのロガーをメイン処理で使ってみます。
# day59_main_with_logging.py
from day59_input import get_search_keyword
from day59_api_client import SearchApiClient
from day59_transformer import transform_search_results
from day59_storage import save_results_to_csv
from day59_logging_setup import setup_logger
def run_search_tool() -> None:
"""
ログ付きの検索APIプロジェクトメイン処理です。
"""
logger = setup_logger()
# 1. 入力
keyword = get_search_keyword()
logger.info(f"検索ツール開始: keyword='{keyword}'")
# 2. APIクライアントの準備
base_url = "https://example.com/api/search"
client = SearchApiClient(base_url=base_url)
try:
# 3. API呼び出し
logger.info("API呼び出しを開始します。")
raw_response = client.search(keyword)
logger.info("API呼び出しが成功しました。")
# 4. データ変換
results = transform_search_results(raw_response)
logger.info(f"整形された結果件数: {len(results)}")
if not results:
logger.warning("検索結果が0件でした。保存は行いません。")
return
# 5. 保存
output_path = save_results_to_csv(results, "search_results.csv")
logger.info(f"結果を保存しました: {output_path}")
except Exception:
# 6. エラー処理+ログ
logger.error("処理中にエラーが発生しました。", exc_info=True)
print("[ERROR] 処理中にエラーが発生しました。詳細はログファイルを確認してください。")
if __name__ == "__main__":
run_search_tool()
Pythonポイントの深掘り:
logger.info()で「正常な流れ」を記録しておくと、後から「どこまで進んでいたか」が分かります。logger.error(..., exc_info=True)で例外の詳細もログに残せるので、トラブルシューティングがしやすくなります。- コンソールとファイルの両方に出すことで、「開発中の確認」と「運用時の記録」を両立できます。
Day 59ミニテンプレート:APIプロジェクト設計の「ひとまとめコード」
最後に、Day 59の内容をコンパクトにまとめた APIプロジェクト設計テンプレート を載せておきます。
# day59_api_project_template.py
import sys
import requests
import csv
import logging
from pathlib import Path
def get_search_keyword() -> str:
args = sys.argv[1:]
if not args:
print("検索キーワードが指定されていません。デフォルト 'python' を使用します。")
return "python"
return " ".join(args)
class SearchApiClient:
def __init__(self, base_url: str):
self.base_url = base_url
def search(self, keyword: str) -> dict:
params = {"q": keyword}
response = requests.get(self.base_url, params=params, timeout=10)
# HTTPエラーなら例外を投げます。
response.raise_for_status()
return response.json()
def transform_search_results(raw_response: dict) -> list[dict]:
items = raw_response.get("items", [])
results: list[dict] = []
for item in items:
results.append(
{
"title": item.get("title", ""),
"url": item.get("link", ""),
"snippet": item.get("snippet", ""),
}
)
return results
def save_results_to_csv(results: list[dict], filename: str) -> Path:
path = Path(filename)
fieldnames = ["title", "url", "snippet"]
with path.open(mode="w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for item in results:
writer.writerow(item)
return path
def setup_logger(log_file: str = "search_tool.log") -> logging.Logger:
logger = logging.getLogger("search_tool")
logger.setLevel(logging.INFO)
if logger.handlers:
return logger
console = logging.StreamHandler()
file_handler = logging.FileHandler(Path(log_file), encoding="utf-8")
formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(name)s - %(message)s")
console.setFormatter(formatter)
file_handler.setFormatter(formatter)
logger.addHandler(console)
logger.addHandler(file_handler)
return logger
def run_search_tool() -> None:
logger = setup_logger()
keyword = get_search_keyword()
logger.info(f"検索ツール開始: keyword='{keyword}'")
client = SearchApiClient(base_url="https://example.com/api/search")
try:
logger.info("API呼び出し開始")
raw = client.search(keyword)
logger.info("API呼び出し成功")
results = transform_search_results(raw)
logger.info(f"整形結果件数: {len(results)}")
if not results:
logger.warning("結果0件のため保存しません。")
return
path = save_results_to_csv(results, "search_results.csv")
logger.info(f"結果保存: {path}")
except Exception:
logger.error("処理中にエラー発生", exc_info=True)
print("[ERROR] エラーが発生しました。詳細はログを確認してください。")
if __name__ == "__main__":
run_search_tool()
PythonDay 59のまとめ
Day 59では、APIプロジェクト設計の基礎として、
- 入力 → API呼び出し → データ変換 → 保存 → エラー処理 → ログ という「ひとつの流れ」を意識してツールを組み立てること
- 入力設計がツールの使い勝手を決めること
- API呼び出しをクライアントクラスにまとめることで、見通しと再利用性が上がること
- データ変換層を作ることで、レスポンスJSONを「使いやすい形」にできること
- 保存処理をユーティリティ関数にしておくと、形式の切り替えがしやすいこと
- エラー処理とログを組み込むことで、「壊れにくく、原因を追いやすい」ツールになること
を、コード例とともにステップバイステップで整理しました。
ここまで来ると、「ちょっとしたAPIツール」を思いついたときに、 どんな部品をどう並べればいいか が、かなりクリアに見えてくるはずです。
