修改大型 XML 文件的有效技术

 

作者:Dare Obasanjo
Microsoft Corporation

2004 年 4 月

总结: Dare Obasanjo 展示了两种有效更新或修改大型 XML 文件(如日志文件和数据库转储)的技术。

Contents

简介
使用 XML 包含技术
将 XmlReader 链接到 XmlWriter
致谢

简介

由于 XML 作为大型信息源的表示格式越来越流行,开发人员开始在编辑大型 XML 文件时出现问题。 对于处理大型日志文件且需要不断向这些文件追加信息的应用程序尤其如此。 编辑 XML 文件的最简单方法是将其加载到 XmlDocument 中,修改内存中的文档,然后将其保存回磁盘。 但是,这样做意味着必须将整个 XML 文档加载到内存中,这可能不可行,具体取决于文档的大小和应用程序的内存要求。

本文介绍修改 XML 文档的一些替代方法,这些方法不涉及将其加载到 XmlDocument 实例中。

使用 XML 包含技术

建议的第一种方法最适用于将值追加到 XML 日志文件。 开发人员面临的一个常见问题是,只需将新条目追加到日志文件中,而不必加载文档。 由于 XML 格式良好的规则,通常很难使用传统方法将条目追加到 XML 日志文件中,但最终不会使日志文件格式不正确。

我将演示的第一种方法针对的是能够快速将条目追加到 XML 文档的情况。 此方法涉及创建两个文件。 第一个文件是格式正确的 XML 文件,而第二个文件是 XML 片段。 格式正确的 XML 文件包括使用 DTD 中声明 的外部实体 或使用 xi:include 元素的 XML 片段。 这样,只需在使用包含文件完成处理时向其追加即可有效地更新包含 XML 片段的文件。 包含文件和包含的文件的示例如下所示:

Logfile.xml: 
<?xml version="1.0"?>
<!DOCTYPE logfile [
<!ENTITY events    
 SYSTEM "logfile-entries.txt">
]>
<logfile>
&events;
</logfile>

Logfile-events.txt:
<event>
 <ip>127.0.0.1</ip>
 <http_method>GET</http_method>
 <file>index.html</file>
 <date>2004-04-01T17:35:20.0656808-08:00</date>
</event>
<event>
 <ip>127.0.0.1</ip>
 <http_method>GET</http_method>
 <file>stylesheet.css</file>
 <date>2004-04-01T17:35:23.0656120-08:00</date>
 <referrer>http://www.example.com/index.html</referrer>
</event>
<event>
  <ip>127.0.0.1</ip>
  <http_method>GET</http_method>
  <file>logo.gif</file>
  <date>2004-04-01T17:35:25.238220-08:00</date>
  <referrer>http://www.example.com/index.html</referrer>
</event>

logfile-entries.txt 文件包含 XML 片段,可以使用典型的文件 IO 方法进行有效更新。 以下代码演示如何通过将条目追加到文本文件的末尾,将其添加到 XML 日志文件:

using System;
using System.IO;
using System.Xml; 

public class Test{ 
  public static void Main(string[] args){

    StreamWriter sw = File.AppendText("logfile-entries.txt");
    XmlTextWriter xtw =  new XmlTextWriter(sw); 

    xtw.WriteStartElement("event"); 
    xtw.WriteElementString("ip", "192.168.0.1");
    xtw.WriteElementString("http_method", "POST");
    xtw.WriteElementString("file", "comments.aspx");
    xtw.WriteElementString("date", "1999-05-05T19:25:13.238220-08:00");    

    xtw.Close();
                 
  }
}

将条目追加到文本文件后,就可以使用传统的 XML 处理技术从 XML 日志文件处理它们。 以下代码使用 XPath 循环访问 logfile.xml 中的日志事件,其中列出了访问的文件和访问时间。

using System;
using System.Xml; 

public class Test2{
 
  public static void Main(string[] args){

    XmlValidatingReader vr = 
    new XmlValidatingReader(new XmlTextReader("logfile.xml"));
    vr.ValidationType = ValidationType.None;          
    vr.EntityHandling = EntityHandling.ExpandEntities; 

    XmlDocument doc = new XmlDocument(); 
    doc.Load(vr); 

    foreach(XmlElement element in doc.SelectNodes("//event")){
      
      string file = element.ChildNodes[2].InnerText; 
      string date = element.ChildNodes[3].InnerText; 
      
      Console.WriteLine("{0} accessed at {1}", file, date);

    }                 
  }
} 

上面的代码生成以下输出:

index.html accessed at 2004-04-01T17:35:20.0656808-08:00
stylesheet.css accessed at 2004-04-01T17:35:23.0656120-08:00
logo.gif accessed at 2004-04-01T17:35:25.238220-08:00
comments.aspx accessed at 1999-05-05T19:25:13.238220-08:00

将 XmlReader 链接到 XmlWriter

在某些情况下,除了将元素追加到根元素之外,还可能需要对 XML 文件执行更复杂的操作。 例如,在存档日志文件之前,可能需要筛选出日志文件中不符合某些特定条件的每个条目。 执行此操作的一种方法是将 XML 文件加载到 XmlDocument 中,然后通过 XPath 选择感兴趣的事件。 但是,这样做涉及将整个文档加载到内存中,如果文档很大,这可能会令人禁止。 另一个选项涉及使用 XSLT 执行此类任务,但这与 XmlDocument 方法存在相同的问题,因为整个 XML 文档必须位于内存中。 此外,对于不熟悉 XSLT 的开发人员,在弄清楚如何正确使用模板匹配时,学习曲线会很陡峭。

