2018年3月26日月曜日

code-prettifyで技術ブログの可読性アップ

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

今回はちょっと臨時番外編をお送ります。

サイトが見辛い

最近はQiitaという技術交流SNSが注目を集めていますよね。
私も技術調査を行っている時にQiitaに掲載されている記事にお世話になることが多いです。

それを見ていて思うのが、何かこのブログって掲載しているブログが見辛いなぁ。。。




このブログを始めた2013年当時、私はGoogleが提供してくれているコード掲載用ライブラリである「code-prettify」を導入したのですが、
今となっては明らかにQiitaとか他の技術ブログより見辛いぞ。

一体どうなってんの???

と言うことで、現状に甘んずることなく、ブログの品質向上を目指して見直しを行うことにしました。

何か良いライブラリ無いかな。。。

code-prettifyだった

そして調査して分かりましたが、私のやりたいことは「code-prettify」で出来るんですね。

code-prettify自身が2013年当時から進化していて、最新版を持って来れば良いんですよ。

2013年の頃はGoogle本家に置いてあったと記憶しているcode-prettifyも、今はGitHubに引っ越ししているようです。


今まで使っているcode-prettifyを消して、新しいのを入れなおしてみましょう。

run_prettify

run_prettifyはAjaxライブラリの一種なので、HTMLのヘッダーで宣言が必要です。

以下のようにCDN(コンテンツデリバリネットワーク)の以下を宣言して下さい。

<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>


CDN……。
要するに、Googleの親サイトに直接繋いでJSファイルを読み込むわけです。

便利である一方、Googleのサイトが落ちたり、無くなっちゃったりすると死んでしまうわけですが、run_prettifyはCDNを使った方が良いと思います。

容易な変更が許されない業務システムで使うライブラリであれば、そのシステムをリリースした時のバージョンを維持する為にJSファイルをローカルにダウンロードしてシステム内部で保持する必要があります。
でも、このブログって業務システムではないですし、何より新しさが重要ですから。

CDNが無くなったり、引っ越ししたりしたら、それに合わせてブログを更新するべきだと思います。
更新が遅い技術ブログなんて役に立たないっしょ?

何年も前に取得した古いライブラリを維持するとかは業務系システムで考えるべきことで、こういうブログは目新しさを求めてCDNを呼べば良いと思います。

さて、「run_prettify.js」ですが、これで描画に必要なスタイルシートとかも全部読み込んでくれています。

ですが、読み込む際にオプションを指定することが出来ます。




いくつかありますが、気になった所を挙げてみましょう。


  • autorun:自動実行。画面のonLoad時に自動実行することで、デフォルトはtrueです。どうも昔は自動実行が無くて、自分でonLoad実行しなきゃいけなかったようですが、今はこのとおり、自動実行でラクチンです。
  • lang:プログラミング言語の指定。code-prettifyは「Java表示モード」「Python表示モード」みたいに、言語毎にグラフィックを変えてくれるというIDEみたいな凄い技をやってくれます。そのデフォルト言語ですね。デフォルト言語をPythonにしておけば、何もしなければPythonモードで表示され、その上でJavaにしたい時はHTMLの中のclassにJavaを指定すればそこだけJavaモードになります。
  • skin:スキンの指定。後述。

大事なのは「skin」かな。




上記のページにあるスキンからお好みのものを選ぶことは出来ます。
当ブログは長らくdefaltでしたが、今回を期に「Sons-Of-Obsidian」に変更します。



<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?skin=sons-of-obsidian"></script>



これで宣言完了。

書く

ここから先の使い方は簡単です。
例えばPythonのコードを載せたい時は、以下のようにHTMLを書けばOK。

<pre class="prettyprint linenums"><code class="language-python">...コード...</code></pre>

「linenums」とは行数を表示するオプションです。
特に理由が無い限りは常につけておいた方が良いでしょう。

実際、このやり方で上記の画像の状況だったソースを掲載すると以下になります。


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]))

これこれ。
これをやりたかったんですよ。

今の時代なら絶対コレしか無い!!

終わりに

よっしゃ。これで綺麗になったぞ。
やっぱね、5年も同じブログを続けていると時代遅れになっている部分があったとしても、どうしても「これでいいや」って気分になりがちなんですよね。
今までそれでやって来ているわけですから。

でも、偶にで良いので振り返って、「いやいや、今ならもっと良いやり方があるぞ」という所を見つけたら、改善を決行しなければなりません。

「今までそれでやって来た」というだけで改善を行わないという怠慢!!

ブログ運営に限らず、業務を行う上では常に気を付けていきたいものです。

2018年3月19日月曜日

Pythonで社内システムを作ってみた~序章~

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

先週まで勉強を兼ねて、PythonとGoogleの機械学習API「CLOUD NATURAL LANGUAGE API」の連載を行っていましたが、
おかげ様で何とか感触を掴めてきました。

しかし「勉強の為の勉強」だと、どうもピントがぼやけた調査になってしまうのが現実のところ。
そこで「社内システムの開発」を想定して、ちょっくらPythonで社内で使えそうなシステムを作ってみようと考えるに至り、ようやくある程度の形になってきたところです。

そこで、今回より新シリーズ「Pythonで社内システムを作ってみた」を始めていきたいと思います。

着想

テーマとして選んだのはSEならみんなお馴染みの、「業務経歴書」です。

弊社の業務経歴書はIT界で悪名高い神エクセルの類です。

エクエルに各自書き込むという方式なので記述粒度が人によって合ってなかったり、印刷してみると微妙に見切れちゃってたり、
とか細々とした問題が多いという感覚が現場の実感としてありました。

私の業務経歴書はこんな感じ。(ちょっとだけお見せします)



