数据读取与存储

数据源类型

  • 文件系统中的不同文件格式数据源:支持文件系统包括NFS,HDFS,Amazon S3,支持的文件格式包括有:文本文件,JSON,SequenceFile,protocal buffer
  • Spark SQL中的结构化数据源: 包括JSON 和 Apache Hive 在内的结构化数据源
  • 数据库与键值存储:可以用来连接 Cassandra、 HBase、Elasticsearch 以及 JDBC 源

文件格式

  • 文本文件:当我们将一个文本文件读取为 RDD 时,输入的每一行都会成为 RDD 的一个元素。也可以将多个完整的文本文件一次性读取为一个 pair RDD,其中键是文件名,值是文件内容。
    • textFile():返回一个RDD,其中文件中的每一行都是一个元素
    • wholeTextFile():返回一个 pair RDD,其中键是输入文件的文件名。在每个文件表示一个特定时间段内的数据时非常有用。
    • saveAsTextFile():将数据保存为本地文件:保存之后会生成一个文件夹文件的格式如下所示
averg.saveAsTextFile("averg.out")

其生成的文件内容如下:

eversilver@debian:/usr/local/spark2.1/mytest$ ls averg.out/
part-00000  _SUCCESS
eversilver@debian:/usr/local/spark2.1/mytest$ cat averg.out/part-00000 
('panda', 2.5)
('coffee', 1.5)
('jxiaolun', 2.0)
('wangcheng', 6.0)

JSON

读取 JSON 数据的最简单的方式是将数据作为文本文件读取, 然后使用 JSON 解析器来对 RDD 中的值进行映射操作。
读取JSON:这种方法假设文件中的每一行都是一条 JSON 记录。如果你有跨行的JSON 数据, 你就只能读入整个文件,然后对每个文件进行解析。

#python读取json数据方式
import json
data = input.map(lambda x: json.loads(x))

保存JSON:写出 JSON 文件比读取它要简单得多,因为不需要考虑格式错误的数据,并且也知道要写出的数据的类型。 可以使用之前将字符串 RDD 转为解析好的 JSON 数据的库,将由结构化数据组成的 RDD 转为字符串 RDD,然后使用 Spark 的文本文件 API 写出去。

(data.filter(lambda x: x["lovesPandas"]).map(lambda x: json.dumps(x))
.saveAsTextFile(outputFile))

CSV文件

逗号分隔值(CSV)文件每行都有固定数目的字段,字段间用逗号隔开。记录通常是一行一条
读取CSV:对于python可以使用自带的库,不需要导入任何包。在 Scala 和 Java中则使用 opencsv 库(http://opencsv.sourceforge.net/
一个CSV文件的例子如下所示:

eversilver@debian:/usr/local/spark2.1/mytest$ cat persons.csv 
wangcheng,24,basketball
jxiaolun,22,dance
tom,22,sell

对其读取的例子如下所示:

#!/usr/bin/env python
# coding=utf-8
from pyspark import SparkConf, SparkContext
import io
import csv
def loadRecords(fileNameContents):
    """读取文件中的所有记录"""
    input = io.StringIO(fileNameContents[1])
    reader = csv.DictReader(input, fieldnames=["name","age","hobby"])
    return reader
conf = SparkConf().setMaster("local").setAppName("My App")
sc = SparkContext(conf = conf)
fullFileData = sc.wholeTextFiles("persons.csv").flatMap(loadRecords)
print(fullFileData.first())

执行的结果如下所示:

eversilver@debian:/usr/local/spark2.1/mytest$ ../bin/spark-submit useCSV.py 
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/local/spark2.1/jars/slf4j-log4j12-1.7.16.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/local/hadoop/share/hadoop/common/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
17/05/04 15:32:32 WARN Utils: Your hostname, debian resolves to a loopback address: 127.0.1.1; using 192.168.142.133 instead (on interface eth0)
17/05/04 15:32:32 WARN Utils: Set SPARK_LOCAL_IP if you need to bind to another address
17/05/04 15:32:34 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
{'age': '24', 'name': 'wangcheng', 'hobby': 'basketball'}

保存csv:和 JSON 数据一样,写出 CSV数据相当简单,同样可以通过重用输出编码器来加速。 CSV 中我们不会在每条记录中输出字段名,为了使输出保持一致,需要创建一种映射关系。 一种简单做法是写一个函数,用于将各字段转为指定顺序的数组。在Python 中, 如果输出字典, CSV 输出器会根据创建输出器时给定的 fieldnames 的顺序帮我们完成这一行为,下面是利用上面读取csv的结果进行保存:

#!/usr/bin/env python
# coding=utf-8
from pyspark import SparkConf, SparkContext
import io
import csv
def loadRecords(fileNameContents):
    """读取文件中的所有记录"""
    input = io.StringIO(fileNameContents[1])
    reader = csv.DictReader(input, fieldnames=["name","age","hobby"])
    return reader

def writeRecords(records):
    """写出csv记录"""
    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=["name", "age", "hobby"])
    for record in records:
        writer.writerow(record)
    return [output.getvalue()]#getValue返回StringIO中的所有数据
