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

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

先準備 [XmlWriterSettings 類別](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwritersettings.aspx)定義輸出 XML 的排版格式與編碼，其中包括文字編碼的 [Encoding](http://msdn.microsoft.com/zh-tw/library/system.text.encoding.aspx) 屬性:

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

但是這裡的 [XmlWriterSettings.Encoding 屬性](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwritersettings.encoding.aspx)根本就是個幌子，不管設定什麼 [Encoding](http://msdn.microsoft.com/zh-tw/library/system.text.encoding.aspx) 輸出的結果一律都是 **UTF-16** 編碼。

接著，我們繼續把 [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 的程式碼寫完如下：

```csharp
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](http://msdn.microsoft.com/zh-tw/library/system.xml.xmldocument.aspx) 物件處理，強迫將輸出編碼修改：

```csharp
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](http://msdn.microsoft.com/zh-tw/library/system.text.encoding.aspx) 屬性了：

```csharp
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 直接輸出到網頁，當然也是可行的：

```csharp
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](http://msdn.microsoft.com/zh-tw/library/system.web.httpresponse.outputstream.aspx))，如果我們需要將 [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 寫入的資料暫時放在記憶體中的話，就必須利用 MemoryStream 的方式當成緩衝區(buffer)。

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

```csharp
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](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 還是以 UTF-8 當成處理所有 [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 內部處理的字元，所以才無法被 [enc.GetString()](http://msdn.microsoft.com/zh-tw/library/744y86tc.aspx) 方法轉換成正確的 Big5 文字。

最後，我試著在 [StreamWriter](http://msdn.microsoft.com/zh-tw/library/system.io.streamwriter.aspx) 加上 Encoding 參數 (如下 enc 部分)，就可以正常處理所有 Big5 的字元，不過卻無法正確處理「非 Big5 字集」的字元。

```
StreamWriter sw = new StreamWriter(ms, enc);
```

輸出的結果是：

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

到最後，我才發現我被我自己先前的程式碼給「制約」了，原來我一直在用 [StreamWriter](http://msdn.microsoft.com/zh-tw/library/system.io.streamwriter.aspx) 在過濾所有應該被寫入 [Stream](http://msdn.microsoft.com/zh-tw/library/system.io.stream.aspx) 的文字，這反而限制了所有應該被正常處理的文字，所以才會導致有部分非字集內的文字沒有正常被轉成 Entities 的形式。

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

```csharp
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);
```

**心得結論**

-   [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 在透過 [Create 方法](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.create.aspx)建立 XmlWriter 物件時有許多多載，也提供許多彈性讓你建立適當的 XML 文件。
-   在使用 [Create(TextWriter, XmlWriterSettings)](http://msdn.microsoft.com/zh-tw/library/ms162620.aspx) 建立 [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 物件時，所有處理的文字都會考量到文字編碼，而寫這裡定義的編碼會覆蓋原本在 [XmlWriterSettings](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwritersettings.aspx) 的定義。
-   在使用 [Create(StringBuilder, XmlWriterSettings)](http://msdn.microsoft.com/zh-tw/library/ms162619.aspx) 建立 [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 物件時，由於 [StringBuilder](http://msdn.microsoft.com/zh-tw/library/system.text.stringbuilder.aspx) 內部皆以 UTF-16 編碼做處理，所以所有「流過」 [StringBuilder](http://msdn.microsoft.com/zh-tw/library/system.text.stringbuilder.aspx) 的文字都以 UTF-16 編碼做處理，且會覆蓋原本在 [XmlWriterSettings](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwritersettings.aspx) 的定義。
-   我個人認為使用 [Create(Stream, XmlWriterSettings)](http://msdn.microsoft.com/zh-tw/library/ms162617.aspx) 是比較沒問題的作法，可以盡量充分運用。
-   在 MSDN 的 [.NET Framework 開發人員手冊](http://msdn.microsoft.com/zh-tw/library/k1s94fta.aspx) 的 [使用 XmlWriter](http://msdn.microsoft.com/zh-tw/library/tx3wa6ka.aspx) 有段章節有提到**注意事項**提醒在 Microsoft .NET Framework 2.0 版中，建議使用 [Create 方法](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.create.aspx)建立 [XmlWriter](http://msdn.microsoft.com/zh-tw/library/system.xml.xmlwriter.aspx) 物件。  
    ![在 Microsoft .NET Framework 2.0 版中，建議使用 Create 方法建立 XmlWriter 物件。](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/229d17e9a156dce05c81-image.png "在 Microsoft .NET Framework 2.0 版中，建議使用 Create 方法建立 XmlWriter 物件。")

**相關連結**

-   [使用 XmlWriter 寫入 XML](http://msdn.microsoft.com/zh-tw/library/4d1k42hb.aspx)
-   [HOW TO: Use the Encoding Property with System.Xml Classes in Visual C# .NET](https://web.archive.org/web/20150221095419/http://support.microsoft.com:80/kb/308061)
