Najlepszy sposób na uzyskanie InnerXml XElement?


147

Jaki jest najlepszy sposób uzyskania zawartości bodyelementu mieszanego w poniższym kodzie? Element może zawierać XHTML lub tekst, ale chcę tylko, aby jego zawartość była w postaci ciągu. XmlElementTypu ma InnerXmlwłaściwość, która jest dokładnie to, co ja jestem po.

Napisany kod robi prawie to, co chcę, ale zawiera otaczający <body>... </body>element, którego nie chcę.

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

Odpowiedzi:


208

Chciałem sprawdzić, które z tych sugerowanych rozwiązań działa najlepiej, więc przeprowadziłem kilka testów porównawczych. W interesie porównałem również metody LINQ ze zwykłą starą metodą System.Xml sugerowaną przez Grega. Odmiana była interesująca i nie tego się spodziewałem, przy czym najwolniejsze metody były ponad 3 razy wolniejsze niż najszybsze .

Wyniki uporządkowane według najszybszego do najwolniejszego:

  1. CreateReader - łowca instancji (0,113 sekundy)
  2. Zwykły stary System.Xml - Greg Hurlman (0,134 sekundy)
  3. Agregowanie z konkatenacją ciągów - Mike Powell (0,324 sekundy)
  4. StringBuilder - Vin (0,333 sekundy)
  5. String.Join on array - Terry (0,360 sekundy)
  6. String.Concat na tablicy - Marcin Kosieradzki (0.364)

metoda

Użyłem pojedynczego dokumentu XML z 20 identycznymi węzłami (zwanymi „wskazówką”):

<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>

Liczby pokazane powyżej w sekundach są wynikiem wyodrębnienia „wewnętrznego kodu XML” 20 węzłów 1000 razy z rzędu i obliczenia średniej (średniej) z 5 przebiegów. Nie uwzględniłem czasu potrzebnego na załadowanie i przeanalizowanie XML do postaci XmlDocument(dla metody System.Xml ) lub XDocument(dla wszystkich pozostałych).

Użyte przeze mnie algorytmy LINQ to: (C # - wszystkie pobierają XElement„rodzica” i zwracają wewnętrzny ciąg XML)

CreateReader:

var reader = parent.CreateReader();
reader.MoveToContent();

return reader.ReadInnerXml();

Agreguj z konkatenacją ciągów:

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 on array:

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

String.Concat na tablicy:

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

Nie pokazałem tutaj algorytmu "Zwykły stary System.Xml", ponieważ po prostu wywołuje .InnerXml na węzłach.


Wniosek

Jeśli ważna jest wydajność (np. Dużo XML, często parsowany), za CreateReaderkażdym razem używałbym metody Daniela . Jeśli robisz tylko kilka zapytań, możesz użyć bardziej zwięzłej metody Aggregate Mike'a.

Jeśli używasz XML na dużych elementach z dużą liczbą węzłów (może 100), prawdopodobnie zaczniesz dostrzegać korzyści płynące z używania StringBuildermetody Aggregate, ale nie ponad CreateReader. Nie sądzę, aby metody Joini Concatbyły kiedykolwiek bardziej wydajne w tych warunkach ze względu na karę konwersji dużej listy na dużą tablicę (nawet oczywiste w przypadku mniejszych list).


Wersję StringBuilder można zapisać w jednej linii: var result = parent.Elements (). Aggregate (new StringBuilder (), (sb, xelem) => sb.AppendLine (xelem.ToString ()), sb => sb.ToString ( ))
Softlion

7
Przegapiłeś parent.CreateNavigator().InnerXml(potrzeba using System.Xml.XPathmetody rozszerzenia).
Richard

Nie pomyślałbym, że potrzebujesz .ToArray()środka .Concat, ale wydaje się, że przyspieszy to
drzaus

W przypadku, gdy nie przewiń do dołu tych odpowiedzi: rozważyć tylko stripping kontener / pierwiastek z .ToString()za tą odpowiedź . Wydaje się jeszcze szybsze ...
drzaus

2
Naprawdę powinieneś zawrzeć to var reader = parent.CreateReader();w instrukcji using.
BrainSlugs83

70

Myślę, że jest to znacznie lepsza metoda (w VB nie powinna być trudna do przetłumaczenia):

Biorąc pod uwagę XElement x:

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

Miły! Jest to o wiele szybsze niż niektóre inne proponowane metody (przetestowałem je wszystkie - szczegóły w mojej odpowiedzi). Chociaż wszystkie wykonują swoją pracę, ten robi to najszybciej - nawet szybciej niż sam System.Xml.Node.InnerXml!
Luke Sampson

4
XmlReader jest jednorazowy, więc nie zapomnij go owinąć za pomocą, proszę (samodzielnie zredagowałbym odpowiedź, gdybym znał VB).
Dmitry Fedorkov

19

A co powiesz na użycie tej metody „rozszerzenia” na XElement? pracował dla mnie!

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

LUB użyj trochę Linq

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

    return innerXml.ToString();
}

