cmd后输入 “spark-shell” 进行scala版的spark
log4j:WARN No appenders could be found for logger (org.apache.hadoop.metrics2.lib.MutableMetricsFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Using Spark's repl log4j profile: org/apache/spark/log4j-defaults-repl.properties
To adjust logging level use sc.setLogLevel("INFO")
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 1.6.0
/_/
Using Scala version 2.10.5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_121)
Type in expressions to have them evaluated.
Type :help for more information.
Spark context available as sc.
...
ark/spark-1.6.0-bin-hadoop2.6/lib/datanucleus-core-3.2.10.jar."
17/04/06 17:17:24 WARN Connection: BoneCP specified but not present in CLASSPATH (or one of dependencies)
17/04/06 17:17:25 WARN Connection: BoneCP specified but not present in CLASSPATH (or one of dependencies)
SQL context available as sqlContext.
scala>
#打开文件
val a=sc.textFile("D:/spark/PythonApplication1/PythonApplication1/README.md")#不可变变量#
#输出 a: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at :27 自动识别了文件类型,string
var a=sc.textFile("readme.md")#var 开头的变量则可以改变其指向,让它指向同一类型的不同对象
// Detected repl transcript paste: ctrl-D to finish.
// Replaying 1 commands from transcript.
scala> var a=sc.textFile("readme.md")
#结果a: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[3] at textFile at :27
#最简单的方法可能就是first 了,该方法向客户端返回RDD 的第一个元素:
a.first()
res1: String = # Apache Spark
#还可以用take 方法,这个方法在first 和collect 之间做了一些折衷,可以向客户端返回
#一个包含指定数量记录的数组。我们来看看如何使用take 方法获取记录关联数据集的前
#10 行记录:
val head=a.take(10)
'''
输出 head: Array[String] = Array(# Apache Spark, "", Spark is a fast and general cluster computing system for Big Data. It provides, high-level APIs in Scala, Java, Python, and R, and an optimized engine that, supports general computation graphs for data analysis. It also supports a, rich set of higher-level tools including Spark SQL for SQL and DataFrames,, MLlib for machine learning, GraphX for graph processing,, and Spark Streaming for stream processing., "",
'''
head.length
#结果 res3: Int = 10
#动作
a.count() #count 动作返回a中对象的个数
#价格res8: Long = 95
a.collect() #collect 动作返回一个包含RDD 中所有对象的Array(数组):
'''结果
res9: Array[String] = Array(# Apache Spark, "", Spark is a fast and general cluster computing system for Big Data. It provides, high-level APIs in Scala, Java, Python, and R, and an optimized engine that, supports general computation graphs for data analysis. It also supports a, rich set of higher-level tools including Spark SQL for SQL and DataFrames,, MLlib for machine learning, GraphX for graph processing,, and Spark Streaming for stream processing., "", , "", "", ## Online Documentation, "", You can find the latest Spark documentation, including a programming, guide, on the [project web page](http://spark.apache.org/documentation.html), and [project wiki](https://cwiki.apache.org/confluence/display/SPARK)., This README file only contains basic setup instruc...
'''
a.saveAsTextFile("D:/spark/PythonApplication1/PythonApplication2/README.md")#saveAsTextFile 动作将RDD 的内容保存到持久化存储(比如HDFS)上
#为了更容易读懂数组的内容,我们可以用foreach 方法并结合println 来打印出数组中的每个值,并且每一行打印一个值:
head.foreach(println)
'''结果
# Apache Spark
Spark is a fast and general cluster computing system for Big Data. It provides
high-level APIs in Scala, Java, Python, and R, and an optimized engine that
supports general computation graphs for data analysis. It also supports a
rich set of higher-level tools including Spark SQL for SQL and DataFrames,
MLlib for machine learning, GraphX for graph processing,
and Spark Streaming for stream processing.
//spark.apache.org/>
'''
'''
很快我们就发现数据有几个问题,这些问题必须在开始对数据分析前解决好。首先,CSV
文件有一个标题行需要过滤掉,以免影响后续分析。我们可以将标题行中出现的"id_1" 字
符串作为过滤条件,编写一个简单的Scala 函数来测试一行记录中是否包含该字符串,代
码如下:
'''
def isHeader(line: String) = line.contains("id_1")
isHeader: (line: String)Boolean
'''
和Python 类似,Scala 声明函数用关键字def。和Python 不同,我们必须为函数指定参数
类型:在示例中,我们指明line 参数是String。函数体部分调用String 类的contains 方
法,用于测试字符串中是否出现"id_1" 字符序列,等号后的部分都是函数体的内容。虽然
我们必须指定line 参数的类型,但是没必要指定函数的返回类型,原因在于Scala 编译器
能根据String 类的信息和String 类contains 方法返回true 或false 这一事实来推断出函
数的返回类型。
'''
'''
有时候我们希望能显式地指明函数返回类型,特别是碰到函数体很长、代码复杂并且包含
多个return 语句的情况。这时候,Scala 编译器不一定能推断出函数的返回类型。为了函
数代码可读性更好,也可以指明函数的返回类型。这样他人在阅读代码的时候,就不必重
新把整个函数读一遍了。可以紧跟在参数列表后面声明返回类型,示例如下:
'''
def isHeader(line: String): Boolean = {
line.contains("id_1")
}
isHeader: (line: String)Boolean
#通过用Scala 的Array 类的filter 方法打印出结果,可以在head 数组上测试新编写的Scala 函数:
head.filter(isHeader).foreach(println)
#结果"id_1","id_2","cmp_fname_c1","cmp_fname_c2","cmp_lname_c1",...
'''
看起来我们的isHeader 方法没什么问题:通过filter 方法将isHeader 作用在head 数组
上,返回的唯一结果是标题行本身。当然我们其实想要的是所有非标题行。为了完成这个
目标,Scala 有几种方法。第一个就是利用Array 类的filterNot 方法:
head.filterNot(isHeader).length
...
res: Int = 9
还可以利用Scala 对匿名函数的支持,在filter 函数里面对isHeader 函数取非:
head.filter(x => !isHeader(x)).length
...
res: Int = 9
Scala 的匿名函数有点儿类似Python 的lambda 函数。在示例代码中我们定义了一个名为x
的参数并把它传给isHeader 函数,再对isHeader 函数的返回值取非。请注意,样例代码
中没必要指定x 变量的类型信息,Scala 编译器能够根据head 的类型是Array[String] 推
断出x 是String 类。
Scala 程序员最讨厌的就是键盘输入。因此Scala 设计了许多小功能来减少输入,比如在匿
名函数的定义中,为了定义匿名函数并给参数指定名称,只输入了字符x=>。但像这么简
单的匿名函数,甚至都没必要这么做:Scala 允许使用下划线(_)表示匿名函数的参数,
因此我们可以少输入4 个字符:
head.filter(!isHeader(_)).length
...
res: Int = 9
有时这种缩写语法使代码更易阅读,因为它省略了明显多余的标识符,但有时也会使代码
更难懂。代码到底是更易懂还是更难懂,这就要靠我们自己判断了。
'''
刚才我们见识了Scala 语言定义和运行函数的多种方式。我们执行的代码都作用在head 数
组中的数据上,这些数据都在客户端机器上。现在,我们打算把在Spark 里把刚写好的代
码应用到关联记录数据集RDD rawblocks,该数据集在集群上的,记录有数百万条。
下面是一段示例代码,是不是觉得特别熟悉?
val noheader = rawblocks.filter(x => !isHeader(x))
用于过滤集群上整个数据集的语法和过滤本地机器上的head 数组的语法一模一样。可以用
noheader 这个RDD 来验证过滤规则是否正确:
noheader.first
…
res: String = 37291,53113,0.833333333333333,?,1,?,1,1,1,1,0,TRUE
这太强大了!它意味着我们可以先从集群采样得到小数据集,在小数据集上开发和调试
数据处理代码,等一切就绪后把代码发送到集群上处理完整的数据集就可以了。最厉害
的是,我们从头到尾都不用离开shell 界面。除了Spark,还真没有哪种工具能给你这种
体验。
val line = head(5)#取出一行数据
#line: String = 4,17,1193
val pieces = line.split(',') #去除逗号分隔
#pieces: Array[String] = Array(4, 17, 1193)
注意访问head 数组元素时用圆括号而不是方括号。Scala 语言访问数组元素是函数调用,
不是什么特殊操作符。Scala 允许在类里定义一个特殊函数apply,当把对象当作函数处理
的时候,这个apply 函数就会被调用,所以head(5) 等同于head.apply(5)。
我们用Java String 类的split 函数把line 中不同部分拆开,并且返回Array[String] 类型
的数组pieces。现在用Scala 的类型转化函数把pieces 的单个元素转换成合适的类型:
val id1 = pieces(0).toInt
#id1: Int = 4
val matched = pieces(2).toBoolean #布尔值
我们还需要转换双精度浮点数类型的九个匹配分值字段。要一次完成全部转换,可以先用
Scala Array 类的slice 方法提取一部分数组元素,然后调用高阶函数map 把slice 中每个
元素的类型从String 转换为Double:
val rawscores = pieces.slice(1,3)
#rawscores: Array[String] = Array(17, 1193)
scala> rawscores.map(s => s.toDouble)
res0: Array[Double] = Array(17.0, 1193.0)
'''忘了rawscores 数组可能有? 了,StringOps 的toDouble 方法不知道怎样把? 转成
Double。我们来写一个函数,它在遇到? 时返回NaN 值,然后在rawscores 数组上运行这
个函数:
'''
def toDouble(s: String) = {
if ("?".equals(s)) Double.NaN else s.toDouble
}
val scores = rawscores.map(toDouble)
接着把所有解析代码合并到一个函数,在一个元组中返回所有解析好
的值:
def parse(line: String) = {
val pieces = line.split(',')
val id1 = pieces(0).toInt
val id2 = pieces(1).toInt
val scores = pieces.slice(2, 11).map(toDouble)
val matched = pieces(11).toBoolean
(id1, id2, scores, matched)
}
val tup = parse(line)
从元组中获取单个字段的值,可以用下标函数,从_1 开始,或者用productElement 方法,它是从0 开始计数的。也可以用productArity 方法得到元组的大小:
tup._1
tup.productElement(0)
tup.productArity
我们希望能创建一个简单的记录类型,它可以根据名称而不是用下标访问
字段。幸运的是,Scala 提供了这样的语法,可以方便地创建这种记录,这就是case class。
case class 是不可变类的一种简单类型,它非常好用,内置了所有Java 类的基本方法,比
如toString、equals 和hashCode。我们来试试为记录关联数据定义一个case class:
case class MatchData(id1: Int, id2: Int,scores: Array[Double], matched: Boolean)
现在修改parse 方法以返回MatchData 实例,这个实例是case class 而不再是元组:
def parse(line: String) = {
val pieces = line.split(',')
val id1 = pieces(0).toInt
val id2 = pieces(1).toInt
val scores = pieces.slice(2, 11).map(toDouble)
val matched = pieces(11).toBoolean
MatchData(id1, id2, scores, matched)
}
val md = parse(line)
这里要注意两点:一,创建case class 时没必要在MatchData 前写上关键字new(再次说明
Scala 开发人员非常讨厌敲键盘);二,MatchData 类有个内置的toString 方法实现,除了
scores 数组字段外,这个方法在其他字段上的表现都还不错。
现在通过名字来访问MatchData 的字段:
md.matched
md.id1
现在完成了在单条记录上测试解析函数,接下来把它用在head 数组的所有元素上(标题行
除外):
val mds = head.filter(x => !isHeader(x)).map(x => parse(x))
很好,通过了。现在将解析函数用于集群数据,在noheader RDD 上调用map 函数:
val parsed = noheader.map(line => parse(line))
记住:和我们本地生成的mds 数组不同,parse 函数并没有实际应用到集群数据上。当在parsed 这个RDD 上执行某个需要输出的调用时,就会用parse 函数把noheader RDD 的每个String 转换成MatchData 类的实例。如果在parsed RDD 上执行另一个调用以产生不同输出,parse 函数会在输入数据上再执行一遍。这没有充分利用集群资源。数据一旦解析好,我们想以解析格式把数据存到集群上,这样就不需要每次遇到新问题时都重新解析。Spark 支持这种使用场景,通过在实例上调用cache 方法,可以指示在内存里缓存某个RDD。现在用parsed 这个RDD 实验一下:
parsed.cache()
这段没看
到目前为止,本章主要讲述了用Scala 和Spark 处理数据的方法,这些方法对本地数据和集
群数据是相似的。本节我们来看看Scala 和Spark API 的一些不同之处,特别是在数据分组
和聚合方面。大多数不同之处在于效率:相比数据在单台机器的内存中就能容纳的情况,
大规模的数据集分布在多台机器上,对其进行聚合时,我们更为担心数据传输的效率。
为了说明这种差异,我们用Spark 分别在本地客户端和集群上对MatchData 执行简单的聚
合操作,目的是计算匹配和不匹配的记录数量。对于mds 数组中的本地MatchData 记录,
我们用groupBy 方法来创建一个Scala Map[Boolean, Array[MatchData]],其中键值基于
MatchData 类的字段matched:
val grouped = mds.groupBy(md => md.matched)
得到grouped 变量中的值以后,就可以通过在grouped 上调用mapValues 方法得到计数。
mapValues 方法和map 方法类似,但作用在Map 对象中的值并得出每个数组的大小:
grouped.mapValues(x => x.size).foreach(println)
就像我们看到的一样,本地数据中的条目都是匹配的,因此map 返回的唯一条目是元组
(true,9)。当然,本地数据只是整个记录关联数据集中的一部分,当这个分组操作运行在
整个数据上时,我们期望能找到很多不匹配的记录。
对集群数据进行聚合时,一定要时刻记住我们分析的数据是存放在多台机器上的,并且聚
合需要通过连接机器的网络来移动数据。跨网络移动数据需要许多计算资源,包括确定每
条记录要传到哪些服务器、数据序列化、数据压缩,通过网络发送数据、解压缩,接着序
列化结果,最后在聚合后的数据上执行运算。为了提高速度,我们需要尽可能少地移动数
据。在聚合前能过滤掉的数据越多,就能越快得到问题的答案。
先来试试创建一个简单的直方图,用它来算一下parsed 中的MatchData 记录有多少matched
字段值为true 或false。幸运的是RDD[T] 类已经定义了一个名为countByValue 的动作,该
动作对于计数类运算效率非常高,它向客户端返回Map[T,Long] 类型的结果。对MatchData
记录中的matched 字段映射调用countByValue 会执行一个Spark 作业,并向客户端返回结果:
val matchCounts = parsed.map(md => md.matched).countByValue()
在Spark 客户端中创建直方图或进行其他类似的值分组时,特别是在涉及的类型变量有很
多值的情况下,我们很想用不同方式对直方图进行排序,比如按键的字母顺序排序或按值
的个数排序,而且排序可以是升序也可以是降序。虽然matchCounts 这个Map 包含的键只
有true 和false,但我们还是想简单看一下怎样以不同方式对内容进行排序。
Scala 的Map 类没有提供根据内容的键或值排序的方法,但我们可以将Map 转换成Scala 的
Seq 类型,而Seq 类支持排序。Scala 的Seq 类和Java 的List 接口类似,都是可迭代集合,
即具有确定的长度并且可以根据下标来查找值:
val matchCountsSeq = matchCounts.toSeq
*Scala 集合
Scala 集合类库很庞大,包括list、set、map 和array。利用toList、toSet 和toArray
方法,各种集合类型可以方便地相互转换。*
matchCountsSeq 序列由(String, Long) 类型的元素组成,我们可以用sortBy 方法控制用哪
个指标排序:
matchCountsSeq.sortBy(_._1).foreach(println)
…
(false,5728201)
(true,20931)
matchCountsSeq.sortBy(_._2).foreach(println)
…
(true,20931)
(false,5728201)
默认情况下,sortBy 函数对数值按升序排序,但很多情况下降序排序对直方图数据更有
用。通过在序列上调用reverse 方法,在打印之前可以改变排序方式:
matchCountsSeq.sortBy(_._2).reverse.foreach(println)
…
(false,5728201)
(true,20931)
看看整个数据集的匹配计数情况,我们发现匹配的记录数和不匹配的记录数差别很大。只
有不到0.4% 的输入对是匹配的。这种差异对记录关联模型影响是重大的:很可能我们提
出的匹配分值函数的误报率很高(也就是许多记录对看起来是匹配的,但实际上不匹配)。