- 会社で XML ファイルからデータを抽出する作業が発生
- 正規表現を駆使して抽出に成功
- ただ、構造化されたデータに正規表現で対応するは愚行
- XMLをテキストファイルではなく、きちんとXMLとして扱う方法を調査すべき
というわけで調べました。なお、会社で使えるのは PowerShell V2 のみなので、V2 の情報になります。
XML データの処理
- XML文書は木構造
- 木構造で身近な例と言えばディレクトリツリー
- ディレクトリツリーと言えば
ls -l *.txt
みたいに glob パターンが便利 - glob のように XML 文書のノードの集合を表す表現があったら便利なのでは?
- その便利な表現が XPath だ!
というわけで XPath 使います。
なお、メモリ内の XML データの処理の記事を見ると、下記3つの方法があります。(Version 2.0 には Select-XML がないので、XML を扱うには .NET Framework を利用する必要があります。)
今回は 2 を選択しますが、XPath ナビゲーションによるノードの選択 にあるように 1 の Dom モデルでも XPath が使えます。
XPath の使い方
だいたい次のコードで使えます。
$xml_doc = [xml](cat $path -enc $encoding)
$xml_nav = $xml_doc.CreateNavigator()
$xml_nav.Select("XPath式") | %{$_.Value}
- XML ファイルを読み込んで XmlDocumentにキャスト
-
CreateNavigator()
メソッドで、XPathNavigatorオブジェクトを作成 -
Select(String)
メソッドで、 XPath 式を元にノード(XPathNavigatorオブジェクト)のイテレーターを選択 -
%{$_.Value}
を噛ませて値を取得
チートシート
XPath の例を眺めてれば目的の式が見つかると思います。
使ってみる
XML ファイルからデータを抽出するコードを XPath で書いてみる
対象のXML ファイル
- 構成は下記のような感じ。
- 要素
TargetElem1
が複数存在する。 - 要素
TargetElem1
は入れ子にならない
(要素TargetElem1
に子要素TargetElem1
は存在しない) - 要素
TargetElem1
は要素TargetElem2
を0または1個含む。
- 要素
<Root>
...
<TargetElem1 ... TargetAttr1 = "foo" ... >
...
<TargetElem2 ... TargetAttr1 = "baz" />
...
</TargetElem1>
...
<TargetElem1 ... TargetAttr1 = "bar" ... >
...
</TargetElem1>
...
</Root>
出力形式
- 出力形式(抽出したい情報)は下記のデータをタブ区切りで1行ずつ列挙する
- ファイル名
- TargetAttr1の値
- TargetAttr2の値(なければ記述不要)
file1.xml foo baz
file1.xml bar
...
ツール
- 使えるスクリプト言語のは最初から入ってる PowerShell V2 のみ
- 会社のPC {OS : Windows7} はイントラネットのみの接続
- ソフトウェアのインストールは会社で定められたもののみ、それ以外は不可
コード
正規表現を使ったコード
filter my-grep
{
Param ($Name, [Parameter(ValueFromPipeline=$True)] $RawLine)
switch -regex ($RawLine)
{
"<TargetElem1" {$line = @($Name, $RawLine -replace ".*TargetAttr1 *= *`"(.*?)`".*", '$1')}
"<TargetElem2" {$line += $RawLine -replace ".*TargetAttr2 *= *`"(.*?)`".*", '$1'}
"</TargetElem" {$line -join "`t"}
}
}
$xml_path = "xmlが格納されているディレクトリのパス"
$out_file = "結果のリダイレクト先のファイル名"
ls $xml_path | %{cat $_.fullname -enc utf8 | my-grep $_.name} > $out_file
XPath を使ったコード
filter get-TargetElem1 ($path, $encoding)
{
$xml_doc = [xml](cat $path -enc $encoding)
$xml_nav = $xml_doc.CreateNavigator()
$xml_nav.Select("//TargetElem1")
}
filter my-grep
{
Param ($Name, [Parameter(ValueFromPipeline=$True)] $TargetElem1)
$line = @($Name, $TargetElem1.GetAttribute("TargetAttr1", ""))
$line += $TargetElem1.Select(".//TargetElem2/@TargetAttr2") | %{$_.Value}
$line -join "`t"
}
$xml_path = "xmlが格納されているディレクトリのパス"
$out_file = "結果のリダイレクト先のファイル名"
ls $xml_path | %{get-TargetElem1 $_.fullname -enc utf8 | my-grep $_.name} > $out_file
感想
正規表現よりは断然使いやすいです。ただ、いちいち%{$_.Value}
やらGetAttribute
を噛ませるのも面倒なので、下記のように、$xml_doc.SelectNodes("XPath式")
して、あとは DOM モデルでよしなにしたほうが楽な気もします。
filter get-TargetElem1 ($path, $encoding)
{
$xml_doc = [xml](cat $path -enc $encoding)
xml_doc.SelectNodes("//TargetElem1")
}
filter my-grep
{
Param ($Name, [Parameter(ValueFromPipeline=$True)] $TargetElem1)
$line = @($Name, $TargetElem1.TargetAttr1)
$line += $TargetElem1.SelectNodes(".//TargetElem2") | %{$_.TargetAttr2}
$line -join "`t"
}
$xml_path = "xmlが格納されているディレクトリのパス"
$out_file = "結果のリダイレクト先のファイル名"
ls $xml_path | %{get-TargetElem1 $_.fullname -enc utf8 | my-grep $_.name} > $out_file