Uwaga : powyższy kod musi być używany element.Nodes()w przeciwieństwie do element.Elements(). Bardzo ważna rzecz do zapamiętania różnicy między nimi. element.Nodes()daje wszystko jak XText, XAttributeetc, ale XElementtylko elementem.


15

Z całym uznaniem dla tych, którzy odkryli i udowodnili najlepsze podejście (dzięki!), Tutaj jest to podsumowane metodą rozszerzającą:

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

10

Zachowaj prostotę i wydajność:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • Agregacja powoduje nieefektywne wykorzystanie pamięci i wydajności podczas łączenia ciągów
  • Używanie Join ("", sth) używa dwa razy większej tablicy ciągów niż Concat ... I wygląda dość dziwnie w kodzie.
  • Używanie + = wygląda bardzo dziwnie, ale najwyraźniej nie jest dużo gorsze niż użycie '+' - prawdopodobnie zostałoby zoptymalizowane do tego samego kodu, ponieważ wynik przypisania jest nieużywany i może zostać bezpiecznie usunięty przez kompilator.
  • StringBuilder jest niezbędny - i każdy wie, że niepotrzebny „stan” jest do niczego.

7

Skończyło się na tym:

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

Spowoduje to wiele konkatenacji ciągów - wolałbym, aby Vin używał StringBuilder samodzielnie. Instrukcja dla wszystkich nie jest negatywna.
Marc Gravell

Ta metoda naprawdę uratowała mnie dzisiaj, próbując napisać XElement za pomocą nowego konstruktora i żadna z innych metod nie nadawała się do tego łatwo, podczas gdy ta zrobiła. Dzięki!
delliottg

3

Osobiście napisałem InnerXmlmetodę rozszerzającą przy użyciu metody Aggregate:

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

Mój kod klienta jest wtedy tak samo zwięzły, jak w przypadku starej przestrzeni nazw System.Xml:

var innerXml = myXElement.InnerXml();

2

@Greg: Wygląda na to, że zredagowałeś swoją odpowiedź, tak aby była zupełnie inną odpowiedzią. Na co moja odpowiedź brzmi `` tak '', mogłem to zrobić za pomocą System.Xml, ale miałem nadzieję, że zmoczę stopy dzięki LINQ to XML.

Zostawię swoją oryginalną odpowiedź poniżej na wypadek, gdyby ktoś inny zastanawiał się, dlaczego nie mogę po prostu użyć właściwości .Value XElement, aby uzyskać to, czego potrzebuję:

@Greg: Właściwość Value łączy całą zawartość tekstową wszystkich węzłów podrzędnych. Więc jeśli element body zawiera tylko tekst, to działa, ale jeśli zawiera XHTML, otrzymuję cały tekst połączony razem, ale żaden z tagów.


Natknąłem się dokładnie na ten sam problem i pomyślałem, że to błąd: miałem 'mieszaną' zawartość (tj. <root>random text <sub1>child</sub1> <sub2>child</sub2></root>), Która stała się random text childchildviaXElement.Parse(...).Value
drzaus

1

// użycie Regex może być szybsze, aby po prostu przyciąć znacznik elementu początkowego i końcowego

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
schludny. jeszcze szybciej po prostu użyć IndexOf:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
drzaus


0

Czy można użyć obiektów przestrzeni nazw System.Xml, aby wykonać zadanie, zamiast używać LINQ? Jak już wspomniałeś, XmlNode.InnerXml jest dokładnie tym, czego potrzebujesz.


0

Zastanawiam się, czy (zauważ, że pozbyłem się b + = i po prostu mam b +)

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

może być nieco mniej wydajny niż

string.Join( "", t.Element.Nodes()
                  .Select( n => n.ToString() ).ToArray() );

Nie jestem w 100% pewien ... ale patrząc na Aggregate () i string.Join () w Reflector ... Myślę , że czytam to jako Aggregate, po prostu dodając zwracaną wartość, więc zasadniczo otrzymujesz:

ciąg = ciąg + ciąg

w porównaniu z ciągiem znaków. Dołącz, jest tam wzmianka o FastStringAllocation lub czymś w tym rodzaju, co sprawia, że ​​myślę, że ludzie z firmy Microsoft mogli dodać tam dodatkowy wzrost wydajności. Oczywiście moje .ToArray () nazywają moją negację w ten sposób, ale chciałem tylko zaoferować kolejną sugestię.


0

wiesz? najlepiej jest wrócić do CDATA :( szukam tutaj rozwiązań, ale myślę, że CDATA jest zdecydowanie najprostszym i najtańszym, a nie najwygodniejszym do rozwijania


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

Wykonam pracę za Ciebie


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

A także jeśli element ma jakieś atrybuty lub nawet tylko spację za dużo, logika zawodzi.
Christoph
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.