2018年1月22日月曜日

【Googleクラウド・機械学習編】Python3でJSONパラメータをpost送信する2

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

最近流行の機械学習について勉強中です。

今回は前回の後編として、Googleクラウドの「CLOUD NATURAL LANGUAGE API」にPythonでリクエストを送りたいと思います。

ソース

結論から行きますと、このソースでリクエスト送信に成功しました。
import urllib.request
import urllib.parse
import json

#送信先URL
url="https://language.googleapis.com/v1/documents:analyzeEntities?key={My_API_KEY}"

#送信するJSONパラメータ
body = {
  'document': {
    'type': 'PLAIN_TEXT',
    'content': '株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。\n\n仕事はシステムエンジニア。\n特技は空手道初段です。\n\n家族は妻が一名ほどいます。\n\n好きなお酒はウイスキーです。\n\n仲良くして下さい。'
  },
  'encodingType': 'UTF8'
}
#JSONパラメータをバイト変換
body = json.dumps(body).encode("utf-8")

#リクエストヘッダー
header = {
    "content-type": "application/json"
}

try:
    #送信実行
    request = urllib.request.Request(url, data=body,  headers=header)
    with urllib.request.urlopen(request) as response:
        #結果を出力
        response_body = response.read().decode("utf-8")
        print(response_body)

except urllib.error.HTTPError as e:
    #エラーだった場合、エラー原因を出力
     print('ERROR!!')
     print(e.code)
     print(e.read())


解説

これが私の初めてのPythonでしたが、ふむ、確かに最近注目を集めるだけのパワーがあるものだと思います。

イチオシの点を上げていきます。

リクエスト送信の簡単さ

この処理の中でリクエスト送信を行っている箇所はここです。

    #送信実行
    request = urllib.request.Request(url, data=body,  headers=header)


headerやbodyの加工という前処理は別ですが、送信実行するだけなら僅か1行。

これをJavaで表現すると、大体こんな感じ。
   URL url = new URL(urlstr);

   HttpURLConnection connection = null;
   DataOutputStream dos = null;

   try {
    connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("POST");
    connection.setDoInput(true);
    connection.setDoOutput(true);

    dos = new DataOutputStream(connection.getOutputStream());

    Map postParamMap = getPostParamMap();

    StringBuilder buls = new StringBuilder();
    if (postParamMap != null) {
     for (String key : postParamMap.keySet()) {
      buls.append(key);
      buls.append("=");
      buls.append(postParamMap.get(key));
      buls.append("&");
     }
    }
    dos.writeBytes(buls.substring(0, buls.length() - 1));

    StringBuilder bul = new StringBuilder();
    try (InputStreamReader isr = new InputStreamReader(
      connection.getInputStream(), StandardCharsets.UTF_8);
      BufferedReader reader = new BufferedReader(isr)) {

     String line;
     while ((line = reader.readLine()) != null) {
      bul.append(line);
      bul.append(System.lineSeparator());
     }

     return bul.toString();

    }

   } finally {

    if (dos != null) {
     dos.close();
    }

    if (connection != null) {
     connection.disconnect();
    }

}

DataOutputStreamとかBufferedReaderとかConnectionとか、最後にclose?
ハッキリ言って「知らんわ!!」としか言いようが無い宣言の嵐。

比べて「request = urllib.request.Request(url, data=body, headers=header)」で済むこのPythonの簡潔さは特筆するべきものと言えるでしょう。

私はWebAPIこそがWebシステムの理想形だと考えています。

インターネット=パソコンだった昔と違い、今ではスマホやタブレットなど様々な端末から参照出来ることが求められている時代ですからね。

MVCモデル(モデル、ビュー、コントローラ)のうち、モデルとコントローラを完全に切り離してWebAPIに設置し、ビュー側は、「パソコン用」「スマホ用」「タブレット用」など、個別に開発する。

これが今の時代に求められるマルチ端末対応の最適解です。

つまり、WebAPIへのリクエスト送信に強い言語は、即ち時代の覇者であるのです。
Pythonはその点を良く分かっている言語と言えるでしょう。

JSON定義

Pythonが便利な点はまだあります。

body = {
  'document': {
    'type': 'PLAIN_TEXT',
    'content': '株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。\n\n仕事はシステムエンジニア。\n特技は空手道初段です。\n\n家族は妻が一名ほどいます。\n\n好きなお酒はウイスキーです。\n\n仲良くして下さい。'
  },
  'encodingType': 'UTF8'
}

JSONが直接定義出来る。

良いですか?
「JSON形式の文字列を読み込む」じゃないです。
JSONそのものを定義しているのです。

「JSON形式の文字列を読み込む」だったらこう書くことになります。

