2018年12月12日

GPU版Tensorflowをインストールしてみた

前置き


思いの外ハマるので備忘録として残す。
公式:https://www.tensorflow.org/install/gpu
お世話になったサイト
http://cookie-box.hatenablog.com/entry/2018/09/19/201044
https://qiita.com/fulcrum/items/ad6719d47b5c33ded241
https://github.com/tensorflow/tensorflow/issues/22794
https://qiita.com/ozaki_physics/items/985188feb92570e5b82d

解説

言ってしまえばなんのことはなく、バージョン不一致だとNGなので必ず同じバージョンを利用する必要がある、というだけの話。指定より新しいバージョンでは駄目で、必ず指定バージョンを利用すること。

2018年12月現在、Tensorflowのバージョンはtensorflow_gpu-1.12.0である。
現時点では、以下のバージョンが最新として利用できる。
* CUDA Toolkit 9.0
* cuDNN v7.4 for CUDA 9.0

この辺は好きなもので良さそう。
* NVIDIA GPU drivers
* VC++2015再配布パッケージ(Windows版)

時期が変わればバージョンも変わると思うが、大事なのは公式で記載しているバージョンと同じものを用意すること。後方互換性がないため上位バージョンがNG、という話。

最新のAnacondaはPythonは3.7だが、Pythonは3.7だとNGなので、仮想環境で3.6を用意するといい。
以下の構文をAnaconda Promptで実行する。なお「tf-gpu」が仮想環境名となる。

conda create -y -n tf-gpu pip python=3.6 anaconda

activate tf-gpu

conda install -y -c aaronzs tensorflow-gpu
conda install -y -c anaconda cudatoolkit
conda install -y -c anaconda cudnn
conda install -y keras-gpu

そして、そのままインタラクティブモードを起動して動作確認をする。

python
from tensorflow.python.client import device_lib
device_lib.list_local_devices()

結果にGPUが表示されたらOKとなる。

上手くできなかったら一回削除してやり直すと吉。
conda remove -n tf-gpu --all

2018年11月24日

小ネタ:pandasのas_matrix()はvaluesに置き換えよう

前置き


最近はデータサイエンティストを目指してPythonを習得しております。
今回は、Python3.7でas_matrix()が非推奨となったので、その話です。
簡単にググって見つからなかったので誰かの助けとなればと思い記載いたします。
元ネタは以下になります。
scikit-learn で線形回帰 (単回帰分析・重回帰分析)
Python: Method .as_matrix will be removed in a future version. Use .values instead


Pandasから配列(リスト)への変換

先に挙げたページから、赤ワインのCSVをPandasへ取り込んで、DataFrame(およびSeries)を配列に変換して予測する処理について、コードを引用いたします。
# %% scikit-learn で線形回帰 (単回帰分析・重回帰分析)
# https://pythondatascience.plavox.info/scikit-learn/%E7%B7%9A%E5%BD%A2%E5%9B%9E%E5%B8%B0

import pandas as pd
import numpy as np

wine = pd.read_csv("winequality-red.csv", sep=";")
wine.head

# %%
# sklearn.linear_model.LinearRegression クラスを読み込み
from sklearn import linear_model
clf = linear_model.LinearRegression()

# 説明変数に "density (濃度)" を利用
X = wine.loc[:, ['density']].as_matrix()  # ★該当箇所

# 目的変数に "alcohol (アルコール度数)" を利用
Y = wine['alcohol'].as_matrix()  # ★該当箇所

# %%
# 予測モデルを作成
clf.fit(X, Y)

# 回帰係数
print(clf.coef_)

# 切片 (誤差)
print(clf.intercept_)

# 決定係数
print(clf.score(X, Y))
※引用コードに対し、一部コメントを追加しています
前回紹介しました、VSCodeのJupyter Interactiveモードで動かす前提で、セルを分割しております。2セル目に「★該当箇所」というコメントを付与しております。

このコードはPython3.6では特に支障なく動きますが、3.7だと以下の警告が発生します。

C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:10: FutureWarning: 
Method .as_matrix will be removed in a future version. Use .values instead.

これは英語のそのままだけれど、メソッドas_matrix()は廃止予定だからvaluesに置き換えてほしいという話です。
ただ、代わりに使えというvaluesってなんじゃらほいっていうのがわからずググってしまった次第です。
そして冒頭に紹介したstackoverflowのページに辿り着いたということです。

解としては、なんのことはなく、valuesというプロパティを使用しろという結論でした。 ですので、書き換えるならば以下のようになります。
# %%
# sklearn.linear_model.LinearRegression クラスを読み込み
from sklearn import linear_model
clf = linear_model.LinearRegression()

# 説明変数に "density (濃度)" を利用
X = wine.loc[:, ['density']].values  # ★変更後

# 目的変数に "alcohol (アルコール度数)" を利用
Y = wine['alcohol'].values  # ★変更後

これで警告が収まりました。
近い将来廃止されるようですし、今からコーディングする方はvaluesに置き換えましょう。

2018年11月20日

小ネタ:ありがとうJupyter拡張、こんにちはPython拡張Interactive

前置き

とても優秀な存在であった、Visual Studio CodeのJupyter拡張ですが、本家Python拡張にて同等機能が取り込まれたことにより、不要な拡張機能となりました。
参考:
https://blogs.msdn.microsoft.com/pythonengineering/2018/11/08/python-in-visual-studio-code-october-2018-release/
https://news.mynavi.jp/article/20181109-721502/

使い方

Python拡張のInteractiveモードは、元祖Jupyter拡張と同じく、Cellを定義してあげれば動きます。
先のリンクにあるように、「# %%」と入れればセルの出来上がりです。Run CellをクリックすればJupyter Notebookのように動いてくれます。たったこれだけなので細かい説明は不要でしょう。

じゃぁ、なぜわざわざブログを起こしたのかといえば、そう、キーバインドです。

今までJupyter 拡張を利用していた人は、Ctrl+Enterでセル実行をするのが当たり前になっていたので、今更ショートカットキーなしの生活には戻れますまい。
そんな訳で、keybindings.jsonに以下の項目を足してあげましょう。


[
    {
        "key": "ctrl+enter",
        "command": "python.datascience.runcell",
        "when": "editorTextFocus"

    },
    {
        "key": "ctrl+shift+enter",
        "command": "python.datascience.runallcells",
        "when": "editorTextFocus"
    }
]
これで、今までどおりに実行できます。ついでにCtrl+Shift+Enterで全セル実行も定義してみました。

補足

Pythonの導入等にお困りの場合には以下のリンクから辿ってください。
https://ntnl-it-wiz.blogspot.com/2018/09/vs-code-pythonelasticsearch.html

追記

記事を上げたときには気が付かなかったけど、Run CellはShift+Enterでキーバインドされていますね。 新規に使用する人は互換性を気にする必要もないだろうし、Jupyter Notebookと同じキーバインドだし、Shift+Enterで覚えればよさげですね。

2018年10月23日

小ネタ:PowershellでATOM(RSS)を取得する

ブログの更新を何かに残そうと考えております。やっぱり格納先はEvernoteでしょうかね。

では更新情報を取得して自動記録がいいだろうということで、前段としてATOMの構造取得をPowershellで確認してみます。

$URL = "https://ntnl-it-wiz.blogspot.com/feeds/posts/default"
$res = Invoke-WebRequest -Uri $URL
[xml]$xml = $res.Content
$xml.feed.entry[0].title

無事取得することができました。

2018年10月15日

小ネタ:ダイソーのUSB TypeCアダプターを購入した

ネタ元は以下の記事。
基本的にまんまなのですが、ダイソーでType-Cアダプターを購入してきました。
https://japanese.engadget.com/2018/10/13/100-usb-type-c/

そして、記事にUSB OTG対応とあったので、実際にマウスが動作するか試してみたところ無事に動いたという人柱的な記事になります。

ただし、指す向きは制限があり、片側しか認識しませんでした。
Type-Cなので両面刺さるのですが、認識は片側のみという点が留意事項となります。

元記事にその記載がなかったのでその補足がメインです。実は個体不良というオチもあるやも知れませんが、参考になれば幸いです。





2018年9月17日

小ネタ:PowershellでInvoke-RestMethodが文字化けする際の対処法

前置き


PowershellのInvoke-RestMethodコマンドレットでは、たまに日本語の文字化けが発生します。これはInboke-Restmethodが文字コードを誤って認識するために発生します。
戻り値の文字コードを指定する方法がないため、追加の対策にて文字コードを強制しましょう。
元ネタは以下です。
http://pierre3.hatenablog.com/entry/2014/12/13/001743

変換方法


まずはさらっとコードのご紹介です。

# NDLサーチからドラえもんの情報を拾う
$SearchData = 'title="ドラえもん" AND from="2017"'

$BaseUri = "http://iss.ndl.go.jp/api/sru?maximumRecords=10&operation=searchRetrieve&query="
Add-Type -AssemblyName System.Web
$OptionUri = [System.Web.HttpUtility]::UrlEncode($SearchData)

$uri = $BaseUri+ $OptionUri
# $xml = Invoke-RestMethod $uri
$res = Invoke-WebRequest $uri

$con =[System.Text.Encoding]::Utf8.GetString($res.RawContentStream.GetBuffer())
[xml]$xml = $con -replace "\u0000",""
$xml.searchRetrieveResponse.records.record[0].recordData
# REST APIの返却がJson形式の場合には以下を使用する。
$json = ConvertFrom-Json $con -replace "\u0000",""

まず、$resより前はリクエストのための情報なので、タイトルとは余り関係のない話ですが一応ご紹介です。
国会図書館サーチというサービスのSRUという検索APIを使用してタイトルに「ドラえもん」が含まれる蔵書(音楽CDなども含む)を検索し、結果をXMLで返しています。

このクエリー自体は実は化けないんですが、蔵書に海外の図書とかが含まれているからでしょうか、検索結果が化けたり化けなかったりするんです。
全部化ける前提のコードにすると化けなかった時に誤ったエンコード指定となってしまいます。

そこで、コマンドレットの自動変換に頼らず、一律でrawcontentからUTF8で読み込んでしまおうという訳です。
ただ、そうすると、パケットの詰め物なのかNUL(文字コード0000)が後方に埋まっているので文字列置換で取り除いてからXMLに変換しております。

補足


先日のElasticsearchの場合はElasticsearch側が仕様に反してましたが、今回はMS側が仕様に反している(一応)ので、そのうちこの回避策を使わなくてもPowershell側で対応してくれる可能性があります。

具体的には、JSONはRFC 8259にてエンコーディングにUTF-8を使用することを要件とされているため、Content-Typeの判定がJSONだった時点で、エンコーディングにISO-8859-1を利用してしまうのは仕様に反しております。
上の例はXMLなのでその制限はありません。ただ、これもコンテンツにencoding指定があるのでそれを元にUTF8で変換されるべきでしょうね。

2018年9月8日

小ネタ:Powershellで半角カタカナを全角カタカナに変換

前置き


データクレンジングとして、英数字は全角を半角にしつつ、半角カタカナは全角カタカナにしたいということは往々にしてあるかと思います。
それをPowershellで実施する手順のご紹介です。ネタ元は、以下のページ2つの合わせ技です。
半角→全角、全角→半角の数値変換スクリプト
半角カナを全角カナに変換する

変換コード


# 初期化
[reflection.assembly]::LoadWithPartialName("Microsoft.VisualBasic") > $Null
$Narrow = [Microsoft.VisualBasic.VbStrConv]::Narrow
$Wide = [Microsoft.VisualBasic.VbStrConv]::Wide

