Лучший способ получить InnerXml XElement?
Какой лучший способ получить содержимое смешанного 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 ответов
Я хотел посмотреть, какое из этих предложенных решений работает лучше всего, поэтому я провел несколько сравнительных тестов. Из интереса я также сравнил методы LINQ с простым старым методом System.Xml, предложенным Грегом. Вариация была интересной и не такой, как я ожидал: самые медленные методы были более чем в 3 раза медленнее, чем самые быстрые.
Результаты упорядочены от самых быстрых до самых медленных:
- CreateReader - Instance Hunter (0,113 секунды)
- Обычный старый System.Xml - Грег Хёрлман (0,134 секунды)
- Агрегат с конкатенацией строк - Майк Пауэлл (0,324 секунды)
- StringBuilder - Vin (0,333 секунды)
- String.Join на массив - Терри (0,360 секунд)
- String.Concat по массиву - Марчин Косерадзки (0.364)
метод
Я использовал один XML-документ с 20 одинаковыми узлами (называемый "подсказка"):
<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>
Числа, показанные в секундах выше, являются результатом извлечения "внутреннего XML" из 20 узлов 1000 раз подряд и получения среднего (среднего) из 5 запусков. Я не включил время, необходимое для загрузки и разбора XML в XmlDocument
(для метода System.Xml) или XDocument
(для всех остальных).
Алгоритмы LINQ, которые я использовал, были: (C# - все принимают XElement
"родитель" и вернуть внутреннюю строку 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());
Я не показал здесь алгоритм "Простой старый System.Xml", так как он просто вызывает.InnerXml на узлах.
Заключение
Если важна производительность (например, много XML, часто анализируется), я бы использовал Daniel CreateReader
метод каждый раз. Если вы просто делаете несколько запросов, вы можете использовать более лаконичный метод Mike Aggregate.
Если вы используете XML на больших элементах с большим количеством узлов (возможно, 100), вы, вероятно, начнете видеть преимущества использования StringBuilder
по совокупному методу, но не более CreateReader
, Я не думаю, что Join
а также Concat
в этих условиях методы были бы более эффективными из-за штрафов за преобразование большого списка в большой массив (даже в случае с небольшими списками).
Я думаю, что это гораздо лучший метод (в VB его не должно быть сложно перевести):
Дано XElement x:
Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml
Как насчет использования этого метода "extension" в 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()
дает вам все как XText
, XAttribute
и т.д., но XElement
только элемент.
С должным уважением к тем, кто нашел и доказал лучший подход (спасибо!), Здесь он обернут в метод расширения:
public static string InnerXml(this XNode node) {
using (var reader = node.CreateReader()) {
reader.MoveToContent();
return reader.ReadInnerXml();
}
}
Держите это простым и эффективным:
String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
- Агрегирование неэффективно при использовании памяти и производительности при объединении строк
- Использование Join("", sth) использует в два раза больший массив строк, чем Concat... И выглядит довольно странно в коде.
- Использование += выглядит очень странно, но, очевидно, не намного хуже, чем использование "+" - возможно, оно будет оптимизировано под тот же код, поскольку результат присваивания не используется и может быть безопасно удален компилятором.
- StringBuilder настолько необходим - и все знают, что ненужное "состояние" - отстой.
Я закончил тем, что использовал это:
Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());
Лично я закончил тем, что написал InnerXml
метод расширения с использованием метода Aggregate:
public static string InnerXml(this XElement thiz)
{
return thiz.Nodes().Aggregate( string.Empty, ( element, node ) => element += node.ToString() );
}
Мой клиентский код будет таким же лаконичным, как и в старом пространстве имен System.Xml:
var innerXml = myXElement.InnerXml();
@ Грег: Похоже, вы отредактировали свой ответ, чтобы он был совершенно другим. На что мой ответ - да, я мог бы сделать это с помощью System.Xml, но надеялся получить удовольствие от LINQ to XML.
Я оставлю свой исходный ответ ниже на тот случай, если кто-нибудь еще удивится, почему я не могу просто использовать свойство.Value XElement, чтобы получить то, что мне нужно:
@Greg: свойство Value объединяет все текстовое содержимое любых дочерних узлов. Поэтому, если элемент body содержит только текст, он работает, но если он содержит XHTML, я получаю объединенный весь текст, но ни один из тегов.
Работает doc.ToString() или doc.ToString(SaveOptions). См. http://msdn.microsoft.com/en-us/library/system.xml.linq.xelement.tostring(v=vs.110).aspx
// использование Regex может быть быстрее, просто обрезать начальный и конечный тег элемента
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);
Можно ли использовать объекты пространства имен System.Xml для выполнения работы здесь вместо использования LINQ? Как вы уже упоминали, XmlNode.InnerXml - это именно то, что вам нужно.
var innerXmlAsText= XElement.Parse(xmlContent)
.Descendants()
.Where(n => n.Name.LocalName == "template")
.Elements()
.Single()
.ToString();
Сделаю работу за вас
Ты знаешь? Лучше всего вернуться к CDATA:(Я смотрю на решения здесь, но я думаю, что CDATA - самый простой и дешевый, но не самый удобный для разработки с этим
Интересно, если (обратите внимание, я избавился от b+= и просто есть b+)
t.Element( "body" ).Nodes()
.Aggregate( "", ( b, node ) => b + node.ToString() );
может быть немного менее эффективным, чем
string.Join( "", t.Element.Nodes()
.Select( n => n.ToString() ).ToArray() );
Не уверен на 100%... но взглянув на Aggregate() и string.Join() в Reflector... Я думаю, что я читаю это как Aggregate, просто добавляя возвращаемое значение, так что по сути вы получите:
строка = строка + строка
В отличие от string.Join, там есть упоминание о FastStringAllocation или о чем-то еще, что заставляет меня задуматься о том, что ребята из Microsoft могли бы внести туда дополнительное повышение производительности. Конечно, мой.ToArray() называет это моим отрицанием, но я просто хотел предложить другое предложение.
public static string InnerXml(this XElement xElement)
{
//remove start tag
string innerXml = xElement.ToString().Trim().Replace(string.Format("<{0}>", xElement.Name), "");
////remove end tag
innerXml = innerXml.Trim().Replace(string.Format("</{0}>", xElement.Name), "");
return innerXml.Trim();
}