web-dev-qa-db-ja.com

Spark from_json with dynamic schema

Spark変数構造(ネストされたJSON)のJSONデータを処理するために使用しています。入力JSONデータは非常に大きく、1行あたりのキー数が1000を超え、1つのバッチが20を超える可能性がありますGB。バッチ全体が30個のデータソースから生成され、各JSONの 'key2'を使用してソースを識別でき、各ソースの構造は事前定義されています。

そのようなデータを処理するための最良のアプローチは何でしょうか?以下のようにfrom_jsonを使用してみましたが、これは固定スキーマでのみ機能し、最初にそれを使用するには、各ソースに基づいてデータをグループ化してからスキーマを適用する必要があります。データ量が多いため、データを1回だけスキャンし、事前定義されたスキーマに基づいて各ソースから必要な値を抽出することをお勧めします。

import org.Apache.spark.sql.types._ 
import spark.implicits._

val data = sc.parallelize(
    """{"key1":"val1","key2":"source1","key3":{"key3_k1":"key3_v1"}}"""
    :: Nil)
val df = data.toDF


val schema = (new StructType)
    .add("key1", StringType)
    .add("key2", StringType)
    .add("key3", (new StructType)
    .add("key3_k1", StringType))


df.select(from_json($"value",schema).as("json_str"))
  .select($"json_str.key3.key3_k1").collect
res17: Array[org.Apache.spark.sql.Row] = Array([xxx])
7
Syntax

これは、@ Ramesh Maharjanの回答を単に言い換えたものですが、より現代的なSpark構文を使用しています。

私はこのメソッドがDataFrameReaderに潜んでいることを発見しました。これにより、JSON文字列を_Dataset[String]_から任意のDataFrameに解析し、同じスキーマ推論を利用できますSpark JSONファイルから直接読み取る場合、spark.read.json("filepath")が提供されます。各行のスキーマは完全に異なる場合があります。

_def json(jsonDataset: Dataset[String]): DataFrame
_

使用例:

_val jsonStringDs = spark.createDataset[String](
  Seq(
      ("""{"firstname": "Sherlock", "lastname": "Holmes", "address": {"streetNumber": 121, "street": "Baker", "city": "London"}}"""),
      ("""{"name": "Amazon", "employeeCount": 500000, "marketCap": 817117000000, "revenue": 177900000000, "CEO": "Jeff Bezos"}""")))

jsonStringDs.show

jsonStringDs:org.Apache.spark.sql.Dataset[String] = [value: string]
+----------------------------------------------------------------------------------------------------------------------+
|value                                                                                                                 
|
+----------------------------------------------------------------------------------------------------------------------+
|{"firstname": "Sherlock", "lastname": "Holmes", "address": {"streetNumber": 121, "street": "Baker", "city": "London"}}|
|{"name": "Amazon", "employeeCount": 500000, "marketCap": 817117000000, "revenue": 177900000000, "CEO": "Jeff Bezos"}  |
+----------------------------------------------------------------------------------------------------------------------+


val df = spark.read.json(jsonStringDs)
df.show(false)

df:org.Apache.spark.sql.DataFrame = [CEO: string, address: struct ... 6 more fields]
+----------+------------------+-------------+---------+--------+------------+------+------------+
|CEO       |address           |employeeCount|firstname|lastname|marketCap   |name  |revenue     |
+----------+------------------+-------------+---------+--------+------------+------+------------+
|null      |[London,Baker,121]|null         |Sherlock |Holmes  |null        |null  |null        |
|Jeff Bezos|null              |500000       |null     |null    |817117000000|Amazon|177900000000|
+----------+------------------+-------------+---------+--------+------------+------+------------+
_

このメソッドは、Spark 2.2.0: http://spark.Apache.org/docs/2.2.0/api/scala/index.html#org.Apache .spark.sql.DataFrameReader @ json(jsonDataset:org.Apache.spark.sql.Dataset [String]):org.Apache.spark.sql.DataFrame

5
Wade Jensen

私の提案があなたに役立つかどうかはわかりませんが、同様のケースがあり、次のように解決しました:

1)つまり、json rapture(または他のjsonライブラリ)を使用して、JSONスキーマを動的にロードするという考えです。たとえば、jsonファイルの1行目を読んでスキーマを見つけることができます(ここでjsonSchemaを使って行うのと同様に)

2)スキーマを動的に生成します。最初に動的フィールドを反復処理し(key3の値をMap [String、String]として投影していることに注意)、それぞれにStructFieldをスキーマに追加します

3)生成されたスキーマをデータフレームに適用します

import rapture.json._
import jsonBackends.jackson._

val jsonSchema = """{"key1":"val1","key2":"source1","key3":{"key3_k1":"key3_v1", "key3_k2":"key3_v2", "key3_k3":"key3_v3"}}"""
val json = Json.parse(jsonSchema)

