XDocument.Save()会删除我文本中的 
 符号实体。

8
我写了一个工具,使用C#和Linq-to-XML修复一些XML文件(即插入缺失的属性/值)。该工具将现有的XML文件加载到XDocument对象中,然后解析节点以插入缺失的数据。之后,调用XDocument.Save()将更改保存到另一个目录。
以上操作都很好,但是存在一个问题:在XML文件中文本中出现的任何
实体都会被替换为新行字符。该实体代表一个新行,但我需要在XML中保留该实体,因为其他消费者需要它。
有没有办法保存修改后的XDocument而不丢失
实体呢?
谢谢。

1
当您加载旧文档还是保存新文档时,是否会替换 ? - Arnold Zokas
@Arnold:当我保存新的时候。 - mahdaeng
理想的解决方案是修复您的XML的使用者,使其正确处理XML。 - svick
2个回答

12

实体在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));
}

这是我见过的最详细的答案之一。我将尝试这个方法。即使它不起作用(但它可能会),你也会得到我的投票。谢谢,道格拉斯! - mahdaeng
不客气 :-) 不要忘记,上述代码仅在源XML中没有无关空格的情况下才能正常工作。如果您有无关空格,我建议您使用另一个答案中的代码(以下)。 - Douglas

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接