function Convert-Wide2Narrow() { # Start Convert-Wide2Narrow()
    Param([string]$Text) # 変換元テキストを引数に指定

    $CT = "" ; $Kana = "" # 変換後テキスト、一時キャッシュ変数初期化
    $NT = [Microsoft.VisualBasic.Strings]::StrConv($Text, $Narrow) # 全角を一括で半角に
    # 半角カタカナだけ全角へ変換
    ForEach ( $i in @(0..$NT.length) ) { # string[]からcharを取り出す
        if ($NT[$i] -match "[\uFF61-\uFF9F+]") { # 半角カタカナ処理
            $Kana = $Kana + $NT[$i] # キャッシュ格納(濁点、半濁点対応)
        } else {
            $Kana = [Microsoft.VisualBasic.Strings]::StrConv($Kana, $Wide) # 半角カタカナを全角に
            $CT = $CT + $Kana + $NT[$i]
            $Kana = ""
        }
    }
    return $CT

} # End Convert-Wide2Narrow()

# ここから変換確認。

$OT = @"
元のテキストに以下の内容が含まれている。
全角英数字:ABCabc123$‘@
半角英数字:ABCabc123$'@
全角カタカナ:アイウエオ、ガギグゲゴ、パピプペポ
半角カタカナ:アイウエオ、ガギグゲゴ、パピプペポ
"@

$OT
[Microsoft.VisualBasic.Strings]::StrConv($OT, $Wide) # すべて全角に変換
[Microsoft.VisualBasic.Strings]::StrConv($OT, $Narrow) # すべて半角に変換
Convert-Wide2Narrow -Text $OT # 英数字を全角から半角にし、半角カタカナは全角に変換
「半角カタカナだけ全角へ変換」のForEach処理だけ切り出せば半角カタカナから全角カタカナへの変換が実現します。
Function全体では英数字の半角化も行っておりデータクレンジングでよくある手法になっています。

なお、ForEachで添字の$iを$NT.lengthにしている($NT.length -1 ではない)ので、最終文字が半角カタカナだった場合でも$CTの結合処理に行くことを実行確認してます。
ですが、バージョンによって挙動が変わる可能性もありますので、最終文字だけ変換されない場合にはReturnする前にElse内の全角化+文字列結合処理を付け加えてください。

また、初期化処理をFunction内に組み込みたい人のために[reflection.assembly]の処理を>$Nullしてます。(出力があるので、Returnに混入する)
大量のテキストを変換させるなら素直にGlobalしたほうが処理は早いと思ったので上記では外出ししてます。

あと、試験結果でわかるように、全角から半角に変換する際にシングルクォートの開始記号がただのシングルクォートになります。そりゃそうですね。
ただ、全角にした際に文字コードFA56のシングルクォートになります。期待値は8166じゃないですかね。この辺はMicrosoftのセンスを疑わざるを得ません。
そんな訳で、過信は禁物です。

2018年9月2日

ElasticsearchでWikipediaの全文検索にチャレンジ (4)

前置き


前回の記事の続きです。
間に記事を挟んだことから類推可能な通り、今回はPythonを採用して検索しております。
動作させるための利用環境は該当記事を参考にしてください。
なお、Elasticsearchについては、これまでの記事(123)の手順に沿ってWikipediaのデータを投入した環境という前提となります。

取得コード


今回は環境構築の記事だけでお腹いっぱいになったので、内容は余り触れずコードと結果をみてみたいと思います。余り大したことしてないですし。

# ライブラリ登録
import MeCab, json, codecs, re, inspect
import pandas as pd
from datetime import datetime
from elasticsearch import Elasticsearch

# 初期設定
ESServer = ["http://localhost:9200"]
es = Elasticsearch(ESServer)
mt = MeCab.Tagger("-Ochasen")
mt.parse('') # MeCab初期化 (不具合回避)

# 関数定義

# 形態素解析し頻度表を作成
# 解析対象文章をリストで渡す。
def get_freq(doclist) : 
    tl = [] # 辞書初期化
    for doc in doclist : # 文書リストを順次処理
        node = mt.parseToNode( doc["text"] )
        while node : # 解析したTermごとに順次処理
            # print(node.surface + " => " + node.feature)
            arr = node.feature.split(",") # Mecabフォーマットを配列(リスト)化
            if arr[0] != "BOS/EOS" : # 開始、終了識別は除外
                if len(arr) > 7 : # 振り仮名が出力されない単語もある
                    Phonetic = arr[7]
                else :
                    Phonetic = "*"
                record = {"Term": node.surface, "Info1": arr[0], 
                    "Info2": arr[1], "Phonetic": Phonetic, "Freq": 1 }
                tl.append(record) # 重複ありで辞書へ投入

            node = node.next

    # 集計処理用に一旦DataFrameに変換
    df = pd.io.json.json_normalize(tl) # 辞書をDataFrameへ変換

    # 以下のパターンはどちらがよいか判断しかねたので両方作成。関数に引数指定させてもいいが、シンプルに一方だけにしたい。
    Morptype = "typeB" # 集計方法のパターン指定
    if Morptype == "typeA" : # 検出時の品詞、かなをそのまま利用
        # Groupby で頻度集計
        grp = df.groupby(['Term','Info1','Info2','Phonetic'], as_index=False) # 列はindex化させない
        res = grp.count()
        # print(res) # for Debug

        # 辞書型に再変換
        tl = res.to_dict(orient='records')
    elif Morptype == "typeB" : # 検出時のsurfaceを再問合わせして品詞を設定
        # Groupby で頻度集計
        grp = df[["Term", "Freq"]].groupby(['Term'], as_index=False) # 列はindex化させない
        res = grp.count()
        # print(res) # for Debug

        # 辞書型に再変換
        tl = res.to_dict(orient='records')
        # MeCabに品詞を再問合わせ
        for row in tl :
            node = mt.parseToNode( row["Term"] )
            while node : # 解析したTermごとに順次処理
                # print(node.surface + " => " + node.feature)
                arr = node.feature.split(",") # Mecabフォーマットを配列(リスト)化
                if arr[0] != "BOS/EOS" : # 開始、終了識別は除外
                    if len(arr) > 7 : # 振り仮名が出力されない単語もある
                        Phonetic = arr[7]
                    else :
                        Phonetic = "*"
                    row["Info1"] = arr[0]
                    row["Info2"] = arr[1]
                    row["Phonetic"] = Phonetic

                node = node.next

    return tl
    # end get-freq()

# メイン処理開始 #################################
key = "ブルータス"
inx = "wikipedia"
body = {"query": {"match": {"text": key}}, "size": 200}
doclist = [] # 辞書初期化(ドキュメントのテーブルとして利用)
res = es.search(index=inx, body=body) # Elasticsearchへクエリーを投入

for doc in res['hits']['hits']:
    row = { "id":doc['_id'] ,"title" :doc["_source"]["title"], 
        "text" :doc["_source"]["text"], "timestamp":doc["_source"]["timestamp"] } 
    doclist.append( row ) # id, title, text, timestamp を1行としてリストに格納

# ElasticsearchにWiki記法ごと入れているため暫定的に処理している。本来は投入前に整形すべき。
for doc in doclist : # 文書リストを順次処理
    doc["text"] = re.sub(r"[\[\]\{\}\*\"\'\|]+", "", doc["text"]) # 本文からWiki記法削除処理

# 解析対象データをファイルに保存
with codecs.open("doclist.json", "w", "utf-8") as f:
    json.dump(doclist, f, indent=4, ensure_ascii=False) 

tl = get_freq(doclist) # 形態素解析を行い頻度表を作成

# 解析後のデータをファイルに保存
with codecs.open("termlist.json", "w", "utf-8") as f:
    json.dump(tl, f, indent=4, ensure_ascii=False)

df = pd.io.json.json_normalize(tl) # 辞書をDataFrameへ変換

# 品詞を名詞に絞り、集計順に並べる
# ※キーワード以外でキーワードよりも極端に多い単語は処理誤り等の可能性が高い
#  係数を3倍に設定し、除外するものとする
#  ただし、固有名詞と判定されたものは取り除かない。
coef = 3
df = df[df['Info1'] == '名詞'] # 名詞だけに絞り込む
KeyFreq = df[df["Term"]==key]['Freq'].iat[0] # 検索キーワードの件数を取得
# print(KeyFreq) # for Debug
# 大量キーワードの除外処理
df = df[ ~( (df['Freq'] > (KeyFreq * coef)) & ~(df['Info2'] == '固有名詞') ) ] 

df = df.sort_values(by='Freq', ascending=False) # 降順ソート

tl = df.to_dict(orient='records') # 辞書型に再変換

with codecs.open("filtertermlist.json", "w", "utf-8") as f:
    json.dump(tl, f, indent=4, ensure_ascii=False)

print( "end : " + datetime.now().strftime("%Y/%m/%d %H:%M:%S") )

120行ちょっとのコードなので適当にデバッグコードを挟みながら動作を確認いただけるといいかなと思います。
頻度表の作成についてはまだ方式を決めかねたため、2パターンのコードが併存しているいけてないコードです。
※公式によるとMeCabは未知の品詞を自動判定させるようなので。

最終的にはきれいにしたものを正式公開したいですね。
その際、ライセンスについてはこのブログについてに書いてある通りですが、Apache2.0でgithub公開にしてもいいかもですね。夢が広がります。

結果


先のコードは指定したキーワード「ブルータス」で検索を掛けて、形態素解析にて単語に分解し、Mecabで品詞を特定した上で、ブルータスに関連しそうな名詞を拾い上げるという処理です。
保存されるファイルの内容は以下の通りです。

  • doclist.json:Elasticsearchから取得した200件の記事内容
  • termlist.json:MeCabで形態素解析を行った結果(頻度表)
  • filtertermlist.json:頻度表から不要と思われる情報を除外した結果

それぞれの情報は各々で実行してご確認ください。以下は結果の一部抜粋です。

    {
        "Freq": 267,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "ローマ",
        "Term": "ローマ"
    },
    {
        "Freq": 248,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ブルータス"
    },
    {
        "Freq": 72,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ブルトゥス"
    },
    {
        "Freq": 196,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "シェイクスピア",
        "Term": "シェイクスピア"
    },
    {
        "Freq": 158,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ジュリアス・シーザー"
    },
    {
        "Freq": 84,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "カエサル"
    },
    {
        "Freq": 25,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ガイウス・ユリウス・カエサル"
    },

キーワード「ブルータス」を基準に確認をしていきましょう。

ローマの人なのでローマのカウント率が高いですね。
英語読みのブルータスから、ラテン語のブルトゥスを拾うことができています。
シェイクスピアの戯曲で有名なので、シェイクスピアが上位に来ていますね。
戯曲名はジュリアス・シーザーでこれも上位に来ています。
というか、カエサルの英語読みなので、上位に来て当然ですが。
ブルータスはカエサルの暗殺に関わり、カエサルから「ブルータスよ、お前もか」と言われた人物として有名なので、カエサルは上位にランクインします。

実際にWikipediaの本人のページを検索するとカエサルが46件ヒットします。

どうやら、それなりに関連性の高い単語を拾えていそうです。

なお、77行目で指定しているキーワードを「カエサル」に変えて同じことをしてみましたが、ブルータス、ブルトゥスともに比率は低めでした。クレオパトラのほうがまだ高かったですね。
あとは戦争、戦記、内戦など、戦いに明け暮れた人なのかと思わせるキーワードが上位でした。
エンジニア的にはシーザー暗号から「暗号」というキーワードに期待したのですが、2件でした。先頭から200件の記事しか参照していないので仕方がないかも知れませんが。

この関わりの深い単語を拾う行為を共起というそうです。せっかくなので次回はこの記事にある他の方式を試したい。 https://qiita.com/nishina555/items/c650dd06b283996210ac

VS Code デバッグ環境を整備してPythonからElasticsearchへ接続する

前置き


