数行でインターネットからデータを取得する
実務で書くPythonプログラムの多くは、どこかのタイミングでネットワーク越しに何かとやり取りすることになります。REST APIの呼び出し、天気情報の取得、GitHubのエンドポイント、ファイルのダウンロードなど、用途はさまざまです。標準ライブラリの urllib でも同じことはできますが、Python界隈で事実上の定番になっているのはサードパーティ製の requests ライブラリです。APIが圧倒的に使いやすいので、pip install を一回実行するだけの価値は十分にあります。
pip install requests
まだ仮想環境を使っていないなら、まずは作っておきましょう。インストール内容をプロジェクト単位で閉じ込めておけるので安心です。
最初の GET リクエストを送ってみる
get を呼んで、ステータスコードを確認して、ボディを見る。このたった3行です。response.text はレスポンスボディを文字列として取り出したもの。ここでは JSON が長いので途中で省略しています。
チートシート感覚で覚えておきたいステータスコードはこのあたり。
- 200 — OK。問題なく成功。
- 201 — Created(POST 成功時によく返ってくる)。
- 301 / 302 — リダイレクト。
requestsはデフォルトで自動的に追従します。 - 400 — Bad Request。送った内容のどこかがおかしい。
- 401 / 403 — 未認証/権限なし。
- 404 — リソースが存在しない。
- 429 — レート制限。リクエストを控えめに。
- 500 — サーバー側のエラー。
JSON レスポンスをパースする
エンドポイントが JSON を返す場合は、レスポンスに対して .json() を呼ぶだけ。ボディをパースして、dict(またはlist)として返してくれます。
内部的には、.json() は json.loads(response.text) と同じ処理です。よく使うパターンのショートカットですね。
クエリパラメータの送り方
?key=value&... をURLに手で繋げるのはやめましょう。params= に辞書を渡せばOKです。
requests は URL エンコードを自動でやってくれるので、スペースや特殊文字、Unicode が含まれていても安全に送信できます。
実際に送られた URL は response.url で確認できます。デバッグのときに便利です。
JSON ボディを送る POST リクエスト
データを送信したいとき ― リソースの作成、フォームの送信、更新系エンドポイントの呼び出しなど ― には requests.post を使います。
import requests
payload = {
"title": "Docs update",
"body": "Added HTTP requests page.",
"labels": ["docs"],
}
response = requests.post(
"https://api.example.com/issues",
json=payload,
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
print(response.status_code)
print(response.json())
json= を渡すと2つのことが同時に行われます。まず payload が JSON にシリアライズされ、さらに Content-Type: application/json ヘッダーが自動でセットされます。もちろん data=json.dumps(payload) と明示的なヘッダー指定で同じことを実現できますが、json= を使うのが定番の書き方です。
一方、昔ながらの HTML フォームが送るようなフォームエンコード形式のデータを送りたい場合は、代わりに data= を使います。
requests.post("https://example.com/login", data={"user": "rosa", "password": "..."})
ヘッダーの設定
カスタムヘッダーを付けたいときは、dict にまとめて渡すだけで OK です。
import requests
response = requests.get(
"https://api.example.com/profile",
headers={
"Authorization": "Bearer abc123",
"User-Agent": "my-tool/1.0",
},
)
ほとんどの API は認証のために Authorization ヘッダーを要求してきます。具体的なスキーム(Bearer か Basic か Token か)は、各 API のドキュメントに書いてあるのでそちらを確認してください。
タイムアウトは必ず設定する
requests はデフォルトだと、レスポンスが返ってくるまで永遠に待ち続けます。実運用のプログラムでこれをやると、サーバーがちょっと不安定になっただけでスクリプトが固まってしまいます。そうならないよう、timeout は必ず指定しましょう:
import requests
try:
response = requests.get("https://api.example.com/slow", timeout=5)
except requests.Timeout:
print("Server took too long.")
数値の単位は秒です。timeout=5 と書けば、「5秒以内にレスポンスが返ってこなければ諦める」という意味になります。より細かく制御したいときは、タプルで (connect_timeout, read_timeout) のように渡すこともできます。
エラーハンドリング
発生しうるトラブルは大きく分けて2種類あります。
- HTTPレベルのエラー(4xxや5xx) — サーバーからレスポンスは返ってきているけれど、その内容がエラーというパターン。
response.status_codeで判別できます。 - ネットワークレベルの問題 — タイムアウト、DNSの失敗、ホストに到達できないなど。こちらは例外として投げられます。
この2つをまとめて扱う定番の書き方がこちらです。
raise_for_status() は、ステータスが 2xx なら何もせず、それ以外なら例外を投げてくれるメソッドです。RequestException は requests が送出するすべての例外の基底クラスなので、「このエンドポイントとの通信で起きたエラーを全部まとめてキャッチしたい」というときにこれ一発で済みます。
ファイルをダウンロードする
大きなバイナリファイルをダウンロードするときは、レスポンスをストリーミングで受け取ってメモリに溜め込まないようにします。
import requests
url = "https://example.com/large.zip"
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with open("large.zip", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
stream=True を指定すると、requests はレスポンスボディを一気に読み込まなくなります。そして iter_content(chunk_size=...) を使えば、ボディをチャンク単位で受け取れるので、そのままディスクに書き出せます。
Session でコネクションとデフォルト設定を使い回す
同じサービスに対して何度もリクエストを投げる場合は、Session を使いましょう。内部の TCP コネクションを再利用できるので高速ですし、共通の設定を一度だけ書けば済みます。
import requests
session = requests.Session()
session.headers.update({"Authorization": "Bearer abc123"})
# Every request through this session carries the header.
a = session.get("https://api.example.com/users/1")
b = session.get("https://api.example.com/users/2")
c = session.post("https://api.example.com/users", json={"name": "Rosa"})
同じ API を何十回も叩くようなスクリプトでは、セッションを使うだけで体感できるレベルで速くなります。
実践例:ミニ GitHub クライアント
リポジトリの最新リリースを取得してみましょう。
import requests
def latest_release(owner, repo):
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return {
"tag": data["tag_name"],
"name": data["name"],
"published": data["published_at"],
"url": data["html_url"],
}
release = latest_release("python", "cpython")
print(release)
15行もあれば十分です。URLを組み立てて、リクエストを投げて、エラーをチェックし、必要なフィールドを取り出す。実際に書くAPIクライアントは、だいたいこの形に収まります。
urllib じゃダメなの?
標準ライブラリの urllib.request でも、requests でできることはひと通りこなせます。ただし、コード量は増えるし、書き味もかなり劣ります。どうしても外部依存を足せない事情があるなら、こちらを使う手もあります。
import json
import urllib.request
with urllib.request.urlopen("https://api.github.com/repos/python/cpython") as r:
data = json.loads(r.read().decode("utf-8"))
print(data["name"])
ちょっとしたスクリプトの範囲を超えるなら、requests(非同期が必要なら httpx)をインストールする価値は十分にあります。
身につけておきたい習慣
- タイムアウトは必ず設定する。 例外なし。
- 成功前提のスクリプトでは
raise_for_status()を使う — エラーレスポンスをそのまま例外として派手に落としてくれます。 params=やjson=には辞書を渡す。自前で文字列を組み立てない。- 同じAPIを何度も叩くときは
Sessionでまとめる。 - デバッグ時は
response.status_codeとresponse.textをログに出す — サーバーが何を嫌がったのか、たいていレスポンスボディに書いてあります。
次は日付と時刻
requests を道具箱に加えたことで、モダンなWeb APIとやり取りしたり、ファイルをダウンロードしたり、サービス間の小さな連携を作ったりできるようになりました。ここまでのJSONとCSVのページと組み合わせれば、「データを取ってきて、読み込んで、何か処理して、また書き出す」という一連の流れがひととおり揃ったことになります。これは実際のPythonスクリプトの多くが取る形そのものです。そして、そうしたデータにはたいてい日時が付きもの。次のページでは、Pythonでの日付・時刻の扱い方と、タイムゾーン周りでハマりやすい落とし穴について見ていきます。
よくある質問
PythonでHTTPリクエストを送るには?
まず pip install requests でライブラリを入れて、GETなら requests.get(url)、POSTなら requests.post(url, json=...) を呼ぶだけです。返ってきたレスポンスオブジェクトには .status_code、.text、.json()、.headers が入っています。例:r = requests.get('https://api.example.com/users/1')。
requestsとurllib、どっちを使えばいい?
基本的には requests 一択です。APIが圧倒的に書きやすく、コードもすっきりします。urllib は標準ライブラリなので外部依存を追加できない場面では使えますが、JSONボディやセッション管理などは自分で書く部分が増えて面倒です。本番コードでは、requests互換でasyncにも対応した httpx を選ぶチームも増えています。
POSTリクエストでJSONを送るには?
requests.post(...) に json={'key': 'value'} を渡すだけでOKです。requests側が自動でdictをJSONにシリアライズし、Content-Type: application/json も付けてくれます。ただし data= と json= を同時に指定するのはNG。どちらか片方だけにしましょう。
requestsでエラーを処理するには?
response.status_code を見て 200 なら成功、という判定でもいいですし、response.raise_for_status() を呼べば4xx/5xxで例外を投げてくれます。タイムアウトやDNS失敗などネットワーク系のエラーは requests.RequestException のサブクラスとして発生するので、これをcatchしておけば両方まとめて拾えます。