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

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

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