先日の記事で紹介した通り、PowershellからElasticsearchを触るのは、Elasticsearch側に問題があるため不向きである。
そこで開き直って言語を変えることにした。最近流行りのPythonを試してみることにした。
そんな訳で、Windows環境で、PythonからElasticsearchへ接続するための環境をどのように用意したのかを紹介いたします。
お品書き
  • MeCab
  • Anaconda
    • elasticsearch
    • flask-jsonpify
    • mecab-python-windows
  • git for Windows
  • Visual Studio Code
    • vscode-icons
    • gitLens
    • Python
  • Build Tools for Visual Studio 2017
  • Visual C++ 2015 ランタイム

導入

MeCab
最初にMeCabです。これはElasticsearchから取得したデータを形態素解析するために入れました。厳密にはElasticsearchに接続するために必要なものではありません。
先日の記事にてWikipediaのデータをElasticsearchに投入したので、それを取得したのちに解析するために入れるという流れです。

以下のURLからWindows向けのバイナリをダウンロードしてセットアップします。
http://taku910.github.io/mecab/#download

注意事項としてはインストール時に「UTF-8」を選択してください。デフォルトのShift-JISだとPythonからでは利用できませんでした。理由は調べてません。

Anaconda
Pythonのディストリビューションで、WindowsかつPython初心者に最適なパッケージです。NumPy、Pandas等のライブラリーがセットで入っている全部入りパッケージです。
歴戦のPython使いなら不要でしょうけれども、歴戦のPython使いはこの記事に参考となる情報は一つもないでしょうから端から対象外です。

Anaconda 公式はAnaconda Cloudになっているようですが、最終的には以下のダウンロードリンクへ誘導されるはずなので、Python 3.6(またはそれ以降)をダウンロードしてください。
https://www.anaconda.com/download/

注意事項としては、Pythonで複数環境を作成する前提のためか、既定の設定では環境変数を通してくれません。初心者であるならば、インストール時はPATHを通すことを推奨します。慣れたら消せばいいし。
PATHを通すとかなんじゃらほいという方は以下を参照してください。なお、上に挙げたvscode-iconsは以下のページで知って早速導入した次第です。
https://beachside.hatenablog.com/entry/2017/12/25/000000
Anaconda:conda/pip
次にPythonで使用するパッケージを追加インストールします。

通常、Pythonはpipというパッケージ管理を利用します。Anacondaの場合はcondaというパッケージ管理が付随しており、こちらを使うように言われます。
両者を相互利用するのは問題があると言われておりますので、私は開き直ってpipで統一することにしました。
http://onoz000.hatenablog.com/entry/2018/02/11/142347
なんかあったら直せばいいし、その調査も経験値になるしなぁという目論見です。

前述のAnaconda導入時にPATHを通していることが前提になります。
Powershellコンソールを開き、-Vオプションで確認します。

python -V
pip -V

両方のバージョンが表示されたらOKです。
pipはバージョンが古かったので更新しました。

pip list
python.exe -m pip install --upgrade pip
pip -V
では入れていきましょう。
pip install elasticsearch
pip install flask-jsonpify
pip install mecab-python-windows
pip list | Select-string "elasticsearch"
pip list | Select-string "flask"
pip list | Select-string "mecab"

Proxy環境下の方は環境変数がないとエラーが出ると思いますので、その場合には環境変数を設定してください。
$ProxyServer = "http://proxyserver:8080" # 自身のProxyサーバー名を設定
$env:http_proxy = $ProxyServer
$env:https_proxy = $ProxyServer

うまく入らなかったら適当にググって対処してください。オンラインであること以外に要件はありません。

git for Windows
gitが入っていないとVS Codeで警告が出るので入れましょう。
https://gitforwindows.org/

入れ方は以下が参考になるとは思います。なお、私は改行コードは「Checkout as-is, commit as-is」にしてます。マルチプラットフォームのコードより環境依存のコードを書くことが多いので、環境違いで改行コードを変えられても困るほうが多いため。
https://qiita.com/toshi-click/items/dcf3dd48fdc74c91b409

gitはとても素晴らしいものですが、それを語るには紙幅が足りないので別の機会に委ねます。
別に紙じゃないしフェルマーの最終定理でもないですがぶん投げておきましょう。
といいつつも、その紙幅を至福に変えるために一応おすすめ記事を挙げておきますね。
https://backlog.com/ja/git-tutorial/

Visual Studio Code

以下からダウンロードしてインストールしてください。.Net バージョンを追加せよ等を言われたら、指示に沿って入れてください。
https://code.visualstudio.com/download

Visual Studio Code:拡張機能/ユーザー設定
以下の画面の赤枠をクリックすると、拡張機能をインストール/管理する画面になります。
また、表示画面自体はユーザー設定画面で黄枠クリックから「設定」を押すと呼び出せます。そして青枠を編集します。












では以下の拡張機能を導入してください。導入手順で不明な点があれば先ほど紹介した記事を見るといいと思います。
  • vscode-icons
  • gitLens
  • Python

ユーザー設定はしなくても構わないですが、Windows環境だとShift_JISとUTF-8の文字コードが混在していることが多いと思われますので、自動認識だけ入れておくといいでしょう。
正答率はそこそこですが、ないよりは全然ましです。
    "files.autoGuessEncoding": true,
Build Tools for Visual Studio 2017
MeCabの時にあれこれ調べたので、転ばぬ先の杖としてVCコンパイラーを入れます。
現状、なくても動作するので、面倒な人はこの作業はスキップして構いません。

Windows版Python3.6はVisual C++ 2015でコンパイルされており、同バージョンのコンパイラーを環境に入れておくというところです。Visual C++ 2015のコンパイラーがBuild Tools for Visual Studio 2017に入っているというのは中々ややこしい話ですが、公式ブログに書いてあったので受け入れざるをありません。
なお、Python公式でも、これとかこれに書いてあるので、間違いではありません。
今後新しいバージョンがリリースされても、公式情報をあたれば間違いないでしょう。
過去バージョンのブログを漁ると、時代の変化に追いついていないのでえらい目にあいます。(3年後くらいにはこの記事もそうなっているだろうからという予防線です)

現時点でのダウンロードリンクは以下になります。
https://visualstudio.microsoft.com/ja/downloads/?q=Build+Tools+for+Visual+Studio+2017

ただ、多分MSのことなので、ダウンロードへのたどり方は頻繁に変わると思われ、余り参考にならないと思います。なので、ググってもダウンロードリンクには余りHITしません。
リンクが切れていたら、ページのどこかにダウンロード検索窓がいるはずなので、「Build Tools for Visual Studio 2017」というキーワードを入力して目当てのものにたどり着いてください。
導入手順は公式を参考にしてください。
https://www.python.jp/install/windows/install_vstools2017.html

Visual C++ 2015 ランタイム
普通は何かしらで既に導入済みと思われますが、入っていなかったらいれてください。
https://www.microsoft.com/ja-jp/download/details.aspx?id=53587

補足


実際のElasticsearchへの検索は以下の記事を参照ください。
ElasticsearchでWikipediaの全文検索にチャレンジ (4)

最近はMeCabは更新が止まっているようなので、JUMAN++への移行も試したいところですが、今のところ時間がないので未着手です。
http://www.yujakudo.com/blogs/develop/ai/jumanpp-on-windows/

追記


Jupyter Notebookの拡張が使えることが判明。VS Code + Pythonが更に便利になるのでマジおススメ。
http://www.atmarkit.co.jp/ait/articles/1806/12/news041.html

Jupyter Notebookのように対話型に近い形でコーディングして、ある程度形が出来てきたら通しで動作を確認し、そのままUnitTestという流れが完璧すぎる。
Cellの区切りもただのコメントでしかないからデバッグ用にそのまま残しておいても大丈夫だし。

追記の追記


以下の記事にある通り、Python拡張の標準機能でJupyter 拡張と同等の機能が実装されたため、この拡張機能は入れなくても問題なくなりました。使い勝手は上述の通りですので、ご安心ください。
https://ntnl-it-wiz.blogspot.com/2018/11/jupyterpythoninteractive.html

2018年8月28日

ElasticsearchでWikipediaの全文検索にチャレンジ (3)

前置き


前回の記事の続きです。推定所要時間はデータ投入と確認で4時間です。
なんか前置きってテンプレートの様に最初に必ず書くのがお約束になってきている。

本番データ投入


では、ハードディスクの残容量を確認して、本番データの投入を行ってください。
ちなみに、手元の環境では14GB程消費しました。

$WorkDir = "C:\Wikipedia"
$OutputDir = Join-Path $WorkDir "output"

# ■Wikipedia XMLデータ
$Columns = "id", "timestamp", "title", "text"

$HeaderFile = Join-Path $WorkDir "xmlHeader.txt"
$FooterFile = Join-Path $WorkDir "xmlFooter.txt"

$HeaderText = Get-Content $HeaderFile -encoding UTF8
$FooterText = Get-Content $FooterFile -encoding UTF8

# ■Elasticsearch 登録先情報
$Hostname = "localhost"
$PortNumber = 9200
$IndexName = "wikipedia"
$TypeName = "page"
$BaseURL = "http://${Hostname}:${PortNumber}"

# ■投入前確認(何も入っていないこと)
$APIURL = "/${IndexName}/${TypeName}/_search"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $Result -Depth 10

# その他確認
Get-Date
Get-PSDrive

# ■Bulk投入定義
$APIURL = "/${IndexName}/${TypeName}/_bulk"
$RequestURL = $BaseURL + $APIURL
$ReqestType = "POST"
$BulkCommand = @{ "index" = @{ "_index" = $IndexName ; "_type" = $TypeName } } | ConvertTo-Json -Compress

# ■投入対象一覧生成 ※ファイル名の都合でソートは無意味である。
$FileList = Get-ChildItem $OutputDir | Sort-Object -Property Name

ForEach ($File in $FileList) {

 # XML生成
 $PageText = Get-Content $File.Fullname
 [xml]$XMLData = $HeaderText + $PageText + $FooterText

 # ■Bulk実行用のJsonデータ生成
 $BulkData = New-Object System.Collections.ArrayList # 新規配列
 ForEach ($page in $XMLData.mediawiki.page) {
  $Row = New-Object PSObject | Select-Object $Columns
  $Row.id = $page.id
  $Row.timestamp = $page.revision.timestamp
  $Row.title = $page.title
  $Row.text = $page.revision.text."#text"
  $JsonData = $Row | ConvertTo-Json -Compress

  $BulkData.Add($BulkCommand) > $Null
  $BulkData.Add($JsonData) > $Null
 }

 # ■ Bulk実行
 $JsonData = $( $BulkData -join "`n" ) + "`n"
 $PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
 $Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
 
 Write-Output $( $File.name + " : " + $(Get-Date) )

}

# ■投入結果確認
$APIURL = "/${IndexName}/${TypeName}/_search"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $Result -Depth 10

# その他確認
Get-Date
Get-PSDrive

1ファイル投入毎に完了時刻を報告するようにしているので、2300ファイル分が大体どの程度で終わりそうかの目安にしてください。
1ファイル5秒なら3時間強くらいですよね、的な。

どうでもいい補足


ソートに関しては、1番目の記事で連番を桁数合わせてないのでうまくいきません。順番通りに投入する必要性は皆無なので問題ありませんが。
行き当たりばったり故の不具合である。
言い訳をすると、最初の時点で全件がどの程度あるのか想像がつかなかったので、何桁おけばいいのか判断できなかったというのがあります。
実際には2300ファイルだったので、安全マージンで1桁増やして変更すればよさそうです。
最初の記事にはコメントアウトで追記しておりますので、変更したい人はどうぞ。

[string]$( $PageCounter / $Cycle )
 ↓
$( $PageCounter / $Cycle ).ToString("00000")