import scala.collection.mutable.ArrayBuffer
import org.Apache.spark.sql.types.StructField
import org.Apache.spark.sql.types.{StringType, StructType}

val schema = ArrayBuffer[StructField]()
//we could do this dynamic as well with json rapture
schema.appendAll(List(StructField("key1", StringType), StructField("key2", StringType)))

val items = ArrayBuffer[StructField]()
json.key3.as[Map[String, String]].foreach{
  case(k, v) => {
    items.append(StructField(k, StringType))
  }
}
val complexColumn =  new StructType(items.toArray)
schema.append(StructField("key3", complexColumn))

import org.Apache.spark.SparkConf
import org.Apache.spark.sql.SparkSession
val sparkConf = new SparkConf().setAppName("dynamic-json-schema").setMaster("local")

val spark = SparkSession.builder().config(sparkConf).getOrCreate()

val jsonDF = spark.read.schema(StructType(schema.toList)).json("""your_path\data.json""")

jsonDF.select("key1", "key2", "key3.key3_k1", "key3.key3_k2", "key3.key3_k3").show()

次のデータを入力として使用しました。

{"key1":"val1","key2":"source1","key3":{"key3_k1":"key3_v11", "key3_k2":"key3_v21", "key3_k3":"key3_v31"}}
{"key1":"val2","key2":"source2","key3":{"key3_k1":"key3_v12", "key3_k2":"key3_v22", "key3_k3":"key3_v32"}}
{"key1":"val3","key2":"source3","key3":{"key3_k1":"key3_v13", "key3_k2":"key3_v23", "key3_k3":"key3_v33"}}

そして出力:

+----+-------+--------+--------+--------+
|key1|   key2| key3_k1| key3_k2| key3_k3|
+----+-------+--------+--------+--------+
|val1|source1|key3_v11|key3_v21|key3_v31|
|val2|source2|key3_v12|key3_v22|key3_v32|
|val2|source3|key3_v13|key3_v23|key3_v33|
+----+-------+--------+--------+--------+

まだテストしていない高度な代替手段は、JSONスキーマからJsonRowと呼ばれるケースクラスを生成して、コードをより保守しやすくするという事実とは別に、シリアル化のパフォーマンスを向上させる強く型付けされたデータセットを用意することです。これを機能させるには、まずJsonRow.scalaファイルを作成する必要があります。次に、ソースファイルに基づいて動的にJsonRow.scala(もちろん複数ある場合があります)のコンテンツを変更するsbt事前ビルドスクリプトを実装する必要があります。クラスJsonRowを動的に生成するには、次のコードを使用できます。

def generateClass(members: Map[String, String], name: String) : Any = {
    val classMembers = for (m <- members) yield {
        s"${m._1}: String"
    }

    val classDef = s"""case class ${name}(${classMembers.mkString(",")});scala.reflect.classTag[${name}].runtimeClass"""
    classDef
  }

GenerateClassメソッドは、文字列のマップを受け入れて、クラスメンバーとクラス名自体を作成します。生成されたクラスのメンバーは、jsonスキーマから再度追加できます。

import org.codehaus.jackson.node.{ObjectNode, TextNode}
import collection.JavaConversions._

val mapping = collection.mutable.Map[String, String]()
val fields = json.$root.value.asInstanceOf[ObjectNode].getFields

for (f <- fields) {
  (f.getKey, f.getValue) match {
    case (k: String, v: TextNode) => mapping(k) = v.asText
    case (k: String, v: ObjectNode) => v.getFields.foreach(f => mapping(f.getKey) = f.getValue.asText)
    case _ => None
  }
}

val dynClass = generateClass(mapping.toMap, "JsonRow")
println(dynClass)

これは出力します:

case class JsonRow(key3_k2: String,key3_k1: String,key1: String,key2: String,key3_k3: String);scala.reflect.classTag[JsonRow].runtimeClass

幸運を

1

質問で述べたようなデータがある場合

val data = sc.parallelize(
    """{"key1":"val1","key2":"source1","key3":{"key3_k1":"key3_v1"}}"""
    :: Nil)

json dataに対してschemaを作成する必要はありません。 Spark sqlは、json文字列からschemaを推測できます。以下のようにSQLContext.read.jsonを使用するだけです

val df = sqlContext.read.json(data)

上記で使用されたrddデータについて、以下のようにschemaが得られます

root
 |-- key1: string (nullable = true)
 |-- key2: string (nullable = true)
 |-- key3: struct (nullable = true)
 |    |-- key3_k1: string (nullable = true)

そして、あなたは単にselectkey3_k1として

df2.select("key3.key3_k1").show(false)
//+-------+
//|key3_k1|
//+-------+
//|key3_v1|
//+-------+

dataframeは自由に操作できます。答えが役に立てば幸いです

0
Ramesh Maharjan