LoginSignup
14
16

More than 5 years have passed since last update.

PowerShell と XPath

Posted at
  • 会社で 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 を利用する必要があります。)

  1. DOM モデルを使用した XML データの処理
  2. XPath データ モデルを使用した XML データの処理
  3. LINQ to XML を使用した XML データの処理

今回は 2 を選択しますが、XPath ナビゲーションによるノードの選択 にあるように 1 の Dom モデルでも XPath が使えます。

XPath の使い方

だいたい次のコードで使えます。

$xml_doc = [xml](cat $path -enc $encoding)
$xml_nav = $xml_doc.CreateNavigator()
$xml_nav.Select("XPath式") | %{$_.Value}
  1. XML ファイルを読み込んで XmlDocumentにキャスト
  2. CreateNavigator()メソッドで、XPathNavigatorオブジェクトを作成
  3. Select(String)メソッドで、 XPath 式を元にノード(XPathNavigatorオブジェクト)のイテレーターを選択
  4. %{$_.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
14
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16