ついでに細かい補足をしておくと、ファイルのエンコード指定は、初回はBOMなしUTF8なので必須ですが、一旦Powershellから出力したUTF8はBOMつきUTF8で出力されているので、それ以降は自動判定可能であるため指定不要です。
あと出力時の改行コードは本文はLFで最後だけCRLFと不一致になるけれども、Get-Contentに-rawオプションをつけていないので、改行を問わず1行1配列のArrayオブジェクトとして生成され、かつXMLオブジェクトに変換されてしまうので、改行コードの不一致について気にする必要はないでしょう。
多分Sakura Editorで開くと保存するときに直すかいと聞かれるはずなので補足しました。

全文検索


では、検索をしてみましょう。先日の記事で示したように、Elasticsearchが標準外の処理をしているのでここでPowershellはお役御免です。
Kibinaを起動しましょう。そしてDev toolsにて以下のクエリーを実行します。これは、「全文検索」というキーワードに対するスコアを取得します。
GET /wikipedia/page/_search?pretty
{
  "query":{
    "match":{
      "text":"全文検索"
    }
  },
  "_source": ["id", "title"],
  "size": 100
}
なお、量が量なので、kibana.ymlにてelasticsearch.requestTimeoutの値を増やさないと変わったクエリーを投げるとタイムアウトしまくる可能性がありますので注意してください。
掲示した検索クエリーの書き方も先ほどのサイトのページがヒットしたので、こちらを参考にいたしました。
http://pppurple.hatenablog.com/entry/2017/01/29/144132

そして、Powershellの活躍がないままなのは悔しいので、生成された結果のJsonをテキストファイルに納めて、強引に活躍の場を与えましょう。
$JsonText = Get-Content C:\Wikipedia\resultjson.txt
$JsonData = $JsonText | ConvertFrom-Json
$JsonData.hits.hits | Format-Table | Out-File C:\Wikipedia\resulttext.txt -encoding UTF8

テーブルフォーマットに直したことで、スコア順に1行ずつ並んでいるので、順位がわかりやすくなりました。 Apache Solrが16番目にランクインしたにも関わらず、Elasticsearchは92番手でした。もう少し頑張りましょう。

なお、続けて「桃太郎」というキーワードで検索を掛けたところ、上位に女性の名前が出てきたので、榊原郁恵がピーターパンやってたような感じかと思って本文を開いてみたら実はセクシー女優さんで、桃太郎映像出版というところが映像作品をリリースしてたからヒットしてしまったというオチでした。
子供向けを狙って検索してみるとオトナ向けの結果が出てしまう可能性に注意いただきたいと思った次第です。また無駄知識が増えてしまった。
次回の記事はPowershellの代わりにPythonを利用することにしました。Pythonに興味があったのでこれを機に覚えてみようということで。
開発環境がOS標準ではなくなるので、間にPythonの環境構築の記事を挟みます。

ElasticsearchでWikipediaの全文検索にチャレンジ (2)

前置き


前回の記事からの続きです。
Wikipediaのファイルをスクリプトから処理可能なサイズに分割し、Elasticsearchのセットアップ作業を終えている前提で話を進めます。

ファイル整形

前回は、230万件のWikipediaの記事を1000件単位に分割してファイルに納めるということをしました。 まず、先頭ファイルと最終ファイルにそれぞれXMLの開始/終了がくっついているので、これを切り出します。
$WorkDir = "C:\Wikipedia"
$OutputDir = Join-Path $WorkDir "output"

# 書き込み順でソート。
$FileList = Get-ChildItem $OutputDir | Sort-Object -Property "LastWriteTime"
$FirstFile = $FileList | Select-Object -First 1
$LastFile = $FileList | Select-Object -Last 1

# 編集対象のファイルがこちらです
$FirstFile.FullName
$LastFile.FullName

# 失敗した時のためにWorkDirへバックアップ
Copy-Item $FirstFile.FullName $WorkDir
Copy-Item $LastFile.FullName $WorkDir

# ■ヘッダー編集
$LineCounter = 0
$Keyword = '<page>' # 記事の開始タグ
$Header = New-Object System.Collections.ArrayList
[System.Collections.ArrayList]$FirstFileData = Get-Content $FirstFile.FullName

# ヘッダ開始位置の確認
ForEach ($Line in $FirstFileData) {
 if ( $Line.Contains($Keyword) ) {break}
 $Header.Add($Line) >$Null
 $LineCounter = $LineCounter +1
}
$LineCounter
$Header

# ヘッダーを別ファイルへ保存
$HeaderFile = Join-Path $WorkDir "xmlHeader.txt"
$Header  -join "`n" | Out-File -FilePath $HeaderFile -Encoding utf8

# Header分離
$FirstFileData[0] #分離前確認
$FirstFileData[$LineCounter] #分離前確認

ForEach ($i in @(1..$LineCounter) ) {
 $FirstFileData.RemoveAt(0)
}

$FirstFileData[0] #分離後確認

# 上書き保存
$FirstFileData -join "`n" | Out-File -FilePath $FirstFile.FullName -Encoding utf8

# ■フッター編集
# 配列ArrayListとしてGet-Contentを実行
[System.Collections.ArrayList]$LastFileData = Get-Content $LastFile.FullName
$LastLine = @($LastFileData).length -1
$LastFileData[$LastLine] # これが最終タグ

# フッターを別ファイルへ保存
$FooterFile = Join-Path $WorkDir "xmlFooter.txt"
$LastFileData[$LastLine] | Out-File $FooterFile -Encoding UTF8

# 最終行を削除
$LastFileData.RemoveAt($LastLine)
@($LastFileData).length

# 上書き保存
$LastFileData -join "`n" | Out-File -FilePath $LastFile.FullName -Encoding utf8

これくらい開発するよりエディタでやったほうが絶対に早いんだけれども、乗り掛かった舟なのでコードにしました。

XMLオブジェクト生成


では、生成したファイルからXMLファイルを合成し、XMLオブジェクトへ変換しましょう。
$WorkDir = "C:\Wikipedia"
$OutputDir = Join-Path $WorkDir "output"
$HeaderFile = Join-Path $WorkDir "xmlHeader.txt"
$FooterFile = Join-Path $WorkDir "xmlFooter.txt"

$HeaderText = Get-Content $HeaderFile -encoding UTF8
$FooterText = Get-Content $FooterFile -encoding UTF8
$PageText = Get-Content $(Join-Path $OutputDir File-0.txt)

# XML生成
[xml]$XMLData = $HeaderText + $PageText + $FooterText

# 生成結果確認
$XMLData
$XMLData.mediawiki
$XMLData.mediawiki.page[0]
$XMLData.mediawiki.page[0].revision
$XMLData.mediawiki.page[0].revision.text

最終的にこれを0~2300のファイルに実行するイメージです。Pageは[0]~[999]です。
※未検証ですが、オフラインだとスキーマにアクセスできないのでXMLの生成に失敗すると思われます。
 その場合にはスキーマをローカルにダウンロードしてヘッダをローカル参照に書き換えてください。

投入データ選定


まず、Elasticsearchへの取り込み対象を絞りましょう。
  • page/id
  • page/revision/timestamp
  • page/title
  • page/revision/text."#text"
ぱっと見では、これだけ取っておけば全文検索には役に立ちそうな気がします。
では試してみましょう。

$Columns = "id", "timestamp", "title", "text"
$Table = New-Object System.Collections.ArrayList # 新規配列

ForEach ($page in $XMLData.mediawiki.page) {
 $Row = New-Object PSObject | Select-Object $Columns
 $Row.id = $page.id
 $Row.timestamp = $page.revision.timestamp
 $Row.title = $page.title
 $Row.text = $page.revision.text."#text"
 $Table.Add($Row) > $Null
}

$Table

それっぽい出力ができました。これを取り込めばよさそうです。
Wiki記法が気になりますが、恐らくSTOPWORDというElasticsearchの機能でなんとかなるんじゃないかと楽観視しているので、一旦編集処理はしません。
もし必要なら$Row.textを文字列置換とかでいい感じに編集するようにしてみてください。

設定ファイル生成


先日の記事でもやりましたが、まずはアップロードするファイルをElasticsearchに納めるうえで、Mappingの定義ファイルを作成します。
まず、先ほど作成した変数$Tableから5件Elasticsearchに登録します。

# ■Elasticsearch 登録先情報
$Hostname = "localhost"
$PortNumber = 9200
$IndexName = "wikipedia"
$TypeName = "page"
$BaseURL = "http://${Hostname}:${PortNumber}"
$APIURL = "/${IndexName}/${TypeName}/_bulk"
$RequestURL = $BaseURL + $APIURL
$ReqestType = "POST"
$BulkCommand = @{ "index" = @{ "_index" = $IndexName ; "_type" = $TypeName } } | ConvertTo-Json -Compress

# ■Bulk実行用のJsonデータ生成
$BulkData = New-Object System.Collections.ArrayList # 新規配列
ForEach ($Counter in @(0..4) ) {
 $JsonData = $Table[$Counter] | ConvertTo-Json -Compress
    $BulkData.Add($BulkCommand) > $Null
    $BulkData.Add($JsonData) > $Null
}

Write-Output $BulkData

# ■ Bulk実行
$JsonData = $( $BulkData -join "`n" ) + "`n"
$PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
$Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
$Result

# ■投入結果確認
$APIURL = "/${IndexName}/${TypeName}/_search"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $Result -Depth 10

# ■現在のMAPを取得
$MapFile = Join-Path $WorkDir "map.json"
$APIURL = "/${IndexName}/${TypeName}/_mapping"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL

ConvertTo-JSON $Result."${IndexName}" -Depth 10 | Out-File -Encoding Default -FilePath $MapFile

では、このMAPファイルを編集していきましょう。データ型は以下のページを参考にします。
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
更にKuromojiを利用するための設定として以下のページの内容も組み込みます。 https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji-tokenizer.html

{
    "mappings": {
        "page": {
            "properties": {
                "id": {
                    "type": "integer"
                },
                "text": {
                    "type": "text",
                    "fielddata": true,
                    "store": true
                },
                "timestamp": {
                    "type": "date"
                },
                "title": {
                    "type": "keyword"
                }
            }
        }
    },
    "settings": {
        "index": {
            "analysis": {
                "tokenizer": {
                    "kuromoji_user_dict": {
                        "type": "kuromoji_tokenizer",
                        "mode": "search"
                    }
                },
                "analyzer": {
                    "wikipedia_analyzer": {
                        "type": "custom",
                        "tokenizer": "kuromoji_user_dict"
                    }
                }
            }
        }
    }
}

textフィールドの謎指定「fielddata」「store」は何かうまく動かなかったので以下を参考に指定してみた的なおまじないです。時間がある時に何者でどんな効果が期待できるのか調べたいです。よくわかってないので一旦ぶん投げておきます。
https://qiita.com/tsgkdt/items/9ac7c8ffd74e2baee79d

では既存Indexを削除してこの定義を取り込んでみましょう。

# ■定義更新
# ○既存Indexの削除
Function Delete-Index () {
    $APIURL = "/${IndexName}"
    $RequestURL = $BaseURL + $APIURL
    $ReqestType = "DELETE"
    Invoke-RestMethod -Uri $RequestURL -Method $ReqestType
    Write-Output $("`t Delete-Index() : " + $(Get-Date) )
}

Delete-Index

# ○MAP定義登録
Function Create-TypeItem ( [string]$JsonFilePath ) {
    $APIURL = "/${IndexName}"
    $TargetURL = $BaseURL + $APIURL
    $ReqestType = "PUT"
    $JsonData = Get-Content $JsonFilePath
    # 文字化けするのでUTF8を強制
    $PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData)
    Invoke-RestMethod -Uri $TargetURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
    Write-Output $("`t Create-TypeItem( ${JsonFilePath} ) : " + $(Get-Date) )
}

$JsonFilePath = Join-Path $WorkDir "map.json"
Create-TypeItem -JsonFilePath $JsonFilePath

