小白学习Spark02-SparkCode(上)

一、RDD编程

1.1 RDD基础
  • RDD:是一个 不可变 的 分布式 对象集合。每个RDD都被分为多个分区,这些分区运行在集群上的不同节点上。
  • 两种方法创建RDD:1).读取一个外部数据集、2).在驱动程序中分发驱动器程序中的对象集合
  • RDD支持两种类型的操作: 1).转换操作(transformation) 、2).行动操作(action)
1.2 创建RDD
  • 两种方式创建RDD:1).读取一个外部数据集(比如textFile()); 2).在驱动程序中分发驱动器程序中的对象集合(parallelize())。
1.3 RDD操作
  • RDD支持两种类型的操作:
    • 1).转换操作(transformation) :转换操作会由一个RDD生成一个新的RDD,比如 map()、filter();
    • 2).行动操作(action):行动操作会对RDD计算出一个结果,并把结果返回到驱动器程序中,或者把结果存储到外部存储系统(如HDFS)中,比如count()、first()
    • 转换操作返回值类型是RDD,而行动操作返回的是其他的数据类型(非RDD)
    • 3).两种操作的区别在于 Spark计算RDD的方式不同。Spark是 惰性计算 这些RDD的,只有在遇到一个行动操作中才会真正执行计算。因为Spark在默认情况下都会在你每次对RDD进行操作时重新计算,因为如果想让多个行动操作重用同一个RDD,可以使用RDD.persist()让Spark将这个RDD缓存下来。
  • 总的来说,每个Spark程序或者Shell会话都会按以下方式进行工作:
    • 1).从外部数据创建出输入RDD
    • 2).使用诸如filter()这样的转化操作对RDD进行转换,以定义新的RDD
    • 3).告诉Spark对需要被重用的中间结果RDD执行persist()操作
    • 4).使用行动操作(比如count()和first()等)来触发一次并行计算,Spark会对计算进行优化再执行。
1.3.1 转化操作
  • 转化操作可以操作任意数量的输入RDD
  • 通过转换操作,从已有的RDD中派生出新的RDD,Spark会使用谱系图来记录这些不同RDD之间的依赖关系。Spark需要用这些信息来按需计算每个RDD,也可以依赖谱系图在持久化的RDD丢失部分数据时去恢复所丢失的数据。
1.3.2 行动操作
  • 通常要把数据写入HDFS或者Amazon S3等分布式的存储系统中。
  • 当调用一个新的行动操作时,整个RDD都会从头开始计算,为了避免这种低效的行为,用户可以将中间结果持久化(缓存到内存等方式)。
1.3.3 惰性求值
  • RDD的转化操作都是惰性求值,这意味着当我们对RDD调用转化操作时,操作不会立即执行,而是Spark会在内部记录下所要求执行的操作的相关信息。
  • 我们应该将RDD当做一个记录如何计算数据的指令列表 而不是存放着特定数据的数据集。
1.4 向Spark传递函数
  • Spark大部分转换操作和一部分行动操作,都需要依赖用户传递的函数来计算。
  • 这里主要介绍Scala,Python和Java自己查阅
1.4.1 Scala
  • 我们可以把 定义的内联函数、方法的引用、静态方法传递给Spark
  • 需考虑一些细节
    • 所传递的函数及其引用的数据需要是可序列化的(实现了Java的Serializable接口)
    • 传递一个对象的方法或者字段时,会包含对整个对象的引用
    • 把需要的字段放到一个局部变量中,可以避免传递包含该字段的整个对象
  • 如果Scala中出现了NotSerializableException,通常问题就是在于我们传递了一个不可序列化的类中的函数或字段,而传递局部可序列化变量或顶级对象中的函数始终是安全的。
