Linq-to-XML XElement.Remove() оставляет ненужные пробелы

У меня есть XDocument, который я создаю из массива байтов (полученного через tcp/ip).

Затем я ищу определенные узлы xml (XElements) и после получения значения «извлекаю» его из Xdocument, вызывая XElement.Remove(). После того, как весь мой синтаксический анализ завершен, я хочу иметь возможность регистрировать xml, который я не анализировал (оставшийся xml в XDocument). Проблема в том, что при вызове XElement.Remove() остаются лишние пробелы. Я хочу знать, как лучше всего удалить эти лишние пробелы, сохранив при этом остальную часть формата в оставшемся xml.

Пример/пример кода

Если я получу следующий xml через сокет:

<?xml version="1.0"?>
<catalog>
   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications with XML.</description>
   </book>
</catalog>

И я использую следующий код для анализа этого xml и удаления ряда XElements:

private void socket_messageReceived(object sender, MessageReceivedEventArgs e)
{
     XDocument xDoc;
     try
     {
         using (MemoryStream xmlStream = new MemoryStream(e.XmlAsBytes))
         using (XmlTextReader reader = new XmlTextReader(xmlStream))
         {
             xDoc = XDocument.Load(reader);
         }

         XElement Author = xDoc.Root.Descendants("author").FirstOrDefault();
         XElement Title  = xDoc.Root.Descendants("title").FirstOrDefault();
         XElement Genre  = xDoc.Root.Descendants("genre").FirstOrDefault();

         // Do something with Author, Title, and Genre here...

         if (Author != null) Author.Remove();
         if (Title  != null) Title.Remove();
         if (Genre  != null) Genre.Remove();

         LogUnparsedXML(xDoc.ToString());

     }
     catch (Exception ex)
     {
         // Exception Handling here...
     }
}

Тогда результирующая строка xml, отправленная в сообщение LogUnparsedXML, будет следующей:

<?xml version="1.0"?>
<catalog>
   <book id="bk101">



      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications with XML.</description>
   </book>
</catalog>

В этом надуманном примере это может показаться не таким уж большим делом, но в моем реальном приложении оставшийся xml выглядит довольно небрежно. Я пытался использовать перегрузку XDocument.ToString, которая использует перечисление SaveOptions, но безрезультатно. Я также пытался вызвать xDoc.Save для сохранения в файл с помощью перечисления SaveOptions. Я пытался поэкспериментировать с несколькими различными запросами linq, которые использовали XElement.Nodes().OfType<XText>(), чтобы попытаться удалить пробелы, но часто в итоге я брал пробелы, которые я хотел сохранить, вместе с пробелами, от которых я пытался избавиться.

Заранее спасибо за помощь.

Джо


person Joe DePung    schedule 27.07.2011    source источник


Ответы (3)


Нелегко ответить переносимым способом, потому что решение сильно зависит от того, как XDocument.Load() генерирует текстовые узлы с пробелами (и существует несколько реализаций LINQ to XML, которые могут не согласиться с этой тонкой деталью).

Тем не менее, похоже, что вы никогда не удаляете последний дочерний элемент (<description>) из <book> элементов. Если это действительно так, то нам не нужно беспокоиться об отступе закрывающего тега родительского элемента, и мы можем просто удалить элемент и все его последующие текстовые узлы, пока не дойдем до другого элемента. TakeWhile() выполнит эту работу.

РЕДАКТИРОВАТЬ: Ну, кажется, вам все-таки нужно удалить последнего дочернего элемента. Поэтому все будет сложнее. Код ниже реализует следующий алгоритм:

  • If the element is not the last element of its parent:
    • Remove all following text nodes until we reach the next element.
  • Otherwise:
    • Remove all following text nodes until we find one containing a newline,
    • If that node only contains a newline:
      • Remove that node.
    • Otherwise:
      • Create a new node containing only the whitespace found after the newline,
      • Вставьте этот узел после исходного узла,
      • Удалите исходный узел.
  • Удалите сам элемент.

