XDocument.Save() 删除我的实体



我编写了一个工具,使用C#和Linq to XML修复一些XML文件(即插入一些丢失的属性/值)。该工具将现有的XML文件加载到XDocument对象中。然后,它向下解析节点以插入丢失的数据。之后,它调用XDocument.Save()将更改保存到另一个目录中。

所有这些都很好,除了一件事:任何&xAXML文件中文本中的实体将替换为换行符。当然,该实体表示一个新行,但我需要在XML中保留该实体,因为另一个消费者需要它。

是否有任何方法可以在不丢失&xA实体?

谢谢。


实体在XML中被技术上称为"数字字符引用",当原始文档加载到XDocument中时,它们被解析。这使得您的问题很难解决,因为在加载XDocument之后,无法区分已解析的空白实体和不重要的空白(通常用于格式化纯文本查看器的XML文档)。因此,只有当您的文档中没有任何不重要的空白时,以下内容才适用。

System.Xml库允许通过将XmlWriterSettings类的NewLineHandling属性设置为Entitize来保留空白空间实体。然而,在文本节点中,这只会将r赋予
,而不会将n赋予


最简单的解决方案是从XmlWriter类派生并重写其WriteString方法,以手动将空白字符替换为其数字字符实体。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));
}

如果您的文档中包含想要与&#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+E000U+F8FF)中的任何代码点。如果您想更加安全,请检查该字符是否已存在于文档中;如果是,则从所述范围中选择另一个字符。由于您只会在内部临时使用角色,因此使用哪一个并不重要。在文档中已经存在所有专用字符的极不可能的情况下,抛出异常;然而,我怀疑这种情况在实践中是否会发生。

最新更新