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 總結
目前只想到兩種方案,其實第二種方案有點大材小用了,如果大家有更好的方案,歡迎交流。