當年紅透半邊天的 XML,如今已成過氣老人,但地球的某些角落仍是由他把關,不小心碰上了,馬上能感受他板著臉孔列出一堆規矩,並不時嚴厲指正:這裡格式錯了、那邊不能這樣寫,當年不偷快的回憶全都回來了,其中最讓我暈頭轉向的莫過於 Namespace。

前天自己牙給,想土砲製作 ePub 電子書,一看規格,嘩~ 滿滿的 XML 呀! 雖然書本內容是用 HTML + CSS,但 HTML 必須待合 XHTML 格式,然後像是書本目錄、檔案清單等定義也清一色走 XML,於是狠狠地「被溫習」 .NET XML 文件處理。.NET 3.5 加入 System.XML.Linq 後,讀寫操作 XML 比方便非常多,但若 XML 涉及 Namespace,就有些小技巧要知道。

直接用我開發遇到三個實例來展示。

首先登場的是 toc.ncx 目錄 XML 檔:

<?xml version='1.0' encoding='utf-8'?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="zh-TW">
  <head>
    <meta content="b1ce20ed-9214-470c-97d0-2648fc2f7760" name="dtb:uid"/>
    <meta content="2" name="dtb:depth"/>
    <meta content="calibre (3.44.0)" name="dtb:generator"/>
    <meta content="0" name="dtb:totalPageCount"/>
    <meta content="0" name="dtb:maxPageNumber"/>
  </head>
  <docTitle>
    <text></text>
  </docTitle>
  <navMap>
   </navMap>
</ncx>

我想在 navMap 加入 navPoint,變成:

  <navMap>
    <navPoint id="ch1" playOrder="1">
      <navLabel>
        <text>ePub 格式解析</text>
      </navLabel>
      <content src="format.html"/>
    </navPoint>
   </navMap>

用 XDocument 來做,直覺會寫成這樣:

    static void Example1()
    {
        var doc = XDocument.Load("toc.ncx");
        doc.Root.Element("navMap").Add(
            new XElement("navPoint",
                new XAttribute("id", "ch1"),
                new XAttribute("playOrder", "1"),
                new XElement("navLabel",
                    new XElement("text", "ePub 格式解析")
                ),
                new XElement("content",
                    new XAttribute("src", "format.html")
                )
            )
        );
        Console.WriteLine(doc.ToString());
    }

結果程式直接爆炸,原因是 doc.Root.Element("navMap") 沒找到元素:

而問題根源在於 toc.ncx 的 XML 宣告 xmlns="http://www.daisy.org/z3986/2005/ncx/" 為預設命名空間,我們在尋找元素時也得加上命名空間才會相符,故要改成這樣:

XNamespace ns = "http://www.daisy.org/z3986/2005/ncx/";
doc.Root.Element(ns + "navMap").Add( //...略

再跑一次,程式不會出錯了,但結果有問題 - navPoint 多了一個 xmlns="":

因此,正確寫法是所有 new XElement("elementName") 都要改成 new XElement(ns + "elementName"):

接著來看有多個 Namespace 的 content.opf,我打算修改 dc:title 並在 manifest 加入一個 item:

<?xml version='1.0' encoding='utf-8'?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0">
  <metadata 
	xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata" 
	xmlns:dc="http://purl.org/dc/elements/1.1/" 
	xmlns:dcterms="http://purl.org/dc/terms/"
	xmlns:opf="http://www.idpf.org/2007/opf" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <dc:language>zh</dc:language>
    <dc:title>ePub 手作練習</dc:title>
    <dc:creator opf:file-as="darkthread" opf:role="aut">黑暗執行緒</dc:creator>
    <meta name="cover" content="cover"/>
	<dc:identifier id="uuid_id" opf:scheme="uuid">b1ce20ed-9214-470c-97d0-2648fc2f7760</dc:identifier>
    <dc:contributor opf:role="bkp"></dc:contributor>
    <dc:publisher>黑暗出版社</dc:publisher>
  </metadata>
  <manifest>
    <item href="cover.jpg" id="cover" media-type="image/jpeg"/>
    <item href="titlepage.xhtml" id="titlepage" media-type="application/xhtml+xml"/>
    <item href="page_styles.css" id="page_css" media-type="text/css"/>
    <item href="stylesheet.css" id="css" media-type="text/css"/>
    <item href="toc.ncx" id="ncx" media-type="application/x-dtbncx+xml"/>
  </manifest>
  <spine toc="ncx">
    <itemref idref="titlepage"/>
  </spine>
  <guide>
    <reference href="titlepage.xhtml" title="Cover" type="cover"/>
  </guide>
</package>

有了之前的經驗,多宣告幾組 XNamespace 並正確搭配,即可輕鬆過關:

static void Example3()
{
    var doc = XDocument.Load("content.opf");
    XNamespace ns = "http://www.idpf.org/2007/opf";
    XNamespace nsDc = "http://purl.org/dc/elements/1.1/";
    doc.Root.Element(ns + "metadata")
        .Element(nsDc + "title").Value = "XML練習";
    doc.Root.Element(ns + "manifest").Add(
        new XElement(ns + "item",
            new XAttribute("href", "darkthread.png"),
            new XAttribute("id", "logo"),
            new XAttribute("media-type", "image/png")
        )
    );
    Console.WriteLine(doc.ToString());
}

最後這個例子,是要將 XHTML 模版中 <div class="content"></div> 內容換成不同文章的 HTML:

<?xml version='1.0' encoding='utf-8'?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-TW" xml:lang="zh-TW">

	<head>
		<title></title>
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<link href="../stylesheet.css" rel="stylesheet" type="text/css" />
		<link href="../page_styles.css" rel="stylesheet" type="text/css" />
	</head>

	<body>
		<div class="page">
			<h1 class="book-title">$Title</h1>
			<div class="content">
			</div>
		</div>
	</body>
</html>

XElement 並不像網頁可以直接置換 InnerHTML,常見替代做法是用 XElement.Parse() 將 XML 轉成 XElement 再加入(這樣還有個好處,若 HTML 內容不符合 XML 規範,Parse() 時會提早發現):

static void Example4()
{
    var doc = XDocument.Load("post_template.html");
    string html = @"
<ul>
<li>月餅</li>
<li>柚子</li>
<li>烤肉架</li>
</ul>";
    XNamespace ns = "http://www.w3.org/1999/xhtml";
    doc.Root.Element(ns + "body").Element(ns + "div").Element(ns + "div")
        .Add(
            XElement.Parse(html)
        );
    Console.WriteLine(doc.ToString());
}

HTML 內容成功插入,但討厭的 Namespace 問題又來了:

查了一下,XElement.Parse() 時無法指定 Namespace,大家慣用的解法是找出所有 Node 把 Namespace 換掉:

var elem = XElement.Parse(html);
elem.DescendantsAndSelf().ToList().ForEach(o =>
{
    o.Name = ns + o.Name.LocalName;
});
doc.Root.Element(ns + "body").Element(ns + "div").Element(ns + "div").Add(elem);

搞定收工!

Tips of how to process XML with namespace definition by System.Xml.Linq.


Comments

# by Whatever

辛苦大師了 雖然以現今的眼光來看,xml 比起 Json 是笨重得多,但是他的配套機制也是最完整的,所以我依然愛它。 包含可以讓 xml 變成 strong-type 那般定義嚴格的 xsd,還有讓 xml 變變變轉轉轉的 xslt,以及 webservice 的 wedlock... 或許我對 Json 認知粗略,請問 Json 上有沒有上述配套方案可用?

# by Whatever

...*wsdl*... 聰明的輸入法幫我換字了 QQ

# by Jeffrey

to Whatever, JSON 要做驗證的話有 JSON Schema https://json-schema.org/understanding-json-schema/ ,由於 JSON 可以輕易轉成物件陣列,搭配各語言平台的 Template 機制就可以轉成任何其他格式(例如微軟的 T4、Razor,我自己覺得比 XSLT 好寫超過十倍吧! 早前 Sharepoint 用很多 XSLT,現在想還是覺得噁心),至於 wsdl,對映到 WebAPI 應該像是 Swagger 這類的解決方案(自動產生線上測試網頁、Client 端程式碼)。依我個人體驗,JSON 的經濟生態系統完整,幾乎想到的事都有人做了,而且仍在蓬勃發展,相較之下 XML 持續改良的動能已很有限了。

Post a comment


44 + 20 =