Python3でBasic認証のあるAPIにx-www-form-urlencodedでPOSTし、Requestsとurllibを理解する

こんにちは。
入社2ヶ月でそろそろ新人ヅラできなくなり、立ち位置に困りはじめた、新米エンジニアのTerryです。

皆さんと一緒に勉強して、１日でも早く重鎮ヅラできるように頑張ります。

今回は、タイトル通り**Python3でAPIを叩く話**についてまとめたいと思います。
「そんなん、よくある話やんけ」という感じ、ごもっともですが、ちょっと珍しいタイプのAPIにPOSTする機会があったので。

それに、Google検索してて気付いたのですが、インターネットの海ではPython2系と3系の話がごっちゃになっていて、整備されていない感じもあったので、その辺明示的にしてあげたいなぁと。

例えば、Python3ではURLをエンコードしてparseする際に、urllibというモジュールを使うことが多いのですが、Python2にはurllib2とかあったりして、今回の投稿ではその辺りケアしていこうかと思います。

それでは早速、よろしくお願いします！

## 何がしたいのか
- 以下の条件でAPIにPOSTしたい
    - 環境：Python3.6.3(要するに3.6系)
    - 送信形式：application/x-www-form-urlencoded
    - Basic認証あり

ちなみに、**Basic認証**とは、コレのことです。
一度くらい見たことがある人がほとんどだと思います。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/110571/52da6567-b026-65af-35ae-0099a7046e57.png)

## スクリプト
さて、僕が今回使ったPythonスクリプトですが、結論から言うとこんな感じ。

```python
import requests
import urllib

# 任意のurl(エンドポイント)
url = 'xxx.co.jp' 

# 送信するパラメータ(例)
params = {
'UserName': 'test',
'UserNameKana': 'テスト',
'MailAddress': 'test@example.com',
'LoginPassword': 'password'
}

# URLをエンコード
params = urllib.parse.urlencode(params)

# headerでコンテンツタイプを指定
headers = {'Content-Type': 'application/x-www-form-urlencoded'}

# authにBasic認証のIDとPASSを設定する
r = requests.post(url=url, data=params, headers=headers, auth=('funfun', 'funfun'))

# 送信結果が知りたい場合には記載
print(r.status_code)
print(r.text)
```
さて、コードの中身を見ていきましょう。

# 使用するモジュール
## Requests
いや今更いいよ、って感じですが一応。

Requestsは、**PythonでHTTPメソッドを実行するプログラムを簡単に書けるモジュール**です。

Requestsのドキュメント（Requests: HTTP for Humans）
（http://docs.python-requests.org/en/latest/）

他にHTTPメソッドを実行する方法として、Pythonにはurllibモジュール（後述）にもrequestというライブラリが用意されていて、そちらを使うことも可能なんですが、requestの後に色々とメソッドを繋いだり引数を設定したりで、どうにも使いづらい。

 urllib.request — URL を開くための拡張可能なライブラリ
（https://docs.python.jp/3/library/urllib.request.html）

そのため、今回は一般的でより使いやすいRequestsを使っていきます。
Requestsについてわかりやすい記事はこちら。

Requestsの使い方(Python Library)
（https://qiita.com/sqrtxx/items/49beaa3795925e7de666）

今回はAPIにデータを送信するので、使用するHTTPメソッドは当然**POST**です。
ここで、今回のキモとなるPOST部分のコードを見てみます。

```python
r = requests.post(url=url, data=params, headers=headers, auth=('funfun', 'funfun'))
```

引数を見ていきましょう。
今回解説が必要なのは、**headers**と**auth**かと思います。

**headers引数**には、POSTしたいデータ形式を`{'Content-Type': 'hogehoge'}`で指定します。
今回は、タイトル通りx-www-form-urlencodedなので、それを指定します。
（x-www-form-urlencodedについては後述）

```py
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
```

次は**auth引数**で、タイトルの「Basic認証」のキモになる部分です。
RequestsはBasic認証についても、非常にシンプルに記述できます。

`auth=('ユーザー名', 'パスワード')`としてあげれば良いのです。
これでユーザー名とパスワードが合っていれば、Basic認証は通ります。

プラスαな話ですが、このままではPOSTがうまくいったかどうか分からないので、業務では**送信したReqiestsのstatus_codeとtextをprintしてあげる**ことをオススメします。

POSTの送信自体がうまくいったかどうかをステータスコードで確認し、POSTしたリクエストに対し期待通りのレスポンスが返ってきているかどうかをtextで確認できます。

HTTP 応答状態コード
（https://developer.mozilla.org/ja/docs/Web/HTTP/Status）

なるほど、と言う感じですが**Requestsひとつでやりたいことのほぼすべてをカバーできている**あたり、Requestsの有能さを感じますね。

## urllib
さてバージョンを意識する上ではコイツがちょっとクセ者なんですが、結論から言うと、**urllib2はurllibに統合されました。**
正確に言うと、**urllib2はurllibに.request()と.error()に分割され、urllibに統合**されたということです。

ドキュメントをよく読むと、そのような旨が書いてありますね。

20.6. urllib2 — URL を開くための拡張可能なライブラリ(注：Python2.7の記事)
（https://docs.python.jp/2.7/library/urllib.html#module-urllib）

ちなみに、実はPythonには**urllib3**というのもあるんですが、Requestsでやりたいことがほとんどできてしまうので、使うことはあまりないかと思います。

urllib3 Sanity-friendly HTTP client.
（http://urllib3.readthedocs.io/en/latest/）

