代码+案例详解:使用Spark处理大数据最全指南(上)

如今,有不少关于Spark的相关介绍,但很少有人从数据科学家的角度来解释该计算机引擎。因此,本文将试着介绍并详细阐述——如何运行Spark?

一切是如何开始的呢?--- MapReduce(用于大规模数据集的编程模型)

假设我们的任务是砍伐森林中的所有树木,有两种选择:

· 让戴夫·巴蒂斯塔(美国职业摔跤运动员)用电动电锯把树一棵接一棵地砍掉。

· 找500个普通人用一般的斧头砍伐不同的树。

你更喜欢哪种方法?

虽然选项1仍然是一些人的选择,但是对选项2的需求促使了MapReduce的出现。

在大数据中,巴蒂斯塔的解决方案称为垂直扩展/扩大,就像在单个工作单元中添加/填充大量内存和硬盘一样。

而第二种解决方法称为水平扩展/横向扩展。就像把许多普通机器连接在一起(用更少的内存),然后并行使用这些机器。

垂直扩展相对于水平扩展而言,有以下几个优势:

· 问题规模较小时,速度更快:假设是2棵树。巴蒂斯塔会用那可怕的电锯一下把两棵树砍掉,而两个普通人则还得用斧头砍这两棵树。

· 易于理解:这就是做事的一贯方式,通常按顺序思考问题,这也是整个计算机体系结构和设计的演变过程。

而水平扩展优势如下:

· 更加便宜:雇佣50个普通人比雇佣一个像巴蒂斯塔这样的人要便宜得多。除此之外,巴蒂斯塔需要很多的照顾和保养,以帮助他保持冷静。他非常敏感,就算对一些小事情也是这样,犹如内存过高的机器。

· 问题规模较大时速度更快:设想一下有1000棵树,1000普通工人VS 巴蒂斯塔。利用水平扩展时,如果面临一个很大的问题,只需要雇佣100或1000个廉价工人即可。但和巴蒂斯塔工作却不是这样。你必须增加内存,而这也意味着需要更多的冷却基础设施和保养费用。

已经为大家精心准备了大数据的系统学习资料,从Linux-Hadoop-spark-......,需要的小伙伴可以点击进入

MapReduce使第二种选择成为可能,通过允许使用计算机集群进行并行化来实现这种可能性。

MapReduce由两个术语组成:

映射:

其主要是apply/map函数。将数据分成n个组块,并将每个块发送给不同的工作单元 (映射器)。若想对数据行应用某个函数,该工作单元就会照做。

归约:

使用基于groupby key的某个函数汇总数据。其主要是利用groupby。

当然,系统如期工作还有许多事情需要完成。

为什么使用Spark?

Hadoop(大数据平台)是引入MapReduce编程范式的首个开源系统,而Spark是使其速度更快(100倍)的系统。

Hadoop过去有很多数据传送指令,因为其过去常常将中间结果写入文件系统。

这就影响了分析速度。

Spark有一个内存模型,因此Spark在工作时不会向磁盘写入太多内容。

简单地说,Spark比Hadoop更快,现在很多人都在使用Spark。

开始使用Spark

安装Spark本身就是一个令人头痛的问题。

如果想了解Spark是如何工作的以及如何真正地使用,建议在社区版在线Databricks上使用Sparks。别担心,这是免费的。

传送门:https://databricks.com/try-databricks?utm_source=databricks&utm_medium=homev2tiletest

注册并登录后,屏幕会出现以下显示。

在此可建立新的笔记本。

选择Python笔记本,自定义笔记本名称。

一旦启动一个新的笔记本并尝试执行任何命令,笔记本会询问是否要启动一个新的集群。点击确定。

下一步检查sparkcontext是否存在。要检查sparkcontext是否存在,只需运行以下命令:

这意味着运行Spark就需要新建一个笔记本。

加载数据

下一步是上传用于学习Spark的一些数据。只需点击主页选项卡上的“导入并查看数据”。

本文末尾会使用多个数据集来说明,但现在先从一些非常简单的东西开始。

添加shakespeare.txt文件,下载传送门:https://github.com/MLWhiz/spark_post

可以看到文件加载到/FileStore/tables/shakespeare.txt这个位置了。

第一个Spark程序

