spark學習筆記--資料讀取與儲存

zxrui發表於2018-07-09

資料讀取與儲存

  • 檔案格式與檔案系統

對於儲存在本地檔案系統或分散式檔案系統(比如 NFS、HDFS、Amazon S3 等)中的資料,Spark 可以訪問很多種不同的檔案格式,包括文字檔案、JSON、SequenceFile,以及 protocol buffer。我們會展示幾種常見格式的用法,以及 Spark 針對不同檔案系統的配置和壓縮選項。

  • Spark SQL中的結構化資料來源

通過Spark SQL 模組,針對包括 JSON 和 Apache Hive 在內的結構化資料來源,提供了一套更加簡潔高效的 API。

  • 資料庫與鍵值儲存

    Spark 自帶的庫和一些第三方庫,它們可以用來連線 Cassandra、HBase、Elasticsearch 以及 JDBC 源。

支援的文字格式

enter image description here

文字檔案

  • 讀取檔案

    只需要使用檔案路徑作為引數呼叫 SparkContext 中的 textFile() 函式,就可以讀取一個文字檔案
    如果要控制分割槽數的話,可以指定 minPartitions

scala:

val input = sc.textFile("file:///home/holden/repos/spark/README.md")

有時候我們希望同時處理多個檔案,除上述方法外,也可以用SparkContext.wholeTextFiles() 方法,該方法會返回一個 pair RDD,其中鍵是輸入檔案的檔名。
scala:

val input = sc.wholeTextFiles("file://home/holden/salesFiles")
val result = input.mapValues{y =>
val nums = y.split(" ").map(x => x.toDouble)
nums.sum / nums.size.toDouble}
  • 儲存檔案

scala:

result.saveAsTextFile(outputFile)

JSON

  • 讀取檔案

    將資料作為文字檔案讀取,然後對 JSON 資料進行解析,這樣的方法可以在所有支援的程式語言中使用。這種方法假設檔案中的每一行都是一條 JSON 記錄。如果你有跨行的 JSON 資料,你就只能讀入整個檔案,然後對每個檔案進行解析。如果在你使用的語言中構建一個 JSON 解析器的開銷較大,你可以使用 mapPartitions() 來重用解析器。

scala:

import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.DeserializationFeature

case class Person(name: String, lovesPandas: Boolean) // 必須是頂級類
// 將其解析為特定的case class。使用flatMap,通過在遇到問題時返回空列表(None)
// 來處理錯誤,而在沒有問題時返回包含一個元素的列表(Some(_))
val result = input.flatMap(record => {
    try {
    Some(mapper.readValue(record, classOf[Person]))
    } catch {
case e: Exception => None
}})
  • 儲存檔案

scala:

//篩選後寫入檔案
result.filter(p => P.lovesPandas).map(mapper.writeValueAsString(_)).saveAsTextFile(outputFile)

CSV(逗號分隔)和 TSV(製表符分隔)

  • 讀取檔案

scala:

import Java.io.StringReader
import au.com.bytecode.opencsv.CSVReader
val input = sc.textFile(inputFile)
val result = input.map{ line =>
val reader = new CSVReader(new StringReader(line));
    reader.readNext();
}

如果在欄位中嵌有換行符,就需要完整讀入每個檔案,然後解析各段
scala:

case class Person(name: String, favoriteAnimal: String)

val input = sc.wholeTextFiles(inputFile)
val result = input.flatMap{ case (_, txt) =>
val reader = new CSVReader(new StringReader(txt));
    reader.readAll().map(x => Person(x(0), x(1)))
}
  • 儲存檔案

scala:

pandaLovers.map(person => List(person.name, person.favoriteAnimal).toArray)
.mapPartitions{people =>
val stringWriter = new StringWriter();
val csvWriter = new CSVWriter(stringWriter);
csvWriter.writeAll(people.toList)
Iterator(stringWriter.toString)
}.saveAsTextFile(outFile)

SequenceFile

SequenceFile 是由沒有相對關係結構的鍵值對檔案組成的常用 Hadoop 格式。SequenceFile 檔案有同步標記,Spark可以用它來定位到檔案中的某個點,然後再與記錄的邊界對齊。這可以讓 Spark 使用多個節點高效地並行讀取 SequenceFile 檔案。
SequenceFile 也是 Hadoop MapReduce 作業中常用的輸入輸出格式,所以如果你在使用一個已有的 Hadoop 系統,資料很有可能是以 SequenceFile 的格式供你使用的。

由於 Hadoop 使用了一套自定義的序列化框架,因此 SequenceFile 是由實現 Hadoop 的 Writable 介面的元素組成。

enter image description here

  • 讀取SequenceFile

    可以用sequenceFile(path, keyClass, valueClass, minPartitions),其中keyClass 和 valueClass 引數都必須使用正確的 Writable 類

scala:

val data = sc.sequenceFile(inFile, classOf[Text], classOf[IntWritable]).
map{case (x, y) => (x.toString, y.get())}
  • 儲存SequenceFile

    因為 SequenceFile 儲存的是鍵值對,所以需要建立一個由可以寫出到 SequenceFile 的型別構成的 PairRDD。
    我們已經進行了將許多 Scala 的原生型別轉為 Hadoop Writable 的隱式轉換,所以如果你要寫出的是 Scala 的原生型別,可以直接呼叫 saveSequenceFile(path) 儲存你的 PairRDD,它會幫你寫出資料。
    如果鍵和值不能自動轉為 Writable 型別,或者想使用變長型別(比如 VIntWritable),就可以對資料進行對映操作,在儲存之前進行型別轉換

scala:

val data = sc.parallelize(List(("Panda", 3), ("Kay", 6), ("Snail", 2)))
data.saveAsSequenceFile(outputFile)

物件檔案

物件檔案看起來就像是對 SequenceFile 的簡單封裝,它允許儲存只包含值的 RDD。和 SequenceFile 不一樣的是,物件檔案是使用 Java 序列化寫出的。

  • 讀取
    用 SparkContext 中的 objectFile() 函式接收一個路徑,返回對應的 RDD。

  • 儲存
    要儲存物件檔案,只需在 RDD 上呼叫 saveAsObjectFile 就行了

Hadoop輸入輸出格式

  • 讀取其他Hadoop輸入格式

hadoopFile()用於舊的API實現Hadoop輸入格式
每一行都會被獨立處理,鍵和值之間用製表符隔開。這個格式存在於 Hadoop 中,所以無需向工程中新增額外的依賴就能使用它。

scala:

val input = sc.hadoopFile[Text, Text, KeyValueTextInputFormat](inputFile)
.map{
    case (x, y) => (x.toString, y.toString)
}

newAPIHadoopFile() 接收一個路徑以及三個類。第一個類是“格式”類,代表輸入格式。

scala:

//在 Scala 中使用 Elephant Bird 讀取 LZO 演算法壓縮的 JSON 檔案
val input = sc.newAPIHadoopFile(inputFile, classOf[LzoJsonInputFormat],
classOf[LongWritable], classOf[MapWritable], conf)
// "輸入"中的每個MapWritable代表一個JSON物件
  • 儲存Hadoop輸出格式

    舊介面用saveAsHadoopFile(),新介面用saveAsNewAPIHadoopFile()
    非檔案系統資料來源可以使用hadoopDataset/saveAsHadoopDataSetnewAPIHadoopDataset/saveAsNewAPIHadoopDataset 來訪問 Hadoop 所支援的非檔案系統的儲存格式。

scala:

val job = new Job()
val conf = job.getConfiguration
LzoProtobufBlockOutputFormat.setClassConf(classOf[Places.Venue], conf);
val dnaLounge = Places.Venue.newBuilder()
dnaLounge.setId(1);
dnaLounge.setName("DNA Lounge")
dnaLounge.setType(Places.Venue.VenueType.CLUB)
val data = sc.parallelize(List(dnaLounge.build()))
val outputData = data.map{ pb =>
    val protoWritable = ProtobufWritable.newInstance(classOf[Places.Venue]);
    protoWritable.set(pb)
    (null, protoWritable)
}
outputData.saveAsNewAPIHadoopFile(outputFile, classOf[Text],
    classOf[ProtobufWritable[Places.Venue]],
    classOf[LzoProtobufBlockOutputFormat[ProtobufWritable[Places.Venue]]], conf)

檔案壓縮

在大資料工作中,我們經常需要對資料進行壓縮以節省儲存空間和網路傳輸開銷。對於大多數 Hadoop 輸出格式來說,我們可以指定一種壓縮編解碼器來壓縮資料

textFile()sequenceFile()讀取檔案,最好不要考慮使用spark的封裝,使用newAPIHadoopFile() 或者 hadoopFile(),並指定正確的壓縮編解碼器。

檔案系統

  • 本地/“常規”檔案系統

    本地檔案要求在叢集中所有節點的相同路徑下都可以找到,路徑形式:file:// 路徑

  • Amazon S3

    路徑形式:s3n:// 開頭的路徑以 s3n://bucket/path-within-bucket。s3支援萬用字元:s3n://bucket/my-Files/*.txt

  • HDFS

    Hadoop 分散式檔案系統(HDFS)是一種廣泛使用的檔案系統,HDFS被設計為可以在硬體上工作,有彈性地應對節點失敗且提供高吞吐量。路徑形式:hdfs://master:port/path

Spark SQL中的結構化資料

在各種情況下,我們把一條 SQL 查詢給 Spark SQL,讓它對一個資料來源執行查詢(選出一些欄位或者對欄位使用一些函式),然後得到由 Row 物件組成的 RDD,每個 Row 物件表示一條記錄。在 Java 和 Scala 中,Row 物件的訪問是基於下標的。每個 Row 都有一個 get() 方法,會返回一個一般型別讓我們可以進行型別轉換。另外還有針對常見基本型別的專用 get() 方法(例如 getFloat()、getInt()、getLong()、getString()、getShort()、getBoolean() 等)

Apache Hive

Apache Hive 是 Hadoop 上的一種常見的結構化資料來源。Hive 可以在 HDFS 內或者在其他儲存系統上儲存多種格式的表。這些格式從普通文字到列式儲存格式,應有盡有。Spark SQL 可以讀取 Hive 支援的任何表。