Полученный код:

public static void RemoveWithNextWhitespace(this XElement element)
{
    IEnumerable<XText> textNodes
        = element.NodesAfterSelf()
                 .TakeWhile(node => node is XText).Cast<XText>();
    if (element.ElementsAfterSelf().Any()) {
        // Easy case, remove following text nodes.
        textNodes.ToList().ForEach(node => node.Remove());
    } else {
        // Remove trailing whitespace.
        textNodes.TakeWhile(text => !text.Value.Contains("\n"))
                 .ToList().ForEach(text => text.Remove());
        // Fetch text node containing newline, if any.
        XText newLineTextNode
            = element.NodesAfterSelf().OfType<XText>().FirstOrDefault();
        if (newLineTextNode != null) {
            string value = newLineTextNode.Value;
            if (value.Length > 1) {
                // Composite text node, trim until newline (inclusive).
                newLineTextNode.AddAfterSelf(
                    new XText(value.SubString(value.IndexOf('\n') + 1)));
            }
            // Remove original node.
            newLineTextNode.Remove();
        }
    }
    element.Remove();
}

Оттуда вы можете сделать:

if (Author != null) Author.RemoveWithNextWhitespace();
if (Title  != null) Title.RemoveWithNextWhitespace();
if (Genre  != null) Genre.RemoveWithNextWhitespace();

Хотя я бы посоветовал вам заменить приведенное выше что-то вроде цикла, загружаемого из массива, или вызова метода params, чтобы избежать избыточности кода.

person Frédéric Hamidi    schedule 27.07.2011

У меня есть более простое решение, чем принятый ответ, который работает для моего случая и, похоже, работает и для вашего. Возможно, есть более сложные случаи, для которых это не сработает, я не уверен.

Вот код:

public static void RemoveWithNextWhitespace(this XElement element)
{
    if (element.PreviousNode is XText textNode)
    {
        textNode.Remove();
    }

    element
    .Remove();
}

Вот мой запрос LINQPad с вашим вариантом использования:

void Main()
{
    var xDoc = XDocument.Parse(@"<?xml version=""1.0""?>
<catalog>
   <book id=""bk101"">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications with XML.</description>
   </book>
</catalog>", LoadOptions.PreserveWhitespace);

    XElement Author = xDoc.Root.Descendants("author").FirstOrDefault();
    XElement Title = xDoc.Root.Descendants("title").FirstOrDefault();
    XElement Genre = xDoc.Root.Descendants("genre").FirstOrDefault();

    // Do something with Author, Title, and Genre here...

    if (Author != null) Author.RemoveWithNextWhitespace();
    if (Title != null) Title.RemoveWithNextWhitespace();
    if (Genre != null) Genre.RemoveWithNextWhitespace();

    xDoc.ToString().Dump();
}

static class Ext
{
    public static void RemoveWithNextWhitespace(this XElement element)
    {
        if (element.PreviousNode is XText textNode)
        {
            textNode.Remove();
        }

        element
        .Remove();
    }
}

Основная причина, по которой я сам не использовал принятый ответ, заключалась в том, что в некоторых случаях он не оставлял мой XML должным образом отформатированным. например в вашем случае использования, если я удалю элемент «описание», останется что-то вроде этого:

<catalog>
   <book id="bk101">
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
         </book>
</catalog>
person phillhutt    schedule 24.10.2019

Чтение xml через XmlReader по умолчанию сохранит пробелы, включая незначащие пробелы, как вы видите здесь.

Вы должны прочитать его, игнорируя пробелы, установив соответствующий параметр чтения xml:

using (var reader = XmlReader.Create(xmlStream, new XmlReaderSettings { IgnoreWhitespace = true }))

Обратите внимание, что это не удаляет значительные пробелы (например, в смешанном содержимом или в области сохранения пробелов), поэтому ваше форматирование останется.

person Jeff Mercado    schedule 25.10.2019