2018年2月28日水曜日

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

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

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

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


集計へ

前回にて、何とか自分の日報メールをAPIに食わせて、結果ファイルを取得するところまで成功しました。
現状、以下のようにJSONファイルがズラリとローカルに保管されております。


一個一個のファイルの中身は、以下のようにJSON文字列が入っています。

{
  "entities": [
    {
      "name": "日報",
      "type": "OTHER",
      "metadata": {},
      "salience": 0.20918807,
      "mentions": [
        {
          "text": {
            "content": "日報",
            "beginOffset": 21
          },
          "type": "COMMON"
        }
      ]
    },
    {
      "name": "結合テスト項目書",
      "type": "OTHER",
      "metadata": {},
      "salience": 0.113717824,
      "mentions": [
        {
          "text": {
            "content": "結合テスト項目書",
            "beginOffset": 169
          },
          "type": "COMMON"
        },
        {
          "text": {
            "content": "結合テスト項目書",
            "beginOffset": 203
          },
          "type": "COMMON"
        }
      ]
    },
    {
      "name": "稼働時間",
      "type": "OTHER",
      "metadata": {},
      "salience": 0.111668386,
      "mentions": [
        {
          "text": {
            "content": "稼働時間",
            "beginOffset": 53
          },
          "type": "COMMON"
        }
      ]
    },

……続く


ファイルには


  • "name": "結合テスト項目書"
  • "salience": 0.113717824

のように「キーとなる単語」と、その重要度が数値でセットされています。
つまり、単語毎にグルーピングして集計すれば、私の日報の中から頻出する重要キーワードが浮かび上がる、と。
こういう段取りなわけです。

では、集計バッチを作ってみましょう。


集計バッチ

こんなものを作りました。

import os
import json

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

#連想配列を作成
map = {}

#ファイルを全部読み込み
for file in files:
    f = open(os.path.join(dirPath,file))
    data = json.load(f)
    f.close()

    #name毎にsalienceを集計
    for entity in data['entities']:

        name = entity['name']

        if name in map:
            map[name] = map[name] + entity['salience']

        else:

            map[name] = entity['salience']

#集計結果をソート
resultList = reversed(sorted(map.items(), key=lambda x:x[1]))

#出力
for data in resultList:
    print(data[0] + '\t' + str(data[1]))

それほど大したバッチではありませんが、特徴は連想配列かな?


  • map = {}

PythonではこれでHashMapを作ったことと同じ事になります。

私の好みの部分もありますが、オブジェクト指向において、Mapというのはちと問題児だと思います。
基本的には「型」を定義して、そこに突っ込むのがオブジェクト指向であって、その「型」は厳密に定められているものです。

Mapは何でもぶっこめるので型も何もあったもんじゃない。
ソースがグジャグジャになる原因の一つだと思います。

しかしですね、ここで考慮しなければならないのが、Pythonが動的型付け言語であるということです。

Mapに何でもぶっこんでおいても、Pythonが動的に型を合わせてくれますので。
静的型付け言語で、必要に応じてキャストまでしなければならないJavaとは大きく違う部分です。

つまり、Pythonはむしろ、気軽にMapを使ってパッパと開発していく、そういう用途の為の言語であると言えるでしょう。

だから今回みたいに、ちょっとした計算バッチはパッパとMapで済ませてしまえばOK。

前も書きましたが、Pythonは「ちょっとした計算の集合体」に向く言語です。
Javaは「定められた一貫性」に優れる言語です。

見方を変えれば、
HTTPリクエストやJSONファイルみたいに、どんな内容でも自由に突っ込まれてくる情報の取り扱いは、Pythonが優れる。
RDBMSみたいに、「このテーブルのこのカラムはVARCHAR2」とガッチリ決まっていて不動の所はJavaが優れる。

ならば、「WebシステムでDBに保存する処理はどっちの言語が良いの?」と問われれば、
これが悩ましいです。

MVCモデルのうち、コントロール部はPython、モデル部はJavaに軍配が上がると思います。(ビュー部はHTMLだから関係無い)

従って、私がここ最近Pythonを触ってみた感触としては、こういう感想です。

  • テーブルの数が多いシステムはJava。
  • テーブルの数が少ないシステムはPython。

指標値とするべきはテーブルの数!!

「テーブルの数が多い=STEP数も多い=画面数も多い」となりがちなので、STEP数や画面数を指標としても同じような判断に落ち着く事も多いと思いますが、
テーブル数主眼が本質という考え方はアリだと思います。

「多い/少ない」の分かれ目は、これも感覚的なものですが……、20コくらいが上限じゃないでしょうか?
一つのシステムで20テーブルを超えたら、もう大規模システムの領域だと思います。
Pythonで似たようなことをやりたいなら、システムを2つに分割して10テーブルの小さなシステムを2つ作るとか、
サービスデザインを見直すべきところでしょう。

集計結果

ともかく、これで集計出来ました。
さあ、私の日報の分析結果は如何に。

日報 30.935778224999996
稼働時間 17.395398603999993
概要 7.594298239
詳細 4.874361350999999
作成 4.006452768100001
打ち合わせ 2.7061136348
提案書作成作業 1.90614234
ネットワーク接続作業 1.6254301270000004
プロパー 1.5154260646000002
作業 1.4572377554000002
ジェニシス 1.4317204425000003
見積もり 1.4068984159999998
稼働 1.245910729
事 1.1966940003
技術調査 1.1899960548
ソース解析 1.07680666
人 1.0335532006
予定 1.002557445
気づき 0.9903137190000001
帳票 0.9701202919999999
サンプル 0.9039525214999999
構築方針書 0.8930587449999999
マネージャー 0.8877154268999999
連絡事項 0.8676672089
システム 0.8229978675000001
プロジェクト 0.8194439760000002
話 0.8187522889000002
資料 0.8143453079000003
Java 0.7839326217000001
客先 0.7562989093
こと 0.7256286095000003
準備 0.6499679165000001
画面 0.6439224732000001
BI 0.6338003121
サンプルHTML 0.6266641692999999
Oracle 0.6084089060000001
GCOM 0.5797013178999999
検討 0.5605384631
機能 0.5536394495999999
パッケージ 0.5513561149999999
調査 0.5406491108
現場 0.5338922546000001
プラン 0.5171976732
方針 0.4683039328
要件定義 0.4537456172
単体テスト 0.44251170900000003
部分 0.4377159611
設計書 0.4054884552
内部 0.3879731024
方 0.3855670599999999
ビジネスフロー 0.384925288
クラウド基盤 0.3830445173
要件定義工程 0.3821089
開発 0.3820660644
結合テスト項目書 0.37300472500000004
チームミーティング 0.36438519610000003
放置状態 0.363936127
STEP数 0.35657688679999994
ビジネスフロー作成フェーズ 0.3412589773
ソースレビュー 0.338786632
BIプラン 0.337376835
提案書 0.32983535729999996
画面構成検討 0.32272407500000005
提案書作成作業:08:00h 0.31764615100000004
ございませ 0.30836847
構成 0.30728194200000003
帰社 0.30420098119999994
レビュー 0.3030037097
状況 0.3007273611
支援 0.29845437710000006
ソース解析:08:00h 0.293045449
もの 0.29084775009999997
待機 0.28608122
調整 0.285563885
チャンス 0.27885413449999996
合宿 0.2785616205
結合テスト 0.277871993
リリース 0.263083156
感じ 0.25053776079999995
問題 0.25040119800000005
導入 0.24158003300000003
勉強会 0.2364971065
画面構成検討:08:00h 0.235985352
案件 0.2346195449
承認 0.2288930412
Oracleクラウド 0.22463897
インターネット 0.22296365950000002
★個人名につき秘密★ 0.22103915889999995
時間 0.21647127900000002
構築方針書作成:08:00h 0.213250314
処理 0.212900709
HTML 0.2093946756
見積もり作業 0.20597136
客先打ち合わせ 0.204712169
内部調整:08:00h 0.204112792
プロジェクト構築検討:08:00h 0.202727818
課題 0.20266155249999998
結合テスト項目書作成:08:00h 0.197733019
ソース解析作業 0.19659552349999998

全部で2000行もあるのでTOP100だけ載せますが……。

ん、ん~……。

何も読み取れないというのが正直なところ。

とは言え、「あっ、この人、SEだ!!」ということは伝わってきますよね。

私だと使い道が思い浮かばなくても、広告業とかやっている人には使い道がありそうな気もしますね。

断定的な処理を行う材料には使えなくて、ヤマを張る為の参考情報くらいが使い道なようです。

例えば、


  • 客先打ち合わせ 0.204712169


「打ち合わせ」というキーワードで引っ掛けて、その数字が0.5を超えたら、それだけ日報に多数の「打ち合わせ」という単語が登場しているということですから、

「コイツは打ち合わせばっかりやっている可能性がある。不要な会議をやっていないか確認して、必要であればテコ入れしろ!!」

とアラートを出すとか、ね。

具体的な実用化案までは考えられませんか、どういうシロモノかという感覚は理解出来た気がします。

終わりに

興味本位で始めた連載でしたが、なかなか面白かったと思います。
自分の業務で即刻役に立つものではありませんが、見分を広めるという意味では良かったかな、と思います。

ネットで記事を読むだけと、ショボくても良いから自分で作ってみるのは、結構違うものです。

今後も色々と調べていきたいですね。

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の入門としては上々の滑り出しだと感じています。

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