The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

解決 XmlWriter 輸出的 XML 無法正確設定 Encoding 的問題

這問題我遇到第二次了,第一次遇到時濛濛懂懂的解決了。但不知為何越來越有種不想讓自己迷迷糊糊過一生的感覺,所以拼了命也要將模糊的地帶給釐清,於是又花了好幾個鐘頭把來龍去脈給釐清,避免日後又再度遇到同樣的問題。這是個關於透過 XmlWriter 撰寫 XML 的問題,不管我如何設定 Encoding 編碼都無法改變輸出的編碼,經過一番努力辯證之後才徹底釐清觀念,以後也不會再混淆了。

先準備 XmlWriterSettings 類別定義輸出 XML 的排版格式與編碼,其中包括文字編碼的 Encoding 屬性:

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = false;
settings.NewLineOnAttributes = true;
settings.Encoding = Encoding.GetEncoding("big5");

但是這裡的 XmlWriterSettings.Encoding 屬性根本就是個幌子,不管設定什麼 Encoding 輸出的結果一律都是 UTF-16 編碼。

接著,我們繼續把 XmlWriter 的程式碼寫完如下:

StringBuilder sb = new StringBuilder();
XmlWriter writer = XmlWriter.Create(sb, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

Response.Write(sb.ToString());

輸出的結果卻是如下:

<?xml version="1.0" encoding="utf-16"?>
<root>
<person>
<name>游錫堃、喆喆創意</name>
</person>
</root>

原本我們預期的 encoding 不是 big5 嗎?為什麼總是輸出 utf-16 呢?

然後我換個方式寫,將最後得到的 StringBuilder 輸出成字串,並轉成 XmlDocument 物件處理,強迫將輸出編碼修改:

XmlDocument xdoc = new XmlDocument();
xdoc.LoadXml(sb.ToString());

XmlDeclaration decl = (XmlDeclaration)xdoc.FirstChild;
decl.Encoding = "big5";

Response.Write(xdoc.OuterXml);

這是換湯不換藥,因為事實上輸出的編碼還是 UTF-16,只是 <?xml version="1.0" encoding="big5"?> 的 encoding 部分被硬改成 big5 而已,而這當然是錯誤示範,小朋友可千萬不要學,叔叔是有練過的。

這時我又再換一種寫法,改用 Stream 的型別傳入就能夠正確處理 Encoding 屬性了:

using (Stream fs = File.Open("a.xml", FileMode.CreateNew))
{
    XmlWriter writer = XmlWriter.Create(fs, settings);

    writer.WriteStartElement("root");
    writer.WriteStartElement("person");
    writer.WriteElementString("name", "游錫堃、喆喆創意");
    writer.WriteEndElement();
    writer.WriteEndElement();
    writer.Flush();

    fs.Close();
}

當然,如果我用 Response.OutputStream 直接輸出到網頁,當然也是可行的:

XmlWriter writer = XmlWriter.Create(Response.OutputStream, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

輸出的結果正如預期的是:

<?xml version="1.0" encoding="big5"?>
<root>
<person>
<name>游錫&#x5803;、&#x5586;&#x5586;創意</name>
</person>
</root>

以上這兩個成功案例,都是用 Stream 型別傳入才成功的,但一個是直接輸出成檔案、一個是直接輸出到網頁(Response.OutputStream),如果我們需要將 XmlWriter 寫入的資料暫時放在記憶體中的話,就必須利用 MemoryStream 的方式當成緩衝區(buffer)。

我首先做了以下嘗試 (錯誤示範):

Encoding enc = Encoding.GetEncoding("big5");

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = false;
settings.NewLineOnAttributes = true;
settings.Encoding = enc;

StringBuilder sb = new StringBuilder();

MemoryStream ms = new MemoryStream();
StreamWriter sw = new StreamWriter(ms);

XmlWriter writer = XmlWriter.Create(sw, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

string result = enc.GetString(ms.ToArray());

Response.ContentType = "text/xml";
Response.Write(result);

但是輸出的結果卻是亂碼:

<?xml version="1.0" encoding="utf-8"?>
<root>
<person>
<name>皜賊?s€????/name>
</person>
</root>

從第一行看到就可以知道,其實 XmlWriter 還是以 UTF-8 當成處理所有 XmlWriter 內部處理的字元,所以才無法被 enc.GetString() 方法轉換成正確的 Big5 文字。

最後,我試著在 StreamWriter 加上 Encoding 參數 (如下 enc 部分),就可以正常處理所有 Big5 的字元,不過卻無法正確處理「非 Big5 字集」的字元。

StreamWriter sw = new StreamWriter(ms, enc);

輸出的結果是:

<?xml version="1.0" encoding="big5"?>
<root>
<person>
<name>游錫?、??創意</name>
</person>
</root>

到最後,我才發現我被我自己先前的程式碼給「制約」了,原來我一直在用 StreamWriter 在過濾所有應該被寫入 Stream 的文字,這反而限制了所有應該被正常處理的文字,所以才會導致有部分非字集內的文字沒有正常被轉成 Entities 的形式。

我最後把 StreamWriter 這一行給抽掉,所有的編碼的問題就解決了。以下是終極正確版:

Encoding enc = Encoding.GetEncoding("big5");

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = false;
settings.NewLineOnAttributes = true;
settings.Encoding = enc;

MemoryStream ms = new MemoryStream();

XmlWriter writer = XmlWriter.Create(ms, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

string result = enc.GetString(ms.ToArray());

Response.ContentType = "text/xml";
Response.Write(result);

心得結論

相關連結