用C#解析XML文件的最快方法是什么?

4

我需要从互联网加载许多XML文件。但为了进行更快速的测试,我已经下载了超过500个以下格式的文件。

<player-profile>
  <personal-information>
    <id>36</id>
    <fullname>Adam Gilchrist</fullname>
    <majorteam>Australia</majorteam>
    <nickname>Gilchrist</nickname>
    <shortName>A Gilchrist</shortName>
    <dateofbirth>Nov 14, 1971</dateofbirth>
    <battingstyle>Left-hand bat</battingstyle>
    <bowlingstyle>Right-arm offbreak</bowlingstyle>
    <role>Wicket-Keeper</role>
    <teams-played-for>Western Australia, New South Wales, ICC World XI, Deccan Chargers, Australia</teams-played-for>
    <iplteam>Deccan Chargers</iplteam>
  </personal-information>
  <batting-statistics>
    <odi-stats>
      <matchtype>ODI</matchtype>
      <matches>287</matches>
      <innings>279</innings>
      <notouts>11</notouts>
      <runsscored>9619</runsscored>
      <highestscore>172</highestscore>
      <ballstaken>9922</ballstaken>
      <sixes>149</sixes>
      <fours>1000+</fours>
      <ducks>0</ducks>
      <fifties>55</fifties>
      <catches>417</catches>
      <stumpings>55</stumpings>
      <hundreds>16</hundreds>
      <strikerate>96.95</strikerate>
      <average>35.89</average>
    </odi-stats>
    <test-stats>
      .
      .
      .
    </test-stats>
    <t20-stats>
      .
      .
      .    
    </t20-stats>
    <ipl-stats>
      .
      .
      . 
    </ipl-stats>
  </batting-statistics>
  <bowling-statistics>
    <odi-stats>
      <matchtype>ODI</matchtype>
      <matches>378</matches>
      <ballsbowled>58</ballsbowled>
      <runsgiven>64</runsgiven>
      <wickets>3</wickets>
      <fourwicket>0</fourwicket>
      <fivewicket>0</fivewicket>
      <strikerate>19.33</strikerate>
      <economyrate>6.62</economyrate>
      <average>21.33</average>
    </odi-stats>
    <test-stats>
      .
      .
      . 
    </test-stats>
    <t20-stats>
      .
      .
      . 
    </t20-stats>
    <ipl-stats>
      .
      .
      . 
    </ipl-stats>
  </bowling-statistics>
</player-profile>

我正在使用

XmlNodeList list = _document.SelectNodes("/player-profile/batting-statistics/odi-stats");

然后使用foreach循环此列表。
foreach (XmlNode stats in list)
  {
     _btMatchType = GetInnerString(stats, "matchtype"); //it returns null string if node not availible
     .
     .
     .
     .
     _btAvg = Convert.ToDouble(stats["average"].InnerText);
  }

即使我离线加载所有文件,解析速度仍然很慢。有没有更快的好方法来解析它们?或者是SQL的问题吗?我正在使用DataSets、TableAdapters和插入命令将从XML提取的所有数据保存到数据库中。

编辑:现在,请为上面的文档提供一些XmlReader代码。目前,我已经完成了这个。

void Load(string url) 
{
    _reader = XmlReader.Create(url); 
    while (_reader.Read()) 
    { 
    } 
} 

可用的XmlReader方法令人困惑。我需要完整地获取击球和投球统计数据,击球和投球统计数据不同,而ODI、T20、IPL等在投球和击球中是相同的。

2
你尝试过使用LINQ to XML来进行解析吗? - Justin Niessner
不,我没有,这是最好的方法吗? - SMUsamaShah
2
您可能需要分离关注点(如文件加载、节点提取和数据库交互等),以确定瓶颈所在。有几种方法可以改善每个组件的性能。 - Humberto
我是一名学生,这项工作是我的项目的一部分。目前我已经写了一个类来解析所有的 XML 文件,并将它们保存到数据库中。虽然最好能够分离组件,但我不知道如何做,请问能否提供一个相关链接供我学习? - SMUsamaShah
8个回答

9

您可以使用 XmlReader 进行单向快速读取。


2
如果有很多缺失的元素,你的性能问题可能与抛出的异常数量有关。异常是昂贵的。在引用节点之前检查其是否存在要便宜得多。 - Adrian
有一个函数叫做getAttribute(string statname),它在try/catch块中使用stats[statname],并在捕获异常时返回string.Empty。 - apoorv020
如果在这里得到一个空引用,那么是因为stats["average"]为空。只需添加一个(if stats["average"] != null))检查即可。 - Carra
@apoorv020 我已经做了那个,GetInnerString()函数可以完成这个任务。 - SMUsamaShah
当节点 "average" 不可用时,@Carra stats["average"].InnerText 会导致空异常。 - SMUsamaShah
显示剩余3条评论