取り込みエラーが出なかったら即座に確認です。
$JsonData = '{"analyzer": "wikipedia_analyzer", "text": "吾輩は猫である。"}'
$ReqestType = "POST"
$APIURL = "/${IndexName}/_analyze"
$RequestURL = $BaseURL + $APIURL
$PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
$Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
ConvertTo-JSON $Result -Depth 10

N-Gramではなく、Kuromojiが適用されていそうです。なお、確認方法は以下のページを参考にしました。
http://pppurple.hatenablog.com/entry/2017/05/28/141143

つまりここでKuromojiを指定しなければ、N-Gramされてしまうのですね。単語間にスペースのついてない言語って英語圏の人たちは頭を抱えたくなるのでしょうか。
$JsonData = '{"text": "monster analyzed you"}'
$ReqestType = "POST"
$APIURL = "/${IndexName}/_analyze"
$RequestURL = $BaseURL + $APIURL
$PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
$Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
ConvertTo-JSON $Result -Depth 10

$JsonData = '{"text": "吾輩は猫である。"}'
$ReqestType = "POST"
$APIURL = "/${IndexName}/_analyze"
$RequestURL = $BaseURL + $APIURL
$PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
$Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
ConvertTo-JSON $Result -Depth 10

あとはデータを投入する処理で完了という感じですが、長くなったので次回に持ち越しです。いいところで続くって続きが楽しみになりませんか。

2018年8月26日

ElasticsearchでWikipediaの全文検索にチャレンジ (1)

前置き


以下のブログで紹介されていたWikipediaの全文検索に興味を持ち、どのようにすればいいのか調査をした記録です。
http://www.mwsoft.jp/programming/munou/wikipedia_solr.html

以降の記事は、以下のWikipediaアーカイブからjawiki-latest-pages-articles.xml.bz2とjawiki-latest-stub-articles.xml.gzをダウンロードし、アーカイブファイルからデータを展開済みという前提で話を進めます。
https://dumps.wikimedia.org/jawiki/latest/

例によって例のごとく、以下のコードは基本的にPowershellコンソールに直接コピペして使う前提です。スクリプトファイルにする必要すらありません。らくちんですね。
なお、推定所要時間はファイルダウンロードに3時間、データ生成に2時間です。

初めに


2018年8月現在、Wikipedia全文を収めたXMLファイルは11GBを超えており、XMLパーサにそのまま読ませてアップロードしようとか正気の沙汰ではありません。
ファイルをシステムで処理可能なレベルまで分割して、順次処理するというのが現実的な話でしょう。
実際に、記事本文を除いた一覧「jawiki-latest-stub-articles.xml」については、ファイルサイズが1.2GBだったので読み込みを試してみたのですが、メモリーを使い果たし、且ついつまで処理が掛かるのかわからない状況であったため断念しました。

# 32GB程度の搭載メモリーでは太刀打ちできない
# $Inputfile = Join-Path $WorkDir "jawiki-latest-stub-articles.xml"
# [xml]$XmlData = Get-Content $InputFile -encoding utf8


ちなみに、環境はCore i7-7700K + 32GBメモリーです。

試行


まずは先頭1000行を読み込んでXMLの構造を確認してみます。
そうすると、<PAGE></PAGE>というタグで1つの記事を示していることがわかります。

$WorkDir = "C:\Wikipedia"
$Inputfile = Join-Path $WorkDir "jawiki-latest-stub-articles.xml" # まずは本文なしで構造確認
$TextData = Get-Content $InputFile -totalcount 1000 -encoding utf8
$TextData

では、ページ単位で取り出すことにしましょう。
5万行で記事単位でXML構造を取得できるか試してみます。

$WorkDir = "C:\Wikipedia"
$OutputDir = Join-Path $WorkDir "output"
If (-not (Test-Path $OutputDir)) { New-Item $OutputDir -ItemType Directory }

$Inputfile = Join-Path $WorkDir "jawiki-latest-stub-articles.xml" # まずは本文なしで構造確認

$PageCounter = 0 # 記事件数カウンター
$Cycle = 100 # 1ファイルに納める上限値
$Keyword = '</page>' # 記事の終了タグ
$FileBase = "File-PickupTest-"
$OutputFile = Join-Path $OutputDir $( $FileBase  + [string]$( $PageCounter / $Cycle ) + ".txt")
$OutputFile

Get-Date
Get-Content $InputFile -totalcount 50000 -encoding utf8 | ForEach { # 5万行で試行
 $_ | Out-File -FilePath $OutputFile -Encoding utf8 -Append
 if ( $_ -match $Keyword ) { # 特定件数に達したら出力ファイルを変更
  $PageCounter = $PageCounter + 1
  if ($PageCounter % $Cycle -eq 0) {
   $OutputFile = Join-Path $OutputDir $( $FileBase + [string]$( $PageCounter / $Cycle ) + ".txt")
   $OutputFile
  }
 }
}

Get-Date
Write-Output "記事件数:${PageCounter}"

今回のファイルでは2631件、3ファイルに出力されました。
5万行縛りなので、恐らく大きくぶれないでしょう。

本番開始


うまくいきそうなので、対象ファイルを本文つきXMLに切り替えて流すことにします。
多大な時間とI/Oが発生しますので、SSD構成+数時間放置推奨です。

$WorkDir = "C:\Wikipedia"
$OutputDir = Join-Path $WorkDir "output"
If (-not (Test-Path $OutputDir)) { New-Item $OutputDir -ItemType Directory }

$InputFile = Join-Path $WorkDir "jawiki-latest-pages-articles.xml"

$PageCounter = 0 # 記事件数カウンター
$Cycle = 1000 # 1ファイルに納める上限値
$Keyword = '</page>' # 記事の終了タグ
$FileBase = "File-"
$OutputFile = Join-Path $OutputDir $( $FileBase  + [string]$( $PageCounter / $Cycle ) + ".txt")
$TextData = New-Object System.Collections.ArrayList # 新規配列

Get-Date
Get-Content $InputFile -encoding utf8 | ForEach { 
 $TextData.Add($_) > $Null # 一旦配列に格納
 # $_ | Out-File -FilePath $OutputFile -Encoding utf8 -Append # 直接出力は時間が掛かるので方式変更
 if ( $_ -match $Keyword ) { # 特定件数に達したら出力ファイルを変更
  $PageCounter = $PageCounter + 1
  if ($PageCounter % $Cycle -eq 0) {
   $TextData -join "`n" | Out-File -FilePath $OutputFile -Encoding utf8
   $TextData = New-Object System.Collections.ArrayList # 新規配列
   $OutputFile = Join-Path $OutputDir $( $FileBase + [string]$( $PageCounter / $Cycle ) + ".txt")
   # 桁数を揃える場合 $( $PageCounter / $Cycle ).ToString("00000") に変更
  }
 }
}

$TextData -join "`n" | Out-File -FilePath $OutputFile -Encoding utf8

Get-Date
Write-Output "記事件数:${PageCounter}"

最初、1行ずつファイル書き出ししてたけど、とても時間が掛かったので、出力する記事1000件分を一旦配列に入れて、1000件分溜まったらまとめてファイル出力するようにしました。速度は圧倒的に後者のが早いので興味があるかたは比べてみてください。
ただ、メモリーにキャッシュする訳で、搭載メモリーが8GB以下の人は$Cycleを200とかにして利用率を確認してから実行いただくのがよいかと思います。
スペックの良いマシンで生成完了まで1時間というところでしょうか。
今回は、約50分で2300ファイル生成されました。記事件数は2,300,820件だそうです。

Elasticsearch導入とKuromojiプラグイン


長くなりましたので、Wikipediaのデータをうんぬんするのは次の記事で扱います。
その際にElasticsearchを利用するので、導入していない人は以下の記事を参考に導入してください。
REST API をPowershellで扱うための Elasticsearch のススメ (1)

ちなみに、この記事を書いてから2週間も経っていないのにバージョンが上がっていました。 そんな訳で自分は入れなおしました。運用してるソフトウェアじゃないし、最新で評価したほうがいいよね。
# URLを以下に変更。
$URL = "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.4.0.zip"
$URL = "https://artifacts.elastic.co/downloads/kibana/kibana-6.4.0-windows-x86_64.zip"

そして、日本語解析としてKuromojiを入れます。
Kuromojiが何ぞやと語れる程知識がないので、以下の記事を紹介をしておきますね。
https://qiita.com/yamamotoshu1127/items/ae081c8fa1c9b2804f83

端的に言えば日本語の文章を単語に分解して品詞を示してくれる機能です。精度としては、完璧ではないけど、実用的な範疇というところです。過信は禁物だけど自分でやらなくてもいいなんてとっても楽ちんです。

では張り切って導入しましょう。

# ■作業ディレクトリーへ移動
# WorkDirにElasticsearchが導入されている前提
$WorkDir = "C:\Elastic"
Set-Location $WorkDir

# ■plugin:kuromoji
# https://www.elastic.co/guide/en/elasticsearch/plugins/6.4/analysis-kuromoji.html#analysis-kuromoji
$URL = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-kuromoji/analysis-kuromoji-6.4.0.zip"
$FileName = Split-Path $URL -Leaf
$FileFullPath = Join-Path $WorkDir $FileName
Invoke-WebRequest -Uri $URL -Outfile $FileFullPath
# ZIP展開は不要

$InstallPath = "file:///${FileFullPath}"
$ESbinDir = Join-Path $WorkDir "elasticsearch-6.4.0\bin"
Set-Location $ESbinDir

# プラグインリスト:事前確認
.\elasticsearch-plugin.bat list

.\elasticsearch-plugin.bat install $InstallPath

# プラグインリスト:事後確認
.\elasticsearch-plugin.bat list

install時にはZIPファイルを指定すればOKで、ZIPの展開は必要ありません。
事前にZIPを置いておけるのでオフライン環境でも安心ですね。

次回の記事に続きます。

2018年8月25日

ブログを作って早々だが、前言を撤回する。ElasticsearchとPowershellは相性が悪い。

前置き

先日、Elasticsearchの操作をPowershellで行うために
REST API をPowershellで扱うための Elasticsearch のススメ(1)、(2)、(3)
という記事を書いたのだけど、根本的にはElasticsearchとPowershell(Invoke-RestMethod)は相性が悪いのでElasticsearchを触りたいという目的の場合にはcurl.exeの利用をお勧めする。
※元々の目的であるInvoke-RestMethodを使用したいという場合には余り影響はない

これはREST APIにおけるGET、POST、PUT、DELETEという各名称(機能)と、そもそもGETとPOSTの違いというHTTPの仕様に起因する話である。
そして、HTTPの仕様で話すならElasticsearchが誤っている。ただ、今後 REST APIが普及するに従って、後追いで拡張される可能性もあるので標準に囚われない仕様が悪いとは必ずしもいえない(それはMSだって通ってきた道)。

追記:search API

あとで知ったのであれだが、この記事は無意味かも。Elasticsearch のsearch APIはGETとPOSTのどちらでも動くので、POSTにしてしまえば支障なく使えます。

内容と回避策(?)

具体的にいうと、GETメソッドにJSONデータを渡す処理ができないのである。
Kibanaにて検索を掛けるとき、以下のような構文になる。これをInvoke-RestMethodではリクエストすることができない。
GET _search
{
  "query": {
    "match_all": {}
  }
}

最初、該当のリクエストはGETメソッドのお作法に則りURLエンコードして渡す処理なんだろうと勝手に勘違いし、上手く動かないなぁと無駄に時間をかけていた。
それもそのはず、実はPOSTメソッドと同様にデータを送っているのである。気が付いた瞬間はマジかーと思ったし、見当違いなコードを書いていた自分が悲しくなったりもした。

そして暫く放置していたのだが、先人の知恵を借りるべくググってみたところ、以下のブログを発見した。
Learning Elasticsearch with PowerShell