本文倾向通过示例学习,所以让我们完成分布式计算的“Hello World”: WordCount 程序。

这是一个小例子,其统计了文档字数并输出了其中的10。

大多数工作是在第二指令中完成的。

如果目前还是跟不上,也别担心,你的任务就是运行Spark。

但是在讨论Spark的基础知识之前,先了解一下Python基础知识。如果使用过Python的函数式编程,那么理解Spark将变得容易得多。

对于没有使用过Python的人,以下是一个简短介绍。

Python中编程的函数方法

已经为大家精心准备了大数据的系统学习资料,从Linux-Hadoop-spark-......,需要的小伙伴可以点击进入

1. 映射

map用于将函数映射到数组或列表中。如果想应用某函数到列表中的各元素中,只需通过使用for循环来实现,但是python lambda函数可允许在python的单行中实现这一点。

my_list = [1,2,3,4,5,6,7,8,9,10]

# Lets say I want to square each term in my_list.

squared_list = map(lambda x:x**2,my_list)

print(list(squared_list))

------------------------------------------------------------

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

在上面的例子中,可将map看作一个函数,该函数输入两个参数—一个函数和一个列表。

然后,其将该函数应用于列表中各元素,而lambda则可供编写内联函数使用。在这里lambda x:x**2定义了一个函数,将x输入,返回x²。

也可以用另外一个合适的函数来代替lambda。例如:

def squared(x):

return x**2

my_list = [1,2,3,4,5,6,7,8,9,10]

# Lets say I want to square each term in my_list.

squared_list = map(squared,my_list)

print(list(squared_list))

------------------------------------------------------------

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

同样的结果,但是lambda表达式使代码更紧凑,可读性更强。

2. 筛选

另一个广泛使用的函数是filter函数。此函数输入两个参数—一个条件和一个筛选列表。

如果想使用条件筛选列表,请使用filter函数。

my_list = [1,2,3,4,5,6,7,8,9,10]

# Lets say I want only the even numbers in my list.

filtered_list = filter(lambda x:x%2==0,my_list)

print(list(filtered_list))

---------------------------------------------------------------

[2, 4, 6, 8, 10]

3. 约归

下面介绍的函数是reduce函数。这个函数将是Spark中的主力部分。

这个函数输入两个参数——一个归约函数,该函数输入两个参数,以及一个应用约归函数的列表。

import functools

my_list = [1,2,3,4,5]

# Lets say I want to sum all elements in my list.

sum_list = functools.reduce(lambda x,y:x+y,my_list)

print(sum_list)

在python2中,约归曾经是Python的一部分,现在我们必须使用reduce,使其作为函数工具的一部分。

在这里,lambda函数输入两个值x和y,返回它们的和。直观地,可以认为约归函数的工作原理如下:

Reduce function first sends 1,2 ; the lambda function returns 3

Reduce function then sends 3,3 ; the lambda function returns 6

Reduce function then sends 6,4 ; the lambda function returns 10

Reduce function finally sends 10,5 ; the lambda function returns 15

在约归中使用的lambda函数的一个条件是它必须是:

· 交换律 a + b = b + a 和

· 结合律 (a + b) + c == a + (b + c).

在上面的例子中,使用了交换律和结合律。另外还可以使用的其他函数:max, min, *等等。

再次回到Spark

既然已经掌握了Python函数式编程的基本知识,现在开始了解Spark。

首先深入研究一下spark是如何工作的。Spark实际上由驱动和工作单元两部分组成。

工作单元通常执行这些需要完成的任务,而驱动则是发布任务指令的。

弹性分布式数据集

RDD(弹性分布式数据集)是一种并行的数据结构,分布在工作单元节点之间。RDD是Spark编程的基本单元。

在wordcount示例中,其第一行

lines = sc.textFile("/FileStore/tables/shakespeare.txt")

获取一个文本文件,将其分布到工作单元节点上,这样RDD就可以并行地处理此文件。还可以使用sc.parallelize函数并行计算列表。

例如:

data = [1,2,3,4,5,6,7,8,9,10]

new_rdd = sc.parallelize(data,4)

new_rdd

---------------------------------------------------------------

ParallelCollectionRDD[22] at parallelize at PythonRDD.scala:267

在Spark中,可以对RDD执行两种不同类型的操作:转换和操作。