8
抛出异常的开销可能远大于XML解析的开销。您需要重写代码,使其不会抛出异常。
一种方法是在请求元素值之前检查元素是否存在。这将起作用,但需要编写大量代码。另一种方法是使用映射表:
Dictionary<string, string> map = new Dictionary<string, string>
{
  { "matchtype", null },
  { "matches", null },
  { "ballsbowled", null }
};

foreach (XmlElement elm in stats.SelectNodes("*"))
{
   if (map.ContainsKey(elm.Name))
   {
      map[elm.Name] = elm.InnerText;
   }
}

这段代码将处理所有你关心的元素,并忽略你不关心的元素。如果Map中的值为null,则表示具有该名称的元素不存在(或没有文本)。
实际上,如果你将数据放入DataTable中,并且DataTable中的列名与XML中的元素名相同,你甚至不需要构建Map,因为DataTable的Columns属性就是你所需的全部映射。此外,由于DataColumn知道它包含的数据类型,因此你不必在代码中重复这些知识。
foreach (XmlElement elm in stats.SelectNodes("*"))
{
   if (myTable.Columns.Contains(elm.Name))
   {
      DataColumn c = myTable.Columns[elm.Name];
      if (c.DataType == typeof(string))
      {          
         myRow[elm.Name] = elm.InnerText;
         continue;
      }
      if (c.DataType == typeof(double))
      {
         myRow[elm.Name] = Convert.ToDouble(elm.InnerText);
         continue;
      }
      throw new InvalidOperationException("I didn't implement conversion logic for " + c.DataType.ToString() + ".");
   }
}

请注意,我没有声明任何变量来存储这些信息,所以我不可能出错并声明一个与存储在其中的列的数据类型不同的变量,或者创建一个表中的列并忘记实现填充它的逻辑。
编辑
好的,这里有一些比较棘手的东西。这是Python中常见的技术;在C#中,我认为大多数人仍然认为这很奇怪。
如果您查看我给出的第二个示例,您可以看到它正在使用 DataColumn 中的元信息来确定将元素的值从文本转换为其基本类型所使用的逻辑。您可以通过构建自己的映射来实现相同的功能,例如:
Dictionary<string, Type> typeMap = new Dictionary<string, Type>
{
   { "matchtype", typeof(string) },
   { "matches", typeof(int) },
   { "ballsbowled", typeof(int) }
}

然后做的事情基本上与我在第二个例子中展示的相同:
if (typeMap[elm.Name] == typeof(int))
{
   result[elm.Name] = Convert.ToInt32(elm.Text);
   continue;
}