1.5 常见的转换操作和行动操作
1.5.1 基本RDD
  • 针对各个元素的转化操作:

    • map():接收一个函数,把这个函数用于RDD中的每个元素,将函数的返回结果作为结果RDD中对应元素的值。map的返回值类型不需要和输入类型一致。
    //Scala版 计算RDD中各值的平方
    val inputs=sc.parallelize(List(1,2,3,4))
    val result=input.map(x=>x*x)
    println(result,collect().mkString(","))
    
    • filter():接收一个函数,并将RDD中满足该函数的元素放入新的RDD中返回
    • flatMap():与map()类似,提供给flatMap()的函数被分别应用到了输入RDD的每个元素上,只不过返回的不是一个元素,而是一个返回值序列的迭代器,得到一个包含各个迭代器可访问的所有元素的RDD。
    //Scala版 的flatMap()将行数据切分为单词
    val lines=sc.parallelize(List("hello world","hi"))
    val words=lines.flatMap(line => line.split(" ")
    words.collect() //返回结果
    
    • map()与flatMap()的区别
      flatMap()得到了一个由各列表中的元素组成的RDD,而不是一个由列表组成的RDD。
      在这里插入图片描述
  • 伪集合操作

    • distinct():可以转化操作来生成一个只包含不同元素的新RDD。 该操作开销大,因其需要将所有的数据经过网络进行混洗(shuffle),以确保每个元素只有一份。
    • union():它会返回一个包含两个RDD中所有元素的RDD。
    • intersection():只返回两个RDD中都有的元素。intersection运行时会去掉所有重复的元素(需要Shuffle)。
    • subtract():接收另一个RDD作为参数,返回一个由只存在于第一个RDD中而不存在于第二个RDD中的所有元素组成的RDD(需要Shuffle)。
    • cartesian():转化操作会返回所有可能的(a,b)对,其中a是源RDD中的元素,而b则是来自另一个RDD。

    在这里插入图片描述

  • 常用的转化操作

    • 一个RDD的转化操作
      在这里插入图片描述
    • 两个RDD的转化操作
      在这里插入图片描述
  • 行动操作

    • reduce():它接收一个函数作为参数,这个函数要操作两个RDD的元素类型的数据并返回一个同样类型的新元素。使用reduce能够方便地计算出RDD中所有元素的总和,元素的个数,以及其他类型的聚合操作。
      //Scala版 的reduce()
      val sum = rdd.reduce((x,y) => x+y)
      
    • fold():接收一个与reduce()接收的函数签名相同的函数,再加上一个"初始值"来作为每个分区第一次调用时的结果。
      • 能够原地修改并返回两个参数中的前一个的值来节约fold()中创建对象的开销。
      • reduce()与fold()都要求函数的返回值类型需要跟我们所操作的RDD中的元素类型相同。
    • aggregate()函数与fold()类似,需要提供返回类型的初始值,然后通过一个函数把RDD中的元素合并起来放入累加器。因考虑到每个节点都是在本地上进行累加的,因此,还需要提供第二个函数来将累加器两两合并。
    	//Scala版 的reduce()
    	val result = input.aggregate((0,0))(
    							(acc,value) => (acc._1 + value,acc._2+1)
    							(acc1,acc2) => (acc1._1+acc2._2,acc1._2+acc2._2)
    	val avg = result._1 /result._2.toDouble 
    
    • collect():会将整个RDD的内容返回。其要求RDD的所有数据都必须能一同放入到单台机器的内存中。
    • take(n):会返回RDD的n个元素,并且尝试值访问尽量少的分区,因为该操作会得到一个不均衡的集合。
    • top(n):返回RDD中的前n个元素。
    • takeSample(withReplacement,num,seed):可以让我们从数据中获取一个采样,并指定是否替换
    • foreach():对整个RDD中的每个元素进行操作,而不需要把RDD发回本地。
    • count():返回RDD中元素的个数。
    • countByValue():返回一个从各值到值对应的计数的映射表。
    • -在这里插入图片描述
1.5.2 在不同RDD类型间转换
  • 在Scala中,将RDD转为特定函数的RDD是通过隐式转换(import org.apache.spark.SparkContext._)来自动处理的
  • 隐式转换能够隐式地将一个RDD转为各种封装类
1.6 持久化(缓存)
  • 在迭代算法中,通常会多次使用同一组数据,如果简单的对RDD调用行动操作,Spark会每次都重新计算RDD以及它的所有依赖,这是非常消耗资源的。因此为了避免多次计算同一个RDD,可以让Spark对这个数据进行持久化操作。
  • 当Spark持久化一个RDD时,计算出RDD的节点会分别保存它们所求出的分区数据,如果一个有持久化数据的节点发生故障,则Spark会在需要用到缓存的数据时重新计算丢失的数据分区。
  • RDD持久化等级,默认是以序列化后的对象存储在JVM堆空间中。
  • -在这里插入图片描述
    	//在Scala中使用persist()
    	val result = input.map(x => x*x)
    	result.persist(StorageLevel.DISK_ONLY)
    	println(result.count())
    	println(result.collect().mkString(","))
  • 在第一次对这个RDD调用行动操作前就调用了persist()方法,而persist()本身不会触发强制求值。
  • 如果缓存的数据过大,内存放不下,则Spark会自动利用最近最少使用(LRU)的缓存策略把最老的分区从内存中一处,对于仅把数据存放在内存中的缓存级别,下一次要用到已经被移除的分区时,这些分区会被重新计算;但对于使用内存和磁盘的缓存级别的分区来说,被移除的分区都会被写入磁盘中。
  • unpersist():该方法可以手动把持久化的RDD从缓存中移除。

二、键值对操作

  • 键值对RDD通常被用来进行聚合计算
2.1 动机
  • Pair RDD:键值对类型的RDD,它们提供了并行操作各个健或跨节点重新进行数据分组的操作接口。
  • reduceByKey():可以分别归约每个键对应的数据
  • join():可以把两个RDD中健相同的元素组合在一起,合并为一个RDD
2.2 创建Pair RDD
  • map():能够把一个普通的RDD转换为Pair RDD
    	//在Scala中使用第一个单词作为键创建出一个Pair RDD
    	val pairs = lines.map(x => (x.split(" ")(0), x))
2.3 Pair RDD的转化操作
  • 由于Pair RDD中包含二元组,因此需要传递的函数应当能够操作二元组而不是独立的元素。
  • -在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    	//在Scala中使用筛选掉长度超过20个字符的行
    	pairs.filter{case (key,value) => value.length < 20}
  • mapValues(func):功能类似于map{case (x,y):(x,func(y))} ,访问pair RDD的值部分
2.3.1 Pair RDD的聚合操作
  • 聚合操作时转化操作 而不是 行动操作
  • reduceByKey()、reduce():它们都接收一个函数,并使用该函数对值进行合并、其中reduceByKey()会为数据集中的每个键进行并行的归约操作,每个归约操作会将键相同的值合并起来、它会返回 一个由各键和对应键归约出来的结果组成的新的RDD.
  • foldByKey()、fold():它们都使用一个与RDD和合并函数中的数据类型相同的零值作为初始值。与fold()类似,foldByKey()操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。
  • 调用reduceByKey()和foldByKey()会在为每个键计算全局的总结果之前先自动在每台机器上进行本地合并。
  • 通过使用reduceByKey()和mapValues()来计算每个键的对应值的均值
    	//在Scala中使用 reduceByKey()和mapValues()来计算每个键对应的平均值
    	rdd.mapValues(x => (x,1)).reduceByKey((x,y) => (x._1 + y._1 , x._2 + y._2))

在这里插入图片描述

  • 通过flatMap()来生成以单词为键,以数字1为值的Pair RDD,然后使用reduceByKey对所有的单词进行计数
    	//在Scala中实现单词计数
    	val input = sc.textFile("hdfs://node..")
    	// 第一种方法
    	val words=input.flatMap(x => x.split(" "))
    	val result =words.map(x => (x,1)).redyceByKey((x,y) => x+y)
   	//第二种方法    countByValue是一个action
   	input.flatMap(x => x.split(" ")).countByValue()
  • combineByKey():常用于基于键进行聚合的函数,大部分基于键聚合的函数都是基于它来实现的。它可以让用户返回与输入数据的类型不同的返回值。
    • 它是如何处理每个元素的? combineByKey()会遍历分区中的所有元素,因此每个元素的键要么没有遇到过,要么就和之前的某个元素相同
    • 如果是一个新元素,combineByKey()会使用一个 createCombiner()函数来创建那个键对应的累加器的初始值,这一过程会在每个分区中第一次出现各个键时发生,而不是整个RDD。
    • 如果是一个在处理当前分区之前就已经遇到的键,combineByKey()会使用一个mergeValue()方法将该键的累加器对应的当前值与整个新值进行合并。
    • 由于每个分区都是独立处理的,因此对于同一个键能有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,则需要使用用户提供的mergeCombiners()方法将各个分区的结果进行合并。
    • combineByKey()有多个参数分别对应聚合操作的各个阶段,因此非常适合用来解释聚合操作各个阶段的功能划分。
    	//在Scala中使用combineByKey()求每个键对应的平均值
    	val result = input.combineByKey(
    	(v) => (v,1),
    	(acc: (Int,Int),v) => (acc._1 + v, acc._2 +1),
    	(acc1: (Int,Int), acc2: (Int,Int)) => (acc1._1 +acc2._1, acc1._2 +acc2._2)
    	).map{case (key,value) => (key,value._1 / value._2.toFloat)}
    	result.collectAsMap().map(println(_))
    

在这里插入图片描述

  • 并行化调度
    • 每个RDD都有固定数目的分区,分区数决定了在RDD上执行操作时的并行度。在执行聚合或分组操作时,可以要求Spark使用给定的分区数。
     	//在Scala中自定义reduceByKey()的并行度
     	val data = Seq(("a",3),("b",4),("a",1))
     	sc.parallelize(data).reduceByKey((x,y) => x+y)
     	sc,parallelize(data).reduceByKey((x,y) => x+y,5)
    
    • repartition():将数据通过网络进行混洗(shuffle),并创建出新的分区集合。
    • coalesce():将RDD合并到比现在的分区数更少的分区中。
2.3.2 数据分组
  • groupByKey():会使用RDD中的键来对数据进行分组,得到的结果RDD类型会是 [K,Iterable[V]]
  • groupBy():可以用于未成对的数据上,也可以根据除键相同以外的条件进行分组。它可以接收一个函数,对源RDD中的每个元素使用该函数,将返回结果作为键再进行分组。
  • rdd.reduceByKey(func)与rdd.groupByKey().mapValues(value => value.reduce(func))等价,但是前者更为高效,因为其避免了为每个键存放值的列表的步骤。
  • 除了对单个RDD的数据进行分组,还可以使用一个叫做cogroup()的函数对多个共享同一个键的RDD进行分组。对两个键的类型均为k而值的类型分别为V和W的RDD进行cogroup()时,得到的结果RDD类型为[(K,(Iterable[V],Iterable[W]))]。
  • cogroup()不仅能够用于实现连接操作,还可以用于求键的交集,其还能同时应用于三个及以上的RDD。
2.3.3 连接
  • 连接的方式:内连接、左外连接、右外连接及交叉连接
  • join():内连接,只有当两个Pair RDD中都存在的键才会输出。
  • leftOuterJoin(other):左外连接,源RDD的每一个键都有对应的记录,允许结果中存在其中的一个pairRDD所缺失的键。
  • rightOuterJoin(other):右外连接,第二个RDD的每一个键都有对应的记录,允许结果中存在其中的一个pairRDD所缺失的键。
2.3.4 数据排序
  • sortByKey():接收一个叫做ascending的参数,表示我们是否想让结果按升序排序(默认是true)
     	//在Scala中以字符串顺序对整数进行自定义排序
     	val input:RDD[(Int,Venue)]= ...
     	implicit val sortIntegersByString = new Ordering[Int]{
     		override def compare(a:Int,b:Int) = a.toString.compare(b.toString)
     		}
     		rdd.sortByKey()
    
2.4 Pair RDD的行动操作
  • 与转化操作一样,所有基础RDD支持的传统行动操作也都可以在Pair RDD上使用。
    在这里插入图片描述
2.5 数据分区(进阶)
  • 在分布式程序中,通信的代价是很大的。因此可以通过控制数据分布以获得最少的网络传输,这样可以极大地提高整体的性能;Spark程序通过控制RDD分区方式来减少通信开销。
  • 在什么时候使用分区是有好处的呢?若给定的RDD只需要被扫描一次,则没必要对其预先进行分区处理;只有当数据集多次在诸如连接这种基于键的操作中使用,分区才有发挥的作用。
  • Spark中所有的键值对RDD都可以进行分区。系统会根据一个针对键的函数对元素进行分组,虽然Spark没有给出显示控制每个键具体落在哪一个工作节点的方法,但是Spark可以确保同一组键出现在同一个节点上。
  • 分区方法:1).哈希值分区法,将键的哈希值对分区个数(比如100)取模,结果相同的记录会被放在同一个节点上
    2).范围分区法,将键在同一个范围区间内的记录都放在同一个节点上。
	//Scala自定义分区方式
	val sc = new SparkContext(....)
	val userData= sc.sequenceFile[UserID,UserInfo]("hdfs://...")
								.partitionBy(new HashPartitioner(100)) //构造100个分区
								.persist()
  • partitionBy():调用了partitionBy 就能知道该RDD是根据键的哈希值来分区的,partitionBy是一个转化操作。
  • 对partitionBy()转化操作的结果如果不进行持久化,那么后面每次用到这个RDD时都会重复地对数据进行分区操作;因此一定要对分区后的结果进行持久化。
