書到用時方恨少,臨時有個需求要從 XML 查詢特定一筆資料,打算用 PowerShell 快速秒殺,卻卡住不知該怎麼寫,只能開了 Visual Studio 用 C# 搞定。

事後檢討,武功招式要能活用,得捲起袖子實際操演熟練,沒有看完教學上場就能出招制敵的好事兒,至少,我不是那種武學奇才,所以有了這篇。

以下是本次練習用的 test.xml:

<?xml version="1.0"?>
<root>
    <meta>
        <title>XML Sample</title>
        <revisions>
            <version no="1.0">2022-09-02</version>
            <version no="1.1">2022-09-03</version>
        </revisions>
    </meta>
    <category name="Language">
        <item>C#</item>
        <item>PowerShell</item>
        <item>JavaScript</item>
    </category>
    <category name="OS">
        <item>Windows</item>
        <item>macOS</item>
        <item>Linux</item>
    </category>
</root>

在 PowerShell 讀取 XML 檔,最簡單的起手式是 [Xml]$xd = (Get-Content .\text.xml ),$xd 將是一個 XmlDocument 物件,如果你不想學任何 PowerShell 技巧,用 .NET 寫法也成。例如:

[xml]$doc = Get-Content test.xml
# $doc 背後為 XmlDocument,可呼叫原有 API 以 .NET 方式操作
$meta = $doc.DocumentElement.FirstChild;
Write-Host '-- Revisions --' -ForegroundColor Cyan
foreach ($ver in $meta.SelectSingleNode('revisions').SelectNodes('version')) {
    $ver.GetAttribute("no")
    $ver.InnerText
}
Write-Host '-- Items --' -ForegroundColor Cyan
foreach ($item in $doc.SelectNodes('//item')) {
    $item.InnerText
}

不過,PowerShell 有個內建指令 Select-Xml,有更簡單的寫法,以上邏輯可以簡化成:

[xml]$doc = Get-Content test.xml
Write-Host '-- Revisions --' -ForegroundColor Cyan
Select-Xml -Xml $doc -XPath '/*/*[1]/revisions/version' | ForEach-Object { 
    $_.Node.no
    $_.Node.InnerText
}
Write-Host '-- Items --' -ForegroundColor Cyan
Select-Xml -Xml $doc -XPath '//item' | ForEach-Object {
    $_.Node.InnerText
}

得到一模一樣的結果:

不過,.NET LINQ 寫多了,老覺得 XPath 寫法是上個世紀的產物,直接用名稱存取內容才是王道呀!

沒問題,PowerShell 也支援用 $doc.root.meta 直接找到 <meta> 結節,但要先了解一下規則。

以 test.xml 為例,$doc 有兩個屬性 xml 及 root。root 則有 meta 跟 category 兩個屬性,前者是一個節點物件,後者則是集合,category 因為包含 name Attribute,PowerShell 會貼心顯示成 {Laguage, OS},否則會是 {category, category}。

meta 下又有 title 及 revisions 兩個屬性;$doc.root.meat.revisions 內含的 <version> 集合會變成 version 屬性,因沒有 name Attribute,顯示為 {version, version}。

最後,$doc.root.meta.revisions.version 會展開集合,每一筆包含 no Attribute 及 #text 內文。

另外,要存取節點內文,分為兩種情況,若節點沒有 Attribute,給節點名稱 PowerShell 便會傳回字串;若則會傳回節點物件,包含 Attribute 及 #text,此時要用 InnerText 取值:

綜合以上,將程式再簡化為:

[xml]$doc = Get-Content test.xml
Write-Host '-- Revisions --' -ForegroundColor Cyan
$doc.root.meta.revisions.version| ForEach-Object { 
    $_.no
    $_.InnerText
}
Write-Host '-- Items --' -ForegroundColor Cyan
$doc.root.category | ForEach-Object {
    $_.item
}

練習完畢。

Tips of using PowerShell to parse and query XML.


Comments

Be the first to post a comment

Post a comment