conf = SparkConf().setMaster("local").setAppName("My App")
sc = SparkContext(conf = conf)
fullFileData = sc.wholeTextFiles("persons.csv").flatMap(loadRecords) # 读取csv文件中的数据
fullFileData.mapPartitions(writeRecords).saveAsTextFile("persons.out.csv")

查看输出文件为:

eversilver@debian:/usr/local/spark2.1/mytest$ cat ./persons.out.csv/part-00000 
wangcheng,24,basketball
jxiaolun,22,dance
tom,22,sell

SequenceFile

SequenceFile 是由没有相对关系结构的键值对文件组成的常用 Hadoop 格式。 SequenceFile文件有同步标记, Spark 可以用它来定位到文件中的某个点,然后再与记录的边界对齐。这可以让 Spark 使用多个节点高效地并行读取 SequenceFile 文件。 SequenceFile 也是Hadoop MapReduce 作业中常用的输入输出格式。(即经常见到的Writeable的键值对文件)。
读取SequenceFile:调用sequenceFile(path,keyClass, valueClass, minPartitions)。前面提到过, SequenceFile 使用 Writable 类,因此 keyClass 和 valueClass 参数都必须使用正确的 Writable 类。下面使用python来读取SequenceFile:

val data = sc.sequenceFile(inFile,
"org.apache.hadoop.io.Text", "org.apache.hadoop.io.IntWritable")

保存SequenceFile:因为 SequenceFile 存储的是键值对,所以需要创建一个由可以写出到 SequenceFile 的类型构成的 PairRDD。如果你要写出的是 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 序列化写出的。

Hadoop输入输出文件格式Spark也可以与任何 Hadoop 支持的格式交互。 Spark 支持新旧两套Hadoop 文件 API。

读取其他Hadoop输入格式:newAPIHadoopFile接收一个路径以及三个类。第一个类是“格式”类,代表输入格式。相似的函数hadoopFile() 则用于使用旧的 API 实现的 Hadoop 输入格式。第二个类是键的类,最后一个类是值的类。如果需要设定额外的 Hadoop 配置属性,也可以传入一个 conf 对象。例如下面这样读取KeyValueTextInputFormat (最简单的 Hadoop 输入格式):

//这里使用的是老式的API
val input = sc.hadoopFile[Text, Text, KeyValueTextInputFormat](inputFile).map{
  case (x, y) => (x.toString, y.toString)
}

下面使用了 Spark 中使用新式Hadoop API,一个使用 Lzo JsonInputFormat 读取 LZO 算法压缩的 JSON 数据的例子

//scala
val input = sc.newAPIHadoopFile(inputFile, classOf[LzoJsonInputFormat],
classOf[LongWritable], classOf[MapWritable], conf)
// "输入"中的每个MapWritable代表一个JSON对象

读取其他Hadoop输入格式: Java API 中没有易用的保存 pair RDD 的函数。我们就把这种情况作为展示如何使用旧式 Hadoop 格式的 API 的例子。下面是如何在Java中保存SequenceFile:

