PowerShellでXMLファイルの内容を検索する

こんにちは、PowerShellが大好きなwakです。今日もPowerShellの話をします。

f:id:nurenezumi:20150501181203j:plain

今回も本文とは関係のないかわいい猫の写真

大きな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があれば簡単です。手順は次の通りです。

  1. PowerShellを起動する
  2. このファイルを読み込んでXMLとしてパースする
  3. 検索する
  4. ついでにソートする
  5. IDをリストとして取り出す

やってみよう

やってみます。チュートリアル方式で進めます。

1. PowerShellを起動

スタートメニューからPowerShellを起動しましょう。Windows8ならエクスプローラーから直接起動できます。またWindows7なら、エクスプローラーのアドレスバーに「powershell」と入力すれば、そのフォルダがカレントディレクトリとなってPowerShellが起動します。

f:id:nurenezumi:20150501180856p:plain

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の内容が一覧表示されています。つまり元のXMLEXP_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でできるということだけは覚えておいてほしいと思います(それさえ知っていれば検索もできます)。良いゴールデンウイークを!(と書いて放っておいたらゴールデンウイークが終わってしまいました。良い休み明けを!)