أفضل طريقة للحصول على 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 مع الأساليب القديمة System.Xml الطريقة التي اقترحها جريج.كان الاختلاف مثيرًا للاهتمام وليس ما كنت أتوقعه، مع وجود أبطأ الأساليب أكثر من 3 مرات أبطأ من الأسرع.
النتائج مرتبة حسب الأسرع إلى الأبطأ:
- CreateReader - صائد المثيلات (0.113 ثانية)
- System.Xml القديم البسيط - جريج هيرمان (0.134 ثانية)
- التجميع مع تسلسل السلسلة - مايك باول (0.324 ثانية)
- StringBuilder - فين (0.333 ثانية)
- String.Join on array - تيري (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 الداخلي" للعقد العشرين، 1000 مرة على التوالي، وأخذ المتوسط (المتوسط) لـ 5 أشواط.لم أقم بتضمين الوقت الذي استغرقه تحميل ملف XML وتحليله في ملف XmlDocument
(ل System.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
عبر الأسلوب التجميعي، ولكن لم ينته بعد 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
طريقة التمديد باستخدام الطريقة التجميعية:
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 للحصول على ما أحتاج إليه:
@ جريج:تقوم خاصية القيمة بتسلسل كافة محتويات النص لأي عقد فرعية.لذا، إذا كان عنصر النص يحتوي على نص فقط، فإنه يعمل، ولكن إذا كان يحتوي على 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، هناك بعض الإشارات إلى FastStringAlllocation أو شيء من هذا القبيل، مما يجعلني أعتقد أن الأشخاص في 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();
}