1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerShellでXML処理とシリアライズ・デシリアライズ入門

Posted at

はじめに

image.png

PowerShellは.NET Frameworkを基盤としているため、XML処理において非常に強力な機能を備えています。本記事では、PowerShellによるXML処理の基本から応用までを、実用的なコード例とともに解説します。

動作確認は、Windows 11上のPowerShell 5.1環境で行いました。

image.png

1. XMLの基本操作

image.png

image.png

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)"
}

image.png

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)円"
}

image.png

2. XMLスキーマ(XSD)検証

image.png

# 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

image.png

3. シリアライズ・デシリアライズ

image.png

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

image.png

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

image.png

4. カレー注文システム

image.png

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

image.png

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
}

まとめ

image.png

PowerShellでは、.NET Frameworkの機能を活用することで、効率的かつ堅牢なXML処理が可能です。用途に応じて、型安全性を重視するならクラスベース、シンプルさを求めるならハッシュテーブルベースのアプローチを選ぶことで、柔軟に対応できます。特にシステム管理やデータ処理の自動化においては、適切なエラーハンドリングと組み合わせることで、信頼性の高いスクリプトを構築できるのが大きな強みです。

参考情報

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?