Spark笔记(三)

数据读取与保存
动机

  有时候,数据量可能大到无法放在一台机器中,这时就需要探索别的数据读取和保存的方法了。Spark支持很多种输入输出源,一部分原因是Spark本身是基于Hadoop生态圈而构建,特别是Spark可以通过Hadoop MapReduce所使用的InputFormat和OutputFormat接口访问数据,而大部分常见的文件格式与存储系统(例如S3、HDFS、Cassandra、HBase等)都支持这种接口。
  不过,基于这些原始接口构建出的高层API会更常用。幸运的是,Spark及其生态系统提供了很多可选方案。这里会介绍以下三类常见的数据源

  • 文件格式与文件系统
    对于存储在本地文件系统或分布式文件系统(比如NFS、HDFS、Amazon S3等)中的数据,Spark可以访问很多种不同的文件格式,有文本文件、JSON、SequenceFile,以及protocol buffer。这里会展示集中常见格式的用法,以及Spark针对不同文件系统的配置和压缩选项。
  • Spark SQL中的结构化数据源
    后面会介绍Spark SQL模块,它针对JSON和Apache Hive在内的结构化数据源,为我们提供了一套更加简洁高效的API,此处会粗略地介绍一下如何使用Spark SQL,而大部分细节将留到后面。
  • 数据库与键值存储
    这里会概述Spark自带的库和一些第三方库,它们可以用来连接Cassandra、HBase、Elasticsearch以及JDBC源
文件格式

  Spark对很多文件格式的读取和保存方式都很简单。从诸如文本文件的非结构化的文件,到诸如JSON格式的半结构化的文件,再到诸如SequenceFile这样的结构化的文件,Spark都可以支持,Spark会根据文件扩展名选择对应的处理方式,这一过程是封装好的,对用户透明。

格式名称结构化备注
文本文件普通的文本文件,每行一条记录
JSON半结构化常见的基于文本的格式,半结构化;大多数库都要求每行一条记录
CSV非常常见的基于文本的格式,通常在电子表格应用中使用
SequenceFiles一种用于键值对数据的常见Hadoop文件格式
Protocol buffers一种快速、节约空间的跨语言格式
对象文件用来将Spark作业中的数据存储下来以让共享的代码读取。改变类的时候它会失效,因为它依赖于Java序列化

除了Spark中直接支持的输出机制,还可以对键数据(或成对数据)使用Hadoop的新旧文件API,由于Hadoop接口要求使用键值对数据,所以也只能这样使用,即使有些格式事实上忽略了键,这时通常使用假的键比如null。
  在Spark中读写文本文件很容易。当将一个文本文件读取为RDD时,输入的每一行都会称为RDD的一个元素,也可以将多个完整的文本文件一次性读取为一个pair RDD,其中键是文件名,值是文件内容。
  读取文本文件。只需要使用文件路径作为参数调用SparkContext中的textFile()函数,就可以读取一个文本文件,如果要控制分区数的话,可以指定minPartitions

#Python 读取一个文本文件
input=sc.textFile("file:///home/holden/repos/spark/README.md")
//Scala
val input=sc.textFile("file:///home/holden/repos/spark/README.md")
#Java
JavaRDD<String> input=sc.textFile("file:///home/holden/repos/spark/README.md")

如果多个输入文件以一个有数据所有部分的目录的形式出现,可以用两种方式来处理,可以仍使用textFile函数,传递目录作为参数,这样它会把各部分都读到RDD中,有时候有必要知道数据的各部分分别来自哪个文件(比如将键放在文件名中的时间数据),有时候则希望同时处理整个文件,如果文件足够小,那么可以使用SparkContext.wholeTextFiles()方法,该方法会返回一个pair RDD,其中键是输入文件的文件名,该方法在每个文件表示一个特定时间段内的数据时非常有用,如果有表示不同阶段销售数据的文件,则可以很容易地求出每个阶段的平均值。

//Scala 求每个文件的平均值
val input=sc.wholeTextFiles("file:///home/holden/salesFiles")
val result=input.mapValues{
  val nums=y.split(" ").map(x=>x.toDouble)
  nums,sum/nums.size.toDouble
}

