urllib.requestのレスポンス：正常時とエラー時の違い（初心者向け）

本記事は比較的初心者向けにPythonの標準モジュールであるurllib.requestの使い方について簡単にまとめたものです。
典型的なパターンとして、Pythonで標準モジュールのみを利用し外部APIへのアクセスを実現するケースを想定しています。

### 環境
```
$ python --version
Python 3.11.3
```

## リクエスト

あるURLに対してGETアクセスするとします。
モジュール名は `as` で `request` に短縮しています。

```python
import urllib.request as request

URL = "https://xxxx"

try:
    req = request.Request(
        URL,
        # TODO: 必要に応じてヘッダー情報を追加
        headers={},
        method="GET",
    )
    with request.urlopen(req) as res:
        print("レスポンスコード： ", res.getcode())
        decoded_res = res.read().decode("utf-8")
        # TODO: データがJSON形式以外の場合は別のデータ処理を書く
        if decoded_res:
            data = json.loads(decoded_res)
        # 以下略：レスポンスボディデータを活用し必要な処理
    xss
except Exception as err:
    raise err
```
#### 上の書き方のポイント：
- HTTPアクセスで400/500などエラーが起きたときは、後に説明するリターンオブジェクトの違いにより `with` 以下の処理に失敗します。その対策としてtry/exceptでエラーを拾えるようにしています
- HTTPアクセスのレスポンスボディがない場合、 `json.loads()` でエラーが出るのでif分岐を書いています
- レスポンスボディがJSONオブジェクトでない場合、つまりレスポンスヘッダーの `"Content-Type"` が `"application/json" 以外の場合は `json.loads()` とは別の処理が必要です`


## レスポンス
上のコードをベースに `print()` を追加してみます。
出力結果でリクエスト結果の正常時とエラー時でオブジェクトが異なることが確認できます。

```python
req = request.Request(
        URL,
        headers={},
        method="GET",
    )
    print(req)
    print(type(req))
```
### 正常時

オブジェクトクラスは `http.client.HTTPResponse` です。

```
<http.client.HTTPResponse object at 0x1064d3820>
<class 'http.client.HTTPResponse'>
```

なお、レスポンスボディはこのままではPythonでデータとして扱えないので、デコードする必要があります。

### エラー時

オブジェクトクラスは `urllib.request.Request`です。中身はリクエストで投げた内容です。
```
<urllib.request.Request object at 0x104828b50>
<class 'urllib.request.Request'>
```

## 正常なレスポンスの中身

`print()` を追加してみます。

```python
URL = "https://xxxx"

req = request.Request(
    URL,
    # TODO: 必要に応じてヘッダー情報を追加
    headers={},
    method="GET",
)
with request.urlopen(req) as res:
    print("レスポンスコード： ", res.status)
    print("URL: ", res.url)
    print(res.headers)
```

```ログ
レスポンスコード： 200
URL： https://xxxx

Date: Fri, 12 Jan 2024 21:38:55 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: close
Cache-Control: no-cache
以下略
```
レスポンスコードのチェックのみしたい場合などに良さそうです。

追記： レスポンスオブジェクトに対して利用できる以下のメソッドはPythonバージョン3.9以降非推奨とのことです（公式）。
- `code`
- `getcode()`
- `geturl()`
- `info()`

### responseオブジェクトのデコード（JSONの場合）

レスポンスのオブジェクトタイプは以下の変換の手順を追ってPythonで扱える型に変えます。

```
http.client.HTTPResponse型
↓
bytes型
↓
str型
↓
dict/array型
```

### http.client.HTTPResponse型 → bytes型

```python
with request.urlopen(req) as res:
    res = res.read()
    (print(type(res))
    print(res)
```

```
<class 'bytes'>
b'{"apiKey":"xxxx", ... }'
```

### bytes型 → str型

`utf-8` を指定し日本語などの文字化けを防ぎます。

```python
res = res.read().decode("utf-8")
    print(res)
    print(type(res))
```

```
<class 'str'>
{"apiKey":"xxxx", ... }
```

### str型 → dict/array型
dict型の例です。
先述のように、HTTPアクセスのレスポンスボディがない場合、 `json.loads()` でエラーが出るのでif分岐を書いています。

```python
res = res.read().decode("utf-8")
if res:
    res = json.loads(res)
    print(type(res))
    print(res)
```

```
{'apiKey': 'xxxx', ...}
<class 'dict'>
```

## エラークラス

以下のモジュールをインポートすることで、エラークラスが使えます。

```python
import urllib.error
```

以下では `as` でモジュール名を変えています。

```python
import urllib.request as request
import urllib.error as request_error

URL = "https://xxxx"

try:
    req = request.Request(
        URL,
        headers={},
        method="GET",
    )
    with request.urlopen(req) as res:
        # 以下略

# HTTPステータスコードが 400 or 500
except request_error.HTTPError as err:
    raise err
# その他HTTP通信の失敗
except request_error.URLError as err:
    raise err
except Exception as err:
    raise err

```

また、この場合エラー内容の一部を取り出せます。

```python
except request_error.HTTPError as err:
    print(err)
    print("エラーコード: ", err.code)
    print("エラー原因: ", err.reason)
```

```
HTTP Error 400: Bad Request
エラーコード: 400
エラー原因: Bad Request
```


## 参考文献：

https://docs.python.org/ja/3/howto/urllib2.html