将元素复制和追加到 XML 文档而不缓冲到 RAM



正如标题所示,我需要将日志数据附加到XML文件中,而不需要缓冲RAM。XML文件由LogEntry元素组成,其中包含82个子元素,这些子元素包含数据。这些文件可能会变得很大,由于它将成为Windows CE6程序的一部分,我们的内存非常有限。

经过大量的研究,很明显,最常见的方法是使用XDocumentLinq to XML在附加到现有文档并写出新文档之前先读入现有文档。协同使用XmlWriterXmlReader似乎是我附加到文件的最佳方式,但到目前为止,我的所有尝试都非常不切实际,并且需要IF语句来指导要写什么,以防止写入重复或无数据元素。

我所做的事情的本质是:

//Create an XmlReader to read current WorkLog.
using (XmlReader xmlRead = XmlTextReader.Create("WorkLog.xml"))
{
   //Create a XmlWriterSettings and set indent 
   //to true to correctly format the document
   XmlWriterSettings writerSettings = new XmlWriterSettings();
   writerSettings.Indent = true;
   writerSettings.IndentChars = "t";
   //Create a new XmlWriter to output to
   using (XmlWriter xmlWriter = XmlWriter.Create("New.xml", writerSettings))
   {
      //Starts the document
      xmlWriter.WriteStartDocument();
      //While the XmlReader is still reading (essentially !EOF)
      while (xmlRead.Read())
      {
         //FSM to direct writing of OLD Log data to new file
         switch (xmlRead.NodeType)
         {
            case XmlNodeType.Element:
               //Handle the copying of an element node
               //Contains many if statements to handle root node &  
               //attributes and to skip nodes that contain text
               break;
            case XmlNodeType.Text:
               //Handle the copying of an text node
               break;
            case XmlNodeType.EndElement: 
               //Handle the copying of an End Element node
               break;
         }
      }
      xmlWriter.WriteEndDocument();
   }
}

我相信我可以用这种方式附加到文件中,但这样做是非常不切实际的——有人知道我搜索了几个小时还没有找到任何内存高效的方法吗?

如果需要的话,我很乐意发布我当前的代码来完成这项工作,但正如我所提到的,它非常大,目前实际上非常糟糕,所以我暂时不考虑它。