ちょっと話が逸れましたが、**一旦はurllibだけ意識しとけばいいよ**、って感じです。
さてさて、今回urllibを用いたのは、以下のコード部分。

```py
params = {
'UserName': 'test',
'UserNameKana': 'テスト',
'MailAddress': 'test@example.com',
'LoginPassword': 'password'
}
params = urllib.parse.urlencode(params)
```

urllibちゃんのあまりイケてないところは、**メソッドが何をしているのかよく分からん**ところです。
ドキュメントに立ち返り、何をしているのかをよく見てみましょう。

21.8. urllib.parse — URL を解析して構成要素にする
（https://docs.python.jp/3/library/urllib.parse.html#urllib.parse.urlencode）

>マッピング型オブジェクトまたは2個の要素からなるタプルのシーケンス(strかbytesオブジェクトが含まれているかもしれません)を、パーセントエンコードされたASCII文字列に変換します。戻り値の文字列がurlopen()関数でのPOST操作のdataで使用される場合はバイト列にエンコードしなければなりません。そうでない場合はTypeErrorが送出されます。戻り値は'&'文字で区切られたkey=valueのペアからなる一組の文字列になります。keyとvalueはquote_viaを使用してクオートされます。デフォルトで、値をクォートするためにquote_plus()が使用されます。つまり、スペースは'+'文字に、'/'文字は%2Fにクォートされます。これはGETリクエストの標準に準拠します(application/x-www-form-urlencoded)。 quote_viaとして渡すことができる別の関数はquote()です。それはスペースを%20にエンコードし、'/'をエンコードしません。何がクォートされるかを最大限コントロールしたければ、quoteを使ってsafeに値を指定してください。

#### なるほど、わからん。

はい、よくわからん場合は、実際にインタプリタで実行して確かめてみましょうね。

```py
>>> import urllib
>>> params = {
... 'UserName': 'test',
... 'UserNameKana': 'テスト',
... 'MailAddress': 'test@example.com',
... 'LoginPassword': 'password'
... }
>>>
>>> params = urllib.parse.urlencode(params)
>>> print(params)
UserName=test&UserNameKana=%E3%83%86%E3%82%B9%E3%83%88&MailAddress=test%40example.com&LoginPassword=password
```

やっとわかりそう！
ポイントは２つほど。

まず１つ目は、**dict型であるパラメーターを`key=value`の形に成形し、各key=valueを`&`で繋いであげている**ことです。
これによって、**プログラムがわかりやすい形にデータを変えてあげている**わけですね。

もう１つは、**日本語が%エンコードされている**ことです。
`'UserNameKana': 'テスト'`が`UserNameKana=%E3%83%86%E3%82%B9%E3%83%88`になっています。

これも趣旨は先ほどと一緒で、**機械に理解できない日本語をわかりやすいようにASCIIに翻訳してくれている**と。

つまりurllib.parse.urlencodeは、**人間にわかりやすい形式だったparamsのデータをparseして、機械にわかるよう(=解析)にしてくれている**わけです。

# application/x-www-form-urlencodedとは
さて、最後に今回送信しようとしているデータ送信形式の`application/x-www-form-urlencoded`について、少し触れておきます。

一般的にデータ送信といえば、<a href='http://www.itmedia.co.jp/news/articles/1806/08/news065.html'>JSON<del>という気味の悪い拡張子</del></a>とかXMLがほとんどなわけですが、今回は珍しくこの形式をクライアントに指定されました。

調べてみると、**フォームの提出のためのデータ送信形式のひとつ**で、OAuthやWebAPIではよく使われるそうです。

ドキュメントを色々と探してみたんですが、味わい深いデザインのサイトが多かったです。
結構レガシーなやり方なんですかね？

通信用語の基礎知識　『application/x-www-form-urlencoded』
（https://www.wdic.org/w/WDIC/application/x-www-form-urlencoded）

application/x-www-form-urlencoded (MIME型)
（https://wiki.suikawiki.org/n/application%2Fx-www-form-urlencoded）

また、調べていくと納得したのですが、**文字列がASCIIによって表現されていて、keyとvalueはパーセント符号化することになっている**みたい。
それで、送りたいデータをurllib.parse.urlencodeするんですね。

# 最後に全体の流れを整理
前後関係を無視してダラダラと説明してしまったので、最後にコードの全体の流れをさらっと整理しておきましょう。

### コード
```python
import requests
import urllib

url = 'xxx.co.jp' 

params = {
'UserName': 'test',,
'UserNameKana': 'テスト',
'MailAddress': 'test@example.com',
'LoginPassword': 'password',
}

params = urllib.parse.urlencode(params)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}

r = requests.post(url=url, data=params, headers=headers, auth=('funfun', 'funfun'))

print(r.status_code)
print(r.text)
```
### 全体の流れ
<ol>
<li>POSTするためのRequestsと、データをparseするためのurllibをimportする
<li>POSTするurlを変数に格納
<li>変数に送信したいデータを格納
<li>urllib.parse.urlencodeで送信する用にデータを成形
<li>headersに送信したいデータ形式を指定
<li>auth引数にユーザー名とpasswordを指定し、POST
<li>（確認用に）ステータスコードとレスポンスを表示させる
</ol>

# 以上です！
長々と書いてしまいましたが、ここまでお付き合いいいただいた方、ありがとうございました！
基本的なモジュールやメソッドについて掘り下げていきましたが、簡単だと侮らず、しっかりと理解した上でコードを書いていきましょう！（自分への戒め）