Spark讀取JSON的小擴展

Spark讀取JSON的小擴展

版本說明:

spark 2.3

前言

前幾天在羣裏摸魚的時候,碰都一位同學問了一個比較有趣的問題,他提問:Spark如何讀取原生JSON?看到這個問題,心裏有些疑惑,Spark不是有JSON數據源支持嗎,怎麼這裏還要問如何讀取原生JSON,這原生JSON又是什麼鬼?經過交流才明白,原來他所說的原生JSON是類似如下這種格式:

{
  "昌平區東小": [
    116.4021289,
    40.05688698
  ],
  "昌平區回龍": [
    116.3412241,
    40.07942604
  ],
  "昌平區北七": [
    116.4179459,
    40.11644403
  ],
  "昌平區陽坊": [
    116.1332611,
    40.13701398
  ]
}

而我們平時使用Spark讀取的JSON是如下格式

{"address":"昌平區東小","location":[116.4021289,40.05688698]}
{"address":"昌平區回龍","location":[116.3412241, 40.07942604]}
{"address":"昌平區北七","location":[116.4179459,40.11644403]}
{"address":"昌平區陽坊","location":[116.1332611,40.13701398]}

如果使用spark.read.json直接讀取他所說的"原生JSON"格式的文件,將會報如下錯誤:

感覺這個問題還是比較有趣的,所以自己就親自試試了,找了種解決方案。

1 需求說明

上文中,我們已經可以看出問題所在。其實就是將所謂的"原生JSON"格式的文件讀取出來轉成如下格式的DataFrame。

+-------+-------------------------+
|address|location                 |
+-------+-------------------------+
|昌平區北七  |[116.4179459,40.11644403]|
|昌平區回龍  |[116.3412241,40.07942604]|
|昌平區陽坊  |[116.1332611,40.13701398]|
|昌平區東小  |[116.4021289,40.05688698]|
+-------+-------------------------+

上文也提到如果我們的JSON文件時如下格式的

{"address":"昌平區東小","location":[116.4021289,40.05688698]}
{"address":"昌平區回龍","location":[116.3412241, 40.07942604]}
{"address":"昌平區北七","location":[116.4179459,40.11644403]}
{"address":"昌平區陽坊","location":[116.1332611,40.13701398]}

那麼使用Spark很容易讀出來,並轉成DataFrame

spark.read.json("/Users/shirukai/hollysys/repository/learn-demo-spark/data/location.json").show(false)

那麼"原生JSON"格式的該如何處理呢,我帶着這個需求,進行了一些嘗試。

2 方案一:手動解析JSON

這個方案的大體思路是:讀取文本內容,手動解析JSON,然後使用flatmap算子轉換,最後生成DataFrame。這個方案也是最先想到的,感覺也是最容易實現的,但是在實現的時候卻遇到了一個小問題,導致我花費了幾個小時的時間才解決,其實主要是因爲基本功不紮實,走了不少彎路。先說一下遇到的問題吧,這裏也做一個小記錄,讀取文本的時候,直接使用了read.text(),結果默認按照行分隔符生成了多條記錄,拿不到一條完整的JSON。其實我的目的就是將整個文本當做一條記錄來處理,一開始想着指定別的行分隔符就能拿到一條記錄了,百度、google了好多方法沒有結果。傻逼了半天,看了看源碼,發現:

  /**
   * Loads text files and returns a `DataFrame` whose schema starts with a string column named
   * "value", and followed by partitioned columns if there are any.
   *
   * By default, each line in the text files is a new row in the resulting DataFrame. For example:
   * {{{
   *   // Scala:
   *   spark.read.text("/path/to/spark/README.md")
   *
   *   // Java:
   *   spark.read().text("/path/to/spark/README.md")
   * }}}
   *
   * You can set the following text-specific option(s) for reading text files:
   * <ul>
   * <li>`wholetext` (default `false`): If true, read a file as a single row and not split by "\n".
   * </li>
   * <li>`lineSep` (default covers all `\r`, `\r\n` and `\n`): defines the line separator
   * that should be used for parsing.</li>
   * </ul>
   *
   * @param paths input paths
   * @since 1.6.0
   */
  @scala.annotation.varargs
  def text(paths: String*): DataFrame = format("text").load(paths : _*)

wholetext這個參數不就是我想要嘛,疏忽了疏忽了竟然忘了這一茬,早在之前整理過Spark的各種數據源,但是隻是簡單的讀取和寫入,對於需要的參數沒有研究,所以這幾天又參照源碼,重新整理了《SparkSQL數據源操作》這個筆記,將每個數據源支持的Option參數都完整的整理了出來。

解決了上述讀取文件的問題之後,也事半功倍了,後面只需要使用FastJSON加載數據,按key分行即可。如下代碼所示:

    import spark.implicits._
    import scala.collection.JavaConverters._
    val text = spark.read
      .option("wholetext", value = true)
      .text("/Users/shirukai/hollysys/repository/learn-demo-spark/data/location.json").as[String]

    val jsonDF = text.flatMap(line => {
      val json = JSON.parseObject(line)
      val keys = json.keySet()
      keys.asScala.map(key => {
        (key, json.getString(key))
      })
    })
    jsonDF.toDF("address", "location").show(false)

2 方案二:自定義數據源

思路:繼承DataSourceV2接口自定義數據源。

針對這一類型的數據單獨寫一個數據源,其實這類型的就是K,V格式的,k做成一列、v做成一列,如果需要經常用到處理這種數據,可以自己寫一個數據源。這裏只是簡單寫一下步驟,真正讀取數據的時候比較複雜。

2.1 創建一個名爲KVJSONDataSource的數據源

代碼如下所示,讀取文件實際上會複雜,涉及到讀本地文件和HDFS文件等等。這裏只是簡單的讀取了一下本地文件。

package com.hollysys.spark.sql.datasource.kvjson

import java.util

import com.alibaba.fastjson.JSON
import org.apache.spark.sql.Row
import org.apache.spark.sql.sources.v2.reader.{DataReader, DataReaderFactory, DataSourceReader}
import org.apache.spark.sql.sources.v2.{DataSourceOptions, DataSourceV2, ReadSupport}
import org.apache.spark.sql.types.StructType

import scala.io.Source

/**
  * Created by shirukai on 2019-06-15 10:50
  * K、V格式JSON數據源
  */
class KVJSONDataSource extends DataSourceV2 with ReadSupport {
  override def createReader(options: DataSourceOptions): DataSourceReader = new KVJSONDataSourceReader(options)
}

class KVJSONDataSourceReader(options: DataSourceOptions) extends DataSourceReader {
  var requiredSchema: StructType = StructType.fromDDL(options.get("schema").get())

  override def readSchema(): StructType = requiredSchema

  override def createDataReaderFactories(): util.List[DataReaderFactory[Row]] = {
    import collection.JavaConverters._
    Seq(
      new KVJSONDataSourceReaderFactory(options.get("path").get()).asInstanceOf[DataReaderFactory[Row]]
    ).asJava
  }
}

class KVJSONDataSourceReaderFactory(path: String) extends DataReaderFactory[Row] {
  override def createDataReader(): DataReader[Row] = new KVJSONDataReader(path)
}

class KVJSONDataReader(path: String) extends DataReader[Row] {

  val data: Iterator[Seq[AnyRef]] = readData

  override def next(): Boolean = data.hasNext

  override def get(): Row = {
    val line = data.next()
    Row(line: _*)
  }

  override def close(): Unit = {}
  def readData: Iterator[Seq[AnyRef]] = {
    import scala.collection.JavaConverters._
    val source = Source.fromFile(path)
    val jsonStr = source.mkString
    val json = JSON.parseObject(jsonStr)
    val keys = json.keySet()
    keys.asScala.map(key => {
      Seq(key, json.getString(key))
    }).toIterator
  }
}

2.2 使用自定義數據源

數據源自定義完成後,我們就可以使用了,使用方式很簡單,如下代碼:

    spark.read.format("com.hollysys.spark.sql.datasource.kvjson.KVJSONDataSource")
      .option("schema","`address` STRING,`location` STRING")
      .option("path","/Users/shirukai/hollysys/repository/learn-demo-spark/data/location.json")
      .load().show(false)

輸出

+-------+-------------------------+
|address|location                 |
+-------+-------------------------+
|昌平區北七  |[116.4179459,40.11644403]|
|昌平區東小  |[116.4021289,40.05688698]|
|昌平區回龍  |[116.3412241,40.07942604]|
|昌平區陽坊  |[116.1332611,40.13701398]|
+-------+-------------------------+

3 總結

目前只想到兩種方案,其實第二種方案有點大材小用了,如果大家有更好的方案,歡迎交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章