2.5.1 获取RDD的分区方式
  • partitioner:获取RDD的分区方式,通过其可以检验各种Spark操作如何影响分区方式,还可以在你的程序中检查想要使用的操作是否会生成正确的结果。
2.5.2 从分区中获益的操作
  • 如果spark操作时将数据根据键跨节点进行混洗的过程,则这些操作就会从数据分区中获益;比如cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()及lookup()。
2.5.3 影响分区方式的操作
  • Spark内部知道各操作会如何影响分区方式,并将会对数据进行分区的操作结果RDD自动设置为对应的分区器
  • 转化操作的结果并不一定会按已知的分区方式进行分区,这时输出的RDD可能就会没有设置分区器。
  • 会为生成的结果RDD设置好分区方式的操作:cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()、partitionBy()、sort()、mapValues(如果父RDD有分区方式的话)、flatMapValues(如果父RDD有分区方式的话)及filter(如果父RDD有分区方式的话)。
  • 对于二元操作,输出数据的分区方式取决于父RDD的分区方式。在默认情况下,结果会采用哈希分区,分区的数量和操作的并行度一样。结果RDD会采用第一个父RDD的分区方式。
2.5.4 自定义分区方式
  • Spark除了HashPartitioner和RangePartitioner两种分区方式,还允许通过提供一个自定义的partitioner对象来控制RDD的分区方式。
  • 自定义分区器需要继承 org.apache.spark.Partitioner类实现下面三个方法
    • numPartitions: Int:返回创建出来的分区数
    • getPartition(key:Any):Int : 返回给定键的分区编号(0 到numPartitions -1)
    • equals():Java判断相等性的标准方法。Spark通过这个方法来检查你的分区器对象是否跟其他分区器实例相同,这样Spark才能判断两个RDD的分区方式是否相同。
    • 要确保getPartition()永远返回一个非负数
