2012-01-10 72 views
6

我編寫了一個工具來使用C#和Linq-to-XML修復一些XML文件(即插入一些缺失的屬性/值)。該工具將現有的XML文件加載到XDocument對象中。然後,它通過節點解析以插入缺失的數據。之後,它調用XDocument.Save()將更改保存到另一個目錄。XDocument.Save()刪除我的 實體

所有這一切都很好,除了一件事:任何& #xA; XML文件中的文本中的實體被替換爲新的行字符。當然,這個實體代表了一條新線,但我需要在XML中保留實體,因爲另一個消費者需要它。

有沒有什麼辦法可以保存修改過的XDocument而不會丟失#xx; & #xA;實體?

謝謝。

+1

在加載舊文檔或保存新文檔時會被替換嗎? – 2012-01-10 23:10:04

+0

@Arnold:當我保存新的。 – mahdaeng 2012-01-12 16:51:22

+0

理想的解決方案是修復XML的使用者,以便正確處理XML。 – svick 2012-01-18 18:25:53

回答

10


實體在技術上被稱爲XML中的「數字字符引用」,當原始文檔被加載到XDocument中時,它們被解析。這會讓您的問題難以解決,因爲在加載XDocument之後,無法區分已解析的空白實體與無關緊要的空白(通常用於格式化純文本查看器的XML文檔)。因此,下面僅適用於您的文檔沒有任何不重要的空白。

System.Xml庫允許通過將XmlWriterSettings類的NewLineHandling屬性設置爲Entitize來保留空白實體。但是,在文本節點內,這隻會使\r
,而不是\n


最簡單的解決方案是從XmlWriter類派生並覆蓋它的WriteString方法,以手動將數字字符實體替換爲空白字符。方法也恰好是.NET賦予不允許出現在文本節點中的字符的地方,例如語法標記&,<>,它們分別被授權給&amp;&lt;&gt;

由於XmlWriter是抽象的,我們將從XmlTextWriter派生,以避免必須實現前一類的所有抽象方法。這裏是一個快速和骯髒的實現:

public class EntitizingXmlWriter : XmlTextWriter 
{ 
    public EntitizingXmlWriter(TextWriter writer) : 
     base(writer) 
    { } 

    public override void WriteString(string text) 
    { 
     foreach (char c in text) 
     { 
      switch (c) 
      { 
       case '\r': 
       case '\n': 
       case '\t': 
        base.WriteCharEntity(c); 
        break; 
       default: 
        base.WriteString(c.ToString()); 
        break; 
      } 
     } 
    } 
} 

如果打算在生產環境中使用,你會想要做掉與c.ToString()的一部分,因爲它是非常低效的。您可以通過批量處理原始text的子字符串來優化代碼,該子字符串不包含任何您想要授權的字符,並將它們一起送入一個base.WriteString調用。

一句警告:以下幼稚的做法是行不通的,因爲基WriteString方法將與&amp;更換任何&字符,從而導致\r擴大到&amp;#xA;

public override void WriteString(string text) 
    { 
     text = text.Replace("\r", "&#xD;"); 
     text = text.Replace("\n", "&#xA;"); 
     text = text.Replace("\t", "&#x9;"); 
     base.WriteString(text); 
    } 

最後,您XDocument保存到目標文件或流,只需使用下面的代碼片段:

using (var textWriter = new StreamWriter(destination)) 
using (var xmlWriter = new EntitizingXmlWriter(textWriter)) 
    document.Save(xmlWriter); 

希望這有助於!

編輯:作爲參考,這裏是覆蓋WriteString方法的優化版本:

public override void WriteString(string text) 
{ 
    // The start index of the next substring containing only non-entitized characters. 
    int start = 0; 

    // The index of the current character being checked. 
    for (int curr = 0; curr < text.Length; ++curr) 
    { 
     // Check whether the current character should be entitized. 
     char chr = text[curr]; 
     if (chr == '\r' || chr == '\n' || chr == '\t') 
     { 
      // Write the previous substring of non-entitized characters. 
      if (start < curr) 
       base.WriteString(text.Substring(start, curr - start)); 

      // Write current character, entitized. 
      base.WriteCharEntity(chr); 

      // Next substring of non-entitized characters tentatively starts 
      // immediately beyond current character. 
      start = curr + 1; 
     } 
    } 

    // Write the trailing substring of non-entitized characters. 
    if (start < text.Length) 
     base.WriteString(text.Substring(start, text.Length - start)); 
} 
+0

這是我見過的最深入的答案之一。我會試試這個。即使它不起作用(它可能會),你會得到我的投票。謝謝道格拉斯! – mahdaeng 2012-02-01 07:10:25

+0

不客氣:-)不要忘記,只有在源XML中沒有無意義的空格時,上述纔會起作用。如果你確實有很小的空白,我建議你使用其他答案中的代碼(如下)。 – Douglas 2012-02-01 18:48:14

0

如果您的文檔包含你想從你的&#xA;實體區分不重要的空白,可以使用下面的(簡單得多)解決方案:將&#xA;字符引用暫時轉換爲另一個字符(該字符不在您的文檔中),執行XML處理,然後將字符轉換回輸出結果。在下面的例子中,我們將使用私人字符U+E800

static string ProcessXml(string input) 
{ 
    input = input.Replace("&#xA;", "&#xE800;"); 
    XDocument document = XDocument.Parse(input); 
    // TODO: Perform XML processing here. 
    string output = document.ToString(); 
    return output.Replace("\uE800", "&#xA;"); 
} 

需要注意的是,由於XDocument解析到其對應的Unicode字符的數字字符引用,該"&#xE800;"實體將被輸出決心'\uE800'

通常,您可以安全地使用Unicode的「專用區域」(U+E000 - U+F8FF)中的任何代碼點。如果您希望保證安全,請檢查文檔中是否存在字符;如果是這樣,則從上述範圍中選擇另一個字符。既然你只是臨時和內部使用角色,你使用哪一個並不重要。在非常不可能的情況下,所有私人使用的字符已經存在於文檔中,拋出異常;然而,我懷疑那是否會在實踐中發生。