2015/11/09
JapanesePhoneticAnalyzerを使ってPowerShellで形態素解析(後編)
分かち書きとは
中編で作ったGet-JpYomi関数は、JapanesePhoneticAnalyzerクラスの読み仮名取得機能にフォーカスを当てたラッパー関数でした。
今回は、JapanesePhoneticAnalyzerクラスの最大の使用目的と思われる、「分かち書き」を目的とした関数を作成します。分かち書きとは、文章を文節単位で分割することと考えて頂いて良いかと思います。
前々回作ったGet-JpWordは単語単位の分割を行うものでしたが、読みやすさや発音のしやすさを目的として文章を分割表記する場合は、単語単位では細かすぎると言えます。
よって単語単位ではなく、文を意味のあるまとまりとして区切ることのできる最小の単位である、文節単位で分割する方法を考えてみます。
Split-JpText関数
前編で作ったGet-JpWord関数をラップし、分かち書きに特化した関数Split-JpTextを作成しました。まずは以下にコードを示します。
function Split-JpText { param( [parameter(ValueFromPipeline=$true)] [PSObject[]] $InputObject, [ValidateSet("Text", "Yomi", "Detail")] [string] $Format = "Text", [ValidateSet("ByWord", "ByPhrase")] [string] $SplitMode = "ByPhrase", [string] $Separator = " ", [switch] $ToArray ) begin { if($Format -eq "Detail"){$ToArray = $true} } process { foreach($o in $InputObject) { $o.ToString() | Get-JpWord | foreach -Begin { $phrases = @() $phrase = $null } -Process { if($_.IsPhraseStart) { if($phrase){$phrases += $phrase} $phrase = New-Object psobject | Add-Member -MemberType ScriptProperty -Name Text -Value { -join $this.Words.DisplayText} -PassThru | Add-Member -MemberType ScriptProperty -Name Yomi -Value { -join $this.Words.YomiText} -PassThru | Add-Member -MemberType NoteProperty -Name Words -Value @() -PassThru } $phrase.Words += $_ } -End { if($phrase){$phrases += $phrase} if($SplitMode -eq "ByPhrase") { $out = switch($Format) { "Text" {$phrases.Text} "Yomi" {$phrases.Yomi} "Detail" {$phrases} } } else { $out = switch($Format) { "Text" {$phrases.Words.DisplayText} "Yomi" {$phrases.Words.YomiText} "Detail" {$phrases.Words} } } if($ToArray) { $out } else { $out -join $Separator } } } } }
パラメータの説明
パラメータ名 | 型 | 説明 |
InputObject | 任意の型 | 入力テキスト。文字列以外の型の場合は文字列に変換して評価される。パイプライン入力可能。 |
Format | string | Text(デフォルト):文字列のみ出力する。 Yomi:文字列ではなく読みを出力する。 Detail:文節の文字列、読み、各文節に含まれる単語の配列を含んだオブジェクトの配列を出力する。(SplitMode=ByPhraseの時のみ) |
SplitMode | string | ByPhrase(デフォルト):文を文節単位で分割する。 ByWord:文を単語単位で分割する。 |
Separator | string | 分割文字を指定。デフォルトは" "(半角スペース)。(Format=DetailもしくはToArray指定時には無効) |
ToArray | switch | 指定すると、単一の文字列ではなく、文字列の配列を出力する。 |
使用法
- 分かち書き(文節)
例:Split-JpText "今日はいい天気ですね。"
出力:今日は いい 天気ですね 。 - 分割文字指定
例:Split-JpText "今日はいい天気ですね。" -Separator /
出力:今日は/いい/天気ですね/。 - 分かち書き(単語)
例:Split-JpText "今日はいい天気ですね。" -SplitMode ByWord
出力:今日 は いい 天気 です ね 。 - 文節単位で読み仮名を表示
例: Split-JpText "今日はいい天気ですね。" -Separator / -Format Yomi
出力:きょうは/いい/てんきですね/。 - 分かち書きした文節を文字列配列として変数に格納
例:$phrases = Split-JpText "今日はいい天気ですね。" -ToArray
解説
ちょっと長めの関数ですが、ポイントはJapanesePhoneticAnalyzerクラスのGetWordsメソッドが返すJapanesePhonemeオブジェクトのIsPhraseStartプロパティです。
IsPhraseStartプロパティは、当該単語(Phoneme)が文節(Phrase)の開始部分にあたる単語であればTrueを返します。すなわち、JapanesePhonemeコレクションを文頭から文末まで列挙していったとき、IsPhraseStartプロパティがFalseからTrueに変わる部分が文節の境界になるわけです。
Split-JpText関数では、単語を列挙していき、文頭もしくは文節の境界に遭遇すると、文節に含まれる文字列(Textプロパティ)とその読み(Yomiプロパティ)と単語の配列(Wordsプロパティ)を格納するオブジェクトを新たに作成し、$phrase変数に代入します。一方で$phrase変数に元々入っていたオブジェクトは、$phrases配列に追加します。
$phraseオブジェクトのWordsプロパティには、列挙中の単語を都度、追加していきます。
なお、$phraseオブジェクトのTextプロパティとYomiプロパティはスクリプトプロパティとして定義しておき、必要時に値を取得するようにしてあります。
まとめ
3回に渡って、JapanesePhoneticAnalyzerクラスの使用法を具体的なラッパー関数を作成して紹介しました。
個人的には、PowerShellからなら中編で挙げた、読みの取得が一番使いでがあるかな?と思いました。今回取り上げた分かち書きは、意外と応用例が思いつきませんでした。
前編のGet-JpWord関数を使って、何らかの文書群の単語リストをあらかじめインデックスとして出力しておき、単語検索コマンドを実装するのも面白そうですね。
ただ、残念ながら品詞情報が取れないので、JapanesePhoneticAnalyzerをmecabとかの形態素解析エンジンの代替にするのはちょっと厳しいかもしれないです。まあ、標準機能のみでちょっとしたものを作れるのは大きいかと思います。何か日本語の文章を解析する必要があるときには使ってみてはいかがでしょうか。
2014/04/15
本当は怖いPowerShell その1 暗黙の型変換の闇
PowerShellはゆるふわな言語ですが、そのゆるふわさがたまによく牙を剥きます。今日はそんなお話。
あえとすさんがこんなツイートをされていました。
$x = 'A' + @('B', 'C') ってしたとき、何故 $x.Length -eq 4 で $x[2] -eq ' ' なのだろうか。 #PowerShell
? アエトス・トリスメギストス (@aetos382) April 14, 2014
直観的には、$xには'A', 'B', 'C'の3要素が格納された配列となるのでLengthは3、$x[2]は最後の要素である'C'が入っていそうです。
さて、何故だかわかりますか。シンキングタイム3分。
…では解説です。
まず、'A' + @('B', 'C')というのは実は3要素の配列を返さず、単一の文字列を返します。というのもPowerShellは+, -等の二項演算子を利用する際、左辺と右辺の型が異なる場合は、まず右辺の型を左辺の型に暗黙の型変換を行ってから演算を行います。この場合だと右辺は@('B', 'C')なので文字列配列(厳密にはobject[])、左辺は文字列型なので、文字列配列が文字列に型変換されるわけです。
さて、ここで配列→文字列の型変換がどうやって行われるかという話なのですが、まず配列要素がそれぞれ文字列型に変換されます。この変換は型によってそれぞれ挙動が違いますが、特にPowerShell上で定義がない場合はToString()されたものが返されます。今回のは配列要素が元々文字列なので変換はありません。
次に、文字列同士がユーザー定義$OFSに格納されている文字列で連結されます。$OFSはデフォルトではnull(定義なし)なのですが、nullの場合は" "(半角スペース)として扱われます。
※ちなみにOFSとはOutput Field Separatorの略です。awkとかPerlとかにも同様の変数があり、PowerShellのはそれらを参考にしたものと思います。
よって、@('B', 'C')が文字列に変換されると、'B'と'C'が$OFSのデフォルトの" "で連結され、'B C'となります。変換の後+演算子が実行されて、'A'と'B C'が連結されるので、'AB C'となります。この値が$xに格納されるわけです。
$xには配列ではなく単一の文字列が格納されているので、Lengthプロパティはstringクラスのものが参照されるので、文字数を返却します。$xの中身はA,B,半角スペース,Cの4文字なので$x.Lengthは4になります。
また文字列変数に数値でのインデックスアクセスをすると、該当文字位置に格納されたchar型の文字が返されるので、$x[2]は$xに格納された3番目の文字(インデックスは0から始まるので)、' '(半角スペース)を返すわけですね。
これであえとすさんの疑問は解消したわけですが、じゃあ本来の目的である、「単一の値と配列を連結して配列を得る」にはどうするか、というと…
@('A', 'B', 'C') が欲しければ @('A') + @('B', 'C') とすればよい。 ('A`)ヴァー
? アエトス・トリスメギストス (@aetos382) April 14, 2014
となるわけです。こうやって非配列値をあらかじめ@()により要素数1の配列にしておくと、+演算子の左辺と右辺がどちらも配列型となるため型変換は行われず、配列同士の+演算、すなわち配列の連結処理が行われるわけですね。
その1とありますがその2があるかは不明。なお、闇が沢山あるのは事実です。('A`)ヴァー
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー