Лучший способ получить 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()
};
Решение
Я хотел посмотреть, какое из предложенных решений работает лучше всего, поэтому провел несколько сравнительных тестов.Ради интереса я также сравнил методы LINQ со старыми добрыми методами. Система.Xml метод, предложенный Грегом.Вариант был интересным и не таким, как я ожидал: самые медленные методы были более чем в 3 раза медленнее самого быстрого.
Результаты упорядочены от самого быстрого к самому медленному:
- CreateReader — Охотник за экземплярами (0,113 секунды)
- Старый добрый System.Xml — Грег Херлман (0,134 секунды)
- Агрегация с конкатенацией строк — Майк Пауэлл (0,324 секунды)
- StringBuilder — Вин (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
(для Система.Xml метод) или XDocument
(для всех остальных).
Я использовал следующие алгоритмы LINQ: (C# - все берут XElement
«родительский» и вернуть внутреннюю строку XML)
СоздатьЧитатель:
var reader = parent.CreateReader();
reader.MoveToContent();
return reader.ReadInnerXml();
Агрегировать с конкатенацией строк:
return parent.Nodes().Aggregate("", (b, node) => b += node.ToString());
Стрингбилдер:
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, часто анализируемого), я бы используйте Дэниела CreateReader
метод каждый раз.Если вы выполняете всего несколько запросов, возможно, вам захочется использовать более краткий агрегатный метод Майка.
Если вы используете XML для больших элементов с большим количеством узлов (возможно, сотен), вы, вероятно, начнете видеть преимущества использования StringBuilder
над методом Aggregate, но не над CreateReader
.Я не думаю, что Join
и Concat
методы были бы более эффективны в этих условиях из-за штрафа за преобразование большого списка в большой массив (здесь это даже очевидно для меньших списков).
Другие советы
Я думаю, что это гораздо лучший метод (в VB его не составит труда перевести):
Учитывая XElement x:
Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml
Как насчет использования этого метода «расширения» в 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, чтобы получить то, что мне нужно:
@Грег:Свойство Value объединяет все текстовое содержимое любых дочерних узлов.Таким образом, если элемент body содержит только текст, он работает, но если он содержит XHTML, я получаю весь текст, объединенный вместе, но ни один из тегов.
// использование 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);
doc.ToString() или doc.ToString(SaveOptions) выполнит всю работу.Видеть http://msdn.microsoft.com/en-us/library/system.xml.linq.xelement.tostring(v=vs.110).aspx
Можно ли использовать объекты пространства имен System.Xml для выполнения этой работы вместо использования LINQ?Как вы уже упомянули, XmlNode.InnerXml — это именно то, что вам нужно.
Интересно, если (обратите внимание, я избавился от 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() вызывает отрицание этого, но я просто хотел предложить еще одно предложение.
ты знаешь?Лучшее, что можно сделать, это вернуться к CDATA :( я ищу решения здесь, но думаю, что CDATA, безусловно, самый простой и дешевый, но не самый удобный для разработки.
var innerXmlAsText= XElement.Parse(xmlContent)
.Descendants()
.Where(n => n.Name.LocalName == "template")
.Elements()
.Single()
.ToString();
Сделаю работу за вас
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();
}