Scala - изменение вложенных элементов в xml - PullRequest
37 голосов
/ 09 июня 2009

Я изучаю Scala, и я ищу, чтобы обновить вложенный узел в некотором XML. У меня что-то работает, но мне интересно, если это самый элегантный способ.

У меня есть xml:

val InputXml : Node =
<root>
    <subnode>
        <version>1</version>
    </subnode>
    <contents>
        <version>1</version>
    </contents>
</root>

И я хочу обновить версию узла в подузле , но не в содержимое .

Вот моя функция:

def updateVersion( node : Node ) : Node = 
 {
   def updateElements( seq : Seq[Node]) : Seq[Node] = 
   {
        var subElements = for( subNode <- seq ) yield
        {
            updateVersion( subNode )
        }   
        subElements
   }

   node match
   {
     case <root>{ ch @ _* }</root> =>
     {
        <root>{ updateElements( ch ) }</root>
     }
     case <subnode>{ ch @ _* }</subnode> =>
     {
         <subnode>{ updateElements( ch ) }</subnode> 
     }
     case <version>{ contents }</version> =>
     {
        <version>2</version>
     }
     case other @ _ => 
     {
         other
     }
   }
 }

Есть ли более лаконичный способ написания этой функции?

Ответы [ 7 ]

56 голосов
/ 20 августа 2009

Все это время и никто на самом деле не дал самый подходящий ответ! Теперь, когда я узнал об этом, вот мой новый взгляд на это:

import scala.xml._
import scala.xml.transform._

object t1 extends RewriteRule {
  override def transform(n: Node): Seq[Node] = n match {
    case Elem(prefix, "version", attribs, scope, _*)  =>
      Elem(prefix, "version", attribs, scope, Text("2"))
    case other => other
  }
}

object rt1 extends RuleTransformer(t1)

object t2 extends RewriteRule {
  override def transform(n: Node): Seq[Node] = n match {
    case sn @ Elem(_, "subnode", _, _, _*) => rt1(sn)
    case other => other
  }
}

object rt2 extends RuleTransformer(t2)

rt2(InputXml)

Теперь несколько пояснений. Класс RewriteRule является абстрактным. Он определяет два метода, каждый из которых называется transform. Один из них занимает один Node, другой Sequence из Node. Это абстрактный класс, поэтому мы не можем создать его экземпляр напрямую. Добавляя определение, в данном случае переопределяя один из transform методов, мы создаем его анонимный подкласс. Каждый RewriteRule должен заботиться об одной задаче, хотя он может выполнять много.

Далее класс RuleTransformer принимает в качестве параметров переменное число RewriteRule. Этот метод преобразования принимает Node и возвращает Sequence из Node, применяя каждый RewriteRule, использованный для его создания.

Оба класса происходят от BasicTransformer, который определяет несколько методов, которыми не нужно заниматься на более высоком уровне. Однако это apply вызовы методов transform, поэтому и RuleTransformer, и RewriteRule могут использовать синтаксический сахар, связанный с ним. В этом примере первый делает, а второй нет.

Здесь мы используем два уровня RuleTransformer, так как первый применяет фильтр к узлам более высокого уровня, а второй применяет изменение к тому, что проходит фильтр.

Также используется экстрактор Elem, так что нет необходимости интересоваться такими деталями, как пространство имен или наличие атрибутов или нет. Не то чтобы содержимое элемента version полностью отбрасывалось и заменялось на 2. При необходимости его можно сопоставить.

Обратите внимание, что последний параметр экстрактора - _*, а не _. Это означает, что эти элементы могут иметь несколько дочерних элементов. Если вы забудете *, совпадение может закончиться неудачей. В этом примере совпадение не будет неудачным, если не будет пробелов. Поскольку пробельные символы преобразуются в Text элементов, один пробел под subnode может привести к сбою совпадения.

Этот код больше, чем другие представленные предложения, но он обладает тем преимуществом, что знает о структуре XML гораздо меньше, чем другие. Он изменяет любой элемент с именем version, который находится ниже - независимо от того, сколько уровней - элемент с именем subnode, независимо от пространства имен, атрибутов и т. Д.

Более того ... хорошо, если у вас есть много преобразований, рекурсивное сопоставление с образцом становится быстро несгибаемым. Используя RewriteRule и RuleTransformer, вы можете эффективно заменить xslt файлы кодом Scala.

13 голосов
/ 06 января 2011

Вы можете использовать CSS-трансформеры Lift и написать:

"subnode" #> ("version *" #> 2)

См. http://stable.simply.liftweb.net/#sec:CSS-Selector-Transforms

11 голосов
/ 10 июня 2009

Я думаю, оригинальная логика хороша. Это тот же код с (позвольте мне сказать?) Более изощренным Scala-ish:

def updateVersion( node : Node ) : Node = {
   def updateElements( seq : Seq[Node]) : Seq[Node] = 
     for( subNode <- seq ) yield updateVersion( subNode )  

   node match {
     case <root>{ ch @ _* }</root> => <root>{ updateElements( ch ) }</root>
     case <subnode>{ ch @ _* }</subnode> => <subnode>{ updateElements( ch ) }</subnode>
     case <version>{ contents }</version> => <version>2</version>
     case other @ _ => other
   }
 }

