こんにちは、PowerShellが大好きなwakです。今日もPowerShellの話をします。
今回も本文とは関係のないかわいい猫の写真
大きなXMLファイルがある
次のような数十MBのXMLファイルがあるとしましょう。このようなレコードが何万件も並んでいるとき、ここからLastUpdateDate
が4/21のものだけを取り出すにはどうしたら良いでしょうか?
<?xml version="1.0" encoding="utf-8"?> <EXP_HogehogeWorkDataTable> <DocumentElement> <EXP_HogehogeWork id="EXP_HogehogeWork1"> <RecordId>00000055643</RecordId> <LastUpdateDate>2015-04-20T11:13:59.000+09:00</LastUpdateDate> </EXP_HogehogeWork> <EXP_HogehogeWork id="EXP_HogehogeWork2"> <RecordId>00000925544</RecordId> <LastUpdateDate>2015-04-21T11:23:45.000+09:00</LastUpdateDate> </EXP_HogehogeWork> <EXP_HogehogeWork id="EXP_HogehogeWork3"> <RecordId>00000007048</RecordId> <LastUpdateDate>2015-04-21T20:12:02.000+09:00</LastUpdateDate> </EXP_HogehogeWork> <EXP_HogehogeWork id="EXP_HogehogeWork4"> <RecordId>00001611089</RecordId> <LastUpdateDate>2015-04-23T13:23:08.000+09:00</LastUpdateDate> </EXP_HogehogeWork> </DocumentElement> </EXP_HogehogeWorkDataTable>
色々な方法があるとは思いますが、皆様のWindowsにも入っているPowerShellがあれば簡単です。手順は次の通りです。
- PowerShellを起動する
- このファイルを読み込んでXMLとしてパースする
- 検索する
- ついでにソートする
- IDをリストとして取り出す
やってみよう
やってみます。チュートリアル方式で進めます。
1. PowerShellを起動
スタートメニューからPowerShellを起動しましょう。Windows8ならエクスプローラーから直接起動できます。またWindows7なら、エクスプローラーのアドレスバーに「powershell」と入力すれば、そのフォルダがカレントディレクトリとなってPowerShellが起動します。
2. ファイル読み込み
次のコマンドで、XMLファイルを読み込み、さらにそれをオブジェクトとしてパースします。
$xml = [xml](Get-Content <ファイル名>)
読み込めたら、この$xml
はもうXMLの階層構造を全て保持したオブジェクトになっています。
PS C:\blog> $xml xml EXP_HogehogeWorkDataTable --- ------------------------- version="1.0" encoding="utf-8" EXP_HogehogeWorkDataTable
難しいことは考えず、$xml.E
まで入力してからTabキーを押してみましょう。
PS C:\blog> $xml.EXP_HogehogeWorkDataTable
補完されました。ここでエンターキーを押すと……
PS C:\blog> $xml.EXP_HogehogeWorkDataTable DocumentElement --------------- DocumentElement
冒頭のXMLの内容と見比べてみてください。正しく階層構造が再現されています。もう2階層下に行ってみます。
PS C:\blog> $xml.EXP_HogehogeWorkDataTable.DocumentElement.EXP_HogehogeWork id RecordId LastUpdateDate -- -------- -------------- EXP_HogehogeWork1 00000055643 2015-04-20T11:13:59.00... EXP_HogehogeWork2 00000925544 2015-04-21T11:23:45.00... EXP_HogehogeWork3 00000007048 2015-04-21T20:12:02.00... EXP_HogehogeWork4 00001611089 2015-04-23T13:23:08.00...
個々のEXP_HogehogeWork
の内容が一覧表示されています。つまり元のXMLにEXP_HogehogeWork
要素がたくさんあるのでオブジェクトの配列になっているのですね。どこまで行ってもオブジェクトですから、このようなこともできます。
PS C:\blog> $xml.EXP_HogehogeWorkDataTable.DocumentElement.EXP_HogehogeWork[0].RecordId 00000055643
いちいち入力するのは面倒ですから、変数に格納しておきましょう。
PS C:\blog> $elements = $xml.EXP_HogehogeWorkDataTable.DocumentElement.EXP_HogehogeWork
3. 検索する
このようなオブジェクトの配列に対して加工したりフィルタをかけたりするのはPowerShellの得意なところです。次のようなLINQを考えてみます。
var result = array.Where((x) => x.RecordId.Contains("5"));
PowerShellではまったく同じことがこのような文法で書けます。$_
は仮変数だと思ってください。
PS C:\blog> $elements | Where-Object { $_.RecordId.Contains("5") } id RecordId LastUpdateDate -- -------- -------------- EXP_HogehogeWork1 00000055643 2015-04-20T11:13:59.00... EXP_HogehogeWork2 00000925544 2015-04-21T11:23:45.00...
同じように日付に対してフィルタをかけてあげればいいわけです。「4/21を検索する」だけなら、"04-21"
を含むものだけを取り出しましょう。(ちゃんと日付として扱いたい人は最後のオマケを見てください)
PS C:\blog> $result = $elements | Where-Object { $_.LastUpdateDate.Contains("04-21") } # いったん変数に入れておく PS C:\blog> $result # 結果は... id RecordId LastUpdateDate -- -------- -------------- EXP_HogehogeWork2 00000925544 2015-04-21T11:23:45.00... EXP_HogehogeWork3 00000007048 2015-04-21T20:12:02.00...
4. ソートする
これで取り出せるのはあくまでオブジェクトの配列です。つまりid
, LastUpdateDate
といった情報は失われていません。日付の逆順(新しい順)でソートしてみましょう。ソートはSort-Object
コマンドレットに渡せばいいです。
PS C:\blog> $result | Sort-Object LastUpdateDate -Descending id RecordId LastUpdateDate -- -------- -------------- EXP_HogehogeWork3 00000007048 2015-04-21T20:12:02.00... EXP_HogehogeWork2 00000925544 2015-04-21T11:23:45.00...
4. IDをリストとして取り出す
ここから特定の項目だけを取り出すには、今度はSelect-Object
を使います。
PS C:\blog> $result | Sort-Object LastUpdateDate -Descending | Select-Object RecordId, id RecordId id -------- -- 00000007048 EXP_HogehogeWork3 00000925544 EXP_HogehogeWork2
最後にRecordId
だけをファイル出力してみましょう。Out-File
コマンドレットを使います。
PS C:\blog> $result | Sort-Object LastUpdateDate -Descending | Select-Object RecordId | Out-File result.txt -Encoding UTF8
できました!
おまけ
上の方では手抜きをして日付検索も文字列として書いてしまいました。せっかくなのでちゃんと書いてみます。
PowerShellの演算子
PowerShellでは、「>」はリダイレクト、「&」はコマンドの実行など、他言語の演算子は別の意味を持っていることが多いため、演算子はこのように置き換えてあげないといけません。(bashと同じです。大文字小文字の区別はありません)
演算子(C#) | 演算子(PowerShell) |
---|---|
> | -gt |
< | -lt |
>= | -ge |
<= | -le |
& | -And |
| | -Or |
書きたいC#のコード
「日付が4/21」とは、厳密に言えば「4/21 0:00以降、4/21 23:59:59まで」ということです。C#で書けばこうなるはずです。
var criteria1 = new DateTime(2015, 4, 21, 0, 0, 0); var criteria2 = new DateTime(2015, 4, 22, 0, 0, 0); var result = elements.Where((x) => { var timestamp = DateTime.Parse(x.Timestmap); return timestamp.CompareTo(criteria1) >= 0 && /* criteria1より後(inclusive) */ timestamp.CompareTo(criteria2) < 0; /* criteria2より前(exclusive) */ });
これをそのままPowerShellに書き換えます。PowerShellでは最後に実行した処理の戻り値がブロックの戻り値と判断されるため、return
はあってもなくても構いません。
$criteria1 = New-Object DateTime(2015, 4, 21, 0, 0, 0) $criteria2 = New-Object DateTime(2015, 4, 22, 0, 0, 0) $result = $elements | Where-Object { $timestamp = [DateTime]$_.LastUpdateDate $timestamp.CompareTo($criteria1) -ge 0 -And $timestamp.CompareTo($criteria2) -lt 0 }
結果は上のものと変わらないので省略します。とても簡単ですね?
最後に
やや文法に癖はあるのですが、こんなこともPowerShellでできるということだけは覚えておいてほしいと思います(それさえ知っていれば検索もできます)。良いゴールデンウイークを!(と書いて放っておいたらゴールデンウイークが終わってしまいました。良い休み明けを!)