【问题标题】:Flatten only deepest level in scala spark dataframe仅展平scala spark数据框中的最深层次
【发布时间】:2020-04-26 22:56:06
【问题描述】:

我有一个 Spark 作业,它的 DataFrame 具有以下值:

{
  "id": "abchchd",
  "test_id": "ndsbsb",
  "props": {
    "type": {
      "isMale": true,
      "id": "dd",
      "mcc": 1234,
      "name": "Adam"
    }
  }
}

{
  "id": "abc",
  "test_id": "asf",
  "props": {
    "type2": {
      "isMale": true,
      "id": "dd",
      "mcc": 12134,
      "name": "Perth"
    }
  }
}

我想优雅地把它展平(因为没有未知的键和类型等),这样道具仍然是struct,但里面的所有东西都被展平了(不管嵌套级别如何)

所需的输出是:

{
  "id": "abchchd",
  "test_id": "ndsbsb",
  "props": {
    "type.isMale": true,
    "type.id": "dd",
    "type.mcc": 1234,
    "type.name": "Adam"
  }
}

{
  "id": "abc",
  "test_id": "asf",
  "props": {
      "type2.isMale": true,
      "type2.id": "dd",
      "type2.mcc": 12134,
      "type2.name": "Perth"
  }
}

我使用了中提到的解决方案 Automatically and Elegantly flatten DataFrame in Spark SQL

但是,我无法保持 props 字段完整。它也会变平。 有人可以帮我扩展这个解决方案吗?

最终的架构应该是这样的:

root
 |-- id: string (nullable = true)
 |-- props: struct (nullable = true)
 |    |-- type.id: string (nullable = true)
 |    |-- type.isMale: boolean (nullable = true)
 |    |-- type.mcc: long (nullable = true)
 |    |-- type.name: string (nullable = true)
      |-- type2.id: string (nullable = true)
 |    |-- type2.isMale: boolean (nullable = true)
 |    |-- type2.mcc: long (nullable = true)
 |    |-- type2.name: string (nullable = true)
 |-- test_id: string (nullable = true)

【问题讨论】:

  • 嗨@mythic,所以你从你的结构中所知道的是,可能有一些props字段是一个对象,属性也是子对象,对吧?
  • @baitmbarek 没错

标签: json scala apache-spark apache-spark-sql flatten


【解决方案1】:

我已经能够通过 RDD API 实现这一点:

val jsonRDD = df.rdd.map{row =>
  def unnest(r: Row): Map[String, Any] = {
    r.schema.fields.zipWithIndex.flatMap{case (f, i) =>
      (f.name, f.dataType) match {
        case ("props", _:StructType) =>
          val propsObject = r.getAs[Row](f.name)
          Map(f.name -> propsObject.schema.fields.flatMap{propsAttr =>
            val subObject = propsObject.getAs[Row](propsAttr.name)
            subObject.schema.fields.map{subField =>
              s"${propsAttr.name}.${subField.name}" -> subObject.get(subObject.fieldIndex(subField.name))
            }
          }.toMap)
        case (fname, _: StructType) => Map(fname -> unnest(r.getAs[Row](fname)))
        case (fname, ArrayType(_: StructType,_)) => Map(fname -> r.getAs[Seq[Row]](fname).map(unnest))
        case _ => Map(f.name -> r.get(i))
      }
    }
  }.toMap

  val asMap = unnest(row)
  new ObjectMapper().registerModule(DefaultScalaModule).writeValueAsString(asMap)
}

val finalDF = spark.read.json(jsonRDD.toDS).cache

由于递归,该解决方案应该接受深度嵌套的输入。

有了你的数据,我们得到了:

finalDF.printSchema()
finalDF.show(false)
finalDF.select("props.*").show()

输出:

root
 |-- id: string (nullable = true)
 |-- props: struct (nullable = true)
 |    |-- type.id: string (nullable = true)
 |    |-- type.isMale: boolean (nullable = true)
 |    |-- type.mcc: long (nullable = true)
 |    |-- type.name: string (nullable = true)
 |-- test_id: string (nullable = true)

+-------+----------------------+-------+
|id     |props                 |test_id|
+-------+----------------------+-------+
|abchchd|[dd, true, 1234, Adam]|ndsbsb |
+-------+----------------------+-------+

