2017/12/10

この記事はPowerShell Advent Calendar 2017の10日目です。

PowerShellはオブジェクトを扱うシェルですが、別にテキストデータを扱えない訳ではありません。むしろ、PowerShellで取得したデータをテキストファイルとして保存したり、スクリプトで用いるデータをテキストファイルで保存しておくことは日常的に行われることだと思います。

ただし、PowerShellで扱うデータはオブジェクトであり、テキストファイルは文字通り文字列であることから、コマンドレットを用いる等、何らかの手段で変換が必要になります。また、テキストデータ形式にも様々な種類があり、それぞれメリット、デメリットが存在します。今回の記事では、PowerShellで用いるデータを保持しておく際のテキストデータ形式について比較をしてみます。

プレーンテキスト

プレーンテキスト、すなわち書式なしのテキストファイルです。もっともシンプルな使い方をする場合、文字列配列の各要素に含まれる文字列が、テキストファイルの1行と対応します。

書き出し
$lines | Set-Content -LiteralPath file.txt -Encoding UTF8

$linesは文字列変数です。

特に理由がなければ文字コードはUTF-8で良いと思います。

追記
$lines | Add-Content -LiteralPath file.txt -Encoding UTF8

Add-Contentは実行のたびにファイルを開いて、書き込んでから閉じるという動作をするので、1行ずつforeachで実行するのはNGです。

読み込み
$lines = @(Get-Content -LiteralPath file.txt -Encoding UTF8)
メリット
  • 文字列配列をテキストファイルに書き出すのは多分これが一番楽だと思います。
  • 書き出したデータは人間にも読みやすい。 編集もしやすい。
デメリット
  • 文字列だけを保存しておきたいというケースがそもそも少ない。
CSV

コンマ等の特別な文字で区切り、1行あたりに複数のデータを保存できる形式です。

PowerShellのコマンドレットで扱う場合、オブジェクトが持つプロパティがヘッダ列名に対応します。各行にオブジェクト配列1要素のプロパティ値が、カンマ区切りで保持されます。

書き出し
$objects | Export-Csv -LiteralPath file.csv -NoTypeInformation -Encoding Default

$objectsは任意のオブジェクト配列です。必要であればSelect-Objectコマンドレットを併用して、プロパティを絞り込みます。

文字コードはExcelでそのまま読み込み/書き出しができるDefault(日本語環境ではShift_JIS)がお勧めです。(最近のExcel2016ならUTF8も一応読めますが)

追記
$objects | Export-Csv -LiteralPath file.csv -Append -NoTypeInformation -Encoding Default
読み込み
$objects = Import-Csv -LiteralPath file.csv -Encoding Default
メリット
  • オブジェクトのプロパティ値が、すべて数値あるいは文字列で表現できる値を持つ場合に最も適合する。
  • 人間にも読みやすく、ある程度は編集もできる。
  • Excelで開ける。
デメリット
  • オブジェクトのプロパティが、数値と文字列以外のオブジェクトである場合、すなわち、階層構造を持つデータの保存には適さない。
  • 数値も文字列として読み込まれてしまうので、数値として扱いたい場合は変換が必要になる。
  • Export-CsvとImport-Csvで扱うCSVファイルはヘッダが必須。つまり、ヘッダなしのCSVファイルが既にあって、それを読み書きするという用途には適さない。(できなくはないが)
  • 書き出し時の列順を制御することができない。つまり、PowerShellで書き出したCSVを、列順が固定であるとの想定である他のプログラムで読み込むことは基本NG。
  • 書き出し時、1つ目の要素に存在しないプロパティは、2つ目以降では存在しないものとして扱われる。同種のオブジェクトで構成される配列なら通常は問題ないのだが、要素によって動的に追加されるプロパティがあったりなかったりすると厄介。(ADでありがち)
JSON

JavaScriptのような表記でデータを保持するデータ形式です。データの受け渡しに様々な言語で利用できます。Web APIでもよく利用されます。

PowerShellではv3からJSONを扱うコマンドレットが提供されています。

書き出し
$objects | ConvertTo-Json | Set-Content -LiteralPath file.json -Encoding UTF8
読み込み
$objects = Get-Content -LiteralPath file.json -Encoding UTF8 -Raw | ConvertFrom-Json
メリット
  • CSVと異なり、階層構造を持ったデータでも扱える。
  • CSVと異なり、数値は数値型のまま読み書き可能。 (整数値はint、小数値はdecimal)
  • 人間にもまぁまぁ読めるし、頑張れば編集できなくもない。