1. 转换:从现有的RDD中创建新的数据集

2. 操作:从Spark中获取结果的机制

转换基础

假设已经以RDD的形式获取了数据。

目前可以通过访问工作机器来重报数据。现在想对数据进行一些转换。

比如你可能想要筛选、应用某个功能等等。

在Spark中,这可以由Transformation函数完成。

Spark提供了很多转换函数。

完整列表:http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations

以下列出一些笔者常用的函数:

1. Map函数:

将给定函数用于RDD。

注意其句法与Python略有不同,但是可以完成同样的操作。现在还不必担心collect操作,因为目前只需要将其视为在squared_rdd中收集数据然后返回列表的函数。

已经为大家精心准备了大数据的系统学习资料,从Linux-Hadoop-spark-......,需要的小伙伴可以点击进入

data = [1,2,3,4,5,6,7,8,9,10]

rdd = sc.parallelize(data,4)

squared_rdd = rdd.map(lambda x:x**2)

squared_rdd.collect()

------------------------------------------------------

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

2. Filter函数:

此处依旧没什么惊喜。将输入作为一个条件,仅保留满足该条件的元素。

data = [1,2,3,4,5,6,7,8,9,10]

rdd = sc.parallelize(data,4)

filtered_rdd = rdd.filter(lambda x:x%2==0)

filtered_rdd.collect()

------------------------------------------------------

[2, 4, 6, 8, 10]

3. distinct函数:

仅返回RDD中的不同元素。

data = [1,2,2,2,2,3,3,3,3,4,5,6,7,7,7,8,8,8,9,10]

rdd = sc.parallelize(data,4)

distinct_rdd = rdd.distinct()

distinct_rdd.collect()

------------------------------------------------------

[8, 4, 1, 5, 9, 2, 10, 6, 3, 7]

4. flatmap函数:

与 map函数相似,但是每个输入项可映射到0个或更多个输出项。

data = [1,2,3,4]

rdd = sc.parallelize(data,4)

flat_rdd = rdd.flatMap(lambda x:[x,x**3])

flat_rdd.collect()

------------------------------------------------------

[1, 1, 2, 8, 3, 27, 4, 64]

5. Reduce By Key函数:

此函数与Hadoop MapReduce中的reduce相似。

目前,Spark若只与Lists一起使用,是无法求得数值的。

在Spark中,有一对RDD的概念使其更加灵活。假设有一个数据,其中包含产品、类别和售价。这种情况下仍然可以并行化数据。

data = [('Apple','Fruit',200),('Banana','Fruit',24),('Tomato','Fruit',56),('Potato','Vegetable',103),('Carrot','Vegetable',34)]

rdd = sc.parallelize(data,4)

现在的RDD rdd 包含各元组。

目前想找出从每个类别中获得的总收入。

要实现这一目标,必须将rdd转换为一对rdd,以使其只包含键值对/元组。

category_price_rdd = rdd.map(lambda x: (x[1],x[2]))

category_price_rdd.collect()

-----------------------------------------------------------------

[(‘Fruit’, 200), (‘Fruit’, 24), (‘Fruit’, 56), (‘Vegetable’, 103), (‘Vegetable’, 34)]

此处应用map函数获取所需格式的rdd。使用文本格式运行时,形成的RDD有很多字符串。然后使用map函数将其转换为所需格式。

所以现在的category_price_rdd中包含产品类别和售价。

如果想将关键类别进行约归并统计总价,那么可以这样做:

category_total_price_rdd = category_price_rdd.reduceByKey(lambda x,y:x+y)

category_total_price_rdd.collect()

---------------------------------------------------------[(‘Vegetable’, 137), (‘Fruit’, 280)]

6. Group By Key函数:

与reduceByKey相似,Group By Key只是把所有元素放入迭代器中,并不会reduce。举个例子,如果想保留关键类别和所有产品你的价值,可以使用此函数。

再次使用map函数,获取所需形式的数据。

data = [('Apple','Fruit',200),('Banana','Fruit',24),('Tomato','Fruit',56),('Potato','Vegetable',103),('Carrot','Vegetable',34)]

rdd = sc.parallelize(data,4)

category_product_rdd = rdd.map(lambda x: (x[1],x[0]))

category_product_rdd.collect()

