PowerShellでXMLを解析する方法:読み取りと検証

XMLはあちこちにあります。角括弧の使用が面倒ですが、XML形式はまだ広く使用されています。設定ファイル、RSSフィード、Officeファイル(.docxの「x」)などがその一部です。XMLファイルを解析するためにPowerShellを使用することは、PowerShellの旅の中で必須のステップです。

このチュートリアルでは、PowerShellがXMLファイルを解析し、検証する方法を紹介します。これにより、XMLデータの整合性を検証し、スクリプトのゲートで誤ったデータを停止するためのツールが提供されます!

前提条件

提示された資料に従うには、次のものが必要です:

  • PowerShellバージョン3.0以上。例はWindows PowerShell v5.1で作成されました。
  • Notepad++、Visual Studio Code、またはXMLを理解する他のテキストエディタ。

Select-Xmlを使用してPowerShell XML要素を解析する

まず、PowerShellを使用してXMLを解析する最も人気で簡単な方法の1つであるSelect-Xmlについて説明します。 Select-Xmlコマンドレットを使用すると、XMLファイルまたは文字列とXPathと呼ばれる「フィルター」を指定して特定の情報を取得できます。

XPathは、要素名の連鎖です。XMLドキュメント内のノードを識別し、ナビゲートするための「パスのような」構文を使用します。

たとえば、コンピュータの一覧が含まれたXMLファイルがあり、このXMLファイルをPowerShellを使用して解析したいとします。各コンピュータには、名前、IPアドレス、レポートに含めるためのInclude要素など、さまざまな要素があります。

要素は、開始タグと終了タグを持ち、その間にテキストがある場合があります。例えば、<Name>SRV-01</Name>

<Computers>
	<Computer>
		<Name>SRV-01</Name>
		<Ip>127.0.0.1</Ip>
		<Include>true</Include>
	</Computer>	
	<Computer>
		<Name>SRV-02</Name>
		<Ip>192.168.0.102</Ip>
		<Include>false</Include>
	</Computer>	
	<Computer>
		<Name>SRV-03</Name>
		<Ip>192.168.0.103</Ip>
		<Include>true</Include>
	</Computer>	
</Computers>

PowerShellを使用して、このXMLファイルを解析し、コンピュータの名前を取得したいとします。そのためには、Select-Xmlコマンドを使用することができます。

上記のファイルでは、コンピュータの名前はName要素の内部テキスト(InnerXML)に表示されます。

InnerXMLは、要素のタグの間のテキストです。

コンピュータの名前を見つけるためには、まず適切なXPath(/Computers/Computer/Name)を指定します。このXPath構文は、Computer要素の下にあるNameノードのみを返します。次に、各Name要素のInnerXMLのみを取得するために、ForEach-Objectループで各要素のNode.InnerXMLプロパティにアクセスします。

Select-Xml -Path C:\Work\computers-elm.xml -XPath '/Computers/Computer/Name' | ForEach-Object { $_.Node.InnerXML }

Select-Xmlを使用してXML属性を解析するPowerShellの使用方法

この問題については、別のアプローチでコンピュータ名を見つけましょう。今回は、XMLの要素ではなく、XMLの属性でコンピュータの記述子が表されます。

属性は、name="SRV-01"のようなキー/値の部分です。属性は常に開始タグ内に現れ、タグ名の直後に配置されます。

以下は、属性でコンピュータの記述子が表されたXMLファイルです。要素ではなく属性として各記述子を見ることができます。

<Computers>
	<Computer name="SRV-01" ip="127.0.0.1" include="true" />
	<Computer name="SRV-02" ip="192.168.0.102" include="false" />
	<Computer name="SRV-03" ip="192.168.0.103" include="true" />
</Computers>

今回は各記述子が属性であるため、XPathを少し調整してコンピュータ要素のみを見つけます。そして、再びForEach-Objectコマンドレットを使用してname属性の値を見つけます。

Select-Xml -Path C:\Work\computers-attr.xml -XPath '/Computers/Computer' | ForEach-Object { $_.Node.name }

そして実際に、同じ結果が出力されます:SRV-01SRV-02SRV-03 :

Reading data using Select-Xml

要素または属性を読み取る場合、Select-Xmlの構文は煩雑です。XPathパラメータを使用し、結果をループに渡し、最後にNodeプロパティの下のデータを探す必要があります。

幸いにも、PowerShellにはXMLファイルを読み取り、XMLオブジェクトに変換するより便利で直感的な方法があります。

関連記事: PowerShellデータ型アクセラレータを使用してコーディングを高速化する方法