一見すると綺麗ですが、これがエクセルで、実は問題もあるんですよ。

デザインの改善点の指摘もあるのですが、何せ全員が各自個別にエクセルファイルを持っている形ですから、
デザイン変更したくても全員が同じようにレイアウト変更作業をやるのは大変だから二の足を踏んだりとか、
やっぱり「個別管理」「エクセル形式」というのは、手っ取り早いのは良いですけど長期的管理体制には不向き。

そこで私は「これをWebシステム化して一元管理したら業務改善になるんじゃないかな?」と考えるに至ったわけです。

課題

「Webシステム化する」と言うだけなら簡単ですが、それをソリューションとしてビルドアップするのは中々ハードルが高いです。
開発進行過程で課題になった点をご紹介しましょう。

言語はPython

これは課題と言うより「前提」ですが、言語は最初からPythonにしようと思っていました。
そもそもはPythonの勉強から始まっている、というのも大きいですが、もう一つの理由としては「優遇」があります。

プログラミング視点では「小規模Webシステムなんてどんな言語使っても対して変わらん」というのが実際のところですが、環境面だと意外にそうでもないです。

過去の連載で私はGoogleAppEngineを使用し、ネタバレになりますが今回はAWSを使用しましたが、その所感としてPythonはクラウド界で優遇されています。

クラウドサービスは言語を指定されている製品がありまして、「この製品はPythonは使えるけでRubyは未対応」など、クラウド製品の都合で言語が縛られるケースがあります。
そんな中、Pythonは常に対応していました。

  • とりあえずPythonならばクラウドはOK

私は寄らば大樹の陰という主義のエンジニアですが、Pythonは寄って安心の大樹だと思います。

基盤

基盤は「クラウド+サーバレス(従量課金制)」しか無いと最初から思っていました。

この業務経歴書管理システムは、その性質上、業務経歴が書き換わるタイミングしか出番が来ないものです。
個人単位だと数ヶ月~数年に一度しか使いません。

そんなシステムの為にサーバを常駐させておくなんてのはコスト的にあり得なくて、システムを使うときだけ費用が発生する従量課金制のクラウド・サーバレスサービスしか無いというのは自明の理でした。

サーバレスサービスは、やはり以前に連載していたGoogleAppEngineが業界代表格でしょう。
この連載の中で別のシステムを作っていましたから、「サーバレスとはどういうものか?」「どういう時に出番が来るのか?」という知見はありました。

今回は同じサーバレスでも別製品、AWSのサーバレスを使うことにしました。
GoogleではなくAWSにした理由は、弊社の方針ですね。
今後、弊社ではAWSに力を入れていこうという方針があるので、そこに乗っかることにしました。

特にAWSの方がGoogleより向いている何かがあったわけではありません。
しかし、あっちこっちの技術に手を伸ばしていると片手落ちになってしまうので、合わせられる部分は合わせよう、とAWSにすることにしました。

Webシステム・フレームワーク

「PythonでWebシステムを作るとしたら、フレームワークはどうしよう?」と思いました。
調べてみると「django(ジャンゴ)」というフレームワークが見つかったのでコレにしました。

と言うか、PythonのWebシステム界ではdjangoが支配的地位です。
django以外の選択肢なんかありません。


帳票出力

最大の問題点。
業務経歴書は印刷して面談に持って行くという紙媒体が運用上絶対に外せない要件です。

  • Webだけなら誰でも出来る。
  • 印刷はどうすりゃいいんだ!?

帳票出力は技術的最大のハードルです。

  • Yahooの時刻表印刷みたいにWeb画面を印刷する。
  • 現在と同じくExcelを出力する。
  • PDFで出力する。

色々考えて検証しましたが、これらにはそれぞれ問題点があって解消出来ませんでした。

  • Web画面印刷⇒ブラウザ依存でどうしても微妙に見栄えが変わってしまう。
  • Excel印刷⇒印刷時の見切れに対応出来ない。
  • PDF⇒柔軟性が無さ過ぎる。例えば営業が「ちょっと急ぎ手直ししたい」という状況が発生した時に書き換えられない。

これらを踏まえて、最終的に「Wordで出力する」という仕様に結論付けました。

Word出力もこれはこれで難しくて。。。
最初はPythonでやろうとしましたが、どうしても実現不可能なケースがあったので、Pythonは諦めてココだけJavaでやることにしました。

Word出力の奮闘記は後の連載でご紹介します。


システム構成

そんなこんなで試行錯誤した結果、以下のようなシステム構成図でまとまりました。



大きく「Webシステム」と「帳票出力API」に二分割されています。

終わりに

今後はシステム構築に至るまでの過程やノウハウの連載を始めていきたいと思います。


  • Webシステム実装:Python&Django編
  • Webシステムインフラ:AWS Elastic Beanstalk編
  • 帳票出力API実装:Apache POI編
  • 帳票出力APIインフラ:AWS Lambda編


システム全体としては色々と詰め込まれていますが、それぞれ非常に疎結合となっています。

「ベンダーロックオン」と言いまして、クラウドの話をすると以後ずっとそのクラウドから抜け出せなくなることを気にされるケースがありますが、作ってみたところ、別にそんなことは無さそうでした。

ログ出力の設定とか多少のことはありますが、「作ったアプリのインフラを別のところに引っ越したいんだけど?」となった場合でも、特に問題にはならない規模です。

従って、連載を進めていく上で「インフラ編を読んだ後じゃないと実装編の意味が分からない」とかにもならないと思います。

連載は、システム規模が


  • Webシステム>>>帳票出力API


なので、まずは手短な帳票出力API編から始めていきたいと思います。

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

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

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