2018年2月19日月曜日

【Googleクラウド・機械学習編】日報解析バッチ作成中3~リクエスト送信~

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

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

初心者の勉強として、簡単なバッチを作っている最中です。
大した内容はありませんが、お付き合いください。


リクエスト送信

前回の作業にて、ファイルを読み込むところまでは確認出来ました。
今度はこれをAPIに送り込まなければなりません。

最難関となるリクエスト送信処理そのものはもっと前に検証して解決しているので、今回行うのは簡単なテキスト加工ですね。

テキスト

私の日報は必ず以下のような形式になっています。


Delivered-To: endo@genesis-net.co.jp
Received: by 10.60.116.6 with SMTP id js6csp2150228oeb;
        Tue, 28 Jul 2015 06:16:25 -0700 (PDT)
X-Received: by 10.60.142.234 with SMTP id rz10mr33362752oeb.4.1438089385353;
        Tue, 28 Jul 2015 06:16:25 -0700 (PDT)
Return-Path: <3qIC3VRMJBDsdbkbpfp-dXb-pbosfZbdjXfi.Zljbkaldbkbpfp-kbq.Zl.gm@2uix4h7xygsz66weerlq.apphosting.bounces.google.com>
Received: from mail-pd0-f197.google.com (mail-pd0-f197.google.com. [209.85.192.197])
        by mx.google.com with ESMTPS id ma3si53036429pdb.163.2015.07.28.06.16.24
        for <endo@genesis-net.co.jp>
        (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
        Tue, 28 Jul 2015 06:16:25 -0700 (PDT)
Received-SPF: pass (google.com: domain of 3qIC3VRMJBDsdbkbpfp-dXb-pbosfZbdjXfi.Zljbkaldbkbpfp-kbq.Zl.gm@2uix4h7xygsz66weerlq.apphosting.bounces.google.com designates 209.85.192.197 as permitted sender) client-ip=209.85.192.197;
Authentication-Results: mx.google.com;
       spf=pass (google.com: domain of 3qIC3VRMJBDsdbkbpfp-dXb-pbosfZbdjXfi.Zljbkaldbkbpfp-kbq.Zl.gm@2uix4h7xygsz66weerlq.apphosting.bounces.google.com designates 209.85.192.197 as permitted sender) smtp.mail=3qIC3VRMJBDsdbkbpfp-dXb-pbosfZbdjXfi.Zljbkaldbkbpfp-kbq.Zl.gm@2uix4h7xygsz66weerlq.apphosting.bounces.google.com
Received: by pdbpo3 with SMTP id po3so221230738pdb.1
        for <endo@genesis-net.co.jp>; Tue, 28 Jul 2015 06:16:24 -0700 (PDT)
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20130820;
        h=mime-version:reply-to:message-id:date:subject:from:to:content-type
         :content-transfer-encoding;
        bh=449+r7RRDqf7+TpVrGlImqmP3nYBDuppv9kcCar1VM8=;
        b=Ex7Z3fWrUdO//VwmvJl4tECF79NZGChauQzCEd5f4uCZ3YIC6m02Damyzwxg+W1pmO
         96vdBP56t3on+zNqYbjXdE50MnZlbq41s+rhfmpN3744ow7ce/ocruBueluTNlMQuHZO
         Ct0+vALQ7aTtyC0UNE+RnNU1oGDcA0hTUueL8e93SFbJVcCPtgg+bvn8zx3zhj0l5Da0
         axnU9Figlz+M/Vazvy95mppTOlBzCQobqXZkeobBupWkFaYLxny0/bKXjkiq16sa5EZ1
         Vp2BYtR/7lc5eHQcg3NHK+dtiR6QU4UsM98P1B8b1jQrWm8MfVJKEcUDP6ls6YROZMDk
         pSoQ==
MIME-Version: 1.0
X-Received: by 10.66.146.227 with SMTP id tf3mr36223044pab.21.1438089384636;
 Tue, 28 Jul 2015 06:16:24 -0700 (PDT)
Reply-To: "endo@genesis-net.co.jp" <endo@genesis-net.co.jp>
X-Google-Appengine-App-Id: s~genesis-gae-service
X-Google-Appengine-App-Id-Alias: genesis-gae-service
Message-ID: <047d7b6dc768ed3c29051bef4663@google.com>
Date: Tue, 28 Jul 2015 13:16:24 +0000
Subject: =?ISO-2022-JP?B?GyRCIVpGfEpzIVsxc0YjGyhCXzIwMTUwNzI4?=
From: "endo@genesis-net.co.jp" <endo@genesis-net.co.jp>
To: "HIMITSU@genesis-net.co.jp" <HIMITSU@genesis-net.co.jp>
Content-Type: text/plain; charset=ISO-2022-JP; format=flowed; delsp=yes
Content-Transfer-Encoding: 7bit
○○ さん
お疲れ様です。遠藤です。
2015年07月28日の日報を送付します。
【稼働時間】・09:00~18:00 (08:00h)
【概要】・実装:08:00h
【詳細】 報告内容がズラズラっと。
【連絡事項】・7/31(金):帰社
以上、よろしくお願いします。
--
================================================
遠藤 太志郎
株式会社ジェニシス技術開発事業部@春日
Mail:endo@genesis-net.co.jp
歯科医院予約管理システムDentNet facebook
http://www.facebook.com/dentnet.genesis
株式会社ジェニシス技術開発事業部ブログ
http://genesis-tdsg.blogspot.jp/
================================================

解析が必要な部分は赤文字の部分だけです。従って、

  • お疲れ様です。
  • 以上、よろしくお願いします。

この2つのキーワードを「開始」と「終了」の目印として使うことが出来そうです。

まあ、私のメールだからこんなやり方でフィルタリングできますが、もっと形態不明なメールを解析するのであれば、違う目印が必要になるでしょうね。

今はテキストベースでやっていますからこういうやり方ですが、ちゃんと環境を整えれば「ヘッダー」と「ボディ」を見分けるくらい出来ると思います。

その条件で、ファイルの文中から解析したい部分だけを切り取るロジックがこちら。

def read_content(file):
    flg = False;
    content = ''
    for line in open(os.path.join(dirPath, file), 'r', encoding='ISO-2022-JP'):

        if line.find('以上、よろしくお願いします。') == 0:
            flg = False

        if flg:
            content = content + line
        else:
            if line.find("お疲れ様です。") == 0:
                flg = True

    return content

特筆することはありません。

ただ、こういう文字列加工ってチョコチョコチョコチョコ作業するものなんですよね。
Pythonというのは、このチョコチョコ作業に向いている性質があると思います。

言語の向き、不向きを肌感触で知っていることは重要だと思いますね。

文字列が構築出来ましたら、後はそれをJSONに組み込んで送り込むだけです。

def send_request(content, file):
    # 送信先URL
    url = "https://language.googleapis.com/v1/documents:analyzeEntities?key=himitsu"

    # 送信するJSONパラメータ
    body = {
        'document': {
            'type': 'PLAIN_TEXT',
            'content': content
        },
        'encodingType': 'UTF8'
    }

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

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

    # リクエストメソッド
    method = "POST"

    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("レスポンスを受信しました。")
            print(response_body)

            f = open(os.path.join(exportPath, file + '.json'), 'w')  # 書き込みモードで開く
            f.writelines(response_body)
            f.close()

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


特筆するべきは、JSONの構築箇所です。

    # 送信するJSONパラメータ
    body = {
        'document': {
            'type': 'PLAIN_TEXT',
            'content': content
        },
        'encodingType': 'UTF8'
    }

これ。
contentが上記で構築した変数ですが、このように、JSONの構造が見える状態で形成することが出来ました。

  • "body = {" + {\'document\': {" + "\'content' + content ……

みたいなエスケープ文字の羅列だと読めないですからね!!
このようにJSONをJSONとして取り扱い出来ることが大変すばらしいです。

後はリクエストを送り、受領したレスポンスをファイルに一時保存すれば終わり。

f = open(os.path.join(exportPath, file + '.json'), 'w')  # 書き込みモードで開く
f.writelines(response_body)
f.close()

ファイルの書き込みはこれでOKです。

終わりに

これでズラーッと解析結果を取得出来ました。


後は、この結果ファイル一覧を読み込んで、どんなデータが入っているか見てみようと思います。

2018年2月5日月曜日

【Googleクラウド・機械学習編】日報解析バッチ作成中2~ファイル読み込み~

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

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

初心者の勉強として、簡単なバッチを作っていきたいと思います。


ファイル読み込み

今回はローカルPC内に置いたファイルを読み込むところまで進めたいと思います。

まず、メールをローカルPCに置きます。




「C:/MyMail/【日報】遠藤_20160201 - 'endo@genesis-net.co.jp' (endo@genesis-net.co.jp) - 2016-02-02 0650.eml」

こんなようなパスのファイルが沢山置かれました。

では、まずはファイル1コを読み込んでみましょう。

ファイル読み込み:1コ


このファイルを読み込むソースはコレです。

filePath = "C:/MyMail/【日報】遠藤_20160201 - 'endo@genesis-net.co.jp' (endo@genesis-net.co.jp) - 2016-02-02 0650.eml"
for line in open(filePath, 'r',encoding='ISO-2022-JP'):
    print(line)

この簡単さよ。

実質2行でファイルを全部開けます。

調べたところ、上記の書き方はCLOSE処理も自動的に行ってくれているようですね。
意識的にクローズしたい場合は、こちら。

filePath = "C:/MyMail/【日報】遠藤_20160201 - 'endo@genesis-net.co.jp' (endo@genesis-net.co.jp) - 2016-02-02 0650.eml"
f = open(filePath, 'r',encoding='ISO-2022-JP')

for line in f:
    print(line)

f.close()

たぶん、読み込み処理だったら上記の書き方だけで大概は解決するんじゃないかな?
ファイルを読む時って、特別な理由が無い限りは頭からお尻まで全部読み込んでしまうものですからね。

今回は登場しませんが、書き込み処理だったら自分でクローズする必要がありそうです。

ファイルを開くことよりもクローズすることを気にする辺りに、私の玄人ぶりを察して頂ければと思います。

「r」とは、読み込みモードで開くという意味です。
「書き込みモード」「追加書き込みモード」「読み書き両用モード」などいくつかあるようですね。

「読み書き両用モード」というのは、何が起きるか分からないので私はオススメ出来ませんが。
同じファイルを読み書きしたいのであれば、専用モードで毎回開きなおすのが正しいと思いますが、必要なシチュエーションもあるのかもしれませんね。

「encoding='ISO-2022-JP'」は、もちろん文字エンコーディングです。
デフォルトはUTF-8なので、何も指定しなければもちろん文字化けします。


ファイル読み込みについては以上です。
実に簡単でしたね。

ファイル読み込み:全部

次に、フォルダ内のファイル全部読み込みに行ってみましょう。

import os

dirPath = 'C:/MyMail'
files = os.listdir(dirPath)

for file in files:
    for line in open(os.path.join(dirPath,file), 'r',encoding='ISO-2022-JP'):
        print(line)

まず、以下の部分が特定フォルダ配下のファイル名の一覧を取得する処理です。

  • files = os.listdir(dirPath)

ここで一つ気になったのは、Javaで言うところの「File型」とかそういうのではなく、単にファイル名の文字列が配列でぶっこまれているだけということですね。

だから、配列をループで回して読み込む際は、以下のようにファイルを絶対パスに直さなければならないのです。
それが「join」です。

  • os.path.join(dirPath,file)

要するに「ディレクトリパス」+「/」+「ファイル名」という結合を行っているのですが、「/」の部分はOSで差があったりしますからね。
その辺をクールに処理してくれるのが「join」なのです。

では、「毎回joinしてフルパスを構築せねばならんのか?」と言いますと、それとは別にglobという書き方があります。

import glob

dirPath = 'C:/MyMail/*'
files = glob.glob(dirPath)

for file in files:
    for line in open(file, 'r',encoding='ISO-2022-JP'):
        print(line)

dirPathを正規表現で記載することで、ファイルを絶対パスで一覧取得する機能です。
どちらが良いかはお好みで良いでしょう。

いずれにせよ、取得するのは「File型」ではなく「パス文字列」という点に、私は着目します。
処理が軽量なんですね。

Pythonは処理速度が速いことが一つの売りですが、その鱗片がこういうところに垣間見えます。

ともかく、後は取得した一覧をグルーッと回していくだけです。


for file in files:
    for line in open(os.path.join(dirPath,file), 'r',encoding='ISO-2022-JP'):
        print(line)


Pythonはこのように処理の開始と終了を{}ではなくてインデントで表現するのが特徴です。
こんな書き方をする理由としては、例えばfor文一つを書くだけでも、

for(){

for()
{

と「{」が同じ行にあるか違う行にあるかどうかで宗教戦争が起きてしまうことを回避する為の作戦だそうです。
誰が書いても似たようなソースになるということを目指した結果です。

しかし、

「いくら理由があるにしたって、インデントで表現するかよ、普通?」

って思いますよね?
思ってたんですけど、実装してみると全然気になりません。

むしろ見易いとさえ思います。
目からウロコが落ちました。

可読性の高い言語だ、という実感があります。
良い言語です。

終わりに

今回は単にファイルを開くだけという簡単な内容でしたが、Pythonの入門としては上々の滑り出しだと感じています。

最初はみんな初心者ですからね。
簡単なところから慣れ親しんでいきたいと思います。

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は如何に綺麗か。

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

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

まとめますと、


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


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

なので、


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


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

次回


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

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

2017年12月25日月曜日

【AndroidStudio】日本語化再び【完全版】

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

今回は臨時投稿。
以前に連載していたAndroidStudioについて更新が発生したのでご連絡です。

【AndroidStudio】インストール&日本語化


2015年の記事「AndroidStudio】インストール&日本語化」で、
AndroidStudioの日本語化について試行錯誤しましたが、
結論はと言うとダメでした。

AndroidStudioの日本語化は不完全。。。
下手に日本語化するより英語でやった方がマシ。。。

と思っていましたが、いつの間にか日本語対応が完成していました。

日本の誇る日本語化アプリ「Pleiades」が対応して下さったようです。
対応時期はどうやら2017年6月末だったの模様。

かなりの大幅アップデートだったようで、私が知っていたPleadesから相当な進化を遂げております。

日本語化手順は公式サイトを見れば分かるかと思いますが、一応、当ブログでもご紹介させて頂きたいと思います。

【AndroidStudio】日本語化手順


1.AndroidStudioのインストール


事前のAndroidStudioのインストールは省略します。
普通にインストールして下さい。

2.Pleadesの取得


Pleadesの公式サイトよりプラグインをダウンロードします。


ここで注意ですが、ダウンロードするのは上にある「Pleiades All in One」ではなく、下にあるプラグインの方です。

ここがPleiadesの凄い所なのですが、Pleiadesは元々はEclipse用に作られた日本語化ツールですが、AndroidStudioなどのIntelliJ系にも適応出来るのです。

「AOP (横断的関心事を解決するテクノロジー)」 と言うそうです。
「Java クラスローダーをフックし、クラスをバイトコードレベルで動的に書き換え……」とのことですが、完全に黒魔術
私は性能測定ツールやモックツール等でメタ領域に突っ込んだ製品を見たことがあるので概念はある程度理解する所ですが、実装イメージが沸くほどには到底及びません。
特殊技術の領域です。

とは言え、使うだけなら単なるツールなので簡単です。

早速プラグインをダウンロードしてみましょう。



3.解凍


ダウンロードしたプラグインを解凍します。


昔は「features」と「plugins」しか無かったのですが、色々と増えました。
これは革命的なことです。
Eclipseのプラグインなんてどれもこれも「features」と「plugins」だけがチョロっと入っているだけのチープなものばかりなんです。
それ以外が含まれた豪華なパッケージなんて初めて見ました。

この中の「setup.exe」をダブルクリックして実行します。

4.セットアップ


セットアップ実行直後の画面は以下です。



「日本語化するアプリケーション」の選択ボタンを押下します。
実行ファイルの選択ダイアログが表示されますので、日本語化したいアプリの実行ファイルを選んで下さい。

私の場合はAndroidStudioの64bit版を日本語化したいので、「studio64.exe」を選びます。



後はもう、スルスルっと設定が行われ、以下の状態になります。



「日本語化する」のボタンを押下して下さい。

これで日本語化完了です。


5.起動


では、期待を込めてAndroidStudioを起動してみましょう。



バッチリです!!

終わりに


いやぁ、流石はレジェンドアプリのPleadesですね。

昔は「自分でパスを通せ」みたいな、実に技術者向けのそっけないツールだったのですが、今や完全GUI対応。
この進化は普通ではありません。

「AndroidStudioを普及させたい勢力が手伝った」とかあるんじゃないかな?
それくらい特別なアップデートが成されています。

日本Android界全体に影響を与える大改修と言って良いでしょう。

しかし、最近はIntelliJベース製品の攻勢が著しいですね。

Android開発はAndroidStudioが正式採用。
Python開発はPyCharmが世界最強と評価されています。

Javaは長年の間、Eclipseが支配的地位を占めてきましたが、

「AndroidはAndroidStudioなのにJavaはEclipseかよ?」

というのは誰もが思っていることでしょう。

私は伝統的JavaエンジニアなのでEclipseに最も慣れ親しんでいます。
しかし、IntelliJの活躍を見ると、慣れたEclipseを離れて新しい環境に挑戦していかなければならないと、最近強く思いますね。

今年度の更新はこれが最後です。
また来年も宜しくお願いします。

2017年12月18日月曜日

【Googleクラウド・機械学習編】CLOUD NATURAL LANGUAGE API その2

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

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

前回に引き続き、「CLOUD NATURAL LANGUAGE API」の実行を目指していきます。

前回まで

前回までで、ひとまずAPIの有効化と共に課金許可まで行いました。

認証ID取得

API管選択画面から「管理」のボタンを押して認証情報の取得画面に進みます。




選択肢が出てきました。

  • APIキー
  • OAuthクライアントID
  • サービスアカウントキー

このうち、単純に実行したい場合に必要なのはAPIキーです。
それ以外のキーは未調査。

APIキーを選択します。


無事に生成されました。

ここで注意事項ですが、APIキーは他人に漏らしてはいけません。

APIキーで個人を識別する仕様ですので、APIキーが漏れることは、ログインID/パスワードやクレジットカード番号が漏れることと同レベルの危機です。

ちなみに今は私が自分で検証しているだけなので考慮不要ですが、もし業務で本格的に使う事になったらAPIキー秘匿の為の策も練る必要があります。

外部への流出対策は言うまでもありませんが、内部に対する秘匿も課題になるでしょう。

例えば、

  • ログにHTTPリクエストのクエリが表示されている。
  • クエリの中にはAPIキーも含まれている。
  • ログは内部の人間ならば誰でも見れる。

なんてコンボをやらかしたら、もう完全にダダ漏れですよね。
こういうのを一つ一つ見落とさないように防いでいくことはなかなか難しいと思います。

クラウドにはクラウドのリスクがありますので、その辺りも理解する必要があります。

ともかくこれで無事に取得出来ました。




ちなみに今、「取得したけど一覧に何も出ない」という挙動しました。
F5を押したら表示されましたが。

ちょっとGoogleさん、画面リフレッシュの実装漏れみたいなバグがありませんか、これ?

Googleって、些細なバグは後回しにするような所がありますからね。
ユーザーも些細な事は気にしないのがお付き合いするコツです。

実行

さて、これで実行に必要な素材は全部揃ったはずなのですが。
ここからが難関。

API仕様書を読み込んで行かなければなりません。


また、HTTPリクエストを作るプログラムも実装しなきゃいけないなど、大変な作業が待っています。
しかし、親切な事にGoogleには「APIエクスプローラ」という動作確認用の機能があります。



これを使ってちょっと動作確認をしてみましょう。

文章

テキトーに考えた私の自己紹介を送り込んでみます。

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

仕事はシステムエンジニア。
特技は空手道初段です。

家族は妻が一名ほどいます。

好きなお酒はウイスキーです。

仲良くして下さい。
======================================

使用する機能

analyzeEntities。
エンティティ解析です。

エンティティとは、「特徴的な単語」と解釈すれば良いでしょう。

結果

以下のようなリクエストを送り込みます。
POST https://language.googleapis.com/v1/documents:analyzeEntities?key={YOUR_API_KEY}
 
{
 "document": {
  "content": "株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。\n\n仕事はシステムエンジニア。\n特技は空手道初段です。\n\n家族は妻が一名ほどいます。\n\n好きなお酒はウイスキーです。\n\n仲良くして下さい。",
  "type": "PLAIN_TEXT"
 },
 "encodingType": "NONE"
}
すると、こんな結果が戻ってきました。
200
 
- Show headers -
  
{
 "entities": [
  {
   "name": "株式会社ジェニシス 技術開発事業部",
   "type": "ORGANIZATION",
   "metadata": {
   },
   "salience": 0.22330247,
   "mentions": [
    {
     "text": {
      "content": "株式会社ジェニシス 技術開発事業部",
      "beginOffset": -1
     },
     "type": "PROPER"
    }
   ]
  },
  {
   "name": "遠藤 太志郎",
   "type": "PERSON",
   "metadata": {
   },
   "salience": 0.19125123,
   "mentions": [
    {
     "text": {
      "content": "遠藤 太志郎",
      "beginOffset": -1
     },
     "type": "PROPER"
    }
   ]
  },
  {
   "name": "Tacy",
   "type": "OTHER",
   "metadata": {
   },
   "salience": 0.19125123,
   "mentions": [
    {
     "text": {
      "content": "Tacy",
      "beginOffset": -1
     },
     "type": "PROPER"
    }
   ]
  },
  {
   "name": "仕事",
   "type": "OTHER",
   "metadata": {
   },
   "salience": 0.07422842,
   "mentions": [
    {
     "text": {
      "content": "仕事",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "システムエンジニア",
   "type": "OTHER",
   "metadata": {
   },
   "salience": 0.07422842,
   "mentions": [
    {
     "text": {
      "content": "システムエンジニア",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "特技",
   "type": "OTHER",
   "metadata": {
   },
   "salience": 0.05605998,
   "mentions": [
    {
     "text": {
      "content": "特技",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "空手道初段",
   "type": "OTHER",
   "metadata": {
   },
   "salience": 0.05605998,
   "mentions": [
    {
     "text": {
      "content": "空手道初段",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "酒",
   "type": "CONSUMER_GOOD",
   "metadata": {
   },
   "salience": 0.03795014,
   "mentions": [
    {
     "text": {
      "content": "酒",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "ウイスキー",
   "type": "OTHER",
   "metadata": {
   },
   "salience": 0.03795014,
   "mentions": [
    {
     "text": {
      "content": "ウイスキー",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "家族",
   "type": "PERSON",
   "metadata": {
   },
   "salience": 0.028858999,
   "mentions": [
    {
     "text": {
      "content": "家族",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  },
  {
   "name": "妻",
   "type": "PERSON",
   "metadata": {
   },
   "salience": 0.028858999,
   "mentions": [
    {
     "text": {
      "content": "妻",
      "beginOffset": -1
     },
     "type": "COMMON"
    }
   ]
  }
 ],
 "language": "ja"
}


考察

ほえ~。

「salience」とは突出率という意味で、この数字が高いほど特徴的で、
低いほどありふれた単語、という意味のようです。


  • 株式会社ジェニシス 技術開発事業部


これで一つの単語として認識されています。

TYPE:ORGANIZATION

しかし、「株式会社ジェニシス」が会社名で「技術開発事業部」が部署名であることまでの粒度には到っていません。

  • 遠藤 太志郎

私の名前も抽出出来ています。

TYPE:PERSON

ジェニシスの方は「TYPE:ORGANIZATION」で私の名前は「TYPE:PERSON」ですから、それが人間の名前なのか組織の名前なのかを識別出来るようです。

なるほどなるほど。
機能のイメージは沸いてきますね。
問題はこれをどう役立てるかということですが、試していくには元となるデータが沢山必要ですね。

同僚の日報とか解析してみると面白いかもしれません。

終わりに


引き続きCLOUD NATURAL LANGUAGE APIの検証を進めていこうと思います。