http://hi.baidu.com/zeratul_bb/blog/item/3e2a44cf085cf33af8dc61de.html
最近做新聞採集器,需要擷取很多網站的xml,載入個別網站經常出現“(十六進位值 0x1F)是無效的字元”問題,百思不的其解。對於問題網站xml的處理,開始的思路是既然直接用 XmlDocument對象的Load()方法不行,就用LoadXML() ,用HttpWebRequest 擷取url讀到流裡再轉為xml,中間可以加一些非有效字元的過濾處理,但仍然無效,僅僅解決了請求逾時的問題...
問題擱置了1周后,終於在今天解決了。
其實很簡單,只加一條語句就搞定了
XmlDocument doc = new XmlDocument();
doc.Normalize();
// 摘要:
// 將此 XmlNode 下子樹完全深度中的所有 XmlText 節點都轉換成“正常”形式,在這種形式中只有標記(即標記、注釋、處理指示、CDATA
// 節和實體引用)分隔 XmlText 節點,也就是說,沒有相鄰的 XmlText 節點。
以下是轉一位仁兄的貼:
最近碰到一個問題,我的一個把資料庫中記錄的資訊暴露出來的Web Service調用時候出問題了。報下面的錯誤資訊:
System.InvalidOperationException was unhandled
Message="XML 文檔(1, 823)中有錯誤。"
Source="System.Xml"
Message="“”(十六進位值 0x0E)是無效的字元。 行 1,位置 823。"
Source="System.Xml"
當這個錯誤發生時,Web Service 伺服器端不會有任何錯誤,而調用這個 Web Service 的用戶端則會報上述錯誤。
是何原因導致的這個問題呢。
答案很簡單,是WEB Service 暴露的XML文檔中存在低序位非列印 ASCII 字元所致。
我們查看 Web Service 返回的XML 文檔文檔中,會有下面的XML文檔節:其中的 就是低序位 ASCII 字元。 對應的字元如後:
<Value> 在神奇天地裏誰叱吒風雨</Value>
會導致這些問題的 低序位非列印 ASCII 字元包含以下字元:
#x0 - #x8 (ASCII 0 - 8)
#xB - #xC (ASCII 11 - 12)
#xE - #x1F (ASCII 14 - 31)
下面就是一個簡單示範這個問題的控制台程式,
為了簡單起見,這裡沒有建立 WebService, 而是把一個類XML序列化儲存到檔案,然後再把這個檔案還原序列化讀取出來:
其中的這個類的Value值中,放了一個低序位非列印 ASCII 字元。
執行這個控制台程式,就會報異常。“XML 文檔(3, 12)中有錯誤。”
using System;using System.Xml.Serialization;using System.IO;using System.Text;using System.Globalization;namespace TextSerialize{[Serializable]public class MyClass{public string Value { get; set; }}class Program{static void Main(string[] args){string fileName = "d://1.txt";MyClass c = new MyClass();c.Value = string.Format("在神奇{0}天地裏誰叱吒風雨", Convert.ToChar(14));SaveAsXML(c, fileName, Encoding.UTF8);object o = ConvertFileToObject(fileName, typeof(MyClass), Encoding.UTF8);MyClass d = o as MyClass;if (d != null) Console.WriteLine(d.Value);else Console.WriteLine("null");Console.ReadLine();}/// <summary>/// 序列化/// </summary>/// <param name="objectToConvert"></param>/// <param name="path"></param>/// <param name="encoding"></param>public static void SaveAsXML(object objectToConvert, string path, Encoding encoding){if (objectToConvert != null){Type t = objectToConvert.GetType();XmlSerializer ser = new XmlSerializer(t);using (StreamWriter writer = new StreamWriter(path, false, encoding)){ser.Serialize(writer, objectToConvert);writer.Close();}}}/// <summary>/// 還原序列化/// </summary>/// <param name="path"></param>/// <param name="objectType"></param>/// <param name="encoding"></param>/// <returns></returns>public static object ConvertFileToObject(string path, Type objectType, Encoding encoding){object convertedObject = null;if (!string.IsNullOrEmpty(path)){XmlSerializer ser = new XmlSerializer(objectType);using (StreamReader reader = new StreamReader(path, encoding)){convertedObject = ser.Deserialize(reader);reader.Close();}}return convertedObject;}}}
上面提到的Web Service 的那個問題,跟這個示範程式是一樣的。
我們需要被序列化的內容中,存在 低序位非列印 ASCII 字元 時, .net 會給我們正常序列化, 會自動把 低序位非列印 ASCII 字元 轉換成 &#x 編碼的字元(這個XML規範中要求這麼做的)。
但是,還原序列化時候,如果需要還原序列化的內容如果存在 &#x 編碼的字元(映射到低序位非列印 ASCII 字元),則還原序列化就會出錯。
如果解決這個問題呢。
當然,最徹底的解決方案是修改還原序列化的代碼,讓這些字元不會出錯。但這個東西很多時候不歸我們控制。這個方案不可行。
下一個方案就是剔除這些搗亂的字元。
我這裡要給出的方案,是對這些字元序列化時作一次預先處理,還原序列化時,作一次反向處理。
這裡為了示範的更有意義,我這裡處理邏輯就是把 低序位非列印 ASCII 字元 轉換成 &#x 編碼的字元 ,和把&#x 編碼的字元 轉換成 低序位非列印 ASCII 字元。
這樣就可以使用我這裡提供的函數,實現更多的處理邏輯。這兩個函數的代碼如下:
/// <summary>/// 把一個字串中的 低序位 ASCII 字元 替換成 &#x 字元/// 轉換 ASCII 0 - 8 -> � - /// 轉換 ASCII 11 - 12 ->  - /// 轉換 ASCII 14 - 31 ->  - /// </summary>/// <param name="tmp"></param>/// <returns></returns>public static string ReplaceLowOrderASCIICharacters(string tmp){StringBuilder info = new StringBuilder();foreach (char cc in tmp){int ss = (int)cc;if (((ss >= 0) && (ss <= 8)) || ((ss >= 11) && (ss <= 12)) || ((ss >= 14) && (ss <= 32)))info.AppendFormat("&#x{0:X};", ss);else info.Append(cc);}return info.ToString();}/// <summary>/// 把一個字串中的下列字元替換成 低序位 ASCII 字元/// 轉換 � -  -> ASCII 0 - 8/// 轉換  -  -> ASCII 11 - 12/// 轉換  -  -> ASCII 14 - 31/// </summary>/// <param name="input"></param>/// <returns></returns>public static string GetLowOrderASCIICharacters(string input){if (string.IsNullOrEmpty(input)) return string.Empty;int pos, startIndex = 0, len = input.Length;if (len <= 4) return input;StringBuilder result = new StringBuilder();while ((pos = input.IndexOf("&#x", startIndex)) >= 0){bool needReplace = false;string rOldV = string.Empty, rNewV = string.Empty;int le = (len - pos < 6) ? len - pos : 6;int p = input.IndexOf(";", pos, le);if (p >= 0){rOldV = input.Substring(pos, p - pos + 1);// 計算 對應的低位字元short ss;if (short.TryParse(rOldV.Substring(3, p - pos - 3), NumberStyles.AllowHexSpecifier, null, out ss)){if (((ss >= 0) && (ss <= 8)) || ((ss >= 11) && (ss <= 12)) || ((ss >= 14) && (ss <= 32))){needReplace = true;rNewV = Convert.ToChar(ss).ToString();}}pos = p + 1;}else pos += le;string part = input.Substring(startIndex, pos - startIndex);if (needReplace) result.Append(part.Replace(rOldV, rNewV));else result.Append(part);startIndex = pos;}result.Append(input.Substring(startIndex));return result.ToString();}
這樣,我們這個示範程式的 Main 函數修改為下面的代碼,也不會有任何錯誤發生。
static void Main(string[] args){Console.WriteLine(GetLowOrderASCIICharacters("123456񐀀"));Console.WriteLine(GetLowOrderASCIICharacters("123456"));Console.WriteLine(GetLowOrderASCIICharacters(""));Console.WriteLine(GetLowOrderASCIICharacters("0123 456789"));Console.WriteLine(GetLowOrderASCIICharacters("/f"));
Console.WriteLine(GetLowOrderASCIICharacters(" =-1"));Console.WriteLine(GetLowOrderASCIICharacters(" "));Console.WriteLine(GetLowOrderASCIICharacters(" "));string fileName = "d://1.txt";MyClass c = new MyClass();c.Value = string.Format("在神奇{0}天地裏誰叱吒風雨", Convert.ToChar(14));c.Value = ReplaceLowOrderASCIICharacters(c.Value);SaveAsXML(c, fileName, Encoding.UTF8);object o = ConvertFileToObject(fileName, typeof(MyClass), Encoding.UTF8);MyClass d = o as MyClass;if (d != null){d.Value = GetLowOrderASCIICharacters(d.Value);Console.WriteLine(d.Value);}else Console.WriteLine("null");Console.ReadLine();}小結 :低序位非列印 ASCII 字元 在很多時候會給我們的系統帶來問題,這部分字元必須作特殊處理。