如果您已经知道您的xml结构,请考虑使用流编写器。1.将文件作为文件流打开2.将点移动到你想要替换的标签,比如:,将你的点(位置(移动到"<"3.用正确的xml格式写入日志数据,并在写入的末尾写入">

"用文本编辑器处理xml文件">

如果黑客攻击是合理的,我会走到文件的末尾,回放结束标记,并写入新元素和结束标记。为了进一步改进,您甚至可以缓存最后一个元素开头的偏移量。

您使用XmlReader的方法实际上是一条。。。但正如你所说,这是非常不切实际的。

那么黑客攻击是合理的吗

这样做的原因是XML有一系列您可能会遇到的特性,这些特性要求您从上到下阅读它。通常情况下,XmlReader会处理这些情况,只留下简单的标记等等

<!ENTITY % pub    "&#xc9;ditions Gallimard" >
<!ENTITY   rights "All rights reserved" >
<!ENTITY   book   "La Peste: Albert Camus, &#xA9; 1947 %pub;. &rights;" >

则实体CCD_ 7的替换文本为:

La Peste: Albert Camus,
© 1947 Éditions Gallimard. &rights;

如果您还没有阅读ENTITY标记,就不可能将其"翻译"为正确的XML。也就是说,幸运的是,没有太多人使用这类构造,所以可以假设XML不使用它们来重写根标记。

也就是说,在XML中关闭标记的唯一有效方法是使用</Foo>,在后面的>之前加上可选空格。(参见http://www.w3.org/TR/2008/REC-xml-20081126/#sec-starttags(。这基本上意味着你可以跳到最后,读取足够的数据,检查它是否包含结束标记——如果包含,你可以插入自己的代码。如果没有,再找回来再试一次。

讨厌的小编码

最后需要注意的是文件的编码。虽然您可以从流中构造XmlTextReader,但流使用字节,读取器检测编码并开始读取。幸运的是,XmlTextReaderEncoding公开为属性,因此您可以使用它。编码很重要,因为每个字符可能需要超过1个字节;尤其是当你遇到UTF-16或UTF-32时,这可能是一个问题。处理此问题的方法是将令牌转换为字节,然后对字节进行匹配。

根=尾部假设

由于我真的不想检查空格和尾随的'>'(请参阅上面的W3C链接(,我还认为它是一个有效的XML文件,这意味着每个打开的标记都是关闭的。这意味着您可以简单地检查</root,从而使匹配过程更加简单。(注意:您甚至可以只检查文件中的最后一个</,但我更希望我的代码对不正确的XML更加健壮(

将其整合在一起

给。。。(我还没有测试过,但它应该或多或少能起作用(

public bool FindAppendPoint(Stream stream)
{
    XmlTextReader xr = new XmlTextReader(stream);
    string rootElement = null;
    while (xr.Read())
    {
        if (xr.NodeType == XmlNodeType.Element)
        {
            rootElement = xr.Name;
            break;
        }
    }
    if (rootElement == null)
    {
        // Well, apparently there's no root... You can start a new file I suppose
        return false;
    }
    else
    {
        long start = stream.Position; // the position we're currently reading (end of start tag)
        long len = stream.Length;
        long end = Math.Min(start, len - 1024);
        byte[] endTag = xr.Encoding.GetBytes("</" + rootElement);
        while (end >= start)
        {
            byte[] data = new byte[len - end];
            stream.Seek(start, SeekOrigin.Begin);
            stream.Read(data, 0, data.Length); // FIXME: read returns an int that we should use!!!
            // Loop backwards till we find the end tag
            for (int i = data.Length - endTag.Length; i >= 0; --i)
            {
                int j;
                for (j = 0; j < endTag.Length && endTag[j] == data[i + j]; ++j) { }
                if (j == endTag.Length)
                {
                    // We found a match!
                    stream.Seek(len - data.Length - i, SeekOrigin.Begin);
                    AppendXml(stream, xr.Encoding)
                    return true;
                }
            }
            // Hmm, we've found </xml with a lot of spaces... oh well
            //
            // It's okay to skip back a bit, just have to make sure that we don't skip <0
            if (end == start)
            {
                end = start - 1; // end the loop
            }
            else
            {
                end = Math.Min(start, end - 1024);
            }
        }
        // Nope, no go.
        return false;
    }
}

只有使用XmlReader才能在内存中加载完整的XML。它也不支持修改,但您可以通过修改从源文档中复制XML。没有其他办法。

将XML解析为文本文档看起来很困难。

最好使用XmlReader/XmlWriter正在解析的类,并且crud逻辑已经通过使用Visitor或State GoF模式的自己的类实现来实现。访问者模式将减少if-s的数量,并使您的设计易于扩展。即使您希望不使用XmlReader/XmlWriter来解析XML文档,我也建议您在这种情况下使用它们。

假设日志文件是这样的(只有两个级别(:

<logs>
    <Log>abc1</Log>
    <Log>abc1</Log>
    <Log>abc1</Log>
</logs>

我使用FileStream来寻找结束并读取结束元素。

private static void Append(string xmlElement)
{
    const byte lessThan = (byte) '<';
    using (FileStream stream = File.Open(@"C:log.xml", FileMode.OpenOrCreate))
    {
        if (stream.Length == 0)
        {
            byte[] rootElement = Encoding.UTF8.GetBytes("<Logs></Logs>");
            stream.Write(rootElement, 0, rootElement.Length);
        }
        List<byte> buffer = new List<byte>();
        stream.Seek(0, SeekOrigin.End);
        do
        {
            stream.Seek(-1, SeekOrigin.Current);
            buffer.Insert(0, (byte) stream.ReadByte());
            stream.Seek(-1, SeekOrigin.Current);
        } while (buffer[0] != lessThan);
        byte[] toAdd = Encoding.UTF8.GetBytes(xmlElement);
        stream.Write(toAdd, 0, toAdd.Length);
        stream.Write(buffer.ToArray(), 0, buffer.Count);
    }
}

最新更新