解决如何处理非常大的 XML 文档问题的一种方法是使用 XmlReader 在 XML 中读取,并在使用 XmlWriter 读取 XML 文档时将其写出。 使用此方法时,整个文档永远不会一次性在内存中,可以更精细地对 XML 进行更精细的更改,而不仅仅是追加元素。 以下代码示例在上一节的 XML 文档中读取,并在筛选掉其 ip 元素值为“127.0.0.1”的所有事件后将其保存为存档文件。

using System;
using System.Xml; 
using System.IO;
using System.Text;
public class Test2{
  static string ipKey;
  static string httpMethodKey;
  static string fileKey; 
  static string dateKey;
  static string referrerKey; 

  public static void WriteAttributes(XmlReader reader, XmlWriter writer){
    
    if(reader.MoveToFirstAttribute()){
      do{
   writer.WriteAttributeString(reader.Prefix, 
                reader.LocalName, 
                reader.NamespaceURI,
                reader.Value); 
      }while(reader.MoveToNextAttribute());
      reader.MoveToElement(); 
    }
  }

  public static void WriteEvent(XmlWriter writer, string ip,
                                 string httpMethod, string file,
                                 string date, string referrer){
    
    writer.WriteStartElement("event"); 
    writer.WriteElementString("ip", ip);
    writer.WriteElementString("http_method", httpMethod);
    writer.WriteElementString("file", file);
    writer.WriteElementString("date", date);    
    if(referrer != null) writer.WriteElementString("referrer", referrer);
    writer.WriteEndElement(); 

  } 

  public static void ReadEvent(XmlReader reader, out string ip,
                              out string httpMethod, out string file,
                              out string date, out string referrer){

    ip = httpMethod = file = date = referrer = null; 

    while( reader.Read() && reader.NodeType != XmlNodeType.EndElement){                
      
 if (reader.NodeType == XmlNodeType.Element) {
          
     if(reader.Name == ipKey){   
       ip = reader.ReadString(); 
     }else if(reader.Name == httpMethodKey){ 
       httpMethod = reader.ReadString();
     }else if(reader.Name == fileKey){ 
       file = reader.ReadString();
     }else if(reader.Name == dateKey){ 
       date = reader.ReadString();
       // reader.Read(); // consume end tag
     }else if(reader.Name == referrerKey){ 
       referrer = reader.ReadString();
      }
        }//if 
    }//while   
  }

  public static void Main(string[] args){
    string ip, httpMethod, file, date, referrer; 
    //setup XmlNameTable with strings we'll be using for comparisons
    XmlNameTable xnt = new NameTable(); 
    ipKey            = xnt.Add("ip"); 
    httpMethodKey    = xnt.Add("http_method"); 
    fileKey          = xnt.Add("file");
    dateKey          = xnt.Add("date");
    referrerKey      = xnt.Add("referrer");
    
    //load XmlTextReader using XmlNameTable above 
    XmlTextReader xr = new XmlTextReader("logfile.xml", xnt);
    xr.WhitespaceHandling = WhitespaceHandling.Significant;

    XmlValidatingReader vr = new XmlValidatingReader(xr);
    vr.ValidationType = ValidationType.None;
    vr.EntityHandling = EntityHandling.ExpandEntities; 


    StreamWriter sw =  
      new StreamWriter ("logfile-archive.xml", false, Encoding.UTF8 ); 
    XmlWriter xw    = new XmlTextWriter (sw);                 
    
    vr.MoveToContent(); // Move to document element   
    xw.WriteStartElement(vr.Prefix, vr.LocalName, vr.NamespaceURI);
    WriteAttributes(vr, xw);    
     
    vr.Read(); // Move to first <event> child of document element
    // Write out each event that isn't from 127.0.0.1 (localhost)
    do
    {
      ReadEvent(vr, out ip, out httpMethod, 
               out file, out date, out referrer);
      if(!ip.Equals("127.0.0.1")){
        WriteEvent(xw,ip, httpMethod, file, date, referrer); 
      }
      vr.Read(); //move to next <event> element or end tag of <logfile>
    } while(vr.NodeType == XmlNodeType.Element);
     
    Console.WriteLine("Done");
    
    vr.Close();
    xw.Close();
  }
}

上面的代码示例导致将以下输出写入logfile-archive.xml文件:

<logfile>
 <event>
   <ip>192.168.0.1</ip>
   <http_method>POST</http_method>
    <file>comments.aspx</file>
    <date>1999-05-05T19:25:13.238220-08:00</date>
  </event>
</logfile>

除了使用 XmlReaderXmlWriter 的链接之外,上述代码中还有一个有趣的点是,在检查 ReadEvent () 方法中元素的标记名称时,它使用 NameTable 来提高文本比较的性能。 MSDN 文档主题将 XmlNameTable 与 XmlReader 配合使用的对象比较中概述了使用此方法检查 XmlReader 中元素的标记名称的好处。

致谢

感谢 Martin Gudgin,他通过建议将 XmlReader 链接到 XmlWriter 来回答有关编辑大型 XML 日志文件的问题,从而激励我撰写本文。