выглядит более компактно (но на самом деле то же самое :))

  1. Я избавился от всего ненужного скобки
  2. Если требуется скобка, она начинается в та же строка
  3. updateElements просто определяет переменную и возвращает его, так что я избавился от этого и вернул результат напрямую

если вы хотите, вы также можете избавиться от updateElements. Вы хотите применить updateVersion ко всем элементам последовательности. Это метод карты . С этим вы можете переписать строку

case <subnode>{ ch @ _* }</subnode> => <subnode>{ updateElements( ch ) }</subnode>

с

case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion (_)) }</subnode>

Поскольку версия обновления принимает только 1 параметр, я на 99% уверен, что вы можете опустить его и написать:

case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion) }</subnode>

И заканчиваем:

def updateVersion( node : Node ) : Node = node match {
         case <root>{ ch @ _* }</root> => <root>{ ch.map(updateVersion )}</root>
         case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion ) }</subnode>
         case <version>{ contents }</version> => <version>2</version>
         case other @ _ => other
       }

Что вы думаете?

6 голосов
/ 07 июля 2009

С тех пор я узнал больше и представил то, что считаю лучшим решением в другом ответе. Я также исправил это, поскольку заметил, что не смог учесть ограничение subnode.

Спасибо за вопрос! Я только что узнал некоторые интересные вещи, когда имел дело с XML. Вот что вы хотите:

def updateVersion(node: Node): Node = {
  def updateNodes(ns: Seq[Node], mayChange: Boolean): Seq[Node] =
    for(subnode <- ns) yield subnode match {
      case <version>{ _ }</version> if mayChange => <version>2</version>
      case Elem(prefix, "subnode", attribs, scope, children @ _*) =>
        Elem(prefix, "subnode", attribs, scope, updateNodes(children, true) : _*)
      case Elem(prefix, label, attribs, scope, children @ _*) =>
        Elem(prefix, label, attribs, scope, updateNodes(children, mayChange) : _*)
      case other => other  // preserve text
    }

  updateNodes(node.theSeq, false)(0)
}

Теперь объяснение. Заявления первого и последнего случая должны быть очевидны. Последний существует, чтобы поймать те части XML, которые не являются элементами. Или, другими словами, текст. Обратите внимание, что в первом утверждении тест на флаг указывает, может ли version быть изменен или нет.

Во втором и третьем операторах case будет использоваться сопоставитель шаблонов для объекта Elem. Это разделит элемент на все его составные части. Последний параметр, "children @ _ *", будет сопоставлять детей со списком чего угодно. Или, более конкретно, Seq [Node]. Затем мы восстанавливаем элемент с извлеченными частями, но передаем Seq [Node] updateNodes, выполняя шаг рекурсии. Если мы сопоставляем элемент subnode, мы меняем флаг mayChange на true, что позволяет изменить версию.

В последней строке мы используем node.theSeq для генерации Seq [Node] из Node и (0) для получения первого элемента Seq [Node], возвращенного в качестве результата. Поскольку updateNodes по сути является функцией карты (for ... yield переводится в карту), мы знаем, что в результате будет только один элемент. Мы передаем флаг false, чтобы гарантировать, что version не будет изменен, если элемент subnode не является предком.

Есть немного другой способ сделать это, более мощный, но немного более многословный и неясный:

def updateVersion(node: Node): Node = {
  def updateNodes(ns: Seq[Node], mayChange: Boolean): Seq[Node] =
    for(subnode <- ns) yield subnode match {
      case Elem(prefix, "version", attribs, scope, Text(_)) if mayChange => 
        Elem(prefix, "version", attribs, scope, Text("2"))
      case Elem(prefix, "subnode", attribs, scope, children @ _*) =>
        Elem(prefix, "subnode", attribs, scope, updateNodes(children, true) : _*)
      case Elem(prefix, label, attribs, scope, children @ _*) =>
        Elem(prefix, label, attribs, scope, updateNodes(children, mayChange) : _*)
      case other => other  // preserve text
    }

  updateNodes(node.theSeq, false)(0)
}

Эта версия позволяет вам изменять любой тег "version", независимо от его префикса, атрибутов и области действия.

3 голосов
/ 04 ноября 2011

Scales Xml предоставляет инструменты для редактирования "на месте". Конечно, все это неизменно, но вот решение в Scales:

val subnodes = top(xml).\*("subnode"l).\*("version"l)
val folded = foldPositions( subnodes )( p => 
  Replace( p.tree ~> "2"))

Синтаксис, подобный XPath, является функцией подписи Scales, l после строки указывает, что у нее не должно быть пространства имен (только локальное имя).

foldPositions выполняет итерацию по результирующим элементам и преобразует их, объединяя результаты вместе.

1 голос
/ 29 апреля 2013

Один из подходов - линзы (например, скалязы). См. http://arosien.github.io/scalaz-base-talk-201208/#slide35 для очень четкого представления.

0 голосов
/ 10 июня 2009

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

Однако есть хороший способ сделать это напрямую с Xml, я бы хотел это увидеть.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...