Spark支持读取给定目录中的所有文件,以及在输入路径中使用通配字符(如part-*.txt),大规模数据集通常存放在多个文件中,因此这一特性很有用,尤其是在同一目录中存在一些别的文件(比如成功标记文件)的时候。
  保存文本文件。输出文本文件也相当简单,saveAsTextFile()方法接收一个路径,并将RDD中的内容都输入到路径对应的文件中,Spark将传入的路径作为目录对待,会在那个目录下输出多个文件,这样,Spark就可以从多个节点上并型输出了,在这个方法中,我们不能控制数据的哪一部分输出到哪个文件中,不过有些输出格式支持控制。

#Python 将数据保存为文本文件
result.saveAsTextFile(outputFile)
JSON

  JSON是一种使用交广的半结构化数据格式,读取JSON数据的最简单的方式是将数据作为文本文件读取,然后使用JSON解析器来对RDD中的值进行映射操作,类似地,也可以使用JSON序列化库来将数据转为字符串,然后将其写出去。在Java和Scala中也可以使用一个自定义Hadoop格式来操作JSON数据,后面还会展示如何使用SPark SQL读取JSON数据。
  将数据作为文本文件读取,然后对JSON数据进行解析,这样的方法可以在所有支持的编程语言中使用。这种方法假设文件中的每一行都是一条JSON记录,如果有跨行的JSON数据,就只能读入整个文件,然后对每个文件进行解析,如果在使用的语言中构建一个JSON解析器的开销较大,可以使用mapPartitions()来重用解析器。
  这里提到的这三种语言中有大量可用的JSON库,为了简单起见,这里职位每种语言介绍一种库。Python中使用的是内建的库(https://docs.python.org/2/libraryjson.html),而在Java和Scala中则使用Jackson(http://jackson.codehaus.org/),之所以选择这些库,是因为它们性能不错,而且使用起来比较简单,如果在解析阶段花费了大量时间,应该选择Scala或Java中别的JSON库。

#Python 读取非结构化的JSON
import json
data=input.map(lambda x:json.loads(x))

在Scala和Java中,通常将记录读入到一个代表结构信息的类中,在这个过程中可能还需要略过一些无效的记录,下例将记录读取为Person类作为一个例子

//Scala 读取JSON
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import com.fasterxml.jackson.module.databind.ObjectMapper
import com.fasterxml.jackson.module.databind.DeserializationFeature
...
case class Person(name:String,lovesPandas:Boolean) // 必须是顶级类
...
//将其解析为特定的case class,使用flatMap,通过在遇到问题时返回空列表
//来处理错误,而在没有问题时返回有一个元素的列表(Some(_))
val result=input.flatMap(recurd=>{
  try{
    SOme(mapper.readValue(record,classOf[Person]))
  } catch{
    case e:Exception=>None
}})
#Java
class ParseJson implements FlatMapFunction<Iterator<String>,Person>{
  public Iterable<Person> call(Iterator<String> lines) throws Exception{
    ArrayList<Person> people=newArrayList<Person>();
    ObjectMapper mapper=new ObjectMapper();
    while(lines.hasNext()){
      String line=lines.next();
      try{
       people.add(mapper.readValue(line,Person.class));
      } catch(Exception e){
        //跳过失败的数据
      }
    }
    return people;
  }
}
JavaRDD<String> input=sc.textFile("file.json");
JavaRDD<String> result=input,mapPartitions(new ParseJson());

  保存JSON。写出JSON文件比读取要简单,因为不需要考虑格式错误的数据,并且也知道要写出的数据类型,可以使用之前将字符串RDD转为解析好的JSON数据的库,将由结构化数据组成的RDD转为字符串RDD,然后使用Spark的文本文件API写出去。假设要选出喜欢熊猫的人,就可以从第一步中获取输入数据,然后筛选出喜欢熊猫的人

#Python 保存为JSON
(data.filter(lambda x:x["lovesPandas"]).map(lambda x:json.dumps(x)).saveAsTextFile(outputFile))
//Scala
result.filter(p=>p.lovesPandas).map(mapper.writeValueAsString(_)).saveAsTextFile(outputFie)
#Java
class WriteJson implements FlatMapFunction<Iterator<Person>,String>{
  public Iterable<String> call(Iterator<Person> people) throws Exception{
    ArrayList<String> text=new ArrayList<String>();
    ObjectMapper mapper=new ObjectMapper();
    while(people.HASNEXT()){
      Person person=people.next();
      text.add(mapper.writeValueAsStringperson));
    }
    reyurn text;
  }
}
JavaRDD<Person> result=input.mapPartitions(new ParseJson()).filter(new LikesPandas());
JavaRDD<String> formatted=result.mapPartitions(new WriteJson());
formatted.saveAsTextFile(outfile);
逗号分隔值与制表符分隔值

  逗号分隔值(CSV)文件每行都有固定数目的字段,字段间用逗号隔开(TSV文件用制表符隔开),记录通常是一行一条,不过也不总是这样,有时也可以跨行,CSV文件和TSV文件有时支持的标准并不一致,主要是在处理换行符、转义字符、非ASCII字符、非整数值等,CSV原生并不支持嵌套字段,所以需要手动组合和分解特定的字段。与JSON中的西段不一样的是,这里的每条记录都没有相关联的字段名,只有对应序号,常规做法是使用第一行中每列的值作为字段名。
  读取CSV。读取CSV/TSV数据和读取JSON数据相似,都需要先把文件当做普通文本文件来读取数据,再对数据进行处理,由于格式标准的缺失,同一个库的不同版本有时也会用不同的方式处理输入数据。与JSON一样,CSV也有很多不同的库,但是只在每种语言中使用一个库,同样,对于Python我们会使用自带的csv库(https://docs.python.org/2/library/csv.html),在Scala和Java中则使用opencsv库(http://opencsv.sourceforge.net/)。Hadoop InputFormat中的CSVInputFormat也可以用于在Scala和Java中读取数据,不过不支持有换行符的记录。如果恰好CSV的所有数据字段均没有换行符,也可以使用textFile()读取并解析数据

#Python 使用textFile()读取CSV
import csv
import STringIO
...
def loadRecord(line):
    “”“解析一行CSV记录”“”
    input=StringIO(line)
    reader=csv.DictReader(input,fieldnames=["name","favouriteAnimal"])
    return reader.next()
input=sc.textFile(inputFile).map(loadRecord)
//Scala 使用textFile()读取CSV
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();
}
#Java 使用textFile()读取CSV
import Java.io.StringReader;
import au.com.bytecode.opencsv.CSVReader;
...
public static class ParseLine implements Function<String,String[]>{
  public String[] call(String line) throws Exception{
    CSVReader reader=new CSVReader(new StringReader(line));
    return reader.readNext();
  }
}
JavaRDD<String> csvFile1=sc.textFile(inputFile);
JavaPairRDD<String[]> csvData=csvFile1.map(new ParseLine());

  如果在字段中嵌有换行符,就需要完整读入每个文件,然后解析各段,如果每个文件都很大,读取和解析的过程可能会很不幸成为性能瓶颈。