XML文字列をオブジェクトにキャストする

PowerShellを使用してXMLを解析する別の方法は、そのXMLをオブジェクトに変換することです。これを行う最も簡単な方法は、[xml]の型アクセラレータを使用することです。

変数名を[xml]で接頭辞付けすることで、PowerShellは元のプレーンテキストのXMLをオブジェクトに変換し、それを操作できるようにします。

[xml]$xmlElm = Get-Content -Path C:\Work\computers-elm.xml
[xml]$xmlAttr = Get-Content -Path C:\Work\computers-attr.xml

XMLオブジェクトの要素の読み取り

今、$xmlElm$xmlAttrの両方の変数は、XMLオブジェクトであり、ドット記法を使用してプロパティに参照することができます。おそらく、各コンピュータ要素のIPアドレスを見つける必要があります。XMLファイルがオブジェクトであるため、単にIP要素を参照することでこれを行うことができます。

$xmlElm.Computers.Computer.ip

PowerShellバージョン3.0以降では、XMLオブジェクトは、要素の内部テキストを読むために使用される構文と同じ構文で属性値を取得します。したがって、IPアドレスの値は、要素ファイルとまったく同じ構文で属性ファイルから読み取られます。

XML属性の読み取り

まったく同じドット記法を使用することで、XML属性も読み取ることができます。XMLの構造には違いがあるにもかかわらずです。

$xmlElm.Computers.Computer.ip
$xmlAttr.Computers.Computer.ip

以下の結果は、両方のファイルから同じデータを取得していることを示しています:

PowerShell to Parse XML : Reading XML Attributes

さらに、オブジェクトがメモリにロードされた場合、PowerShell ISEを使用している場合は、IntelliSenseを使用してタブ補完することができます。以下に示すように、例を挙げます。

IntelliSense and tab completion for XML object

XMLデータの反復処理

XMLファイルを直接XMLオブジェクトに読み込んだ後([xml]の型アクセラレータを利用する)、PowerShellオブジェクトの全機能を利用できます。

例えば、include=”true” 属性を持つすべてのコンピュータをXMLファイルからループ処理して、接続状態をチェックする必要があるとします。以下のコードは、その方法を示しています。

このスクリプトは、以下のようなものです。

  • ファイルを読み込んでXMLオブジェクトにキャストする。
  • 該当するコンピュータを繰り返し処理して、接続状態を取得する。
  • 最後に、結果の出力オブジェクトをパイプラインに送信する。
 ## ファイルテキストをXMLオブジェクトにキャストする
 [xml]$xmlAttr = Get-Content -Path C:\Work\computers-attr.xml

 ## include="true" が設定されたコンピュータをループ処理する
 $xmlAttr.Computers.Computer | Where-Object include -eq 'true' |  ForEach-Object {
     ## 現在のコンピュータがオンラインかどうかを確認する
     if(Test-Connection -ComputerName $_.ip -Count 1 -Quiet)
     {
         $status = 'Connection OK'
     }
     else
     {
         $status = 'No Connection'
     }

     ## 結果のオブジェクトを出力する
     [pscustomobject]@{
         Name = $_.name
         Ip = $_.ip
         Status = $status
     }
 }

上記のスクリプトの結果は以下の通りです:

Connection status results

XMLスキーマ

前のセクションで、異なる方法でデータセットを表す2つの異なるXMLファイルを見ました。これらの方法はXMLスキーマと呼ばれます。XMLスキーマは、特定の XMLドキュメントの許可される構成要素を定義します:

  • 特定のドキュメントに表示できる要素と属性の名前。
  • 子要素の数と順序。
  • 要素と属性のデータ型。

スキーマは、基本的にXMLの構造を定義します。

XMLデータの検証

XMLファイルは正しい構文を持っているかもしれません(Notepad++のようなエディタではそれがない場合に警告が表示されます)、しかし、そのデータがプロジェクトの要件に一致しないかもしれません。これがスキーマが登場する場所です。XMLデータに頼る場合、定義されたスキーマに従ってデータが有効であることを確認する必要があります。

ランタイムでデータエラーが発見されるのは望ましくありません。スクリプトの途中で500行も深くなっていた場合、すでにファイルシステムやレジストリ上でいくつかの不可逆的な操作を実行しているかもしれません。

では、データが正しいかどうかを事前に確認する方法はありますか?まず、いくつかの可能なエラータイプを見てみましょう。

XMLデータの可能なエラー

一般的に、XMLファイルで見つかるエラーは、メタデータのエラーとデータ自体のエラーのいずれかのカテゴリに属します。

XMLメタデータのエラー

