在Python中使用Spark,首先需要导入PySpark,然后创建一个SparkConf对象配置你的应用,再基于这个SparkConf创建一个SparkContext。以创建一个名为’MyApp’的本地应用为例:
from pyspark import SparkConf, SparkContext
conf = SparkConf().setMaster('local').setAppName('MyApp')
sc = SparkContext(conf=conf)
也可以通过SparkSession实现之前命令行中的PySpark编程,比如找出’Sepal Width’大于3的数据:
from pyspark import SparkConf, SparkContext
from pyspark.sql import SparkSession
conf = SparkConf().setMaster('local').setAppName('MyApp')
sc = SparkContext(conf=conf)
spark = SparkSession(sc)
iris = spark.read.csv('/Users/data/iris.csv', header=True)
iris.filter(iris['Sepal Width'] > 3.0).show(5)
展示结果为:
+------------+-----------+------------+-----------+-----------+
|Sepal Length|Sepal Width|Petal Length|Petal Width| Label|
+------------+-----------+------------+-----------+-----------+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa|
+------------+-----------+------------+-----------+-----------+
only showing top 5 rows
通过SparkSession读取的数据是以DataFrame的形式存储的,如果想使用RDD需要进行转换:
print(type(iris))
#
rdd = iris.rdd
print(rdd.first())
# Row(Sepal Length='5.1', Sepal Width='3.5', Petal Length='1.4', Petal Width='0.2', Label='Iris-setosa')
像Python中的filter()函数一样,我们可以将一个自定义的函数传递给RDD的filter(),从而让它按照我们定义的方式进行过滤。当函数比较简单时,可以使用匿名函数lambda传递;
from pyspark import SparkConf, SparkContext
conf = SparkConf().setMaster('local').setAppName('MyApp')
sc = SparkContext(conf=conf)
even_numbers = rdd.filter(lambda x: x % 2 == 0).collect()
print(even_numbers) # [0, 2, 4, 6, 8, 10]
如果函数较为复杂,则可以通过def关键字定义后将函数传入filter()中:
def find_prime_number(num):
if num <= 1:
return None
for i in range(2, num):
if num % i == 0:
return None
return num
rdd = sc.parallelize(range(12))
prime_numbers = rdd.filter(find_prime_number).collect()
print(prime_numbers) # [2, 3, 5, 7, 11]
在Python中,如果被传递的函数是某个对象的成员,或包含了对某个对象的引用,那这个对象本身也会被传递到工作节点上。一方面,传输一个对象很可能开销巨大;同时,Python很可能因为无法将传递的对象序列化传输而报错。比如下面这种情况:
class PrimeNumChecker:
def __init__(self, prime_num_list):
self.prime_num_list = prime_num_list
def is_prime(self, num):
return num in self.prime_num_list
def find_prime_numbers(self, rdd):
return rdd.filter(self.is_prime)
checker = PrimeNumChecker(prime_num_list=[7, 11, 13, 17, 19])
checker.find_prime_numbers(rdd).collect()
传递self.is_prime时,整个checker都会被传递到工作节点上。如果需要传递某个变量,最好将它先放在一个局部变量中,再传递这个局部变量:
class PrimeNumChecker:
def __init__(self, prime_num_list):
self.prime_num_list = prime_num_list
def is_prime(self, num):
return num in self.prime_num_list
def find_prime_numbers(self, rdd):
prime_nums = self.prime_num_list
return rdd.filter(lambda x: x in prime_nums)
最常见的转化操作就是filter(),它根据过滤条件将原RDD中符合条件的元素转化为新的RDD。比如筛选出含有数字1的元素:
print(rdd.filter(lambda x: '1' in str(x)).collect())
# [1, 10, 11]
另一个常用操作是map(),他对原RDD中的每个元素执行指定操作,并存入新的RDD中。比如对每个元素求平方:
print(rdd.map(lambda x: x ** 2).collect())
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
当某一操作返回值为多个值时,可以使用flatMap(),它对原RDD中的每个元素执行指定操作,并将这些元素所返回的所有值存入一个新的RDD中。比如分词操作:
sentences = sc.parallelize(['Python Spark', 'Scala Spark', 'Java Spark'])
words = sentences.flatMap(lambda x: x.split())
print(words.collect())
# ['Python', 'Spark', 'Scala', 'Spark', 'Java', 'Spark']
使用distinct()可以对RDD元素去重,但是由于distinct()需要对所有数据进行混洗(shuffle),因此开销非常大。
words1 = words.distinct()
print(words1.collect())
# ['Python', 'Spark', 'Scala', 'Java']
union()返回两个RDD的所有元素。由于RDD本身允许包含重复元素,这里的union操作也不是集合操作,会保留所有重复的元素。
words2 = sc.parallelize(['Python', 'Apache', 'RDD', 'Python'])
print(words1.union(words2).collect())
# ['Python', 'Spark', 'Scala', 'Java', 'Python', 'Apache', 'RDD', 'Python']
intersection()返回两个RDD的共有元素。与distinct()一样,intersection()也会导致数据混洗,因此开销很大。但是同时也会移除重复的元素。
print(words1.intersection(words2).collect())
# ['Python']
这又是一个需要混洗的操作,subtract()会返回原RDD中不存在于传入的RDD中的元素,相当于一个RDD减去它与另一个RDD的交集部分。
print(words1.subtract(words2).collect())
# ['Spark', 'Java', 'Scala']
cartesian()是用来计算笛卡尔积的操作,可以返回两个RDD中的元素两两组合的全部情况。大数据量的情况下,笛卡尔积操作的开销巨大。
print(words1.cartesian(words2).collect())
# [('Python', 'Python'), ('Python', 'Apache'), ('Python', 'RDD'), ('Python', 'Python'), ('Spark', 'Python'), ('Spark', 'Apache'), ('Spark', 'RDD'), ('Spark', 'Python'), ('Scala', 'Python'), ('Scala', 'Apache'), ('Scala', 'RDD'), ('Scala', 'Python'), ('Java', 'Python'), ('Java', 'Apache'), ('Java', 'RDD'), ('Java', 'Python')]
reduce()接受一个函数,该函数会对两个同类型的元素进行操作,之后遍历RDD中的元素,依次对它们执行这个接收到的函数,从而实现并行整合RDD中的所有数据。比如构建一个求和函数:
print(rdd.reduce(lambda x, y: x + y))
# 66
在这个reduce()中,传入的函数是将两个值x和y相加。于是reduce()会遍历RDD中的所有元素,然后对他们进行求和运算,得到的结果再求和,从而计算出从0到11的和。
fold()与reduce()功能一致,但需要提供初始值,作为每个分区第一次调用时的结果。为了避免对影响计算结果,这个初始值应当是这个运算的不变元。比如加法不变元是0,乘法不变元是1,因此不同运算对应的初始值也应当不同。
rdd.fold(0, lambda x, y: x + y)
# 66
无论是reduce()还是fold(),都要求返回值与处理的RDD元素的类型一致,而aggregate()则不需要。aggregate()接收的函数可以处理不同类型元素,比如当我们计算一个RDD中元素的平均值时,输入的参数是RDD中的某一个元素,输出则是一个包含元素和以及元素个数的元组,这是reduce()无法直接处理的。与fold()类似,aggregate()也需要传入每个分区首次调用时的初始值。下面是使用aggregate()求平均值的代码:
total, cnt = rdd.aggregate(
(0, 0),
lambda sum_and_cnt, value: (sum_and_cnt[0] + value, sum_and_cnt[1] + 1),
lambda x, y: (x[0] + y[0], x[1] + y[1])
)
print(total / cnt)
# 5.5
其中第一个lambda函数用来分别统计元素和以及元素个数,第二个lambda将各个节点的累加器进行合并。
由于RDD是惰性求值的,因此当我们需要多次使用一个RDD时,为了避免重复的计算,我们需要对这个RDD的数据进行持久化磁盘上或缓存在内存中。对于需要持久化的RDD,只需要调用persist()就可以完成该操作。在pyspark.StorageLevel中,有不同的持久化等级,可以根据需要为persist()指定具体的持久化级别。
from pyspark import StorageLevel
words1.persist(storageLevel=StorageLevel.MEMORY_ONLY)
持久化操作本身不会触发强制求值,你可以在行动操作之前就调用它。如果缓存的数据太多了,Spark会根据LRU策略清除掉不常用的分区移除,之后如果再次被用到就需要重新计算。你也可以使用unpersist()手动释放一些不需要的分区。
上一篇:PySpark大数据分析(2):RDD操作
下一篇:PySpark大数据分析(4):键值对操作