#Python 完整读取CSV
def loadRecords(fileNameContents):
    """"读取给定文件中的所有记录"""
    input=StringIO.StringIO(fileNameContents[1])
    reader=csv.DictReader(input,fieldnames=["name","favoriteAnimal"])
    return reader
fullFileData=sc.wholeTextFiles(inputFile).flatMap(loadRecords)
//Scala 完整读取CSV
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)))
}
#Java 完整读取CSV
public static class ParseLine
  implements FlatMapFunction<Tuple2<String,String>,String[]>{
    public Iterable<String[]> call(Tuple2<String,String> file) throws Exception{
      CSVReader reader=new CSVReader(new StringReader(file._2()));
      return reader.readAll();
    }
  }
  JavaRDD<String,String> csvData=sc.wholeTextFiles(inputFile);
  JavaRDD<String[]> keyedRDD=csvData.flatMap(new ParseLine());

如果只有一小部分输入文件,需要使用wholeTextFile()方法,可能还需要对输入数据进行重新分区使Spark能更高效并行化执行后续操作。
  保存CSV。和JSON一样,写出CSV/TSV数据相当简单,同样可以通过重用输出编码器来加速。由于在CSV中不会在每条记录中输出字段名,因此为了使输出保持一致,需要创建一种映射关系,一种简单做法是写一个函数,用于将各字段转为指定顺序的数组,在Python中,如果输出字典,CSV输出器会根据创建输出器时给定的fieldnames的顺序帮助完成这一行为。这里所使用的CSV库要输出到文件或者输出器,所以可以使用StringWriter或StringIO来将结果放到RDD中

#Python 写CSV
def writeRecords(records):
    """写出一些CSV记录"""
    output=StringIO.StringIO()
    writer=csv.DictWriter(output,fieldnames=["name","favoriteAnimal"])
    for record inrecords:
        writer.writerow(record)
        return [output.getValue()]