デメリット
  • -Depthパラメータによりプロパティを展開する階層の深さを指定はできるが、プロパティに応じて深さ指定を変化させるというようなことはできない。基本的には、自分で構築したPSCustomObjectを使うか、JSON化する前に自分で元オブジェクトを整形しておく必要がある。
  • 直接ファイルに書き出し、追記、ファイルから読み込みするコマンドレットはない。
  • 実は細かい話をしだすと色々と罠があります…。
CLIXML

PowerShellではPSリモーティング等、プロセス間でオブジェクトのやり取りを行う際に、CLIXML形式を介してシリアライズ/デシリアライズが実行されます。シリアライズ対象によっては、完全に元のクラスのオブジェクトに復元されます。(復元されないオブジェクトにはクラス名にDeserialized.との接頭辞が付与され、プロパティ値のみ復元される)

ユーザーもコマンドレットを用いて、任意のデータをCLIXML形式でシリアライズし、XMLファイルとして保存することができます。

書き出し
$objects | Export-Clixml -LiteralPath file.xml
読み込み
$objects = Import-Clixml -LiteralPath file.xml
メリット
  • 元のオブジェクトの構造、プロパティ値と型情報を含めてほぼ完全にテキストファイルに保存できる。
  • 復元したオブジェクトはプロパティ値を参照できるのはもちろん、オブジェクト全体が完全にデシリアライズされ、元の型に戻った場合には、メソッドを実行することも可能。
  • 例え元の型に戻らず、Deserialized.との接頭辞が付いた状態でも、コンソールに表示する場合は元の型のフォーマットが使われるので見やすい。
デメリット
  • すべてのオブジェクトが元の型に戻せるわけではない。戻せるかどうかは確認が必要。
  • 人間が読み書きするようなものではない。

ちなみに、ConvertTo-Xmlという似たようなコマンドレットがありますが、出力形式はCLIXMLではない上、復元の手段もなく、かといって別に読みやすいXMLというわけでもなく、正直何のために使うのかよく分かりません(適切なxsltでも用意すればいいのかな?)。まだConvertTo-Htmlの方が使えそうです。

psd1

psd1は「PowerShellデータファイル」で、モジュールマニフェストやローカライズデータに使われるファイル形式です。スクリプトファイルの1種ですが、数値や文字列リテラル、配列、連想配列、コメントなど基本的な言語要素のみ使用可能です。PowerShell 5.0以降ではImport-PowerShellDataFileコマンドレットを用いて、任意のpsd1ファイルのデータを読み込み、変数に格納することが可能です。

書き出し

書き出し用のコマンドレットはありません。

読み込み

例えば以下のような内容をbackup_setting.psd1として保存しておきます。ルート要素は必ず連想配列にします。

@{
	Directories = @(
		@{
			From = "C:\test1"       # コピー元
			To = "D:\backup\test1"  # コピー先
			Exclude = @("*.exe", "*.dll")
			Recurse = $true
		},
		@{
			From = "C:\test2"
			To = "D:\backup\test2"
			Exclude = @("*.exe")
			LimitSize = 50MB
		},
		@{
			From = "C:\test3"
			To = "D:\backup\test4"
		}
	)
	Start = "0:00"
}

なお、dataセクションで全体を括ってもいいですが、psd1で許容される言語要素はdataセクションより更に制限がきついので、敢えてしなくてもいいんじゃないかと思います。

このファイルは以下のように読み込めます。

$setting = Import-PowerShellDataFile -LiteralPath backup_setting.psd1

$settingには連想配列が格納され、以下のように値が参照できます。

$setting.Directories | foreach {Copy-Item -Path $_.From -Destination $_.To}
メリット
  • PowerShellの構文でデータを記述できる。
  • 通常のps1ファイルを呼び出すのとは異なり、式の評価やコマンド実行などはされない分、セキュアである。
  • 配列と連想配列の組み合わせにより、JSONライクな階層構造を持てる。型情報も保持される。
  • JSONとは違い、コメントが入れられる。
デメリット
まとめ

PowerShellで扱うデータをテキストファイルとして保存する際には、各テキストデータ形式の特性を理解し、メリット、デメリットを踏まえて選定する必要があります。

また、当然ながらテキストファイルに保持することが不適切なデータもありますので、そこは注意してください。(画像データを敢えてBase64とかでエンコードしてテキストファイル化する意味があるのか、とかですね)

個人的には…

ちょっとした作業ログ等を記録しておきたい→プレーンテキスト

.NETオブジェクトの一部のプロパティだけ抜き出してファイル化したい→CSV

自分で構築したPSCustomObjectをファイル化したい→JSON

.NETオブジェクト全体をファイル化したい→CLIXML

