【问题标题】:Scala convert XML to key value mapScala 将 XML 转换为键值映射
【发布时间】:2018-07-23 20:05:53
【问题描述】:

This topic相关

问题如下,想象一个没有任何特定模式的 XML

<persons>
  <total>2</total>
  <someguy>
     <firstname>john</firstname>
     <name>Snow</name>
  </someguy>
  <otherperson>
     <sex>female</sex>
  </otherperson>
</persons>

对于处理,我想在键值映射中使用它:

"Persons/total" -> 2
"Persons/someguy/firstname" -> john
"Persons/someguy/name" -> Snow
"Persons/otherperson/sex" -> female

最好我有一些很好的递归函数,我在其中深度优先遍历 XML 代码并简单地堆叠所有标签,直到找到一个值并将该值与标签堆栈一起返回。不幸的是,当我返回输入序列时,我正在努力将返回类型与输入类型连接起来。让我向您展示我到目前为止所拥有的,显然 foreach 是一个问题,因为它返回 Unit,但地图也不会工作,因为它返回一个 Seq。

def dfs(n: NodeSeq, keyStack: String, map: Map[String,String])
 :(NodeSeq, String, Map[String,String]) = {
  n.foreach(x => {
    if (x.child.isEmpty) {
      dfs(x.child, keyStack, map + (keyStack+ x.label + " " -> x.text))
    }
    else {
      dfs(x.child, keyStack+ x.label + "/", map)
    }
  }
  )
}

非常感谢您的帮助!

【问题讨论】:

  • 如果你得到Seq[(String, String)],你可以打电话给.toMap来获取地图。
  • 无论如何,如果您允许在 XML 中进行任意级别的嵌套,则不应在 foreach/map/flatMap 等函数的参数中进行递归调用 - 您可以在 @987654327 中编写另一个辅助函数@ 将累积处理每个n 的结果,并在最后将它们连接起来。
  • 我知道可以这样做,但是每个 xml 都可以深度优先遍历,因此应该可以遍历和创建地图,最好以递归方式

标签: xml scala scala-xml


【解决方案1】:

在玩了一些之后,这是我能做到的最优雅的方式。我不喜欢的是:

  • 它对每个孩子都是深度优先的,因此您需要在之后平展结果。这也是我错过根节点标签的原因。
  • 它一路拖了很多 XML,所以它可能太占用内存?

如果您有想法,请改进!

import scala.xml._

val xml = "<persons><total>2</total><someguy><firstname>john</firstname><name>Snow</name></someguy><otherperson><sex>female</sex></otherperson></persons>"
val result: Elem = scala.xml.XML.loadString(xml)

def linearize(node: Node, stack: String, map: Map[String,String])
: List[(Node, String, Map[String,String])] = {
  (node, stack, map) :: node.child.flatMap {
    case e: Elem => {
      if (e.descendant.size == 1) linearize(e, stack, map ++ Map(stack + "/" + e.label -> e.text))
      else linearize(e, stack + "/" + e.label, map)
    }
    case _ => Nil
  }.toList
}

linearize(result, "", Map[String,String]()).flatMap(_._3).toMap

之后我们仍然需要展平地图,但至少递归部分相当短。上面的代码应该可以在您的 Scala 工作表中使用。

【讨论】:

  • 标签名相同,只会返回最后一个标签,覆盖之前的标签。
  • 没错,对于我的用例来说,这很好,但很高兴指出。使用多图可能是一种解决方案
【解决方案2】:

受 Sparky 的回答启发,但更适用于更普遍的情况:

val emptyMap = Map.empty[String,List[String]]

def xml2map(xml: String): Map[String,List[String]] = add2map(XML.loadString(xml), "", emptyMap)

private def add2map(node: Node, xPath: String, oldMap: Map[String,List[String]]): Map[String,List[String]] = {

  val elems = node.child.filter(_.isInstanceOf[Elem])
  val xCurr = xPath + "/" + node.label

  val currElems = elems.filter(_.child.count(_.isInstanceOf[Elem]) == 0)
  val nextElems = elems.diff(currElems)

  val currMap = currElems.foldLeft(oldMap)((map, elem) => map + {
    val key = xCurr + "/" + elem.label

    val oldValue = map.getOrElse(key, List.empty[String])
    val newValue = oldValue ::: List(elem.text)

    key -> newValue
  })

  nextElems.foldLeft(currMap)((map, elem) => map ++ add2map(elem, xCurr, emptyMap))
}

对于 XML 之类的

<persons>
  <total>2</total>
  <someguy>
    <firstname>john</firstname>
    <name>Snow</name>
    <alive>in 1st season</alive>
    <alive>in 2nd season</alive>
    <alive>...</alive>
    <alive>even in last season</alive>
    <alive>how long more?</alive>
  </someguy>
  <otherperson>
    <sex>female</sex>
  </otherperson>
</persons>

它在下面生成一个 Map[String,List[String]](在 .toString() 之后):

Map(
  /persons/total -> List(2),
  /persons/someguy/firstname -> List(john),
  /persons/someguy/alive -> List(in 1st season, in 2nd season, ..., even in last season, how long more?),
  /persons/otherperson/sex -> List(female),
  /persons/someguy/name -> List(Snow)
)

【讨论】:

    【解决方案3】:

    需要考虑的一种情况是元素具有前缀时:

    val xml = <a>
      <b>
        <c>1</c>
        <d>2</d>
        <e>
          <z:f>3</z:f>
        </e>
      </b>
    </a>
    

    还有其他需要考虑的场景(包括实体、cmets、声明),但这是一个很好的起点:

    def nodeToMap(xml: Elem): Map[String, String] = {
    
      def nodeToMapWithPrefix(prefix: String, xml: Node): Map[String, String] = {
        val pathAndText = for {
          child <- xml.child
        } yield {
          child match {
            case e: Elem if e.prefix == null =>
              nodeToMapWithPrefix(s"$prefix/${e.label}", e)
            case e: Elem => 
              nodeToMapWithPrefix(s"$prefix/${e.prefix}:${e.label}", e)
            case t: Text => Map(prefix -> t.text)
            case er: EntityRef => Map(prefix -> er.text)
          }
        }
        pathAndText.foldLeft(Map.empty[String, String]){_ ++ _}
      }
    
      nodeToMapWithPrefix(xml.label, xml)
    }
    

    要考虑的另一种情况是文本不在叶元素中时:

    val xml = <a>
      <b>text
        <c>1</c>
        <d>2</d>
      </b>
    </a>
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-02-18
      • 1970-01-01
      • 2013-05-30
      • 2017-01-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多