获取XElement的InnerXml的最佳方法是什么?

154
下面的代码中,获取混合的body元素的内容的最佳方法是什么?该元素可能包含XHTML或文本,但我只想以字符串形式获取其内容。 XmlElement类型具有InnerXml属性,这正是我想要的。
写成现在这样的代码几乎可以满足我的需求,但它包括我不想要的周围的<body>...</body>元素。
XDocument doc = XDocument.Load(new StreamReader(s));
var templates = from t in doc.Descendants("template")
                where t.Attribute("name").Value == templateName
                select new
                {
                   Subject = t.Element("subject").Value,
                   Body = t.Element("body").ToString()
                };
15个回答

213
我想看看这些建议的解决方案中哪个表现最佳,因此我进行了一些比较测试。出于兴趣,我还将LINQ方法与Greg建议的旧的System.Xml方法进行了比较。变化很有趣,不是我所期望的,最慢的方法比最快的方法慢 3倍以上
按从最快到最慢的顺序列出的结果如下:
  1. CreateReader - Instance Hunter(0.113秒)
  2. Plain old System.Xml - Greg Hurlman(0.134秒)
  3. Aggregate with string concatenation - Mike Powell(0.324秒)
  4. StringBuilder - Vin(0.333秒)
  5. String.Join on array - Terry(0.360秒)
  6. String.Concat on array - Marcin Kosieradzki(0.364秒)

方法

我使用了一个包含20个相同节点(称为“hint”)的XML文档:

<hint>
  <strong>Thinking of using a fake address?</strong>
  <br />
  Please don't. If we can't verify your address we might just
  have to reject your application.
</hint>

上面显示的秒数是提取20个节点的“内部XML”,连续1000次并取5次平均值的结果。我没有包括将XML加载和解析为XmlDocument(对于System.Xml方法)或XDocument(对于其他所有方法)所需的时间。
我使用的LINQ算法是:(C#-所有算法都需要一个XElement“parent”并返回内部XML字符串) CreateReader:
var reader = parent.CreateReader();
reader.MoveToContent();

return reader.ReadInnerXml();

使用字符串连接进行聚合:

return parent.Nodes().Aggregate("", (b, node) => b += node.ToString());

StringBuilder:

StringBuilder sb = new StringBuilder();

foreach(var node in parent.Nodes()) {
    sb.Append(node.ToString());
}

return sb.ToString();

使用 String.Join 连接数组:

return String.Join("", parent.Nodes().Select(x => x.ToString()).ToArray());

数组上的String.Concat:

return String.Concat(parent.Nodes().Select(x => x.ToString()).ToArray());

我没有展示“Plain old System.Xml”算法,因为它只是在节点上调用.InnerXml。


结论

如果性能很重要(例如,有大量XML频繁解析),我建议每次使用Daniel的CreateReader方法。如果你只查询几个节点,你可能想使用Mike更简洁的Aggregate方法。

如果您正在使用具有大量节点的大型元素的XML(可能有100个以上的节点),那么您可能会开始看到使用StringBuilder优于Aggregate方法的好处,但不优于CreateReader。我认为在这些条件下,JoinConcat方法永远不会更有效,因为将一个大型列表转换为大型数组的惩罚很大(即使在较小的列表中也很明显)。


StringBuilder版本可以一行编写:var result = parent.Elements().Aggregate(new StringBuilder(), (sb, xelem) => sb.AppendLine(xelem.ToString()), sb => sb.ToString())。 - Softlion
8
您错过了 parent.CreateNavigator().InnerXml 这一段代码(需要引用 System.Xml.XPath 命名空间来使用扩展方法)。 - Richard
我本来没想到在.Concat里需要使用.ToArray(),但它似乎可以让它更快。 - drzaus
如果您没有滚动到这些答案的底部,请考虑根据此答案仅从.ToString()中剥离容器/根。看起来甚至更快... - drzaus
2
你应该在 var reader = parent.CreateReader(); 周围加上 using 语句。 - BrainSlugs83
赞同@Richard的评论。parent.CreateNavigator().InnerXml对于投影来说特别好,因为它是内联的。 - ccook

72

我认为这是一种更好的方法(在VB中,应该很容易翻译):

给定一个XElement x:

Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml

1
不错!这比其他一些提出的方法要快得多(我测试了它们所有的 - 详见我的答案)。虽然它们都能完成任务,但这个方法是最快的 - 甚至似乎比 System.Xml.Node.InnerXml 本身还要快! - user87453
4
XmlReader是可被释放的,所以请别忘了用using来封装它(如果我会VB,我会自己编辑答案)。 - Dmitry Fedorkov

22

使用XElement的这个"扩展"方法怎么样?对我很有效!

public static string InnerXml(this XElement element)
{
    StringBuilder innerXml = new StringBuilder();

    foreach (XNode node in element.Nodes())
    {
        // append node's xml string to innerXml
        innerXml.Append(node.ToString());
    }

    return innerXml.ToString();
}

或者使用一点 Linq

public static string InnerXml(this XElement element)
{
    StringBuilder innerXml = new StringBuilder();
    doc.Nodes().ToList().ForEach( node => innerXml.Append(node.ToString()));

    return innerXml.ToString();
}

注意:上面的代码必须使用element.Nodes()而不是element.Elements()。 记住这两者之间的区别非常重要。 element.Nodes()可以提供类似于XTextXAttribute等所有内容,而XElement仅仅是一个元素。


17

所有功劳归于发现和证明最佳方法的人(谢谢!),这里将其封装在扩展方法中:

public static string InnerXml(this XNode node) {
    using (var reader = node.CreateReader()) {
        reader.MoveToContent();
        return reader.ReadInnerXml();
    }
}

11

保持简单高效:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • 使用Aggregate在连接字符串时会浪费内存并降低性能。
  • 使用Join("", sth)会使用两倍于Concat的字符串数组大小... 并且代码看起来相当奇怪。
  • 使用+=看起来非常奇怪,但显然不比使用'+'更糟糕——可能会被优化为相同的代码,因为赋值结果未使用,编译器可以安全地删除它。
  • StringBuilder过于命令式——而且每个人都知道不必要的"状态"很糟糕。

7
我最终使用了这个:

我最终采用了这个:

Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());

