2018年1月29日月曜日

【Googleクラウド・機械学習編】日報解析バッチ作成中1

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

最近は機械学習について勉強中です。
現在はGoogleの機械学習API「CLOUD NATURAL LANGUAGE API」の検証と、Pythonの勉強を並行して進行しています。

現状

「CLOUD NATURAL LANGUAGE API」の「analyzeEntities」は、文章の構文解析です。
文章中から特徴のあるキーワードを抽出することが可能です。

前にちょっと実験してみた結果がこちらに載っています。

しかしこれ、現状だと何の役にも立ちません。

新技術全般によくあることなのですが、

「技術的な意味は分かった。」
「それが一体何の役に立つのか?」

という状況に陥ってしまいます。

通常の業務だとこうです。

  • 先に案件が存在する。⇒それに必要な技術を習得する。

しかし、新技術の領域だと「案件」なんかありませんので、こうなります。

  • とりあえず技術だけ覚える。⇒後で何か使い道が無いか考える。

使い道は自分で考えなければなりません。
過酷な道ですが、そこは楽しくやっていきましょう。


使い道探し

そして使い道を考えてみました。
このAPIは「文章解析」ですから、インプットとなる文章を調達することが最初の課題となります。

一番の大本命は、弊社サービスのDentNet(デントネット)


歯科医院向けのWebシステムでして、「歯科医院からのアンケート」とか「電話サポートの記録」とか、そういうのを入手して解析を行うのが一番の使い道です。

しかし、それを行う場合、本来の仕事をしている営業チームやサポートチームの時間を割いて貰わなければならなくなってしまうので、話が大きくなり過ぎます。

もっと手元にある情報で、かつ文章形式の情報が望ましいです。

ありますね。

サラリーマンの必需品、メールです。




私の所属する技術開発事業部は「日報」という形式で毎日の終わりに上長に業務報告を行います。
(宛先は上長ですが、メールは全員に届きます。)

日報の形式はフリーフォーマットの文章でして、報告する人が報告したい内容を書く運用になっています。

部員はそれを見て、「ああ、あの人はそんな仕事をやっているんだな」「アイツ、最近忙しそうだな」みたいな事を思うわけですね。

今のところは日報は今日の話を見るだけの運用ですが、構文解析APIを実行して過去に遡って大量データを解析すると、頻出登場単語とかが浮かび上がってくるかもしれません。

日報という不定形の文章を、何とか定量化して、数値根拠のある指標に転換することが出来ないだろうか……。

そう考えまして、私はこれから「日報解析バッチ」を作ろうと思います。

実現イメージ

日報解析バッチの実現イメージはこちらです。


1.メールの取得

メールサーバから解析対象のメールをテキストファイルとして手動ダウンロードします。
バッチ自体が通信してメールを取得する機能は省略します。
(高機能を言い出したらキリが無い。最小限の機能で形を作ります)

2.リクエスト実行

日報解析バッチにて実現。メールを構文解析APIに送り込みます。
「ローカルフォルダの中に置いてあるテキスト形式のメールを全部送る」という形で、まとまった単位で送信したいと思います。

3.レスポンス取得

日報解析バッチにて実現。構文解析APIからのレスポンスを受信します。
レスポンスはJSON形式になっていますが、JSONだけベロッと出されても意味不明ですので、
バッチにて正規化して意味が読み取れる形に整形します。


4.ドキュメント化

最後に、人様にお見せ出来るよう表形式に整えて完了です。


日報解析バッチはPythonで作ります。
Python初心者の学習記録も兼ねていきたいと思います。

次回

次回から実装に入っていきます。
最初は「ローカルフォルダの中に置いてあるテキスト形式のメールを読み込む」というファイルIOからです。

ソースは既に手元にありますが、Pythonならではの簡略で綺麗なソースに仕上がっています。

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」の能力を見てみたいと思います。

2018年1月9日火曜日

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

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

2018年になりました。本年も宜しくお願いします。 

さて、2017年から引き続き、最近流行の機械学習について勉強中です。

 リクエスト送信プログラム作成


さて、現在はGoogleクラウドの「CLOUD NATURAL LANGUAGE API」を検証中なわけですが、
その為には「リクエスト送信プログラム」を作らなければなりません。

ちょっと実行するだけならネット上に転がっているリクエスト送信支援ツールを使えば良いですが、
手元の大量データを送り込んだりとか、独自のカスタマイズを施す為には自前開発が必要不可欠です。