您的结果不再可以是Dictionary<string, string>,因为现在它们可以包含非字符串的内容;它们必须是Dictionary<string, object>
但这种逻辑似乎有点笨重;您需要多次测试每个项目,还有continue语句来跳出它——这并不可怕,但它可以更加简洁。如何做呢?通过使用另一个映射,将类型映射到转换函数:
Dictionary<Type, Func<string, object>> conversionMap = 
   new Dictionary<Type, Func<string, object>>
{
   { typeof(string), (x => x) },
   { typeof(int), (x => Convert.ToInt32(x)) },
   { typeof(double), (x => Convert.ToDouble(x)) },
   { typeof(DateTime), (x => Convert.ToDateTime(x) }
};

如果您不熟悉lambda表达式,那么这可能有点难以理解。类型“Func<string, object>”指定了一个以字符串为参数并返回对象的函数。而该映射中的值就是lambda表达式,也就是函数。它们接受一个字符串参数(x),并返回一个对象。(我们怎么知道x是一个字符串?因为“Func<string, object>”告诉了我们。)
这意味着转换一个元素只需要一行代码:
result[elm.Name] = conversionMap[typeMap[elm.Name]](elm.Text);

从内部到外部表达式:此代码查找typeMap中元素的类型,然后查找conversionMap中的转换函数,并调用该函数,将elm.Text作为参数传递给它。在您的情况下,这可能不是理想的方法,我真的不知道。我在这里展示它,因为有一个更大的问题需要解决。正如Steve McConnell在《代码大全》中指出的那样,调试数据比调试代码更容易。使用这种技术可以将程序逻辑转换为数据。使用这种技术的情况下,可以极大地简化程序的结构。值得了解。

谢谢!我删除了所有的try和catch语句,并用一个函数替换它们,如果元素为null或不存在,则返回null或零。目前我正在将所有数据保存到预定义变量中,然后使用insert(var1,var2,var3....)。你告诉我的方法看起来更方便,我正在尝试学习和理解如何实现它。 - SMUsamaShah
哇,太棒了,现在我知道第一种方法了。它更简单,但是由于我正在使用tableAdapter存储数据,所以我只能使用第一种方法。 - SMUsamaShah
由于TableAdapter的整个目的是简化将DataTables适配到SQL,所以这个评论对我来说似乎毫无意义。 - Robert Rossney
我目前正在使用 Dictionary<string, string>,但它只能存储一种类型的数据。而 XML 有不同类型的数据,如 int、double、DataTime、TimeSpan 等等。我该如何使用字典存储这些类型的数据呢? - SMUsamaShah
简短的回答是你应该使用Dictionary<string, object>。更详细的解释请看我的编辑。 - Robert Rossney

3
如果您确定XML文件是一致且格式良好的,您可以避免进行真正的XML解析并将其作为纯文本文件处理。这种方法存在风险,不具备可移植性且容易出错。但这将是最快的解决方案(运行速度上而言,不是编码速度)。

3
不推荐提供如何创建风险高、不可移植和脆弱解决方案的建议。 - John Saunders

2
您可以尝试使用 LINQ to XML。或者您也可以使用 this 来找出应该使用什么。

谢谢,这意味着我必须使用XmlReader,仍在寻找一个好的GUI应用程序教程。 - SMUsamaShah

0
如果文档很大,那么基于流的解析器(适合您的需求)比使用XmlDocument更快,这主要是因为开销较低。请查看XmlReader的文档。

请问您能否提供一小段代码给我的文档? - SMUsamaShah

0

我不会说LINQ是最好的方法。我在谷歌上搜索并看到了一些关于HTML Agility Pack的参考。

我认为,如果你要遇到速度瓶颈,那么它将出现在下载过程中。换句话说,似乎你的性能问题不在于XML代码。我认为有可能提高下载速度或文件I/O的方法,但我不知道具体是什么。


不,我已经说过了为了速度我已经将所有文件下载到电脑上,现在不会再从互联网获取它们了。 - SMUsamaShah
HTML Agility Pack 用于解析 html。它比解析 xml 更容易。但是,检查瓶颈是否在下载文件中是一个好主意。 - Carra

0
一个XmlReader是解决您问题的方案。XmlDocument存储了大量的元信息,使得Xml易于访问,但它会占用过多的内存。我曾经看到一些小于50 KB的Xml被转换为几MB(10左右)的XmlDocument。

请问您能否提供一些XmlReader的代码来读取我的文档?到目前为止,我已经完成了以下内容:void Load(string url) { _reader = XmlReader.Create(url); while (_reader.Read()) { } }XmlReader的可用方法很令人困惑。我需要完整地获取击球和投球统计数据,而击球和投球统计数据是不同的,而ODI、T20、IPL等在投球和击球中都是相同的。 - SMUsamaShah
void Load(string url) { _reader = XmlReader.Create(url); while (_reader.Read()) { _reader.Name; // 给出名称 _reader.Value; // 以字符串形式给出值 } }请查看 MSDN 获取更多详细信息。您需要检查 HasValues、HasAttributes 等。 - Sudesh Sawant
XmlReader 很难实现,XmlDocument 很容易,问题不在于 XmlDocument,而是由于 try catch 语句导致速度变慢。感谢您的帮助。 - SMUsamaShah

0

如果您已将该信息转换为 DataSet 以将其插入到表中,请使用 DataSet.ReadXML() - 并使用从数据创建的默认表进行处理。

这个玩具应用程序就是这样做的,并且它可以使用您上面定义的格式工作。

项目文件:http://www.dot-dash-dot.com/files/wtfxml.zip 安装程序:http://www.dot-dash-dot.com/files/WTFXMLSetup_1_8_0.msi

它允许您使用树和网格格式浏览编辑 XML 文件 - 网格中列出的表格是在 DataSet 执行 ReadXML() 后自动创建的表格。


谢谢!我已经为数据库创建了 DataSet,而不是 XML。我正在解析 Xml 文件,提取数据,将此数据传递给 TableAdapter.Insert,因此将其保存到数据库中,然后通过将 Gui 组件与数据库绑定来显示。 - SMUsamaShah

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