gpt4 book ai didi

json - 使用 circe 递归地将 JSON 树转换为其他格式(XML、CSV 等)

转载 作者:数据小太阳 更新时间:2023-10-29 02:23:19 31 4
gpt4 key购买 nike

为了使用 circe 将 JSON 节点转换为 JSON 以外的其他格式(如 XML、CSV 等),我想出了一个解决方案,我必须访问 circe 的内部数据结构。

这是我将 JSON 转换为 XML 字符串的工作示例(并不完美,但您明白了):

package io.circe

import io.circe.Json.{JArray, JBoolean, JNull, JNumber, JObject, JString}
import io.circe.parser.parse

object Sample extends App {

def transformToXMLString(js: Json): String = js match {
case JNull => ""
case JBoolean(b) => b.toString
case JNumber(n) => n.toString
case JString(s) => s.toString
case JArray(a) => a.map(transformToXMLString(_)).mkString("")
case JObject(o) => o.toMap.map {
case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
}.mkString("")
}

val json =
"""{
| "root": {
| "sampleboolean": true,
| "sampleobj": {
| "anInt": 1,
| "aString": "string"
| },
| "objarray": [
| {"v1": 1},
| {"v2": 2}
| ]
| }
|}""".stripMargin

val res = transformToXMLString(parse(json).right.get)
println(res)
}

结果:

<root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

如果低级 JSON 对象(如 JBoolean、JString、JObject 等)不是 package private in circe,那一切都很好而且花花公子,这只会使这段代码如果将其放在包 package io.circe 中,则上述工作。

如何使用公共(public) circe API 获得与上述相同的结果?

最佳答案

Json 上的fold 方法允许您非常简洁地执行此类操作(并且以一种强制穷举的方式,就像对密封特征进行模式匹配一​​样) :

import io.circe.Json

def transformToXMLString(js: Json): String = js.fold(
"",
_.toString,
_.toString,
identity,
_.map(transformToXMLString(_)).mkString(""),
_.toMap.map {
case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
}.mkString("")
)

然后:

scala> import io.circe.parser.parse
import io.circe.parser.parse

scala> transformToXMLString(parse(json).right.get)
res1: String = <root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

与您的实现结果完全相同,但字符少一些,并且不依赖于实现的私有(private)细节。

所以答案是“使用 fold”(或另一个答案中建议的 asX 方法——这种方法更灵活,但通常可能更少惯用且更冗长)。如果你关心我们为什么设计决定不暴露构造函数,你可以跳到这个答案的末尾,但是这种问题经常出现,所以我也想解决一些相关的问题首先。

关于命名的附注

请注意,此方法使用的名称“fold”是从 Argonaut 继承而来的,可以说是不准确的。当我们谈论递归代数数据类型的变形(或折叠)时,我们指的是一个函数,我们在传入的函数的参数中看不到 ADT 类型。例如,列表折叠的签名看起来像这样:

def foldLeft[B](z: B)(op: (B, A) => B): B

不是这个:

def foldLeft[B](z: B)(op: (List[A], A) => B): B

因为 io.circe.Json 是一个递归的 ADT,它的 fold 方法应该是这样的:

def properFold[X](
jsonNull: => X,
jsonBoolean: Boolean => X,
jsonNumber: JsonNumber => X,
jsonString: String => X,
jsonArray: Vector[X] => X,
jsonObject: Map[String, X] => X
): X

代替:

def fold[X](
jsonNull: => X,
jsonBoolean: Boolean => X,
jsonNumber: JsonNumber => X,
jsonString: String => X,
jsonArray: Vector[Json] => X,
jsonObject: JsonObject => X
): X

但在实践中前者似乎用处不大,所以circe只提供了后者(如果要递归,则必须手动完成),并沿袭了Argonaut的说法,将其称为fold。这一直让我有点不舒服,以后这个名字可能会改变。

关于性能的旁注

在某些情况下,实例化 fold 期望的六个函数可能非常昂贵,因此 circe 还允许您将这些操作捆绑在一起:

import io.circe.{ Json, JsonNumber, JsonObject }

val xmlTransformer: Json.Folder[String] = new Json.Folder[String] {
def onNull: String = ""
def onBoolean(value: Boolean): String = value.toString
def onNumber(value: JsonNumber): String = value.toString
def onString(value: String): String = value
def onArray(value: Vector[Json]): String =
value.map(_.foldWith(this)).mkString("")
def onObject(value: JsonObject): String = value.toMap.map {
case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
}.mkString("")
}

然后:

scala> parse(json).right.get.foldWith(xmlTransformer)
res2: String = <root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

使用 Folder 的性能优势会有所不同,具体取决于您使用的是 2.11 还是 2.12,但是如果您对 JSON 值执行的实际操作很便宜,您可以期望 Folder 版本以获得大约两倍于 fold 的吞吐量。顺便说一下,它也比内部构造函数的模式匹配快得多,至少在 benchmarks we've done 中是这样。 :

Benchmark                           Mode  Cnt      Score    Error  Units
FoldingBenchmark.withFold thrpt 10 6769.843 ± 79.005 ops/s
FoldingBenchmark.withFoldWith thrpt 10 13316.918 ± 60.285 ops/s
FoldingBenchmark.withPatternMatch thrpt 10 8022.192 ± 63.294 ops/s

那是在 2.12 上。我相信您应该在 2.11 上看到更多差异。

关于光学的旁注

如果你真的想要模式匹配,circe-optics为您提供案例类提取器的高性能替代方案:

import io.circe.Json, io.circe.optics.all._

def transformToXMLString(js: Json): String = js match {
case `jsonNull` => ""
case jsonBoolean(b) => b.toString
case jsonNumber(n) => n.toString
case jsonString(s) => s.toString
case jsonArray(a) => a.map(transformToXMLString(_)).mkString("")
case jsonObject(o) => o.toMap.map {
case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
}.mkString("")
}

这几乎与原始版本的代码完全相同,但每个提取器都是一个单片眼镜棱镜,可以与来自 the Monocle library 的其他光学器件组合在一起。 .

(这种方法的缺点是您失去了详尽检查,但不幸的是,这无济于事。)

为什么不只是案例类

当我第一次开始研究 circe 时,我在 a document about some of my design decisions 中写了以下内容:

In some cases, including most significantly here the io.circe.Json type, we don't want to encourage users to think of the ADT leaves as having meaningful types. A JSON value "is" a boolean or a string or a unit or a Seq[Json] or a JsonNumber or a JsonObject. Introducing types like JString, JNumber, etc. into the public API just confuses things.

我想要一个非常精简的 API(尤其是一个避免暴露无意义类型的 API)并且我想要优化 JSON 表示的空间。 (我也根本不希望人们使用 JSON AST,但那更像是一场失败的战斗。)我仍然认为隐藏构造函数是正确的决定,尽管我没有真正利用它他们(还)没有进行优化,尽管这个问题经常出现。

关于json - 使用 circe 递归地将 JSON 树转换为其他格式(XML、CSV 等),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55074967/

31 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com