//Scala自定义分区方式
	class DomainNamePartitioner(numParts:Int) extends Partitioner{
		override def numPartitions: Int = numParts
		override def getPartition(key:Any):Int = {
			val domain = new Java.net.URL(key.toString).getHost()
			val code = (domain.hashCode % numPartitions)
			if(code <0){
				code + numPartitions //使其非负
			}else{
				code
			}
		}
		//用来让Spark区分分区函数对象的Java equals方法
		override def equals(other :Any):Boolean = other match {
			case dnp: DomainNamePartitioner =>
				dnp.numPartitions == numPartitions
			case _ => 
				false
			}
		}
  • 在equals()方法中,使用Scala的模式匹配操作符(match)来检查other是否是DomainNamePartitioner,并在成立时自动进行类型转换。
  • 使用自定义的Partitioner是比较容易的:只需要把它传给partitionBy()方法即可。

三、数据读取与保存

3.1 动机
  • 文件格式与文件系统
    • 文件格式:文本文件(非结构化)、JSON(半结构化)、SequenceFile(结构化)及protocol buffer
    • 文件系统:NFS、HDFS、Amazon S3等
  • Spark SQL 中的结构化数据源
    • Json以及Apache Hive在内的结构化数据源
  • 数据库与键值存储
    • Cassandra、HBase、Elasticsearch及JDBC源