那将会有很多字符串拼接 - 我更喜欢 Vin 使用 StringBuilder 的方式。手动 foreach 并不是负面的。 - Marc Gravell
这个方法今天真的救了我,尝试使用新构造函数写出XElement,但其他方法都不方便,而这个方法却很好用。谢谢! - delliottg

3

个人而言,我最终编写了一个使用Aggregate方法的InnerXml扩展方法:

public static string InnerXml(this XElement thiz)
{
   return thiz.Nodes().Aggregate( string.Empty, ( element, node ) => element += node.ToString() );
}

使用新的System.Xml.Linq命名空间后,我的客户端代码和以前使用旧的System.Xml命名空间时一样简洁:

var innerXml = myXElement.InnerXml();

2

@Greg:看来你已经编辑了你的答案,变成了一个完全不同的答案。对此我的回答是,是的,我可以使用System.Xml来完成这个任务,但我希望能够用LINQ to XML来尝试一下。

我会保留我的原始回复,以防其他人想知道为什么我不能只使用XElement的.Value属性来获取我需要的内容:

@Greg:Value属性将所有子节点的文本内容连接在一起。因此,如果body元素仅包含文本,则可行,但如果它包含XHTML,则我会得到所有文本内容串在一起但没有标签的结果。


我遇到了完全相同的问题,认为这是一个错误:我有“混合”内容(即<root>随机文本<sub1>子元素</sub1><sub2>子元素</sub2></root>),通过XElement.Parse(...).Value变成了随机文本子元素子元素 - drzaus

1

1
不,它并不是这样的。它还包括具有其所有属性的元素。只想要开始和结束标记之间的内容。 - Christoph

1

//使用正则表达式可能更快地简单修剪开始和结束元素标记

var content = element.ToString();
var matchBegin = Regex.Match(content, @"<.+?>");
content = content.Substring(matchBegin.Index + matchBegin.Length);          
var matchEnd = Regex.Match(content, @"</.+?>", RegexOptions.RightToLeft);
content = content.Substring(0, matchEnd.Index);

1
更简洁的方法是使用 IndexOf 函数:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin); - drzaus

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