Flaw Workaround
.NET is right. Elasticsearch is wrong. Breaking the rules for the sake of what you personally feel makes you a vigilante. Forcing a square peg into a round hole just for the sake of making sure "gets" are done with GET makes you an extremist. Bodies belong in POST and PUT, not GET.

私が言いたかったことが書いてあった。そして回避策まで提示してくれている。かっこいい!
ただ、リクエストで使っている「source」というJsonを投げるパラメーターがうまく動かない。
記事が2015年のものだし、公式(現時点の最新:6.4)ドキュメントにも載ってないし、現在では廃止された!?
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html

結論

そんな訳で、Invoke-RestMethodでElasticsearchをあれこれするのは諦めることにした。
Powershellで処理したい人は素直にcurl.exeをダウンロードして連携してください。


2018年8月19日

小ネタ:Windows Spotlightの画像を取得する

Windows10のロック画面で表示される壁紙は、Windows Spotlightという機能で表示されています。そして、その画像を気に入ったのに、気が付くともう別の画像に差しか割っているということが往々にしてあります。
以下のページでその保存方法を知ったので、簡単に取得できるようにPowershellで書いてみました。気に入った画像を見つけた時には実行してみてください。
http://ascii.jp/elem/000/001/091/1091657/

使い方は、Powershellプロンプトを立ち上げて、以下のコードをコピーして張り付けるだけです。
事前に適当なフォルダーへSet-Location(CD)しておくといいと思います。

$MinSize = 200kb # 足切りサイズ
$WorkDir = Join-Path $(Get-Location).Path "SpotLight"
If (-not (Test-Path $WorkDir) ) { New-Item $WorkDir -ItemType Directory}

$SpotlightFolder = Join-Path $(Get-Item env:LOCALAPPDATA).value "\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets"
$FileList = Get-ChildItem $SpotlightFolder | Where-Object {$_.Length -gt $MinSize}

foreach ($File in $FileList){ 
 $DstName = Join-Path $WorkDir $($File.Name + ".jpg")
 Copy-Item $File.Fullname $DstName
}

Start $WorkDir

なお、足きりを200KBに設定してるのは目視確認で正確な値ではないです。
該当フォルダーにはSpotlightの壁紙以外にもスタートボタン押したときに表示される画像の類が格納されます。それらは小さいjpegであるため、200KBで足切りすれば大抵除外されるでしょうという話です。

2018年8月18日

Google Search Consoleを試してみた。

前置き

ブログを作成して1週間ほど経ったが、想定するキーワードで検索しても一向に自分のブログにヒットしないなと思って調べ始めた。
その結果、Bloggerを使う場合にはGoogleのSearch Consoleというツールで登録してあげるといいという話が見つかった。

そこで、Search Consoleに登録することにした。
https://www.google.com/webmasters/tools/home?hl=ja

登録

まず、自分のBlogのURLである「https://ntnl-it-wiz.blogspot.com/」を登録した。


そして、サイト情報が表示されたのだが、見事にクロールエラーとなった。
なるほど。これではGoogle八分と変わらない。

この状態を脱却するにはサイトマップを登録する必要があるとのこと。
で、これはATOMを登録すればいいという話である。

では、ATOMはどこよという話になるが、これは適当なページでソースコードを開いてみれば直ぐにわかる話です。
以下のように、MIMEがapplication/atom+xmlのlinkタグを探せば書いてあります。
<link rel="alternate" type="application/atom+xml"
 title="No technology, no life. - Atom"
 href="https://ntnl-it-wiz.blogspot.com/feeds/posts/default" />
※長かったので改行入れましたがソースコードに記載されている該当のタグは1行です。

該当サイトのどこにサイトマップがあるかという指定をすればいいので、
「feeds/posts/default」だけ送信するとサイトが登録できました。


クロールはすぐには行われないということなので、これで一晩待ってから様子を確認すればよいかと考えております。
以下によると、ATOMを登録しても数日掛かるもののようですね。。。
https://support.google.com/webmasters/answer/6065812

蛇足

livedoor blogではこんな面倒なことしなくてもよかったので、専用のblogサービスを使うというのはそれなりに便利なことなんだなと改めて思った次第。
https対応さえちゃんとできそうであればなぁ。

追記(2018/08/25)


その後、Search Consoleにてクロールされたことが確認できた。
インデックスまでにはSearch Consoleへ登録してから数日掛かるので、しばらく放置してブログコンテンツの作成にいそしむのが正解ということのようだ。


2018年8月16日

RedmineにPrism.jsを組み込んで、シンタックスハイライトを拡張する

前置き

2019年6月1日追記:
Redmine 4.0でPowershellを含めた100を超える言語に対応したので、あえてPrismを組み込む必要はなくなりました。公式対応はありがたいですね。
https://www.redmine.org/projects/redmine/wiki/RedmineCodeHighlightingLanguages

※以下の元記事も、行の強調などを採用してみたい方のために残しておきます。

Windows屋なのでPowershellをよく書くのだけれども、RedmineにはPowershellのハイライト機能がないのである。<PRE></PRE>だと見目麗しくない。
苦々しく思っていたところ、先日の記事でPrism.jsのハイライト手法がRedmineと同じだったので、「これ、いれるんじゃない?」と思いついて試してみたら本当にできてしまったというだけの話です。


ということで、邪道ではありますが、ありものを組み合わせてそれっぽく使ってしまおうというのが今回のお題です。
なお、実用範囲とは思っていますが、若干いけてない部分もあるので、それも併せてお知らせします。

設定

以下の流れです。
  1. prism.js、prism.cssを公式サイトからダウンロードする
  2. redmineのpublicディレクトリ(Apache公開ディレクトリ)にファイルを配置する
  3. 以下のファイルにリンクを付与する。
    /var/lib/redmine/app/views/layouts/base.html.erb
1は公式サイトからダウンロードしてください。
なお、成功パターンのパラメーターでのリンクも張り付けておきます。(いつまでちゃんと動くかわかりませんが)
うまくハイライトされないという場合には、成功パターンのリンクからそのままダウンロードして試してください。
うまくいったらパラメーターを変えて、自分向けにアレンジしてください。

2はprism.jsとprism.cssをscpで転送後、以下のコマンドで公開ディレクトリーに配置するイメージです。
mkdir /var/lib/redmine/public/prism
cp ./prism.* /var/lib/redmine/public/prism/

chown -R apache:apache /var/lib/redmine/public/prism
ls -la /var/lib/redmine/public/prism/

# ファイル公開チェック
curl http://localhost/redmine/prism/prism.css
curl http://localhost/redmine/prism/prism.js
※構築はほぼ以下のブログにある通りなので、/var/lib/redmine へ導入している想定の手順です。環境に応じて適宜読み替えて実施ください。
http://blog.redmine.jp/articles/3_1/installation_centos/
※MariaDB(MySQL)が好きなので、あえてVer.3.1版の記事を拾ってます。

3はRedmineが出力するHTMLのテンプレートを直接編集する作業です。
※以下は趣味で設定変更前後の差分を取ってますが、やってることはbase.html.erbをviで編集してhttpdを再起動しているだけなので惑わさらないようにしてください。
# ○変数設定
BKDIR=~/bkconfig/; [ ! -e $BKDIR ] && mkdir -p $BKDIR
TARGETFILE=/var/lib/redmine/app/views/layouts/base.html.erb

# ○事前バックアップ
cat $TARGETFILE
echo $TARGETFILE | sed -e 's/\//_/g' | xargs -I{} cp -pi $TARGETFILE ${BKDIR}bk$(date +%Y%m%d){}
ls -la --time-style=long-iso $BKDIR
ls -la --time-style=long-iso $TARGETFILE

# ○編集
vi $TARGETFILE

# 以下の行を</head>の直前に追加
# --------------------------------------
<link href='/redmine/prism/prism.css' media='all' rel='stylesheet' type='text/css'/>
<script src='/redmine/prism/prism.js' type='text/javascript'></script>
# --------------------------------------

# ○事後確認
cat $TARGETFILE
echo $TARGETFILE | sed -e 's/\//_/g' | xargs -I{} diff $TARGETFILE ${BKDIR}bk$(date +%Y%m%d){}
ls -la --time-style=long-iso $TARGETFILE

# ○設定の反映
systemctl restart httpd

注意事項

CODEタグのあとは何も書かずに1行空けるのがRedmineのお作法ですが、Prism.jsだと1行空けずにCODEタグの後ろが書き始めとなります。
なので、既存の場所に適用すると1行空いてしまいます。行番号がないので些事かとは思いますが。
プラグインはRedmine組み込み側の何かしらとバッティングするらしく、行番号とかも動きません。
デフォルトのToolbar以外は外してしまうのがいいと思います。
時間があったら解析したいけど、それだったら、いっそ組み込み可能なテーマとか作ってしまったほうが建設的なのではという思いさえあります。まぁやらないだろうな。。。

ヘッダへの組み込みのファイル名は以下のページを参考にさせていただきました。面倒なんでRuby記法使わずに直接埋め込んでますけど。感謝。
http://www.lesstep.jp/step_on_board/redmine/785/

なお、prismのライセンスはMITとのことです。
https://github.com/PrismJS/prism/blob/master/LICENSE

2018年8月15日

REST API をPowershellで扱うための Elasticsearch のススメ (3)

前置き

前回の記事からの続きです。
以下のページにある内容をPowershellの Invoke-RestMethod コマンドレットで実施していきます。
https://qiita.com/math1101/items/311277789868ebd07835


MAP再作成

例によって例のごとく、以下のコードをPowershellのプロンプトへ順次張り付けていきます。
# ■作業ディレクトリー再設定
$WorkDir = "C:\Elastic\data"
Set-Location $WorkDir

# ■Elasticsearch 登録先情報
$Hostname = "localhost"
$PortNumber = 9200
$IndexName = "air"
$TypeName = "ppm"
$BaseURL = "http://${Hostname}:${PortNumber}"
$RequestURL = $BaseURL + $APIURL

# ■MAP定義更新
# ○既存Indexの削除
Function Delete-Index () {
    $APIURL = "/${IndexName}"
    $RequestURL = $BaseURL + $APIURL
    $ReqestType = "DELETE"
    Invoke-RestMethod -Uri $RequestURL -Method $ReqestType
    Write-Output $("`t Delete-Index() : " + $(Get-Date) )
}

Delete-Index

# ○MAP定義登録
Function Create-TypeItem ( [string]$JsonFilePath ) {
    $APIURL = "/${IndexName}"
    $TargetURL = $BaseURL + $APIURL
    $ReqestType = "PUT"
    $JsonData = Get-Content $JsonFilePath
    # 文字化けするのでUTF8を強制
    $PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData)
    Invoke-RestMethod -Uri $TargetURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
    Write-Output $("`t Create-TypeItem( ${JsonFilePath} ) : " + $(Get-Date) )
}

$JsonFilePath = Join-Path $WorkDir "map.json"
Create-TypeItem -JsonFilePath $JsonFilePath

# ■現在のMAPを取得
$MapFile = Join-Path $WorkDir "newmap.json"
$APIURL = "/${IndexName}/${TypeName}/_mapping"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $Result.air -Depth 10 | Out-File -Encoding Default -FilePath $MapFile

# ■件数チェック
$APIURL = "/${IndexName}/${TypeName}/_count"
$RequestURL = $BaseURL + $APIURL
$( Invoke-RestMethod -Uri $RequestURL ).count
新しく作成したnewmap.jsonを確認すると、TimeStampのデータ型が無事dateになっていることが確認できます。あとはデータを投入するだけで完了です。

実は先に挙げた元記事で、一番変更したかったのもここです。
元の内容は、CSVをBULK用のJSONファイルに変換しており、その変換処理だけで数時間掛かるので、気軽に試せなかったんですね。
でも、前回の記事にて示したように、CSVから直接BULK登録が可能なので、その工程を省いてしまえばいいのです。