3.2 文件格式

在这里插入图片描述

  • 除了Spark直接支持的输出机制,还可以对键数据(或成对数据)使用Hadoop的新旧文件API
3.2.1 文本文件
  • Spark将一个文本文件读取为RDD时,输入的每一行都会成为RDD的一个元素
  • Spark将多个完整的文本文件一次性读取为一个Pair RDD时,键为文件名 、而值为文件内容
  • 读取文本文件
    • textFile()
    	//Scala读取一个文本文件
     	val input = sc.textFile("...")
    
    • wholeTextFile():读取一系列小文件,会返回一个Pair RDD,其中键为输入文件的文件名。其在每个文件表示一个特定时间段内的数据时非常适用。
    //Scala 求每个文件的平均值
    	val input = sc.wholeTextFiles("...")
    	val result = input.mapValues{y =>
    		val nums = y.split(" ").map(x => x.toDouble)
    		nums.sum / nums.size.toDouble
    	}
    
  • 保存文本文件
    • saveAsTextFile()
     //Scala读取一个文本文件
     	val outputFile =".." //保存路径
     	result.savaAsTextFile(outputFile)
    
3.2.2 JSON
  • 读取JSON
    • Json是一种半结构化数据,读取Json数据最简单的方式就是 将数据作为文本文件读取,然后通过Json解析器来对RDD中的值进行映射操作。
    • 这种方法 就是假设文件中的每一行都是一条Json记录,如果有跨行的Json数据,则只能读入整个文件,然后对每个文件进行解析
    • 在Scala中,通常将记录读入到一个代表结构信息的类中。
     	//Scala读取一个Json文件
     	...
     	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
    	}})
    
    • 对于Json这种半结构化数据而言,出现格式是比较正常的,如果是小数据集,可以再遇到错误的输入时就停止程序,而对于大规模数据集而言,选择跳过格式不正确的数据,并尝试使用累加器来跟踪错误的个数。
  • 保存Json文件
    • 使用之前将字符串RDD转为解析好的Json数据的库,将由结构化数据组成的RDD转为字符串RDD,然后使用Spark的文本文件API写出去。
    //Scala保存Json文件
     	result.filter(p => P.lovesPandas).map(mapper.writeValueAsString(_))
     		.saveAsTextFile(outputFIle)
    
