Python の HTTP クライアントは urllib.request で十分

AWS Lambda が Python 3 に対応してからというもの Lambda Functions はだいたい Python 3 で書くようになったが、Lambda では外部モジュールに依存する関数を作ろうとすると一気に面倒臭さが増してしまう。[^zip] そもそも Python は標準モジュールが充実していてよほど複雑なことをしなければ標準モジュールだけで済ませることができるのだから、極力外部モジュールに頼らない書き方ができるとより気軽に Lambda Function を書けるようになる。

[^zip]: どうしても外部モジュールを使う必要がある場合はこういうことをする https://qiita.com/hoto17296/items/a579c333a8690c96a56c

例えば Python から HTTP API を利用する場合、何も考えずにすぐ [requests](http://docs.python-requests.org) などの便利な HTTP クライアントモジュールを入れてしまいがちだが、冷静になって考えると「その程度の用途なら [urllib.request](https://docs.python.jp/3/library/urllib.request.html) で十分では」と思うことは少なくない。

そこで今回は HTTP API を利用する場合の [urllib.request](https://docs.python.jp/3/library/urllib.request.html) の使い方についてまとめる。

## `GET` リクエストを投げる
`Request` オブジェクトを作成し、`urlopen` に渡すことでリクエストが送信される。

``` python
import urllib.request

url = 'https://example.com/api/v1/resource'

req = urllib.request.Request(url)
with urllib.request.urlopen(req) as res:
    body = res.read()
```

`urlopen` で得られるレスポンスは [http.client.HTTPResponse オブジェクト](https://docs.python.jp/3/library/http.client.html#httpresponse-objects) なので、レスポンスヘッダやステータスコードなどもこのオブジェクトから取得することができる。ただし HTTP ステータスコードが 4xx と 5xx の場合は [urllib.error.HTTPError]
(https://docs.python.jp/3/library/urllib.error.html#urllib.error.HTTPError) 例外が投げられるため、例外処理を書く必要がある。([後述](#例外処理))

### QueryString でリクエストパラメータを送る
`urllib.parse.urlencode` を使って URL を組み立てる。

``` python
import urllib.request

url = 'https://example.com/api/v1/resource'
params = {
    'foo': 123,
}

req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
with urllib.request.urlopen(req) as res:
    body = res.read()
```

## `POST` リクエストを投げる
`Request` オブジェクト作成時に `data` パラメータを指定すると `POST` メソッドとしてリクエストが投げられる。

以下は `Content-Type: application/json` でデータを送信する例。

``` python
import json
import urllib.request

url = 'https://example.com/api/v1/resource'
data = {
    'foo': 123,
}
headers = {
    'Content-Type': 'application/json',
}

req = urllib.request.Request(url, json.dumps(data).encode(), headers)
with urllib.request.urlopen(req) as res:
    body = res.read()
```

## `PUT` `PATCH` `DELETE` リクエストを投げる
`Request` に `method` パラメータを渡せば `GET` `POST` 以外のメソッドも利用できる。

``` python
req = urllib.request.Request(url, json.dumps(data).encode(), headers, method='PUT')
```

それ以外は「POST リクエストを投げる」と同様。

## レスポンスの JSON を parse する
レスポンスオブジェクトをそのまま `json.load` に投げる。

``` python
import json
import urllib.request

url = 'https://example.com/api/v1/resource'

req = urllib.request.Request(url)
with urllib.request.urlopen(req) as res:
    body = json.load(res)
```

## 例外処理
`urlopen` は二種類の例外を投げる。

- [urllib.error.URLError](https://docs.python.jp/3/library/urllib.error.html#urllib.error.URLError) : HTTP 通信に失敗したとき
- [urllib.error.HTTPError](https://docs.python.jp/3/library/urllib.error.html#urllib.error.HTTPError) : HTTP ステータスコードが 4xx または 5xx だったとき

``` python
import urllib.request

url = 'https://example.com/api/v1/resource'

req = urllib.request.Request(url)
try:
    with urllib.request.urlopen(req) as res:
        body = res.read()
except urllib.error.HTTPError as err:
    print(err.code)
except urllib.error.URLError as err:
    print(err.reason)
```

注意点として `HTTPError` は `URLError` のサブクラスであるため、両方を `except` したい場合は `HTTPError` を先に書く必要がある。

## セッション (Cookie) を扱う
セッション (Cookie) を扱いたい場合[^session]はかなりやり方が異なる。

[^session]: 例えば何らかの Web サービスにログインした上でページの情報を取得したい場合など

``` python
import json
import urllib.request

# Cookie を扱える OpenerDirector オブジェクトを作る
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor())

# ログインするための Request オブジェクトを組み立てる
url = 'https://example.com/login'
data = {
    'username': 'foo',
    'password': 'deadbeef',
}
headers = {
    'Content-Type': 'application/json',
}
req = urllib.request.Request(url, json.dumps(data).encode(), headers)

# urllib.request.urlopen ではなく opener.open を使ってリクエストを投げる
res = opener.open(req)
body = res.read()
res.close()

# 以降、opener.open を使った場合は Cookie が保持されている
```

ここまでやるくらいなら requests とかを使ってもいいのでは？と思うが。

## 補足
HTTP API を叩くための最低限の使い方についてまとめたが、より詳細な使い方については公式ドキュメントを読むと良い。

- [urllib パッケージを使ってインターネット上のリソースを取得するには — Python 3 ドキュメント](https://docs.python.jp/3/howto/urllib2.html)
- [21\.6\. urllib\.request — URL を開くための拡張可能なライブラリ — Python 3 ドキュメント](https://docs.python.jp/3/library/urllib.request.html)