この処理は、再利用性を考慮してスクリプトファイルとして保存しましょう。
※Powershellの実行ポリシーを変更していないと動かないのでまだの人は Set-ExecutionPolicy でググってください。
以下の内容を「UpdateESFromCsv.ps1」というファイル名で、フォルダー「C:\Elastic\data」に保存します。
<#
■Elasticsearch へのBulk投入
#>
####################################################################### 引数定義
[CmdletBinding()]
Param(
    $Hostname = "localhost"
    ,$PortNumber = 9200
    ,$IndexName = "air"
    ,$TypeName = "ppm"
    ,$CsvFile = ".\target.csv"
)
##################################################################### 初期化処理
# ■ ログ採取 //////////////////////////////////////////////
# Set-PSDebug -Trace 2
# ログファイル名設定
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path # スクリプトパス指定
$LogFileName = $MyInvocation.MyCommand.Name.substring(0,$MyInvocation.MyCommand.Name.length - 4) + $(Get-Date).ToString("yyyyMMddHHmm") + "_" + (Get-ChildItem env:computername).value + ".log"
$LogFilePath = Join-Path $ScriptPath $LogFileName

# ログ採取開始
Start-Transcript $LogFilePath

# ログ出力の折り返し防止設定
$BufferSize = $(Get-Host).UI.RawUI.BufferSize
$BufferSize.Width = 512 ; $BufferSize.Height = 512
$(Get-Host).UI.RawUI.BufferSize = $BufferSize

# ■ 既定値入力 ////////////////////////////////////////////
$BulkMax = 5000 # Bulkの実行件数
$BaseURL = "http://${Hostname}:${PortNumber}"
$BulkCommand = @{ "index" = @{ "_index" = $IndexName ;"_type" = $TypeName } } | ConvertTo-Json -Compress
$Counter = 0 # 初期化
$BulkData = New-Object System.Collections.ArrayList # 新規配列

####################################################################### 関数定義
Function BulkCreate-DocsItemFromJson ( [array]$BulkData ) {
    $TimeoutSec = 600 # タイムアウト時間を秒で指定(一括取り込みに時間が掛かる)
    $APIURL = "/${IndexName}/${TypeName}/_bulk"
    $RequestURL = $BaseURL + $APIURL
    $ReqestType = "POST"
    $JsonData = $( $BulkData -join "`n" ) + "`n"
    $PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
    $Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType -TimeoutSec $TimeoutSec
    Write-Output $("`t BulkCreate-DocsItemFromJson() : " + $(Get-Date) + " <= ${CsvFile}")
}

##################################################################### メイン処理
# ■メイン処理開始 /////////////////////////////////////////

Write-Output $("■メイン処理開始 : " + $(Get-Date) )

$CsvData = Import-Csv -Path $CsvFile -Encoding Default | Where-Object { $_.'測定項目名称' -like "*${TypeName}*"}
ForEach ($CsvRecord in $CsvData) {
    If ($Counter -gt $BulkMax) { # 規定件数を超えたらBULK実行
        BulkCreate-DocsItemFromJson $BulkData
        $Counter = 0 # 初期化
        $BulkData = New-Object System.Collections.ArrayList # 新規配列
    }
    $Counter = $Counter + 1
 $TimeStamp = $CsvRecord."年" + $CsvRecord."月" + $CsvRecord."日" + "-" + $CsvRecord."時"
 $CsvRecord | Add-Member -NotePropertyName "TimeStamp" -NotePropertyValue $TimeStamp
 $JsonData = $CsvRecord | ConvertTo-Json -Compress
    $BulkData.Add($BulkCommand) > $Null
    $BulkData.Add($JsonData) > $Null
}
BulkCreate-DocsItemFromJson $BulkData

# 終了処理 ////////////////////////////////////////////////
Write-Output $("■スクリプト終了 : " + $(Get-Date) )

Stop-Transcript

前回の記事で5件だけ登録した処理を、スクリプトファイルに変更し、一定件数毎にBULK実行するように改定したものになります。5000件であれば、10以内にはBULK実行するのではないでしょうか。
余り応答がないと不安になるでしょうし、配列に突っ込むためのメモリーも無尽蔵ではないので上限値を設けています。5000でメモリーが足りないという方は$BulkMaxを1000なりに値を変更してください。

そして、以下のコードを実行すれば順次CSVファイルを読み取ってElasticsearchへ登録してくれます。
# ■作業ディレクトリー再設定
$WorkDir = "C:\Elastic\data"
Set-Location $WorkDir

# ■ファイル確認
Get-Item ".\UpdateESFromCsv.ps1"

# ■スクリプト実行
$CsvList = Get-ChildItem $WorkDir | Where-Object {$_.Extension -eq ".csv" }
ForEach ($CsvFile in $CsvList) {
 .\UpdateESFromCsv.ps1 -CsvFile $CsvFile.FullName
}

# ■投入結果確認
$Hostname = "localhost"
$PortNumber = 9200
$IndexName = "air"
$TypeName = "ppm"
$BaseURL = "http://${Hostname}:${PortNumber}"
$APIURL = "/${IndexName}/${TypeName}/_search"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $Result -Depth 10

# ■件数チェック
$APIURL = "/${IndexName}/${TypeName}/_count"
$RequestURL = $BaseURL + $APIURL
$( Invoke-RestMethod -Uri $RequestURL ).count

ここの内容は全CSVファイルを先のスクリプトで順次処理しているだけですので、詳細な説明は割愛します。
結果が126040件になってれば同じ結果です。うまくいかなかった人は頑張って原因を探してください。

三回に渡ってお送りしましたが、件の記事をForkした箇所は以上となります。お付き合いいただきありがとうございました。
なお、Kibanaの内容はWebUIですので、Windows関係ないし、変更点もありません。
元記事を参照して試してみてください。

REST API をPowershellで扱うための Elasticsearch のススメ (2)

前置き

前回の記事からの続きです。
以下のページにある内容をPowershellの Invoke-RestMethod コマンドレットで実施していきます。
https://qiita.com/math1101/items/311277789868ebd07835

なお、本記事はPowershell Ver.5.0向けです。3.0以降なら動きそうな気もしますが、2.0は存在しないコマンドレット多数、import-csvのencoding指定もNGのはず、ということで対象外としております。
Windows7のサポート期限も目前なので問題ないでしょう。

元データダウンロード

神戸市大気汚染常時監視結果 から灘大気測定局のデータを過去3年分(現在2018年なので2015年~2017年)ダウンロードします。
なお、観測局も年数も開始年も特に意味はありませんので、自由に変更いただいて構いません。

前回の記事と同様に、以下のコードをPowershellのプロンプトへ順次張り付けていきます。

# ■作業ディレクトリー作成
$WorkDir = "C:\Elastic\data"
if ( -not (Test-Path -Path $WorkDir) ) { # フォルダチェック
 New-Item -path $WorkDir -ItemType "directory"
}

# ■作業ディレクトリーへ移動
Set-Location $WorkDir

# ■ZIPファイルダウンロード
Function Get-TargetFile( $WorkDir, $URL ) {
    $FileName = Split-Path $URL -Leaf
    $FileFullPath = Join-Path $WorkDir $FileName
    Invoke-WebRequest -Uri $URL -Outfile $FileFullPath
}

Get-TargetFile -WorkDir $WorkDir -URL "http://kobe-taikikanshi.jp/kankyo/download/hour/kaku/2017_0103_hour_kakutei.zip"
Get-TargetFile -WorkDir $WorkDir -URL "http://kobe-taikikanshi.jp/kankyo/download/hour/kaku/2016_0103_hour_kakutei.zip"
Get-TargetFile -WorkDir $WorkDir -URL "http://kobe-taikikanshi.jp/kankyo/download/hour/kaku/2015_0103_hour_kakutei.zip"

# ZIPファイルの展開
$ZipList = Get-ChildItem $WorkDir | Where-Object {$_.Extension -eq ".zip" }
ForEach ($ZipFile in $ZipList) {
 Expand-Archive -Path $ZipFile.FullName -DestinationPath $WorkDir -Force
}

# ■展開後のファイル確認
Get-ChildItem $WorkDir

以上で該当ZIPファイルを展開して、以下のCSVファイルを取得するところまでできているはずです。
  • 2015_0103_hour.csv
  • 2016_0103_hour.csv
  • 2017_0103_hour.csv

これで投入データの準備が整いました。

ちなみに、3ファイル以上取得する人のために即席で関数 Get-TargetFile を作ってます。
このように即席で関数が作れるのがPowershellのありがたいところです。


MAP作成

前回の記事でも軽く触れましたが、Elasticsearchでは、Json形式でデータを投入できます。
ただ、何も考えずにデータを投入すると、勝手にデータ型を文字列型にして登録します。
日付をDate型にしたいという場合には「Mapping」を先に行います。
データベースでいうところのテーブル定義(Create Table)です。

この作業は、一旦すべてを文字列として登録し、そのMAP情報を取得して、必要なところだけデータ型を変更して、それを正式なMAPファイルとするというのが一番手間が掛かりません。
MS SQL ServerでGUIでテーブルを生成してからSQL構文を生成するようなものですね。え?違う??

ともかくやってみましょう。
以下、「■」のついている単位で順に実行してください。
(まとめて実行しても支障はありませんが、ステップ実行したほうが流れがくみ取れるかと思います)
# ■作業ディレクトリー再設定
$WorkDir = "C:\Elastic\data"
Set-Location $WorkDir

# ■Elasticsearch 登録先情報
$Hostname = "localhost"
$PortNumber = 9200
$IndexName = "air"
$TypeName = "ppm"
$BaseURL = "http://${Hostname}:${PortNumber}"
$APIURL = "/${IndexName}/${TypeName}/_bulk"
$RequestURL = $BaseURL + $APIURL
$ReqestType = "POST"
$BulkCommand = @{ "index" = @{ "_index" = $IndexName ; "_type" = $TypeName } } | ConvertTo-Json -Compress

# ■CSVファイル読み込み
$CsvList = Get-ChildItem $WorkDir | Where-Object {$_.Extension -eq ".csv" }
$CsvFile = Join-Path $WorkDir $CsvList[0]
$CsvData = Import-Csv -Path $CsvFile -Encoding Default

# ■CSV取り込み対象データ抽出
$FilterData = $CsvData | Where-Object { $_.'測定項目名称' -like "*${TypeName}*" } 
$FilterData.count

# ■Bulk実行用のJsonデータ生成
$BulkData = New-Object System.Collections.ArrayList # 新規配列
ForEach ($Counter in @(0..4) ) {
 $CsvRecord = $FilterData[$Counter]
 $TimeStamp = $CsvRecord."年" + $CsvRecord."月" + $CsvRecord."日" + "-" + $CsvRecord."時"
 $CsvRecord | Add-Member -NotePropertyName "TimeStamp" -NotePropertyValue $TimeStamp
 $JsonData = $CsvRecord | ConvertTo-Json -Compress
    $BulkData.Add($BulkCommand) > $Null
    $BulkData.Add($JsonData) > $Null
}

Write-Output $BulkData

# ■ Bulk実行
$JsonData = $( $BulkData -join "`n" ) + "`n"
$PostParam = [System.Text.Encoding]::UTF8.GetBytes($JsonData) # 日本語が文字化けするのでUTF8を強制
$Result = Invoke-RestMethod -Uri $RequestURL -Body $PostParam -ContentType 'application/json' -Method $ReqestType
$Result

# ■投入結果確認
$APIURL = "/${IndexName}/${TypeName}/_search"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $Result -Depth 10

$APIURL = "/${IndexName}/${TypeName}/_count"
$RequestURL = $BaseURL + $APIURL
$( Invoke-RestMethod -Uri $RequestURL ).count