3.2.3 逗号分隔值与制表符分隔值
  • 逗号分隔值(CSV)文件每行都有固定数目的字段,字段间使用逗号分隔开(在制表符分隔值文件中,即TSV文件中用制表符隔开)
  • CSV与TSV文件有时支持的标准不一致,主要是在处理换行符、转义字符、非ASCII字符以及非整数值等方面。
  • CSV原生并不支持嵌套字段,需要手动组合和分解特定的字段。
  • 常规的做法 是使用第一行每列的值作为字段名
  • 读取CSV
    • 首先需要将文件当做普通文本文件来读取数据 然后对数据进行处理。
    • opencsv()、textFile()(需要CSV的所有数据字段均没有包含换行符)
    • 如果CSV的所有数据字段均没有包含换行符
     //Scala使用textFile()读取CSV
     	val input = sc.textFile(inputFile)
     	val result = input.map{ line =>
     		val reader = new CSVReader(new StringReader(line));
     		reader.readNext();
     	}
    
    • 如果字段中嵌有换行符,则需要完整读入每个文件,然后解析各段
        	//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)))
        	}
    
  • 保存CSV
    • 由于在CSV文件中 我们不会在每条记录中输出字段名,因此为了保证输出保持一致,需要创建一种映射关系(写一个函数,用于将各字段转为指定顺序的数组)。
    	//Scala中写入CSV
    pandaLovers.map(person => List(person.name, person.favoriteAnimal).toArray)
    	.mapPartitons{people =>
    		val stringWriter = new StringWriter();
    		val csvWriter  = new CSVWriter(stringWriter);
    		csvWriter.writeAll(people.toList)
    		Iterator(stringWriter.toString)
    	}.saveAsTextFile(outFile)
    
3.2.4 SequenceFile
  • SequenceFile 是由 没有 相对关系结构的 健值对 文件组成的常用的 Hadoop格式。它是由实现了Hadoop的Writeable接口的元素组成的。
  • SequenceFile文件有同步标记,Spark可以用它来定位到文件中的某个点,然后再与记录的边界对齐,这可以让Spark使用多个节点高效地并行读取SequenceFile文件。
  • Hadoop的RecordReader会让每条记录重用同一个对象,因此直接调用RDD的cache会导致失败,而实际上,只需要使用一个简单的map()操作然后将结果缓存即可。
    在这里插入图片描述
  • 读取SequenceFile
    • sequenceFile(path,keyClass,valueClass,minPartitions),SequenceFile使用Writeable类,因此keyClass和valueClass参数必须使用正确的Writable类。
     //Scala读取SequenceFile
     	val data = sc.sequenceFile(inFile,classOf[Text],classOf[IntWritable]).
     		map{case (x,y) => (x.toString,y.get())}
    
  • 保存SequenceFile
    • SequenceFile存储的是键值对,所以需要创建一个由可以写出到SequenceFile的类型构成的PairRDD。
    • 如果键和值不能自动转为Writale类型,或者想使用变长类型,就可以对数据进行映射操作,在保存之前进行类型转换。
     //Scala保存SequenceFile
     	val data = sc.parallelize(List(("Pandas",3),("Kay",6),("Snail",2)))
     	data.saveAsSequenceFile(outputFile)
    