以下のファイルMorePeople.xmlは、構文的には完全に有効です。下記に示すように、ファイルには単一のPeople要素(ルート要素)があり、その内部には3つのPerson要素が含まれています。この構造は完全に受け入れられます。しかし、1つの例外があります。見えますか?

<People>
	<Person Name="Debra" County="Canada" IsAdmin="true" />
	<Person Name="Jacob" Country="Israel" IsAdmin="true" />
	<Person Name="Olivia" Country="Cyprus" IsAdmin="false" />
</People>

見えなかった場合は心配しないでください。問題は最初の内部要素にあります:

Countryであるべき場所で、誤ってCountyになっています。

XMLデータのエラー

Countryの問題をMorePeople.xmlで修正した後、別の問題が潜んでいます。

<People>
	<Person Name="Debra" Country="Canada" IsAdmin="yes" />
	<Person Name="Jacob" Country="Israel" IsAdmin="true" />
	<Person Name="Olivia" Country="Cyprus" IsAdmin="false" />
</People>

メタデータ、つまり要素と属性は問題ありません。では何が問題なのでしょうか?今回の問題は、再び最初のPersonの行で、valuesの1つにあります。誰かがyestrueの代替として十分なものだと判断したようですが、以下のようなコードでは、最初の要素を取得するためにtrueではなくyesを探しているため、エラーが発生します:

$peopleXml.People.Person | Where-Object IsAdmin -eq 'true'

XMLスキーマの作成

これまでに発生しうるエラーの種類を把握したので、スキーマファイルがどのように役立つのかを示します。最初のステップは、サンプルデータファイルを作成することです。サンプルは最小の例であり、単一の内部要素のみを含んでいる場合があります。上記の例では、次のようなサンプルファイルPeople.xmlを作成しましょう:

<People>
	<Person Name="Jeff" Country="USA" IsAdmin="true" />
</People>

次に、以下のようなPowerShell関数を作成し、それをサンプルデータと共に使用して.xsdスキーマを作成します。

function New-XmlSchema
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -Path $_ })]
        [ValidatePattern('\.xml')]
        [string]$XmlExample
    ) 

    $item = Get-Item $XmlExample
    $dir = $item.Directory
    $name = $item.Name
    $baseName = $item.BaseName

    ## スキーマパスを'.xml'から'.xsd'に置き換えて作成する
    $SchemaPath = "$dir\$baseName.xsd"

    try
    {
        $dataSet = New-Object -TypeName System.Data.DataSet
        $dataSet.ReadXml($XmlExample) | Out-Null
        $dataSet.WriteXmlSchema($SchemaPath)
        
        ## 作成されたスキーマファイルを表示する
        Get-Item $SchemaPath
    }
    catch
    {
        $err = $_.Exception.Message
        Write-Host "Failed to create XML schema for $XmlExample`nDetails: $err" -ForegroundColor Red
    }
}

この関数をISEまたはお気に入りのPowerShellエディタにコピーし、メモリに読み込んでスキーマを作成します。関数が読み込まれたら、スキーマを作成するためのコードは、次の1行です:

New-XmlSchema -XmlExample 'C:\Work\People.xml'

結果は、新しく作成されたスキーマへのパスが表示されます:

Creating XML schema from a sample data file

スキーマファイルを使用してデータを検証する

上記の結果で指定された場所を確認してください。 .xsdファイルがそこにある場合、バリデーションが機能している正しい方法です。確認ステップでは、以下の関数を使用してください:

function Test-XmlBySchema
{
    [CmdletBinding()]
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -Path $_ })]
        [ValidatePattern('\.xml')]
        [string]$XmlFile,
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -Path $_ })]
        [ValidatePattern('\.xsd')]
        [string]$SchemaPath
    )

    try
    {
        [xml]$xml = Get-Content $XmlFile
        $xml.Schemas.Add('', $SchemaPath) | Out-Null
        $xml.Validate($null)
        Write-Verbose "Successfully validated $XmlFile against schema ($SchemaPath)"
        $result = $true
    }
    catch
    {
        $err = $_.Exception.Message
        Write-Verbose "Failed to validate $XmlFile against schema ($SchemaPath)`nDetails: $err"
        $result = $false
    }
    finally
    {
        $result
    }
}

関数をメモリに読み込み、2つのエラー例からMorePeople.xmlをバリデーションに使用してください。バリデーションをトリガーするには、以下のコマンドを使用してください:

Test-XmlBySchema -XmlFile 'C:\Work\MorePeople.xml' -SchemaPath 'C:\Work\People.xsd' -Verbose

実際の結果は、MorePeople.xmlの内容に依存します。