scala:

import org.apache.spark.sql.hive.HiveContext

val hiveCtx = new org.apache.spark.sql.hive.HiveContext(sc)
val rows = hiveCtx.sql("SELECT name, age FROM users")
val firstRow = rows.first()
println(firstRow.getString(0)) // 欄位0是name欄位

JSON

如果你有記錄間結構一致的 JSON 資料,Spark SQL 也可以自動推斷出它們的結構資訊,並將這些資料讀取為記錄,這樣就可以使得提取欄位的操作變得很簡單。

scala:

val tweets = hiveCtx.jsonFile("tweets.json")
tweets.registerTempTable("tweets")
val results = hiveCtx.sql("SELECT user.name, text FROM tweets")

Spark連線資料庫

Java資料庫連線

Spark 可以從任何支援 Java 資料庫連線(JDBC)的關係型資料庫中讀取資料,包括 MySQL、Postgre 等系統。要訪問這些資料,需要構建一個 org.apache.spark.rdd.JdbcRDD,將 SparkContext 和其他引數一起傳給它。

scala:

//用於對資料庫建立連線的函式
def createConnection() = {
    Class.forName("com.mysql.jdbc.Driver").newInstance();
    DriverManager.getConnection("jdbc:mysql://localhost/test?user=holden");
}

//將輸出結果從 java.sql.ResultSet轉為對運算元據有用的格式的函式
def extractValues(r: ResultSet) = {
    (r.getInt(1), r.getString(2))
}

//讀取一定範圍內資料的查詢,以及查詢引數中 lowerBound 和 upperBound 的值
val data = new JdbcRDD(sc,
    createConnection, "SELECT * FROM panda WHERE ? <= id AND id <= ?",
    lowerBound = 1, upperBound = 3, numPartitions = 2, mapRow = extractValues)
println(data.collect().toList)

Cassandra

隨著 DataStax 開源其用於 Spark 的 Cassandra 聯結器

Maven 依賴:

<dependency> <!-- Cassandra -->
    <groupId>com.datastax.spark</groupId>
    <artifactId>spark-cassandra-connector</artifactId>
    <version>1.0.0-rc5</version>
</dependency>
<dependency> <!-- Cassandra -->
    <groupId>com.datastax.spark</groupId>
    <artifactId>spark-cassandra-connector-java</artifactId>
    <version>1.0.0-rc5</version>
</dependency>

scala:

//配置Cassandra屬性
val conf = new SparkConf(true)
    .set("spark.cassandra.connection.host", "hostname")

val sc = new SparkContext(conf)

//對整張鍵值對錶讀取為RDD
// 為SparkContext和RDD提供附加函式的隱式轉換
import com.datastax.spark.connector._

// 將整張表讀為一個RDD。假設你的表test的建立語句為
// CREATE TABLE test.kv(key text PRIMARY KEY, value int);
val data = sc.cassandraTable("test" , "kv")
// 列印出value欄位的一些基本統計。
data.map(row => row.getInt("value")).stats()

在scala中儲存資料到Cassandra

val rdd = sc.parallelize(List(Seq("moremagic", 1)))
rdd.saveToCassandra("test" , "kv", SomeColumns("key", "value"))

HBase

scala:

import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat

val conf = HBaseConfiguration.create()
conf.set(TableInputFormat.INPUT_TABLE, "tablename") // 掃描哪張表

val rdd = sc.newAPIHadoopRDD(
conf, classOf[TableInputFormat], classOf[ImmutableBytesWritable],classOf[Result])

Elasticsearch

Spark 可以使用 Elasticsearch-Hadoop,從 Elasticsearch 中讀寫資料。Elasticsearch 是一個開源的、基於 Lucene 的搜尋系統。

在 Scala 中使用 Elasticsearch 輸出

val jobConf = new JobConf(sc.hadoopConfiguration)
jobConf.set("mapred.output.format.class", "org.elasticsearch.hadoop.
mr.EsOutputFormat")
jobConf.setOutputCommitter(classOf[FileOutputCommitter])
jobConf.set(ConfigurationOptions.ES_RESOURCE_WRITE, "twitter/tweets")
jobConf.set(ConfigurationOptions.ES_NODES, "localhost")
FileOutputFormat.setOutputPath(jobConf, new Path("-"))
output.saveAsHadoopDataset(jobConf)

在 Scala 中使用 Elasticsearch 輸入

def mapWritableToInput(in: MapWritable): Map[String, String] = {
    in.map{case (k, v) => (k.toString, v.toString)}.toMap
}

val jobConf = new JobConf(sc.hadoopConfiguration)
jobConf.set(ConfigurationOptions.ES_RESOURCE_READ, args(1))
jobConf.set(ConfigurationOptions.ES_NODES, args(2))
val currentTweets = sc.hadoopRDD(jobConf,
    classOf[EsInputFormat[Object, MapWritable]], classOf[Object],
    classOf[MapWritable])
// 僅提取map
// 將MapWritable[Text, Text]轉為Map[String, String]
val tweets = currentTweets.map{ case (key, value) => mapWritableToInput(value) }

相關文章