public static class ConvertToWritableTypes implements
  PairFunction<Tuple2<String, Integer>, Text, IntWritable> {
  public Tuple2<Text, IntWritable> call(Tuple2<String, Integer> record) {
    return new Tuple2(new Text(record._1), new IntWritable(record._2));
  }
}
JavaPairRDD<String, Integer> rdd = sc.parallelizePairs(input);
JavaPairRDD<Text, IntWritable> result = rdd.mapToPair(new ConvertToWritableTypes());
result.saveAsHadoopFile(fileName, Text.class, IntWritable.class,
  SequenceFileOutputFormat.class);

非文件系统数据源:除 了 hadoopFile() 和 saveAsHadoopFile() 这 一 大 类 函 数, 还 可 以 使 用 hadoopDataset/saveAsHadoopDataSet 和newAPIHadoopDataset/saveAsNewAPIHadoopDataset 来访问 Hadoop 所支持的非文件系统的存储格式。hadoopDataset() 这一组函数只接收一个 Configuration 对象,这个对象用来设置访问数据源所必需的 Hadoop 属性。你要使用与配置 Hadoop MapReduce 作业相同的方式来配置这个对象。所以你应当按照在 MapReduce 中访问这些数据源的使用说明来配置,并把配置对象传给 Spark。

读取Protocol Buffer:
PB 由可选字段、必需字段、重复字段三种字段组成。在解析时,可选字段的缺失不会导致解析失败,而必需字段的缺失则会导致数据解析失败。因此,在往 PB 定义中添加新字段时,最好将新字段设为可选字段,因为不是所有人都会同时更新到新版本。支持许多预定义类型, 或者另一个 PB 消息。这些类型包括 string、 int32、 enum等。(https://developers.google.com/protocol-buffers有其资料)
下面为从一个简单的 PB 格式中读取许多 VenueResponse 对象。 VenueResponse
是只包含一个重复字段的简单格式,这个字段包含一条带有必需字段、可选字段以及枚举类型字段的 PB 消息。

//PB的定义如下
message Venue {
  required int32 id = 1;
  required string name = 2;
  required VenueType type = 3;
  optional string address = 4;
  enum VenueType {
    COFFEESHOP = 0;
    WORKPLACE = 1;
    CLUB = 2;
    OMNOMNOM = 3;
    OTHER = 4;
  }
}
message VenueResponse {
  repeated Venue results = 1;
}
//在 Scala 中使用 Elephant Bird 写出 protocol buffer
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 输出格式来说,我们可以指定一种压缩编解码器来压缩数据。我们已经提过,Spark 原生的输入方式(textFile 和 sequenceFile)可以自动处理一些类型的压缩。在读取压缩后的数据时,一些压缩编解码器可以推测压缩类型,下面是不同的压缩选项带来的影响:


文件系统:

本地文件系统:Spark 支持从本地文件系统中读取文件,不过它要求文件在集群中所有节点的相同路径下
都可以找到。

val rdd = sc.textFile("file:///home/holden/happypandas.gz")

Amazon S3:用 Amazon S3 来存储大量数据正日益流行。当计算节点部署在 Amazon EC2 上的时候,使用 S3 作为存储尤其快,但是在需要通过公网访问数据时性能会差很多.在 Spark 中访问 S3 数据,你应该首先把你的 S3 访问凭据设置为 AWS_ACCESS_KEY_ID 和AWS_SECRET_ACCESS_KEY 环境变量。你可以从 Amazon Web Service 控制台创建这些凭据。接下来,将一个以 s3n:// 开头的路径以 s3n://bucket/path-within-bucket 的形式传给Spark 的输入方法。 和其他所有文件系统一样, Spark 也能在 S3 路径中支持通字符,例如 s3n://bucket/myFiles/*.txt。如果你从 Amazon 那里得到 S3 访问权限错误,请确保你指定了访问密钥的账号对数据桶有“read”(读)和“list”(列表)的权限。 Spark 需要列出桶内的内容,来找到想要读取的数据。

HDFS :在 Spark 中使用 HDFS 只需要将输入输出路径指定为hdfs://master:port/path 就够了。注意:HDFS 协议随 Hadoop 版本改变而变化,因此如果你使用的 Spark 是依赖于另一个版本的 Hadoop 编译的,那么读取会失败。默认情况下, Spark 基于Hadoop 1.0.4 编译 7。如果从源代码编译,你可以在环境变量中指定 SPARK_HADOOP_VERSION= 来基于另一个版本的 Hadoop 进行编译;也可以直接下载预编译好的 Spark 版本。你可以根据运行 hadoop version 的结果来获得环境变量要设置的值。

Spark SQL结构化数据(非数据库)

结构化数据:结构化数据指的是有结构信息的数据——也就是所有的数
据记录都具有一致字段结构的集合。在 Java 和 Scala 中, Row 对象的访问是基于下标的。每个 Row 都有一个get() 方法,会返回一个一般类型让我们可以进行类型转换。另外还有针对常见基本类型的 专 用 get() 方 法( 例 如 getFloat()、 getInt()、 getLong()、 getString()、 getShort()、getBoolean() 等)。在 Python 中,可以使用 row[column_number] 以及 row.column_name 来访问元素。
Hive:Apache Hive 是 Hadoop 上的一种常见的结构化数据源。可以在 HDFS 内或者在其他存储系统上存储多种格式的表。 这些格式从普通文本到列式存储格式,应有尽有。 SparkSQL 可以读取 Hive 支持的任何表。
JSON:要读取 JSON 数据,首先需要和使用 Hive 一样创建一个HiveContext。然后使用 HiveContext.jsonFile 方法来从整个文件中获取由 Row 对象组成的RDD。除了使用整个 Row 对象,你也可以将 RDD注册为一张表,然后从中选出特定的字段。例如,假设有一个包含推文的 JSON 文件,格式如例 5-33 所示,每行一条记录。

数据库

Java数据库连接:Spark 可 以 从 任 何 支 持 Java 数 据 库 连 接(JDBC) 的 关 系 型 数 据 库 中 读 取 数 据, 包括 MySQL、 Postgre 等系统。要访问这些数据,需要构建一个 org.apache.spark.rdd.JdbcRDD,将 SparkContext 和其他参数一起传给它。
• 首先,要提供一个用于对数据库创建连接的函数。这个函数让每个节点在连接必要的配
置后创建自己读取数据的连接。
• 接下来,要提供一个可以读取一定范围内数据的查询,以及查询参数中 lowerBound 和
upperBound 的值。这些参数可以让 Spark 在不同机器上查询不同范围的数据,这样就不
会因尝试在一个节点上读取所有数据而遭遇性能瓶颈。 8
• 这个函数的最后一个参数是一个可以将输出结果从 java.sql.ResultSet(http://docs.
oracle.com/javase/7/docs/api/java/sql/ResultSet.html)转为对操作数据有用的格式的函数。
在例 5-37 中, 我们会得到 (Int, String) 对。如果这个参数空缺, Spark 会自动将每行
结果转为一个对象数组。

//下面是一个Scala的例子
def createConnection() = {
  Class.forName("com.mysql.jdbc.Driver").newInstance();
  DriverManager.getConnection("jdbc:mysql://localhost/test?user=holden");
}
def extractValues(r: ResultSet) = {
  (r.getInt(1), r.getString(2))
}
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:Spark 的 Cassandra 连接器目前只能在 Java 和 Scala 中使用。

//配置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>
//配置Cassandra属性
val conf = new SparkConf(true)
    .set("spark.cassandra.connection.host", "hostname")
val sc = new SparkContext(conf)
//在 Scala 中将整张键值对表读取为 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()

HBase: Spark 可 以 通 过Hadoop 输入格式访问 HBase。 这个输入格式会返回键值对数据,其中键的类型为 org.apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.Result。 Result 类包含多种根据列获取值的方法,在其 API 文档(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Result.html)中有所描述。

//从 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])

TableInputFormat 包含多个可以用来优化对 HBase 的读取的设置项,比如将扫描限制到
一部分列中, 以及限制扫描的时间范围。你可以在 TableInputFormat 的 API 文档(http://
hbase.apache.org/apidocs/org/apache/hadoop/hbase/mapreduce/TableInputFormat.html)中找到
这些选项,并在 HBaseConfiguration 中设置它们,然后再把它传给 Spark。
Elasticsearch:Elasticsearch 是一个开源的、基于 Lucene 的搜索系统。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容