------------------------------------------------------------

[('Fruit', 'Apple'), ('Fruit', 'Banana'), ('Fruit', 'Tomato'), ('Vegetable', 'Potato'), ('Vegetable', 'Carrot')]

然后像下面这样使用groupByKey:

grouped_products_by_category_rdd = category_product_rdd.groupByKey()

findata = grouped_products_by_category_rdd.collect()

for data in findata:

print(data[0],list(data[1]))

------------------------------------------------------------

Vegetable ['Potato', 'Carrot']

Fruit ['Apple', 'Banana', 'Tomato']

此处groupByKey函数运行,其返回该类别中的类别和产品列表。

操作基础

至此已经筛选了数据,并在其上映射了一些函数。接下来要完成计算。

现在希望获取本地计算机上的数据或将其保存到文件中,或者以excel或任何可视化工具中的某些图形的形式显示结果。

为此需要进行一些操作。

完整操作列表:http://spark.apache.org/docs/latest/rdd-programming-guide.html#actions

笔者倾向使用的一些常见操作如下:

1. collect

上文已多次使用过此操作。该操作将整个RDD返回到应用程序中。

2. reduce

使用函数func(该函数接受两个参数并返回一个)来聚合数据集的元素。该函数可交换和组合,以便并行进行正确计算。

rdd = sc.parallelize([1,2,3,4,5])

rdd.reduce(lambda x,y : x+y)

---------------------------------

15

3. take

有时需要查看RDD包含内容,但无需获取内存中的所有元素。take操作返回包含RDD前n个元素的列表。

rdd = sc.parallelize([1,2,3,4,5])

rdd.take(3)

---------------------------------

[1, 2, 3]

4. takeOrdered

takeOrdered操作使用自然顺序或自定义比较器返回RDD的前n个元素。

rdd = sc.parallelize([5,3,12,23])

# descending order

rdd.takeOrdered(3,lambda s:-1*s)

----

[23, 12, 5]

rdd = sc.parallelize([(5,23),(3,34),(12,344),(23,29)])

# descending order

rdd.takeOrdered(3,lambda s:-1*s[1])

---

[(12, 344), (3, 34), (23, 29)]

至此所有的基础都已涉及,接下来回到wordcount示例。

理解WordCount示例

目前已基本了解Spark所提供的转换和操作。

现在理解wordcount程序应该不难。接下来一起逐行完成该程序。

第一行创建RDD并将其分发给工作单位。

lines = sc.textFile("/FileStore/tables/shakespeare.txt")

此RDD行包含文件中的语句列表。使用take操作可查看rdd内容。

lines.take(5)

--------------------------------------------

['The Project Gutenberg EBook of The Complete Works of William Shakespeare, by ', 'William Shakespeare', '', 'This eBook is for the use of anyone anywhere at no cost and with', 'almost no restrictions whatsoever. You may copy it, give it away or']

此RDD格式如下:

['word1 word2 word3','word4 word3 word2']

实际上,下一行是整个工序中的主要函数。

counts = (lines.flatMap(lambda x: x.split(' '))

.map(lambda x: (x, 1))

.reduceByKey(lambda x,y : x + y))

该函数包含对RDD行进行的一系列转换。首先进行flatmap转换。flatmap转换将行作为输入,单词作为输出。因此,进行flatmap转换之后,RDD的形式如下:

['word1','word2','word3','word4','word3','word2']

接下来,对flatmap输出进行map转换,将RDD转换为:

[('word1',1),('word2',1),('word3',1),('word4',1),('word3',1),('word2',1)]

已经为大家精心准备了大数据的系统学习资料,从Linux-Hadoop-spark-......,需要的小伙伴可以点击进入

最后,进行reduceByKey转换以计算每个单词出现的时间。

随后,RDD接近最终的理想形式。

[('word1',1),('word2',2),('word3',2),('word4',1)]

下一行是一个操作,它在本地获取生成的RDD的前10个元素。

output = counts.take(10)

此行仅输出结果。

for (word, count) in output:

print("%s: %i" % (word, count))

以上就是wordcount程序。

到目前为止,我们讨论了Wordcount示例以及可以在Spark中使用的基本转换和操作。但是在现实生活中并不做文字计数。

推荐阅读更多精彩内容