在xml文档末尾添加新节点的最快方法是什么?

7
我有一个大约 10 MB 的 XML 文件,结构如下所示:
<Errors>
   <Error>.......</Error>
   <Error>.......</Error>
   <Error>.......</Error>
   <Error>.......</Error>
   <Error>.......</Error>
</Errors>

我的需求是在</Errors>标签之前添加一个新节点<Error>。在.NET中实现这一点的最快方法是什么?

你是用什么方式写的呢?使用DOM?SAX?还是直接写入? :-P 我个人更喜欢DOM方法;你只需访问Errors节点并调用appendChild()即可。 - C. K. Young
2
请针对此情况定义“最快”("fastest")的含义;您是指“执行最快”还是“开发最快”? - Paul Sonier
10个回答

10

您需要使用XML包含技术。

您的error.xml文件(不要更改,只是一个存根。用于供XML解析器读取):

<?xml version="1.0"?>
<!DOCTYPE logfile [
<!ENTITY logrows    
 SYSTEM "errorrows.txt">
]>
<Errors>
&logrows;
</Errors>

你的 errorrows.txt 文件有修改,导致 xml 解析器无法理解它:

<Error>....</Error>
<Error>....</Error>
<Error>....</Error>

然后,要将条目添加到errorrows.txt:

using (StreamWriter sw = File.AppendText("logerrors.txt"))
{
    XmlTextWriter xtw = new XmlTextWriter(sw);

    xtw.WriteStartElement("Error");
    // ... write error messge here
    xtw.Close();
}

或者您甚至可以使用.NET 3.5 XElement,将文本附加到StreamWriter中:

using (StreamWriter sw = File.AppendText("logerrors.txt"))
{
    XElement element = new XElement("Error");
    // ... write error messge here
    sw.WriteLine(element.ToString());
}

参见Microsoft的文章 Efficient Techniques for Modifying Large XML Files


7
首先,我会淘汰System.Xml.XmlDocument,因为它是一个DOM,需要在可以附加到之前解析和构建整个树。这意味着您的10 MB文本将在内存中超过10 MB。这意味着它是“内存密集型”和“耗时的”。
其次,我会淘汰System.Xml.XmlReader,因为它需要先解析整个文件,才能到达您可以附加到它的点。您必须将XmlReader复制到XmlWriter中,因为您无法修改它。这需要在附加之前首先在内存中复制您的XML。
比XmlDocument和XmlReader更快的解决方案是字符串操作(它自己有内存问题):
string xml = @"<Errors><error />...<error /></Errors>";
int idx = xml.LastIndexOf("</Errors>");

xml = xml.Substring(0, idx) + "<error>new error</error></Errors>";

删去结尾标签,加入新错误,再加上结尾标签。
我猜你可以对此进行大量操作,通过截断文件9个字符并追加到其中。这样就不必读取文件,让操作系统优化页面加载(只需要加载最后一个块或某些内容)。
System.IO.FileStream fs = System.IO.File.Open("log.xml", System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite);
fs.Seek(-("</Errors>".Length), System.IO.SeekOrigin.End);
fs.Write("<error>new error</error></Errors>");
fs.Close();

如果您的文件为空或仅包含"<Errors></Errors>",那么这将遇到问题,这两种情况都可以通过检查长度轻松处理。

OpenText()函数打开一个文件以供读取,并返回一个StreamReader对象。 - Daniel Brückner
太棒了!你解决了一个非常大的问题,我不知道为什么这个答案没有得到超过1k的投票。 - FARHAD AFSAR
谢谢,Colin - Seek 技术是一个非常有用、简单的解决方案,适用于我经常遇到的情况。 - harpo

2
最快的方式可能是直接访问文件。
using (StreamWriter file = File.AppendText("my.log"))
{
    file.BaseStream.Seek(-"</Errors>".Length, SeekOrigin.End);
    file.Write("   <Error>New error message.</Error></Errors>");
}

但是您会失去所有美好的XML功能,并可能很容易损坏文件。


1
这也是我会建议的。 - Joey Robert
我正在尝试这个,但在 .Seek 行上遇到了一个“无法向后寻找以覆盖在追加模式下打开的文件中先前存在的数据”的错误。这个示例正确吗? - Simon
不,这个例子是不正确的,但是让它正常工作的所有你需要做的就是用 'new StreamWriter(File.Open(filePath, FileMode.Open, FileAccess.Write)' 替换 'File.AppendText(...)'。 - user65199

1
我会使用XmlDocument或XDocument来加载你的文件,然后相应地操纵它。
我会考虑将这个XmlDocument缓存到内存中,以便快速访问该文件。
你需要什么样的速度? 你已经有性能瓶颈了吗,还是你预计会出现一个?
(请注意,本篇翻译仅供参考,因具体语境、需求等情况而异,若需要准确翻译,请结合上下文进行理解。)

