Какой лучший способ получить содержимое смешанного элемента 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 раза медленнее, чем самые быстрые.
Результаты, упорядоченные быстрее всего медленнее:
Метод
Я использовал один 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, которые я использовал, были: (С# - все принимают родительский элемент 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 on array:
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
каждый раз. Если вы просто выполняете несколько запросов, вы можете использовать более сжатый метод Aggregate Майка.
Если вы используете XML на больших элементах с большим количеством узлов (возможно, 100-ые), вы, вероятно, начнете видеть преимущество использования 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 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())
В итоге я использовал это:
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();
@Greg: Кажется, вы отредактировали свой ответ, чтобы быть совершенно другим ответом. На что я отвечаю "да", я мог бы сделать это с помощью System.Xml, но надеялся, чтобы мои ноги были мокрыми с LINQ to XML.
Я оставлю свой первоначальный ответ ниже, если кто-то еще задается вопросом, почему я не могу просто использовать свойство XElement.Value, чтобы получить то, что мне нужно:
@Greg: свойство Value объединяет все текстовые содержимое любых дочерних узлов. Поэтому, если элемент body содержит только текст, он работает, но если он содержит XHTML, я получаю весь текст, конкатенированный вместе, но ни один из тегов.
<root>random text <sub1>child</sub1> <sub2>child</sub2></root>
), которое стало random text childchild
через XElement.Parse(...).Value
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);
IndexOf
: var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
ты знаешь? самое лучшее, что нужно сделать, это вернуться на CDATA:( im смотрю на решения здесь, но я думаю, что 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 = строка + строка
и string.Join, в нем есть некоторые упоминания о FastStringAllocation или что-то еще, что делает меня тем, что люди в Microsoft, возможно, добавили дополнительный прирост производительности. Конечно, мой .ToArray() вызывает мое отрицание, но я просто хотел предложить другое предложение.
Можно ли использовать объекты пространства имен System.Xml для выполнения задания здесь вместо использования LINQ? Как вы уже упоминали, XmlNode.InnerXml - это именно то, что вам нужно.
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();
}