3.2.5 对象文件
  • 对象文件是使用Java序列化写出的,像是对SequenceFile的简单封装,它只允许存储值包含值的RDD。
    • 和普通的SequenceFile不同,对于同样的对象,对象文件的输出和Hadoop的输出不一样
    • 与其他文件格式不同,对象文件通常用于Spark作业间的通信
    • Java序列化有可能相当慢
  • 读取对象文件:objectFile()
  • 保存对象文件:savaAsObjectFile()
3.2.6 Hadoop输入输出格式
  • 读取其他Hadoop输入格式
    • newAPIHadoopFile接收一个路径以及三个类
      • 第一个类 是 “格式”类,代表输入格式
      • 第二个类 是 “键”类
      • 第三个类 时 “值”类
      • 如果需要设定额外Hadoop配置属性,也可以传入一个conf对象
    • KeyValueTextInputFormat是最简单的Hadoop输入格式之一,可以用于从文本文件中读取键值对数据,每一行都会被独立处理,键和值之间用制表符隔开。
     //Scala中使用老式API读取KeyValueTextInputFormat()
     	val input = sc.hadoopFile[Text,Text, KeyValueTextInputFormat](inputFile).map{
     		case (x,y) => (x.toString ,y.toString)
     	}
    
  • 保存其他Hadoop输入格式
  • 非文件系统数据源
    • 除了hadoopFile()和saveAsHadoopFile()这一大类函数,还可以使用 hadoopDataset/saveAsHadoopDataSet和newAPIHadoopDataset/saveAsNewAPIHadoopDataset 来访问Hadoop所支持的非文件系统的存储格式。
    • hadoopDataset()这一组函数 只接收一个Configuration对象,用来设置访问数据源所必需的的Hadoop属性。
3.2.7 文件压缩
  • 在大数据工作中,我们经常需要对数据进行压缩以节省存储空间和网络传输开销。
  • 对于大多数Hadoop输出格式,我们可以指定一种压缩编解码器来压缩数据,Spark原生的输入方式(textFile和SequenceFile)可以自动处理一些类型的压缩
  • 这些压缩选项只适用于支持压缩的Hadoop格式,也就是那些写出到文件系统的格式,而写入到数据库的Hadoop格式一般没有实现压缩支持。
  • 选择一个输出压缩编解码器可能会对这些数据以后的用户产生巨大的影响。对于Spark这种分布式系统,我们通常会尝试从多个不同机器上一起读入数据。
    在这里插入图片描述
3.3 文件系统
3.3.1 本地/“常规”文件系统
  • Spark 支持从本地文件系统中读取文件,不过它要求文件在集群中所有节点的相同路径下都可以找到
3.3.2 Amazon S3
3.3.3 HDFS
  • Hadoop分布式文件系统(HDFS)是一种广泛使用的文件系统,Spark可以很好地使用它。
  • 将Spark和HDFS部署在同一批机器上,Spark可以利用数据分布来避免一些网络开销
3.4 Spark SQL中的结构化数据
  • Spark SQL支持多种结构化数据源作为输入
  • Spark SQL执行流程
    • 将SQL查询给Spark SQL,让它对一个数据源执行查询
    • 得到由Row对象组成的RDD,每个Row对象表示一条记录。(在Scala/Java中,Row对象的访问是基于下标的,每个Row都有一个get()方法,会返回一个一般类型让我们可以进行类型转换)
