Найкращий спосіб отримати InnerXml XElement?


147

Який найкращий спосіб отримати вміст змішаного 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()
                };

Відповіді:


208

Я хотів побачити, яке із запропонованих рішень найкраще працює, тому я провів кілька порівняльних тестів. Не цікавившись, я також порівняв методи LINQ із звичайним старим методом System.Xml, запропонованим Грегом. Варіація була цікавою і не такою, яку я очікував, причому найповільніші методи були в 3 рази повільніші, ніж найшвидші .

Результати впорядковані найшвидшими та повільними:

  1. CreateReader - Миттєвий мисливець (0.113 секунди)
  2. Звичайна стара система.Xml - Грег Хурльман (0,134 секунди)
  3. Сукупність зі зв'язком струн - Майк Пауелл (0,324 секунди)
  4. StringBuilder - Vin (0,333 секунди)
  5. String.Join на масиві - Террі (0,360 секунд)
  6. String.Concat на масиві - Marcin Kosieradzki (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, часто розбирається), я б використовував CreateReaderметод Даніеля кожен раз . Якщо ви лише робите кілька запитів, можливо, ви захочете скористатися більш стислим методом агрегації Майка.

Якщо ви використовуєте XML для великих елементів з великою кількістю вузлів (можливо, 100-х), ви, мабуть, почнете бачити перевагу використання StringBuilderметоду Aggregate, але не більше CreateReader. Я не думаю, що методи Joinта Concatметоди ніколи не будуть ефективнішими в цих умовах через штраф за перетворення великого списку у великий масив (навіть очевидний тут із меншими списками).


Версію StringBuilder можна записати в одному рядку: var result = parent.Elements (). Aggregate (новий StringBuilder (), (sb, xelem) => sb.AppendLine (xelem.ToString ()), sb => sb.ToString ( ))
Softlion

7
Ви пропустили parent.CreateNavigator().InnerXml(потрібен using System.Xml.XPathметод розширення).
Річард

Я б не подумав, що вам потрібен .ToArray()всередині .Concat, але, здається, це зробить швидше
drzaus

У разі , якщо ви не прокручуються в нижній частині цих відповідей: розглянути тільки позбавивши контейнер / корінь з .ToString()за цієї відповіді . Здається, навіть швидше ...
drzaus

2
Вам слід дійсно зафіксувати це var reader = parent.CreateReader();у використанні оператора.
BrainSlugs83

70

Я думаю, що це набагато кращий метод (у VB, не повинен бути важким для перекладу):

Дано XElement x:

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

Приємно! Це набагато швидше, ніж деякі інші запропоновані методи (я протестував їх усі - детальніше див. Мою відповідь). Хоча всі вони роблять роботу, цей робить це найшвидше - навіть виходить швидше, ніж сам System.Xml.Node.InnerXml!
Люк Сампсон

4
XmlReader є одноразовим, тому не забудьте завершити його з використанням, будь ласка (я б сам редагував відповідь, якби знав VB).
Дмитро Федорков

19

Як щодо використання цього методу "розширення" на 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тільки елемент.


15

З усією належною заслугою до тих, хто виявив і зарекомендував себе найкращим підходом (спасибі!).

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

10

Нехай це буде просто та ефективно:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • Сукупність пам’яті та продуктивності неефективна при об'єднанні рядків
  • Використання Join ("", sth) використовує в два рази більший масив рядків, ніж Concat ... І виглядає в коді досить дивним.
  • Використання + = виглядає дуже дивним, але, мабуть, не набагато гірше, ніж використання "+" - можливо, він буде оптимізований під той самий код, результат присвоєння значень не використовується та може бути безпечно видалений компілятором.
  • StringBuilder настільки необхідний - і всі знають, що зайвий "стан" смокче.

7

Я в кінцевому підсумку скористався цим:

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

Це зробить багато струнних зв'язків - я вважаю за краще сам Він використовувати StringBuilder. Ручне передбачення не є негативним.
Марк Гравелл

Цей метод насправді врятував мене сьогодні, намагаючись виписати XElement з новим конструктором, і жоден з інших методів не піддавався йому на користь, поки цей. Дякую!
delliottg

3

Особисто я закінчив писати 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();

2

@Greg: Схоже, ви відредагували свою відповідь зовсім іншою відповіддю. На що моя відповідь "так", я могла це зробити за допомогою System.Xml, але сподівалася змочити ноги LINQ до XML.

Я залишу свою первинну відповідь нижче, якщо хтось ще задумається, чому я не можу просто використати властивість XElement .Value, щоб отримати те, що мені потрібно:

@Greg: Властивість Value об'єднує весь текстовий вміст будь-яких дочірніх вузлів. Отже, якщо елемент тіла містить лише текст, він працює, але якщо він містить XHTML, я злучаю весь текст разом, але жоден з тегів.


Я зіткнувся з цією самою проблемою і подумав, що це помилка: у мене був "змішаний" контент (тобто <root>random text <sub1>child</sub1> <sub2>child</sub2></root>), який став random text childchildчерезXElement.Parse(...).Value
drzaus

1

// використання 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);

1
акуратний. ще швидше просто використовувати IndexOf:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
drzaus


0

Чи можливо використовувати об’єкти простору імен System.Xml, щоб виконати роботу тут, а не використовувати LINQ? Як ви вже згадували, XmlNode.InnerXml - саме те, що вам потрібно.


0

Цікаво, чи (зауважте, я позбувся 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 = string + string

порівняно з string.Join, там є деяка згадка про FastStringAllocation або щось подібне, що змушує мене робити те, що люди в Microsoft можуть поставити там додаткове підвищення продуктивності. Звичайно мій .ToArray () називає це моїм негативом, але я просто хотів запропонувати ще одну пропозицію.


0

ти знаєш? найкраще зробити, це повернутися до CDATA :( я дивлюся на рішення тут, але я думаю, що CDATA - це найпростіший і дешевий, не найзручніший для розробки з Тхо


0
var innerXmlAsText= XElement.Parse(xmlContent)
                    .Descendants()
                    .Where(n => n.Name.LocalName == "template")
                    .Elements()
                    .Single()
                    .ToString();

Зробимо роботу за вас


-2
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();
}

А також якщо елемент має якісь атрибути або навіть лише пробіл, занадто багато логіки не вдається.
Крістоф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.