pandaLovers.mapPartitions(writeRecords).saveAsTextFile(outputFile)
//Scala 写CSV
pandaLovers.map(person=>List(person.name,person.favoriteAnimal).toArray).mapPartitions{people=>
  val stringWriter=new StringWriter();
  val csvWriter=newCSVWriter(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接口的元素组成,这里列出了一些常见的数据类型以及它们对应的Writable类,标准的经验法则是尝试在类名之后加上Writable词,然后检查它是否是org.apache.hadoop.io.Writable已知的子类,如果无法为要写出的数据找到对应的Writable类型(比如自定义的case class),可以通过重载org.apache.hadoop.io.Writable中的readfields和write来实现自己的Writable类。Hadoop的RecordReader会为每条记录重用同一个对象,因此直接调用RDD的cache会导致失败,实际上,只需要使用一个简单的map()操作然后将结果缓存即可,还有,许多Hadoop Writable类没有实现java.io.Serializable接口,因此为了让它们能在RDD中使用,还是要用map()来转换它们。

Scala类型Java类型Hadoop Writable类
IntIntegerIntWritable或VIntWritable
LongLongLongWritable或VLongWritable
FloatFloatFloatWritable
DoubleDoubleDoubleWritable
BooleanBooleanBooleanWritable
Array[Byte]byte[]BytesWritable
StringStringText
Array[T]T[]ArrayWritable< TW>
List[T]ListArrayWritable< TW>
Map[A,B]Map<A,B>ArrayWritable<AW,BW>

  在Spark1.0以及更早版本中,SequenceFile只能在Java和Scala中使用,不过Spark1.1加入了在Python中读取和保存SequenceFile的功能,但要注意,还是需要使用Java或Scala来实现自定义Writable类,Spark的Python API只能将Hadoop中存在的基本Writable类转为Python类型,并尽量基于可用的getter方法处理别的类型。
  读取SequenceFile。在SparkContext中,可以调用sequenceFile(path,keyClass,valueClass,minPartitions),SequenceFile使用Writable类,因此keyClass和valueClass参数都必须使用正确的Writable类,举个例子假设要从一个SequenceFile中读取人员以及他们所见过的熊猫数目,在这个例子中,keyClass是Text,而valueClass则是INtWritable或VIntWritable。

#Python 读取SequenceFile
data=sc.sequenceFile(inFile,"org.apache.hadoop.io.Text","org.apache.hadoop.io.IntWritable")
//Scala
val data=sc.sequenceFile(inFile,classOf[Text],classOf[IntWritable]).map{case (x,y)=>(x.toString,y.get())}
#Java
public static class ConverToNativeTypes implements PairFunction<Tuple2<Text,IntWritable>,String,Integer>{
  public Tuple2<String,INteger> call(Tuple2<Text,IntWritable> record){
    return new Tuple2(record._1.toString(),record._2.get());
  }
}

JavaRDD<Text,IntWritable> input=sc.sequenceFile(fileName,Text.class,IntWritable.class);
JavaRDD<String,Integer> result=input,mapToPair(new ConvertToNativeTypes());

在Scala中有个很方便的函数可以自动将Writable对象转为对应的Scala类型,可以调用sequenceFile[Key,Value](path,minPartitions)返回Scala原生数据类型的RDD,而无需指定keyClass和valueClass。
  保存SequenceFile。在Scala中将数据写出到SequenceFile的做法也很类似,首先,因为SequenceFile存储的是键值对,所以需要创建一个由可以写出到SequenceFile的类型苟岑的PairRdd,我们已经进行了将许多Scala的原生类型转为Hadoop Writable的隐式转换,所以如果要写出的是Scala的原生类型,可以直接调用saveSequenceFile(path)保存PairRDD,它会写出数据。如果键和值不能自动转为Writable类型,或者想使用变长类型(比如VIntWritable),就可以对数据进行映射操作,在保存之前进行类型转换

//Scala 保存SequenceFile
val data=sc.parallelize(List(("Panda",3),("Kay",6),("Snail",2)))
data.saveAsSequenceFile(outputFile)

在Java中保存SequenceFile要稍微复杂一些,因为JavaPairRDD上没有saveAsSequenceFile()方法,我们要使用Spark保存自定义Hadoop格式的功能来实现。

对象文件

  对象文件看起来就像是对SequenceFile的简单封装,它允许存储只有值的RDD,和SequenceFile不一样的是,对象文件是使用Java序列化写出的。如果修改了类——比如增减了几个字段——已经生成的对象文件就不可再读了,对象文件使用Java序列化,它对兼容同一个类的不同版本有一定程度的支持,但是需要程序员去实现。对对象文件使用Java序列化有几个要注意的地方,首先,和普通的SequenceFile不同,对于同样的对象,对象文件的输出和Hadoop的输出不一样,其次,与其它文件格式不同的是,对象文件通常用于Spark昨夜见的通信,最后,Java序列化可能相当慢。
  要保存对象文件,只需在RDD上调用saveAsObjectFile就行了,读回对象文件也相当简单:用SparkContext中的objectFile()函数接收一个路径,返回对应的RDD。了解了关于使用对象文件的这些注意事项,可能想知道为什么会有人使用它,使用对象文件的主要原因是它可以用来保存几乎人以对象而不需要额外的工作。对象文件在Python中无法使用,不过Python中的RDD和SparkContext支持saveAsPickleFile()和pickleFile()方法作为替代,这使用了Python的pickle序列化库,不过,对象文件的注意事项同样适用于pickle:pickle库可能很慢,并且在修改类定义后,已经产生的数据文件可能无法再读出来。

Hadoop输入输出格式

  除了Spark封装的格式外,也可以与任何Hadoop支持的格式交互,Spark支持新旧两套Hadoop文件API,提供了很大的灵活性。
  读取其它Hadoop输入格式。要使用新版的Hadoop API读入一个文件,需要告诉Spark一些东西,newAPIHadoopFile接收一个路径以及三个类,第一个类是“格式”类,代表输入格式,相似的函数hadoopFile()则用于使用旧的API实现的Hadoop输入格式,第二个类是键的类,最后一个类是值的类,如果需要设定额外的Hadoop配置属性,也可以传入一个conf对象。
  KeyValueTextInput是最简单的Hadoop输入格式之一,可以用于从文本文件中读取键值对数据,每一行都会被独立处理,键和值之间用制表符隔开,这个格式存在于Hadoop中,所以无需向工程中添加额外的依赖就能使用它

//Scala 使用旧的API读取KeyValueTextInputFormat
val input=sc.hadoopFile[Text,Text,KeyValueTextInputFormat](inputFile).map{case (x,y)=>(x.toString,y.toString)}

我们学习了通过读取文本文件并加以解析以读取JSON数据的方法,事实上,我们也可以使用自定义Hadoop输入格式来读取JSON数据,该示例需要设置一些额外的压缩选项,我们暂且跳过关于设置压缩选项的细节。Twitter的Elphant Bird包支持很多种数据格式,有JSON、Lucene、Protocol Buffer相关的格式等,这个包也适用于新旧两种Hadoop文件API,为了展示如何在Spark中使用新格式Hadoop API,来看一个使用Lzo JsonInputFormat读取LZO算法压缩的JSON数据的例子。

//Scala 使用Elephant Bird读取LZO算法压缩的JSON文件
val input=sc.newAPIHadoopFile(inputFile,classOf[LzoJsonInputFormat],classOf[LongWritable],classOf[MapWritable],conf)
//"输入"中的每个MapWritable代表一个JSON对象

LZO的支持要求先安装hadoop-lzo包,并放到Spark的本地库中,如果使用Debian包安装,在调用spark-submit时加上–driver-library-path /usr/lib/hadoop/lib/native/ --driver-class-path /usr/lib/hadoop/lib就可以了。使用旧的Hadoop API读取文件在用法上几乎一样,除了需要提供旧的InputFormat类,Spark许多自带的封装好的函数(比如sequenceFile())都是使用旧的Hadoop API实现的。
  保存Hadoop输出格式。在Java API中没有易用的保存pair RDD的函数,把这种情况作为展示如何使用旧的Hadoop格式的API的例子,新接口(saveAsNewAPIHadoopFile)的调用方法也是类似的

//#Java 保存SequenceFile
public static calss 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/saveAsNewAPIHadoopDateset来访问Hadoop所支持的非文件系统的存储格式,例如,许多像HBase和MongoDB这样的键值对存储都提供了用来直接读取Hadoop输入格式的接口,我们可以在Spark中很方便地使用这些格式。
  hadoopDataset()这一组函数只接收一个Configuration对象,这个对象用来设置访问数据源所必需的Hadoop属性,要使用与配置 Hadoop MapReduce作业相同的方式来配置这个对象。所以应当按照在MapReduce中访问这些数据员的使用说明来配置,并把配置对象传给Spark。
  示例protocol buffer。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值