スクリプトで使う設定データを用意したい→psd1

みたいな感じでなんとなく使い分けていると思います。psd1はまだ採用例はないですが…。

今回はビルトインのコマンドレットで扱えるもののみ取り上げましたが、他にもyaml等のテキストデータ形式が存在し、有志によるモジュールを用いて扱うことが可能です。

2015/11/09

前編(前々回)中編(前回)の続きです。

分かち書きとは

中編で作った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はゆるふわな言語ですが、そのゆるふわさがたまによく牙を剥きます。今日はそんなお話。

あえとすさんがこんなツイートをされていました。

直観的には、$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から始まるので)、' '(半角スペース)を返すわけですね。

これであえとすさんの疑問は解消したわけですが、じゃあ本来の目的である、「単一の値と配列を連結して配列を得る」にはどうするか、というと…

となるわけです。こうやって非配列値をあらかじめ@()により要素数1の配列にしておくと、+演算子の左辺と右辺がどちらも配列型となるため型変換は行われず、配列同士の+演算、すなわち配列の連結処理が行われるわけですね。

その1とありますがその2があるかは不明。なお、闇が沢山あるのは事実です。('A`)ヴァー

2011/09/08

PowerShellでクリップボードを読み書きする方法については5年ほど前にクリップボードアクセス(未完)II という記事を書きました。タイトル通り未完でして、このたび完結編を書こうと思いました。

System.Windows.Forms.Clipboardクラスを使う場合は先の記事にあるようにPowerShellをSTAで動作させる必要があります。ver.1の当時はその方法がありませんでしたが、ver.2ではpowershell.exeに-staパラメータが追加され、STAでの実行が可能となっています。

-staパラメータを使用しない場合は相変わらずこの方法は使えません。powershell.exe –staのプロセスを逐次起こして、クリップボードアクセス部分だけSTAの子プロセスでやらせることも不可能ではないですが、冗長ですしパフォーマンスの点で難があります。

ver.2ではAdd-Typeコマンドレットが追加され、任意のC#コードを動的に読み込むことができるようになりました。これを利用してクリップボードアクセスクラスを書くという手もあると思います。その中ではSTAのスレッドを起こしてクリップボードアクセスをすることになります。実際PowerShell Community Extensionsに含まれるOut-Clipboard/Get-Clipboardコマンドレットはそのような実装がされているようです。ですが動的にコードを読み込みコンパイルはやはりパフォーマンス上不利ですし、PSCXをインストールできない場合などもあるでしょう。

ところでSystem.Windows.Formsに含まれるTextBoxクラスはその名の通りテキストボックスコントロールなのですが、このコントロールにはCopy()メソッドとPaste()メソッドがあり、これらを使うとクリップボードに書き込み、クリップボードから読み込みが可能です。TextBoxクラスの良いところは、STAである必要がないところです。この方法を最初に提唱したのはこちらの方かな?

この方法を用いた関数もすでに公開されているのですが、PowerShellの作法に則った使い方ができるように少し改良を加えました。具体的には

Set-ClipBoard:  配列の指定、パイプラインからの入力を可能にした。

Get-ClipBoard: クリップボードの中身が複数行のテキストの場合、文字列配列を返すようにした。(Get-Contentと同様)

という変更を加えています。

function Set-ClipBoard
{
    param([parameter(Mandatory=$true,
            ValueFromPipeline=$true)][object[]]$InputObject)
    begin
    {
        Add-Type -AssemblyName System.Windows.Forms
        $tb = New-Object System.Windows.Forms.TextBox
        $tb.Multiline = $true
        $out=@()
    }
    process
    {
        $out+=$InputObject
    }
    end
    {
        $tb.Text = $out -join "`r`n"
        $tb.SelectAll()
        $tb.Copy()
    }
}

function Get-ClipBoard
{
    Add-Type -AssemblyName System.Windows.Forms
    $tb = New-Object System.Windows.Forms.TextBox
    $tb.Multiline = $true
    $tb.Paste()
    $tb.Text -replace "`r`n","`n" -replace "`r","`n"  -split "`n"
}

使い方例

# ファイルのフルパスをクリップボードに格納
Get-ChildItem|select -expand fullname|Set-ClipBoard

# CSV文字列がクリップボードに入っている場合以下のコマンドを実行すると、
# クリップボードの中身が対応するHTML tableタグに置換される
Get-ClipBoard|ConvertFrom-Csv|ConvertTo-Html -Fragment|Set-ClipBoard

やっぱりパイプで繋ぐのがPowerShellの醍醐味ですよね!

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/09/08/202547.aspx


Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー

Twitter

Books