2つの例を見てみましょう。 MorePeople.xmlがエラーがない場合、上記の関数はTrueを返します。

Validation success

MorePeople.xmlファイルに間違ったデータ(CountryキーがCountyと誤って綴られている)が含まれている場合、関数はいくつかの失敗の詳細を返し、Falseを返します。

Validation failure – wrong attribute name detected

見てわかるように、詳細な出力で指定されたエラーは非常に情報が豊富です。問題が発生したファイルを指し示し、それが発生した正確なコンポーネントを指し示します。

バリデーションスキーマの微調整

スキーマファイルを見て、さらに改善する方法を見てみましょう。

New-XmlSchemaによって作成されるデフォルトのスキーマは以下の通りです:

<?xml version="1.0" standalone="yes"?>
<xs:schema id="People" xmlns="" xmlns:xs="<http://www.w3.org/2001/XMLSchema>" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="People" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="Person">
          <xs:complexType>
            <xs:attribute name="Name" type="xs:string" />
            <xs:attribute name="Country" type="xs:string" />
            <xs:attribute name="IsAdmin" type="xs:string" />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

上記のデフォルトのスキーマは良いですが、完璧ではありません。実際には、Country属性のタイポをキャッチしています。ただし、スキーマをそのままにしておくと、他の期待値が満たされない場合には、Test-XmlBySchemaのバリデーションでこれらの問題がエラーとして報告されません。これを解決しましょう。

下の表は、検証エラーとは見なされず、Test-XmlBySchemaで見逃されるいくつかのケースを示しています。各行の右側の列には、スキーマに必要な保護を追加するための手動の変更方法が示されています。

A modified version of the schema file, with all protections added, is shown right after the table.

デフォルトのスキーマに設定を追加する-例

Required Behavior Relevant setting on the schema file
At least one Person element Set the minOccurs value of the Person element to 1
Make Name, Country and IsAdmin attributes mandatory Add use=”required” to each of these attributes declaration
Allow only true/false values for IsAdmin Set type=”xs:boolean” for the IsAdmin declaration
Allow only Country names between 3 and 40 characters Use the xs:restriction (see detailed explanation after the schema text)
<?xml version="1.0" standalone="yes"?>
<xs:schema id="People" xmlns="" xmlns:xs="<http://www.w3.org/2001/XMLSchema>" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="People" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="Person">
          <xs:complexType>
            <xs:attribute name="Name" type="xs:string" use="required" />
	    <xs:attribute name="Country" use="required">
	      <xs:simpleType>
		<xs:restriction base="xs:string">
		  <xs:minLength value="3"/>
		  <xs:maxLength value="40"/>
		</xs:restriction>
	      </xs:simpleType>
	    </xs:attribute>	
            <xs:attribute name="IsAdmin" type="xs:boolean" use="required" />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

例でIsAdmin属性にブール制約がある場合、その値は小文字 trueまたはfalse

xs:restrictionを使用した文字列の長さの検証

文字列の長さの検証は少し複雑です。上記の変更スキーマの一部として表示されているため、もう少し詳しく説明します。

Country属性の元のスキーマ項目(use=”required”を手動で追加した後)は、以下のようになります:

<xs:attribute name="Country" type="xs:string" use="required" />

長さの保護を追加するには、<xs:simpleType>要素を追加し、その中に<xs:restriction base="xs:string">を追加します。この制約には、xs:minLengthxs:minLengthで宣言された必要な制限が含まれています。

これらの変更により、最終的なxs:attributeの宣言は、1行から8行の巨大なノードになりました:

<xs:attribute name="Country">
  <xs:simpleType>
	<xs:restriction base="xs:string">
	  <xs:minLength value="3"/>
	  <xs:maxLength value="40"/>
	</xs:restriction>
  </xs:simpleType>
</xs:attribute>

上記の説明を聞いても頭がくらくらしない場合、検証を実際に見る権利を得ました。それを行うために、意図的に値Canadaを2文字の音節に短縮してみましょう:Ca

短い国名が設定され、MorePeople.xmlが保存されたら、以下の検証コマンドを実行する準備ができます。

Test-XmlBySchema -XmlFile 'C:\Work\MorePeople.xml' -SchemaPath 'C:\Work\People.xsd' -Verbose

そして、結果は実際に複雑なスキーマがその役割を果たしたことを示しています:

A string length error detected by the schema validation

XMLスキーマの検証は複雑さを増し、特に正規表現と組み合わせると思いつくパターンをほぼすべて検証することができます。

Source:
https://adamtheautomator.com/powershell-parse-xml/