はじめに
PowerShellは.NET Frameworkを基盤としているため、XML処理において非常に強力な機能を備えています。本記事では、PowerShellによるXML処理の基本から応用までを、実用的なコード例とともに解説します。
動作確認は、Windows 11上のPowerShell 5.1環境で行いました。
1. XMLの基本操作
1.1 XMLの読み込みと基本的な操作
# XMLデータの定義
$xmlData = @"
<Recipe>
<Name>スパイスカレー</Name>
<CookingTime>60</CookingTime>
<Ingredients>
<Ingredient>
<Name>玉ねぎ</Name>
<Amount>2個</Amount>
</Ingredient>
<Ingredient>
<Name>鶏肉</Name>
<Amount>300g</Amount>
</Ingredient>
</Ingredients>
</Recipe>
"@
# XMLの読み込み
try {
[xml]$xml = $xmlData
Write-Host "レシピ名: $($xml.Recipe.Name)"
Write-Host "調理時間: $($xml.Recipe.CookingTime)分"
# 材料の取得
Write-Host "材料:"
foreach ($ingredient in $xml.Recipe.Ingredients.Ingredient) {
Write-Host "- $($ingredient.Name): $($ingredient.Amount)"
}
}
catch {
Write-Error "XML解析エラー: $($_.Exception.Message)"
}
1.2 XPath検索とクエリ
# カレーメニューのXML
$menuXml = @"
<CurryMenu>
<Curry spice-level="3">
<Name>インドカレー</Name>
<Price>1200</Price>
</Curry>
<Curry spice-level="2">
<Name>タイカレー</Name>
<Price>980</Price>
</Curry>
<Curry spice-level="1">
<Name>日本風カレー</Name>
<Price>800</Price>
</Curry>
</CurryMenu>
"@
[xml]$menu = $menuXml
# XPath検索
$spicyCurries = $menu.SelectNodes("//Curry[@spice-level='3']")
Write-Host "辛さレベル3のカレー:"
foreach ($curry in $spicyCurries) {
Write-Host "- $($curry.Name): $($curry.Price)円"
}
# 価格フィルタリング
$expensiveCurries = $menu.SelectNodes("//Curry[Price >= 1000]")
Write-Host "`n高価格カレー:"
foreach ($curry in $expensiveCurries) {
Write-Host "- $($curry.Name): $($curry.Price)円"
}
2. XMLスキーマ(XSD)検証
# XSDスキーマの定義
$xsdSchema = @"
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Recipe">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
<xs:element name="CookingTime" type="xs:int"/>
<xs:element name="Ingredients">
<xs:complexType>
<xs:sequence>
<xs:element name="Ingredient" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
<xs:element name="Amount" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
"@
# スキーマ検証関数
function Test-XmlSchema {
param(
[string]$XmlString,
[string]$SchemaString
)
try {
# スキーマの作成
$schema = [System.Xml.Schema.XmlSchema]::Read(
[System.IO.StringReader]::new($SchemaString),
$null
)
# XML文書の作成
$xmlDoc = [System.Xml.XmlDocument]::new()
$xmlDoc.LoadXml($XmlString)
$xmlDoc.Schemas.Add($schema)
# 検証
$errors = @()
$xmlDoc.Validate({
param($sender, $e)
$errors += $e.Message
})
if ($errors.Count -eq 0) {
Write-Host "✅ XMLは有効です" -ForegroundColor Green
return $true
} else {
Write-Host "❌ XMLは無効です" -ForegroundColor Red
$errors | ForEach-Object { Write-Host " エラー: $_" -ForegroundColor Red }
return $false
}
}
catch {
Write-Host "❌ 検証エラー: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
# 有効なXML
$validXml = @"
<Recipe>
<Name>キーマカレー</Name>
<CookingTime>40</CookingTime>
<Ingredients>
<Ingredient>
<Name>ひき肉</Name>
<Amount>300g</Amount>
</Ingredient>
</Ingredients>
</Recipe>
"@
# 無効なXML(CookingTimeが文字列)
$invalidXml = @"
<Recipe>
<Name>ビーフカレー</Name>
<CookingTime>とても長い時間</CookingTime>
<Ingredients>
<Ingredient>
<Name>牛肉</Name>
<Amount>500g</Amount>
</Ingredient>
</Ingredients>
</Recipe>
"@
Write-Host "=== 有効なXMLのテスト ==="
Test-XmlSchema -XmlString $validXml -SchemaString $xsdSchema
Write-Host "`n=== 無効なXMLのテスト ==="
Test-XmlSchema -XmlString $invalidXml -SchemaString $xsdSchema
3. シリアライズ・デシリアライズ
3.1 PSCustomObjectを使った構造化データ処理
# PowerShellオブジェクトの定義
class CurryIngredient {
[string]$Name
[string]$Amount
CurryIngredient([string]$name, [string]$amount) {
$this.Name = $name
$this.Amount = $amount
}
}
class CurryRecipe {
[string]$Name
[int]$CookingTime
[CurryIngredient[]]$Ingredients
CurryRecipe([string]$name, [int]$cookingTime, [CurryIngredient[]]$ingredients) {
$this.Name = $name
$this.CookingTime = $cookingTime
$this.Ingredients = $ingredients
}
}
# XMLからオブジェクトへの変換
function ConvertFrom-RecipeXml {
param([xml]$XmlData)
try {
$ingredients = @()
foreach ($ing in $XmlData.Recipe.Ingredients.Ingredient) {
$ingredients += [CurryIngredient]::new($ing.Name, $ing.Amount)
}
return [CurryRecipe]::new(
$XmlData.Recipe.Name,
[int]$XmlData.Recipe.CookingTime,
$ingredients
)
}
catch {
Write-Error "XML変換エラー: $($_.Exception.Message)"
return $null
}
}
# オブジェクトからXMLへの変換
function ConvertTo-RecipeXml {
param([CurryRecipe]$Recipe)
try {
$xmlDoc = [System.Xml.XmlDocument]::new()
$root = $xmlDoc.CreateElement("Recipe")
# 基本情報の追加
$nameNode = $xmlDoc.CreateElement("Name")
$nameNode.InnerText = $Recipe.Name
$root.AppendChild($nameNode)
$timeNode = $xmlDoc.CreateElement("CookingTime")
$timeNode.InnerText = $Recipe.CookingTime
$root.AppendChild($timeNode)
# 材料の追加
$ingredientsNode = $xmlDoc.CreateElement("Ingredients")
foreach ($ingredient in $Recipe.Ingredients) {
$ingNode = $xmlDoc.CreateElement("Ingredient")
$ingNameNode = $xmlDoc.CreateElement("Name")
$ingNameNode.InnerText = $ingredient.Name
$ingNode.AppendChild($ingNameNode)
$ingAmountNode = $xmlDoc.CreateElement("Amount")
$ingAmountNode.InnerText = $ingredient.Amount
$ingNode.AppendChild($ingAmountNode)
$ingredientsNode.AppendChild($ingNode)
}
$root.AppendChild($ingredientsNode)
$xmlDoc.AppendChild($root)
# 整形して返す
$stringWriter = [System.IO.StringWriter]::new()
$xmlWriter = [System.Xml.XmlTextWriter]::new($stringWriter)
$xmlWriter.Formatting = [System.Xml.Formatting]::Indented
$xmlDoc.WriteTo($xmlWriter)
return $stringWriter.ToString()
}
catch {
Write-Error "XML生成エラー: $($_.Exception.Message)"
return $null
}
}
# テスト
Write-Host "=== デシリアライズテスト ==="
[xml]$testXml = $xmlData
$recipe = ConvertFrom-RecipeXml -XmlData $testXml
if ($recipe) {
Write-Host "レシピ名: $($recipe.Name)"
Write-Host "調理時間: $($recipe.CookingTime)分"
Write-Host "材料:"
foreach ($ingredient in $recipe.Ingredients) {
Write-Host "- $($ingredient.Name): $($ingredient.Amount)"
}
}
Write-Host "`n=== シリアライズテスト ==="
$newRecipe = [CurryRecipe]::new(
"グリーンカレー",
30,
@(
[CurryIngredient]::new("ココナッツミルク", "400ml"),
[CurryIngredient]::new("グリーンカレーペースト", "大さじ2"),
[CurryIngredient]::new("鶏肉", "250g")
)
)
$xmlOutput = ConvertTo-RecipeXml -Recipe $newRecipe
Write-Host $xmlOutput
3.2 ハッシュテーブルによるシンプルなアプローチ
# ハッシュテーブルを使ったシンプルな変換
function ConvertFrom-XmlToHashtable {
param([xml]$XmlData)
$result = @{
Name = $XmlData.Recipe.Name
CookingTime = [int]$XmlData.Recipe.CookingTime
Ingredients = @()
}
foreach ($ingredient in $XmlData.Recipe.Ingredients.Ingredient) {
$result.Ingredients += @{
Name = $ingredient.Name
Amount = $ingredient.Amount
}
}
return $result
}
# JSON形式での出力
[xml]$sampleXml = $xmlData
$hashtable = ConvertFrom-XmlToHashtable -XmlData $sampleXml
$jsonOutput = $hashtable | ConvertTo-Json -Depth 3
Write-Host "=== JSON変換結果 ==="
Write-Host $jsonOutput
4. カレー注文システム
class CurryOrderSystem {
[hashtable]$Menu
[int]$OrderCounter
CurryOrderSystem() {
$this.Menu = @{
"001" = @{ Name = "チキンカレー"; Price = 1000; SpiceLevel = 2 }
"002" = @{ Name = "ビーフカレー"; Price = 1200; SpiceLevel = 3 }
"003" = @{ Name = "野菜カレー"; Price = 800; SpiceLevel = 1 }
"004" = @{ Name = "シーフードカレー"; Price = 1400; SpiceLevel = 2 }
}
$this.OrderCounter = 1
}
[hashtable] CreateOrder([string]$CustomerName, [hashtable[]]$Items) {
$orderItems = @()
$total = 0
foreach ($item in $Items) {
$menuItem = $this.Menu[$item.Id]
if ($menuItem) {
$subtotal = $menuItem.Price * $item.Quantity
$orderItems += @{
Menu = $menuItem
Quantity = $item.Quantity
Subtotal = $subtotal
}
$total += $subtotal
}
}
$order = @{
OrderId = "ORD{0:D4}" -f $this.OrderCounter
CustomerName = $CustomerName
Items = $orderItems
TotalAmount = $total
OrderTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
$this.OrderCounter++
return $order
}
[string] OrderToXml([hashtable]$Order) {
$xmlDoc = [System.Xml.XmlDocument]::new()
$root = $xmlDoc.CreateElement("Order")
$root.SetAttribute("id", $Order.OrderId)
$root.SetAttribute("timestamp", $Order.OrderTime)
# 顧客情報
$customerNode = $xmlDoc.CreateElement("Customer")
$customerNode.InnerText = $Order.CustomerName
$root.AppendChild($customerNode)
# 注文アイテム
$itemsNode = $xmlDoc.CreateElement("Items")
foreach ($item in $Order.Items) {
$itemNode = $xmlDoc.CreateElement("Item")
$nameNode = $xmlDoc.CreateElement("Name")
$nameNode.InnerText = $item.Menu.Name
$itemNode.AppendChild($nameNode)
$quantityNode = $xmlDoc.CreateElement("Quantity")
$quantityNode.InnerText = $item.Quantity
$itemNode.AppendChild($quantityNode)
$subtotalNode = $xmlDoc.CreateElement("Subtotal")
$subtotalNode.InnerText = $item.Subtotal
$itemNode.AppendChild($subtotalNode)
$itemsNode.AppendChild($itemNode)
}
$root.AppendChild($itemsNode)
# 合計金額
$totalNode = $xmlDoc.CreateElement("Total")
$totalNode.InnerText = $Order.TotalAmount
$root.AppendChild($totalNode)
$xmlDoc.AppendChild($root)
# 整形して返す
$stringWriter = [System.IO.StringWriter]::new()
$xmlWriter = [System.Xml.XmlTextWriter]::new($stringWriter)
$xmlWriter.Formatting = [System.Xml.Formatting]::Indented
$xmlDoc.WriteTo($xmlWriter)
return $stringWriter.ToString()
}
}
# 使用例
$system = [CurryOrderSystem]::new()
$orderItems = @(
@{ Id = "001"; Quantity = 2 },
@{ Id = "003"; Quantity = 1 }
)
$order = $system.CreateOrder("山田さん", $orderItems)
Write-Host "=== 注文情報 ==="
Write-Host "注文ID: $($order.OrderId)"
Write-Host "顧客: $($order.CustomerName)"
Write-Host "合計: $($order.TotalAmount)円"
Write-Host "`n=== XML出力 ==="
$xmlOutput = $system.OrderToXml($order)
Write-Host $xmlOutput
5. エラーハンドリングとベストプラクティス
# エラーハンドリングの例
function Invoke-SafeXmlOperation {
param(
[string]$Operation,
[scriptblock]$ScriptBlock
)
try {
Write-Verbose "開始: $Operation"
$result = & $ScriptBlock
Write-Verbose "完了: $Operation"
return $result
}
catch [System.Xml.XmlException] {
Write-Error "XML形式エラー in $Operation`: $($_.Exception.Message)"
}
catch [System.ArgumentException] {
Write-Error "引数エラー in $Operation`: $($_.Exception.Message)"
}
catch {
Write-Error "予期しないエラー in $Operation`: $($_.Exception.Message)"
}
}
# 使用例
$result = Invoke-SafeXmlOperation -Operation "XMLパース" -ScriptBlock {
[xml]$testXml = "<invalid>xml<content>"
return $testXml
}
まとめ
PowerShellでは、.NET Frameworkの機能を活用することで、効率的かつ堅牢なXML処理が可能です。用途に応じて、型安全性を重視するならクラスベース、シンプルさを求めるならハッシュテーブルベースのアプローチを選ぶことで、柔軟に対応できます。特にシステム管理やデータ処理の自動化においては、適切なエラーハンドリングと組み合わせることで、信頼性の高いスクリプトを構築できるのが大きな強みです。
参考情報