私はJavaエンジニアですので、プログラムもJavaで作るのが一番簡単な道です。
アンドロイドアプリでWebAPIのリクエストプログラムとか作ったこともありますし。
しかし、新年も始まったことですし、ここは志を高く持ってPythonで作ることにチャレンジしていきたいと思います。

 作成プログラム


目標


CLOUD NATURAL LANGUAGE APIに対する最も基本的な疎通確認レベルのPOST送信。

条件


言語はPythonとする。バージョンは3。

結論を言いますと、もの凄く苦労しました。
この記事はJavaだったら30分で作れたはずのプログラムを6時間も悪戦苦闘した奮闘記となります。

Python開発の困難点


1.Python2の情報が混ざっている


前の記事で書きましたが、Pythonは「Python2」と「Python3」で互換性がありません。

JavaだったらJava1.5で書いたソースは概ねJava8でも動作します。
(完全互換ではないのはミソ。以前に予約後の問題でJavaのバージョンを上げたら動かなくなったことがありました。とはいえ、殆どOKなので苦労はしません)

Pythonの場合は全然ダメです。
「バージョン間で互換性」は、まあハッキリ言って程度問題でして、「完全互換を保証するものではないが、大体は流用OK」くらいの言語が多いです。

でも、Python2とPython3は雰囲気が似ているだけの別物というくらい全然動きません。
そして、伝統的にはPython2が長く使われており、Python3は最新鋭です。

よって、「Python post送信」とかで検索すると、Python2とPython3の情報が混ざって出てくるんです。
私が必要としているのはPython3ですから、Python2の情報は罠でしか無い。
これが厄介です。

ちなみに調べている中で偶然知ったのですが、「Perlは互換性に強い」そうですね。

Perl...
相当古くから根付いている言語ですが、Perlは互換性が強い故に、何十年も前からサーバを引っ越ししつつ代々引き継がれているようなソースでも問題無く動くそうな。

「何十年も維持するプログラムだったらPerlが良い」

という記事を見ました。

なるほど。
こういうブログをやっていると最新情報ばかりに目が行ってしまいますが、業務系システムでは恒久的保守をどう行っていくかも重要な能力です。

「古い技術であっても侮ってはならない」という教訓になりました。

2.情報が少ない


やっぱりですね、少ないですよ、情報が。
無くは無いですが。

まず日本語の情報だけでは全然足りないので、英語情報に手を伸ばすことは当然
しかし、英語情報も踏まえても、イマイチ必要な情報が見つからない。

JavaとかPHPとか、長年の蓄積がある言語と比べると、ネット上における情報量の差というものを肌で感じます。
これは大きなリスクです。

仮に会社の案件としてPythonを採用したとすると、体制を構築するのは至難でしょう。

JavaとかPHPだともうネット上に情報が溢れているので誰でも何とかなるのですが、Pythonはそうは行きません。
強力な少数精鋭の体制以外では対応出来ないと思います。

これをチャンスと見るか、リスクと見るかは見解が分かれる所だと思います。

3.デタラメ


これが一番厄介!!

ネットに嘘が書いてある!!

嘘ではないにしても説明不足だったりとか、肝心な情報が抜けている記事が多い。
(もしくは、昔はそのソースで動いたのかもしれませんが、今では動かないとか)

情報源の少なさ故に淘汰されていないからなのでしょう。
ネットの技術情報は量が質を生むものです。
大量の情報が溢れかえって切磋琢磨し淘汰された結果、役に立つ情報が検索結果の上の方に来るのがネット界。

現在の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())


どうです?
私はこれを見ると「あっ、綺麗だな💛」と思います。

Javaだったらもっとゴチャゴチャですもん。
InputStreamだとか、OutputStreamだとか、close処理とか……。

比べて、要は


  • urllib.request.Request.urlopen(url, data=body,  headers=header)


で完結するPythonは如何に綺麗か。

確かによく洗練された言語だと思います。

私はまだ使い始めたばかりですが、慣れれば確かに他の言語より使い勝手が良いんじゃないかな、という感触は感じられましたね。

まとめますと、


  • 言語そのものが持つパフォーマンスは優れている。
  • 枯れておらず、情報がまだ少ないのがリスク。


というのが私の所感です。

なので、


  • プロジェクトのメイン言語として採用するのはハイリスク過ぎる。
  • 社内用のちょっとしたツール開発用なら、生産性も高くノウハウ蓄積にも適している。


こういう使い方でスキルアップしていきたいなぁ、と思うところです。

次回


次回は上記ソースの解説と、作成過程で蹴躓いた箇所の説明、デバック解説です。

同じ原因でハマっている人が検索から飛んできてくれたら嬉しいですね。