XmlDocument是一种DOM模型,比如XmlReader中的SAX要慢。XmlDocument需要将整个10 MB表示为对象存储在内存中(因此总共超过10 MB)。XmlReader会更快(我相当确定XmlDocument是基于XmlReader构建的),但仍然需要解析整个文档。如果Ramesh只是将日志文件附加到日志文件中(似乎是这种情况),那么对我来说,两者都不够“快”。 - Colin Burnett
我完全同意,但我总是避免使用文本附加方式编写XML。我的答案是找出他是否能将文档加载到内存中,然后对其进行写入。这将非常快速。然后再有另一个进程定期将XmlDocument写入文件中。这完全取决于具体情况。 - Robin Day

0

试一下这个:

        var doc = new XmlDocument();
        doc.LoadXml("<Errors><error>This is my first error</error></Errors>");

        XmlNode root = doc.DocumentElement;

        //Create a new node.
        XmlElement elem = doc.CreateElement("error");
        elem.InnerText = "This is my error";

        //Add the node to the document.
        if (root != null) root.AppendChild(elem);

        doc.Save(Console.Out);
        Console.ReadLine();

3
这绝对不是最快的方法。 - user65199

0

这是如何在C中实现的,.NET应该类似。

游戏很简单,跳到文件末尾,跳过标记,添加新的错误行,然后写入新标记。

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char** argv) {
        FILE *f;

        // Open the file
        f = fopen("log.xml", "r+");

        // Small buffer to determine length of \n (1 on Unix, 2 on PC)
        // You could always simply hard code this if you don't plan on 
        // porting to Unix.
        char nlbuf[10];
        sprintf(nlbuf, "\n");

        // How long is our end tag?
        long offset = strlen("</Errors>");

        // Add in an \n char.
        offset += strlen(nlbuf);

        // Seek to the END OF FILE, and then GO BACK the end tag and newline
        // so we use a NEGATIVE offset.
        fseek(f, offset * -1, SEEK_END);

        // Print out your new error line
        fprintf(f, "<Error>New error line</Error>\n");

        // Print out new ending tag.
        fprintf(f, "</Errors>\n");

        // Close and you're done
        fclose(f);
}

0

使用基于字符串的技术(例如寻找文件结尾,然后向后移动关闭标记的长度)容易受到文档结构意外但完全合法的变化的影响。

文档可能以任意数量的空格结束,这是您可能会遇到的最常见的问题。它也可能以任意数量的注释或处理指令结束。如果顶层元素不是命名为Error会发生什么?

而且,这里有一种情况,使用字符串操作无法完全检测出来:

<Error xmlns="not_your_namespace">
   ...
</Error>

如果您使用 XmlReader 来处理 XML,虽然它可能不像定位到 EOF 那样快,但它也可以让您处理所有这些可能的异常情况。

他提供的文件看起来像是一个日志文件,我猜测他遇到了一个问题,即向其中添加内容变得越来越慢,因此他才会问这个问题。可以说,我认为日志格式完全在他的控制之下。 - Colin Burnett
做这些假设通常是完全可以的。但我曾经修复过很多代码,因为开发人员猜错了。在大多数情况下,开发人员甚至不知道他在猜测。 - Robert Rossney

0
最快的方法可能是使用XmlReader读取文件,然后使用XmlWriter将每个读取的节点复制到新流中。当您遇到关闭的</Errors>标记时,只需要在继续“读取和复制”循环之前输出您的附加<Error>元素即可。这种方式肯定比将整个文档读入DOM(XmlDocument类)更难,但对于大型XML文件来说,速度要快得多。诚然,使用StreamReader/StreamWriter仍然会更快一些,但在代码中使用起来相当糟糕。

0
你的XML文件在代码中是如何表示的? 你使用System.XML类吗? 如果是这样,你可以使用XMLDocument.AppendChild。

0

我尝试使用其他答案建议的代码,但遇到了一个问题,有时调用字符串的 .length 方法并不等于字符串的字节数,因此我会不一致地丢失字符。我修改了代码以获取字节计数。

var endTag = "</Errors>";
var nodeText = GetNodeText();
using (FileStream file = File.Open("my.log", FileMode.Open, FileAccess.ReadWrite))
{
    file.BaseStream.Seek(-(Encoding.UTF8.GetByteCount(endTag)), SeekOrigin.End);
    fileStream.Write(Encoding.UTF8.GetBytes(nodeText), 0, Encoding.UTF8.GetByteCount(nodeText));
    fileStream.Write(Encoding.UTF8.GetBytes(endTag), 0, Encoding.UTF8.GetByteCount(endTag));
}

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