+-------+-----------+--------+---------+
|type.id|type.isMale|type.mcc|type.name|
+-------+-----------+--------+---------+
|     dd|       true|    1234|     Adam|
+-------+-----------+--------+---------+

但我们也可以传递更多嵌套/复杂的结构,例如:

val str2 = """{"newroot":[{"mystruct":{"id":"abchchd","test_id":"ndsbsb","props":{"type":{"isMale":true,"id":"dd","mcc":1234,"name":"Adam"}}}}]}"""

...

finalDF.printSchema()
finalDF.show(false)

给出以下输出:

root
 |-- newroot: array (nullable = true)
 |    |-- element: struct (containsNull = true)
 |    |    |-- mystruct: struct (nullable = true)
 |    |    |    |-- id: string (nullable = true)
 |    |    |    |-- props: struct (nullable = true)
 |    |    |    |    |-- type.id: string (nullable = true)
 |    |    |    |    |-- type.isMale: boolean (nullable = true)
 |    |    |    |    |-- type.mcc: long (nullable = true)
 |    |    |    |    |-- type.name: string (nullable = true)
 |    |    |    |-- test_id: string (nullable = true)

+---------------------------------------------+
|root                                         |
+---------------------------------------------+
|[[[abchchd, [dd, true, 1234, Adam], ndsbsb]]]|
+---------------------------------------------+

编辑:正如您所提到的,如果您有不同结构的记录,您需要将上述subObject 值包装在一个选项中。
这是固定的unnest 函数:

def unnest(r: Row): Map[String, Any] = {
  r.schema.fields.zipWithIndex.flatMap{case (f, i) =>
    (f.name, f.dataType) match {
      case ("props", _:StructType) =>
        val propsObject = r.getAs[Row](f.name)
        Map(f.name -> propsObject.schema.fields.flatMap{propsAttr =>
          val subObjectOpt = Option(propsObject.getAs[Row](propsAttr.name))
          subObjectOpt.toSeq.flatMap{subObject => subObject.schema.fields.map{subField =>
            s"${propsAttr.name}.${subField.name}" -> subObject.get(subObject.fieldIndex(subField.name))
          }}
        }.toMap)
      case (fname, _: StructType) => Map(fname -> unnest(r.getAs[Row](fname)))
      case (fname, ArrayType(_: StructType,_)) => Map(fname -> r.getAs[Seq[Row]](fname).map(unnest))
      case _ => Map(f.name -> r.get(i))
    }
  }
}.toMap

新的printSchema 给出:

root
 |-- id: string (nullable = true)
 |-- props: struct (nullable = true)
 |    |-- type.id: string (nullable = true)
 |    |-- type.isMale: boolean (nullable = true)
 |    |-- type.mcc: long (nullable = true)
 |    |-- type.name: string (nullable = true)
 |    |-- type2.id: string (nullable = true)
 |    |-- type2.isMale: boolean (nullable = true)
 |    |-- type2.mcc: long (nullable = true)
 |    |-- type2.name: string (nullable = true)
 |-- test_id: string (nullable = true)

【讨论】:

  • 嗨@baitmbarek,这似乎工作!看起来简单而强大!太感谢了。刚刚有一个关于嵌套字段的问题,如果我执行 finalDf.select("props.type.id") 它会抛出一个错误,指出找不到该字段。这是因为 spark 正在寻找一个名为 type 的强大结构,因此它失败了。一种解决方法是将子字段重命名为 type_id 并用下划线替换点。有没有其他方法可以让我继续使用 dot 而不是 struct 字段?
  • 嗨@mythic,您需要“转义”点:finalDF.select("props.`type.isMale`").show()。这将呈现type.isMale
  • 嗨@baitmbarek,当 df 具有不同的命名字段时,此解决方案似乎会中断。我在 df 的另一行中有一个名为“type”的字段和一个名为“type1”的字段。它抛出一个空指针异常! :(
  • 嗨@mythic你能分享一下结构,以便我修复/调整方法吗?
  • @mythic 我刚刚编辑了答案,改进了处理可能丢失的属性
猜你喜欢
  • 1970-01-01
  • 2023-03-03
  • 2019-07-05
  • 2021-09-08
  • 2016-12-09
  • 2021-12-20
  • 2020-08-24
  • 2016-05-07
  • 1970-01-01
相关资源
最近更新 更多