3.4.1 Apache Hive
  • Apache Hive 是Hadoop上一种常见的结构化数据源,Hive可以在HDFS内或者在其他存储系统上存储多种格式的表,Spark SQL可以读取Hive支持的任何表
  • 如何把Spark SQL连接到已有的 Hive上
    • 提供Hive配置文件,将hive-site.xml文件复制到Spark的 ./conf 目录下
    • 创建出HiveContext对象(即Spark SQL的入口)
    • 使用Hive查询语言(HQL)来对你的表进行查询,并以由行组成的RDD的形式拿到返回数据
     //Scala中创建HiveContext并查询数据
     	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字段
    
3.4.2 JSON
  • Spark SQL也可以自动推断 记录间结构一致的JSON数据 的结构信息,并将这些数据读取为记录
  • Spark SQL读取JSON数据
    • 创建HiveContext(不需要安装Hive,即不需要hive-site.xml文件)
    • 使用HiveContext.jsonFile方法来从整个文件中获取由Row对象组成的RDD。(除了使用整个Row对象,也可以将RDD注册为一张表,然后从中选出特定的字段)
     //Scala中使用SparkSQL读取JSON数据
     val tweets = hiveCtx.jsonFile("tweets.json")
     tweets.registerTempTable("tweets")
     val results = hiveCtx.sql("SELECT user.name ,text FROM tweets") 	
    
3.5 数据库
3.5.1 Java数据库连接
  • Spark可以从任何支持Java数据库连接(JDBC)的关系型数据库中读取数据,包括MySQL、Postgre等系统。
  • JdbcRDD需要接受这样几个参数
    • 提供一个用于对数据库创建连接的函数(这个函数让每个节点在连接必要的配置后创建自己读取数据的连接)
    • 提供一个可以读取一定范围内数据的查询 以及查询参数中lowerBound和upperBound的值。
    • 这个函数的最后一个参数是一个可以将输出结构从java.sql.ResultSet转为对操作数据有用的格式的函数
	 //Scala中的JdbcRDD
	def createConnection(){
		Class.forName("com.mysql.jdbc.Driver").newInstance();
		DriverMannger.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 pandas WHERE ? <= id AND id <= ?",
		lowerBound =1,upperBound = 3,numPartitions = 2, mapRow = extractValues)
	println(data.collect().toList)
3.5.2 Cassandra
  • 跟ElasticSearch类似,Cassandra连接器要读取一个作业属性来决定连接到哪个集群
 //Scala中配置Cassandra属性
	val conf = new SparkConf(true).set("spark.cassandra.connection.host","hostname")
	val sc = new SparkContext(conf)
  • Datastax 的Cassandra连接器使用Scala中的隐式转换来为SparkContext和RDD提供一些附加函数
 //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()
  • Cassandra连接器支持把多种类型的RDD保存到Cassandra中,可以直接保存由CassandraRow对象组成的RDD,也可以通过制定列的映射关系,存储不是行的形式而是元祖和列表的形式的RDD
 //Scala中保存数据到Cassandra
	val rdd = sc.parallelize(List(Seq("moremagic",1)))
	rdd.saveToCassandra("test","kv",SomeColumns("key","value"))
3.5.3 HBase
  • Spark可以通过Hadoop输入格式访问HBase。这个输入格式会返回健值对数据,其中键的类型为org.apache.hadoop.hbase.io.ImmutableBytesWritable ,而值的类型为org.apache.hadoop.hbase.client.Result。
  • result类包含多种根据列获取值的方法
 //Scala 从HBase读取数据
	val conf = HBaseConfiguration.create()
	conf.set(TableInputFormat.INPUT_TABLE,"tablename") //扫码哪张表
	val rdd = sc.newAPIHadoopRDD(conf,classOf[TableInputFormat],
		classOf[ImmutaleBytesWritable],classOf[Result])
3.5.4 Elasticsearch
  • Elasticsearch是一个开源的、基于Lucene的搜索系统
  • Elasticsearch 连接器会忽略我们提供的路径信息,而依赖于在SparkContext中设置的配置项。
 //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,arg(1))
	jonConf.set(ConfigurationOptions.ES_NODES,arg(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{map (key,value) => mapWritableToInput(value)}
  • 就输出而言,ElasticSearch可以进行映射推断,如果你要存储字符串以外的数据类型,最好明确指定类型映射。





  • 关注「一个热爱学习的计算机小白」公众号 ,对作者的小小鼓励,后续更多资源敬请关注。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值