# ■現在のMAPを取得
$MapFile = Join-Path $WorkDir "map.json"
$APIURL = "/${IndexName}/${TypeName}/_mapping"
$RequestURL = $BaseURL + $APIURL
$Result = Invoke-RestMethod -Uri $RequestURL

ConvertTo-JSON $Result.air -Depth 10 | Out-File -Encoding Default -FilePath $MapFile
急に何を始めたのかというレベルになってきたので解説を挟みつつ。
まず、登録先URLの変数「$RequestURL」は「http://localhost:9200/air/ppm/_bulk」になります。
他のサイトなどでElasticsearchの解説を見ながら確認いただきたいですが、以下になります。
  • index:データベースだとdatabaseに相当する概念
  • type:データベースだとtableに相当する概念
そして「_bulk」がREST APIです。
index:air、type:ppmの中にまとめてデータを流し込む_bulk APIを使用するというのをURLで示している訳です。
以下のページのDocuments APIとか参考になるかもしれません。
https://medium.com/hello-elasticsearch/elasticsearch-api-83760ce1424b

そして、CSVファイルを読み込んで、登録対象「ppm」でフィルタリングします。
※ちなみに、ppmはperts per millionの略で体積濃度だそうです。門外漢のため細かいことは知りません。

続けて、MAP作成のひな型として、5件データを登録します。
フィルターしたCSVファイルから5件だけ情報を取得し、Timestanpというプロパティ(CSVの列に相当)を追加して、その1行をJson形式に変換します。

その後、_bulkのお作法に則ってJsonのリストを生成します。
index指定($BulkCommand)で1行、投入データ内容($JsonData)で1行の2行で1セットになっています。
_bulkのお作法は以下を参照してください。
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html

そして、「■ Bulk実行」というところで5件のデータを登録しております。
Forで回しているときには配列に入れておき、Bulk実行直前でjoinで改行付きテキストに変換し、文字コードUTF8のバイトデータに置き換えてPOSTしております。
なお、_bulkに渡すJsonDataは、Forの中で直接テキストを生成しても問題ないです。配列に入れるのはForとかで回しやすいから癖で入れてるようなものです。

実行時にエラーが出なければ、投入結果確認のところで、投入されたデータと件数が表示されます。
問題がなければ、mapファイルを出力します。

このmapファイルをメモ帳等で開いてみましょう。
ちなみに、おすすめはVisual Studio Codeです。「ドキュメントのフォーマット」機能でJsonファイルを見やすく整えてくれます。エンコード指定しないとすぐに化けるのが難点ですが。
このファイルのTimeStampを編集します。
Before:
                "TimeStamp": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                },

After:
                "TimeStamp": {
                    "type":"date",
                    "format":"YYYYMMdd-HH"
                },
長くなったので続きは次の記事とします。
ただ、この記事の中で、本来の目的である Invoke-WebRequest によるREST APIの操作方法はなんとなく理解できたと思いますので、それが目的の人はここで離脱すればいいと思います。
せっかくなんでCSV全データ投入まで見てやるぜという人はもう少しお付き合いください。

REST API をPowershellで扱うための Elasticsearch のススメ (1)

前置き

この記事は、PowershellでREST APIを扱うためのコマンドレット Invoke-RestMethod を扱う記事と見せかけて、実は少し違います。
もともと、WindowsでElasticsearchを動かそうということでWebを探したところ、以下の記事がヒットしました。
https://qiita.com/math1101/items/311277789868ebd07835
これはこれでありがたかったのですが、Windows屋さんとしては、やはりcurlをダウンロードしてきて使うという時点で、「OS標準であるPowershellで同じことができるんですよ!」って思ってしまったので、そのフォロー記事ということでForkさせてみました。

Elasticsearchは簡単に導入できて、かつREST APIが利用できます。
ですので、いきなり本番サービスへ Invoke-RestMethod して使い方を覚えるより、Elasticsearchにあれこれ試してみて、使い方を把握すると便利ですよという趣旨のもと、本記事を公開いたします。

Elasticsearchとは

Elasticsearch公式:https://www.elastic.co/jp/

ElasticsearchはJsonで扱える全文検索エンジンです。
KibanaはそれをWebから確認するためのツールです。
本記事では詳細は扱わないので、触ってみて興味を持ったのであれば色々と調べてください。
Logstashと連携させて管理サーバーのログを取り込んで解析するとかメジャーな使い方のようです。

Elasticsearchの導入

以下、Windows 10にElasticsearchを導入して動かすところまでPowershellで書いてます。
PS1ファイルで保存してもいいですが、1回しか実施しないのでPowershellのプロンプトに直接コピペで張り付ければそれだけでOKです。
# ■前提:Java導入済み環境
Get-Item "Env:JAVA_HOME"
java.exe -version
# Elasticsearchに対応した Java が入っていなければ公式からダウンロード&セットアップ
# https://java.com/ja/download/

# ■作業ディレクトリー作成
$WorkDir = "C:\Elastic"
if ( -not (Test-Path -Path $WorkDir) ) { # フォルダチェック
 New-Item -path $WorkDir -ItemType "directory"
}

# ■作業ディレクトリーへ移動
Set-Location $WorkDir

# ■Elasticsearch
# ○製品ダウンロード:https://www.elastic.co/jp/
$URL = "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.zip"
$FileName = Split-Path $URL -Leaf
$FileFullPath = Join-Path $WorkDir $FileName
Invoke-WebRequest -Uri $URL -Outfile $FileFullPath

# ○ZIP展開:Powershell 5.0以下なら素直にGUIから展開してね。
$FolderName = $FileName -replace "\.zip$", ""
Expand-Archive -Path $FileFullPath -DestinationPath $WorkDir -Force
Start "${FolderName}\bin"

# ■ツール:Kibina
# ○製品ダウンロード:https://www.elastic.co/jp/
$URL = "https://artifacts.elastic.co/downloads/kibana/kibana-6.3.2-windows-x86_64.zip"
$FileName = Split-Path $URL -Leaf
$FileFullPath = Join-Path $WorkDir $FileName
Invoke-WebRequest -Uri $URL -Outfile $FileFullPath

# ○ZIP展開:Powershell 5.0以下なら素直にGUIから展開してね。
$FolderName = $FileName -replace "\.zip$", ""
Expand-Archive -Path $FileFullPath -DestinationPath $WorkDir -Force
Start "${FolderName}\bin"
以上で「C:\Elastic」に「elasticsearch-6.3.2」と「kibana-6.3.2-windows-x86_64」が生成されます。
該当バージョンは本日時点の最新版なので採用しただけです。新しいバージョンが出ていればそちらを採用いただいて構いません。

Elasticsearchの起動

binフォルダーの中にある、製品名と同名のバッチファイルを起動すればそれだけで動きます。
設定等は一切なくていいので、動かなかったら多分Javaのせいじゃないですかね。64bit版入れてJAVA_HOME通してあれば普通は動きます。
※localhost宛なのでWindows Firewallは影響ないですが、アンチウィルスソフトがポートブロックするようであれば解除も忘れずに

以下の内容を「C:\Elastic\StartES.bat」」として保存しておけば、ワンクリックでElasticsearchとKibanaを両方起動できます。
なお、「bin\elasticsearch-service.bat」を使うとサービス登録もできるようなので、サーバー利用の場合には検討してください。
Set ScriptPath=%~dp0
Set ES_PATH=%ScriptPath%elasticsearch-6.3.2
Set KB_PATH=%ScriptPath%kibana-6.3.2-windows-x86_64

CD /D "%ES_PATH%\bin"
Start "Elasticsearch" /min ".\elasticsearch.bat"

REM Elasticsearch 起動までの待機:環境依存なので適当に
Timeout /T 60

CD /D "%KB_PATH%\bin"
Start "Kibana" /min ".\kibana.bat"

Echo 起動が完了しました。
REM pause

Elasticsearchの動作確認

簡単に動作確認しましょう。先ほどと同様、プロンプトへ以下を直接貼り付けます。
# ■動作確認
# ○バージョン確認
$Hostname = "localhost"
$PortNumber = 9200
$BaseURL = "http://${Hostname}:${PortNumber}"
$RequestURL="${BaseURL}/"

$JsonData = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $JsonData

# ○プラグイン確認
$RequestURL="${BaseURL}/_nodes/plugins"
$JsonData = Invoke-RestMethod -Uri $RequestURL
ConvertTo-JSON $JsonData -Depth 10
Json形式でバージョン情報、プラグイン情報が表示されればOKです。
ちゃんと内容を確認しなくても支障はないので、エラーにさえならなければ、なんか出たぞくらいに思ってください。

以上にて、GETメソッドによるInvoke-RestMethodの動作が確認できました。
結構長くなったので、先に挙げた記事の内容については次の記事にて記載します。

まずはシンタックスハイライトを設定、改。

ITブログなら、コードも沢山載せていきたいよなぁということで、 コードハイライトを設定することを検討した。
最初は google-code-prettify にしようかと考えたが、2013年から 更新がないので、恐らくメインストリームではないと判断し、別のものを探した。

検索した結果、以下のサイトで紹介されていたprism.jsを使用することにした。
https://thk.kanzae.net/net/wordpress/t1171/

表示例は以下の通り。
function main()
{
  alert("Hello world!");
}

使用方法は、先に挙げたブログにある通り、以下の公式サイトにて言語とプラグインを選択してjavascript「prism.js」とcss「prism.css」をダウンロードする。
https://prismjs.com/index.html

然る後、該当ファイルをアップロードして、カスタムスクリプトをブログで有効化した。
使い方も先のブログにある通り、<pre><code>で指定する。

ここまでは引っ越し前のlivedoor blogと同じ内容だけど、bloggerではファイルアップロード機能がないので、代替手段を検討した。
http://blogger.weblix.net/2014/07/blogger-external-file-github-pages.html
http://blogger.weblix.net/2014/01/github-pages-create-web-site.html

そのものずばりのページをみつけたので、それにそってgithubにprismのファイルを置かせていただくことにした。
ゆくゆくはWebサイトとして構成したい。気が向いたら。

ともあれ、ファイルは格納したので、テーマのHTML編集機能でヘッダに組み込んだ。

2018年8月14日

心機一転、再び

インターネットで学んだ内容を備忘録としてまとめるブログを作ろうと決意しました。
月1回くらい、何かしらの記事が書ければいいなぁと考えてます。

そして、livedoor blog から引っ越すこと、引っ越し先をBloggerにすることを決意した!
1週間も経たないうちにお引越しである。

2018年8月1日

このブログについて

このブログでは主に技術に関する紹介を行います。

本ブログで公開しているコードについて、著作権は放棄しませんが、一切の権利を主張しません。
そして、コードを利用したことで不利益を被ったとしても一切の責任を負いません。

簡単に言ってしまえば、以下のライセンスです。
Do What the Fuck You Want to Public License(WTFPL)
http://www.wtfpl.net/

認証されてないから通らないということであれば次点でApache 2.0とみなしてください。
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0

参考:
https://qiita.com/lovee/items/484ae3fc038314a64ee2

ちなみに、著作権を放棄すると、他者が権利を主張した挙句利用料を請求される可能性が存在するので放棄しないという程度です。
何らかの制約を科したいわけではないので安心してご利用ください。

例外として、いずれかのWebサイト等から引用したコードに関しては、それを引用物として表記いたします。
これらの引用コードは、それを作成した人が著作権を有するものであるため、原典の著作権を確認ください。
※利便性を考慮し、ライセンス規約は極力探して提示するようにします。

TIPS:VSCodeで日本語化がうまくいかないとき

前置き Visual Studio Codeで拡張機能「 Japanese Language Pack for Visual Studio Code 」を入れたら日本語になりますよね。 でも、「 Remote Development 」で色々な環境を日本語化してると、偶に...