body = "{
  \'document\': {
    \'type\': \'PLAIN_TEXT\',
    \'content\': \'株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。\n\n仕事はシステムエンジニア。\n特技は空手道初段です。\n\n家族は妻が一名ほどいます。\n\n好きなお酒はウイスキーです。\n\n仲良くして下さい。\'
  },
  \'encodingType\': \'UTF8\'
}"

「エスケープ文字「\」が邪魔くせえ」という地味だけど面倒な問題点がPytonでは解消されているのです。

可読性が大変高くなっています。
データ定義にJSONを使うのは今や常識。

Pythonは良く分かっていますね。

JSONバイト変換

実装中にハマってしまったのがココです。
#JSONパラメータをバイト変換
body = json.dumps(body).encode("utf-8")

HTTPリクエストを送り込む前に、JSONをバイト変換しなければならない。
当然と言えば当然なのですが、ネット上に情報が錯綜していましてね、エラーの原因が分からず非常に苦労しました。

エラーハンドリングの奮闘記を記載させて頂きましょう。

まず、バイト変換しない場合です。
上記の「json.dumps(body).encode("utf-8")」を消すと、以下のようになエラーが出てしまいます。

Traceback (most recent call last):
  File "C:/Users/endo/PycharmProjects/Test/postjson.py", line 30, in 
    with urllib.request.urlopen(request) as response:
  File "C:\Python\Python36-32\Lib\urllib\request.py", line 223, in urlopen
    return opener.open(url, data, timeout)
  File "C:\Python\Python36-32\Lib\urllib\request.py", line 526, in open
    response = self._open(req, data)
  File "C:\Python\Python36-32\Lib\urllib\request.py", line 544, in _open
    '_open', req)
  File "C:\Python\Python36-32\Lib\urllib\request.py", line 504, in _call_chain
    result = func(*args)
  File "C:\Python\Python36-32\Lib\urllib\request.py", line 1361, in https_open
    context=self._context, check_hostname=self._check_hostname)
  File "C:\Python\Python36-32\Lib\urllib\request.py", line 1318, in do_open
    encode_chunked=req.has_header('Transfer-encoding'))
  File "C:\Python\Python36-32\Lib\http\client.py", line 1239, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Python\Python36-32\Lib\http\client.py", line 1285, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Python\Python36-32\Lib\http\client.py", line 1234, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Python\Python36-32\Lib\http\client.py", line 1064, in _send_output
    + b'\r\n'
TypeError: can't concat str to bytes



  • can't concat str to bytes


「文字列をバイトに変換してからじゃないとHTTP送信出来ねえよ」というエラーログが出ます。
当たり前ですね。

次の罠がこちら。

#JSONパラメータの間違った変換
body = urllib.parse.urlencode(body).encode(encoding='utf-8')

これ。

  • urllib.parse.urlencode(body).encode(encoding='utf-8')

「urllib.parse.urlencode(body).encode(encoding='utf-8')」はNGです。

「urllib.parse.urlencode(body).encode(encoding='utf-8')」は、どうやらJSONをJSONではなく「普通の文字列」としてエンコードしてしまうみたいなんですよ。

urllib.parse.urlencode(body).encode(encoding='utf-8')した瞬間に、JSONはJSONではなく、単なる変な文字列になってしまう。

なのでWebサーバ側のバリデーションチェックでエラーになってしまう。

ERROR!!
400
b'{\n  "error": {\n    "code": 400,\n    "message": "Invalid JSON payload received. Unexpected token.\\ndocument=%7B%27type%\\n^",\n    "status": "INVALID_ARGUMENT"\n  }\n}\n'


だから、JSONのエンコードはこっち。


  • json.dumps(body).encode("utf-8")


JSONではない汎用エンコードはこっち。

  • urllib.parse.urlencode(body).encode(encoding='utf-8')

私が調べた限り、現在のネット情報だと「JSONのPOST送信とそれ以外のPOST送信は違いますよ」ってことがハッキリ書いている所が見つからなかったです。
理解した今となっては分かりますが、最初は両者の違いが分かりませんでした。

だから後者の汎用エンコードのサイトを見ていると「あれ? ちゃんとエンコードしているのにエラーになる。変だなぁ?」とハマッてしまう。
これは要注意です。

Pytonでは、JSONのPOST送信とそれ以外のPOST送信ではエンコードのやり方が違います。

要注意です。

終わりに

ともかく、これでWebAPIの一番の難関である「疎通確認」は成功しました。

ここから先は色々なデータを流し込んで「CLOUD NATURAL LANGUAGE API」の能力を見てみたいと思います。

0 件のコメント:

コメントを投稿