开始使用PySpark(Spark核心和RDDs)--Spark第二部分
Apache Spark是一个分布式集群计算引擎,使大数据的计算变得高效。它提供了一个简单的编程接口,可以用隐含的数据并行性对整个集群进行编程。这实质上导致了大数据的快速计算。Spark不要求用户拥有具有强大计算能力的高端、昂贵的系统。它将大数据分割成多个核心或集群中可用的系统,并优化利用这些计算资源,以分布式方式处理这些数据。因此,Spark是处理大量数据和快速获得查询结果的一个伟大的解决方案,而且不会使系统过热。
在本教程中,我们将通过一个例子深入了解Spark的核心编程概念。在这个例子中,我们将使用一个包含278,858个用户对271,379本书提供1,149,780个评分的大型数据集来实现哪本书的评分最多。
Python是Spark上使用最广泛的语言,所以我们将使用其Python API - PySpark来实现Spark程序。为了学习用PySpark编程的概念和实现,在本地安装PySpark。虽然可以使用终端来编写和运行这些程序,但使用Jupyter笔记本更方便。
安装Spark(并在Jupyter笔记本上运行PySpark API)
第0步:确保你在系统中安装了Python 3和Java 8或更高版本。
$ python3 --version
Python 3.7.6
$ java -version
java version "13.0.1" 2019-10-15
Java(TM) SE Runtime Environment (build 13.0.1+9)
Java HotSpot(TM) 64-Bit Server VM (build 13.0.1+9, mixed mode, sharing)
第1步:从官方页面下载Spark 3。
第2步:从压缩文件中解压,如果你想的话,把它移到任何其他文件夹中(最好是主页)。
$tar -xzf spark-{version}-bin-hadoop{version}.tgz
第3步:在~/.bash_profile(mac)或~/.bashrc(linux)中,添加这些行,指明Spark及其bin的路径。
export SPARK_HOME={path-to-spark}/spark-3.0.0-preview2-bin-hadoop2.7
export PATH=$PATH:$PATH_HOME/bin
第4步:安装jupyter笔记本
$ pip install jupyter
第5步:在~/.bash_profile(mac)或~/.bashrc(linux)中,添加这些行,表明PySpark的配置。
export PYSPARK_PYTHON=python3
export PYSPARK_DRIVER_PYTHON=jupyter
export PYSPARK_DRIVER_PYTHON_OPTS='notebook'
现在重新启动你的终端,在上面运行'pyspark'。它应该会打开jupyter笔记本,并允许你编写和运行PySpark程序!
我们将在我们的例子中使用Books数据集,所以下载它并将数据集放在你将存储PySpark脚本的同一文件夹中。
初始化Spark和RDD
打开Jupyter笔记本,让我们开始编程!
将这些pyspark库导入到程序中。
from pyspark import SparkConf, SparkContext
SparkContext是利用Spark功能的入口,因为它指示程序访问集群。因此,每个spark程序都应该创建SparkContext对象。
SparkConfig对象用于定义我们正在编码的应用程序的特征,例如,应用程序的名称,分配给Driver和Executor节点的内存等。然后这个对象被用来建立SparkContext对象。
conf = SparkConf().setMaster("local[*]").setAppName("Books")
conf.set("spark.executor.memory", "6G")
conf.set("spark.driver.memory", "2G")
conf.set("spark.executor.cores", "4")
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
conf.set("spark.default.parallelism", "4")
spark_context = SparkContext.getOrCreate(conf=conf)
这里,setMaster(local[*])表示我们正在配置SparkContext以在所有可用的本地逻辑核心上运行工作节点线程。Spark.serializer设置用于选择数据序列化器的种类(将数据转换为不同结构的过程,以便高效地存储和传输到分布式网络中的不同节点,同时也允许重建数据的原始结构)。我们需要序列化我们的数据,以便我们可以将它们存储为弹性分布式数据集(RDD)。Kryo序列化器比默认的Spark序列化器--Java序列化器更有效率。
弹性分布式数据集(RDD)构成了Spark编程的核心,通过使用RDD对象为大型数据集的分布式转换编码提供了一个抽象。它们可以在本地或分布式的节点集群上运行,可以自动处理多个执行节点的故障。
将数据集加载为RDDs
用于处理RDDs的功能分为2种类型--变形器和行动。
变形器是可以对RDDs进行的操作。这些操作以某种方式改变了数据,即它们对RDD进行了转换。Spark支持许多转换操作。map()是一个转换函数,它返回一个新的分布式RDD,其中RDD的所有元素已经通过一个函数,在本例中是一个lambda(内联)函数,用'";"分割每一行。
动作函数被用来从RDD中检索信息,这些信息可能已经被转换,也可能没有被转换。这些动作会触发对程序中迄今为止发生的所有转换的评估。*因此,在不调用动作函数的情况下试图打印一个RDD,只会打印RDD的位置而不是值。*这是因为Spark遵循**懒人评估。**它只触发了DAG的创建。**DAG(有向无环图)**引擎被用来优化Spark的计算工作流程。这意味着,结果是通过最短的计算路径来计算的。因此,即使一个程序指定了一个涉及许多进程的工作流程来获得一个结果,DAG引擎也只经过必要的步骤,并跳过所有它认为不必要的计算来获得相同的结果。因此,DAG跳过了RDD所遭受的所有不必要的转换。调用动作函数会将数据从转换后的RDD带入驱动脚本中,因此有必要确保RDD能够装入内存中
在这个例子中,我们将 "BX-Books.csv "文件加载到程序中,并将其作为一个RDD存储。我们执行的第一个动作是count() --它返回行数。接下来我们想看看数据到底是什么样子的。我们可以使用load()函数,它将整个数据集作为一个列表返回。然而,由于这个数据集有超过200,000行,而我们只想偷看一下数据,将其带入内存是不明智的。因此,我们倾向于使用take(n)--它返回一个包含n行的列表。
在这里,我们已经加载了'BX-Books.csv'文件,并将其转换为RDD,使用SparkContext对象,没有对其进行任何转换。

books_file = spark_context.textFile("./BX-CSV-Dump/BX-Books.csv")
print("number of books = ",books_file.count())
print("First 3 rows are - \n",books_file.take(3))
在这里,我们对同一个RDD应用了map()函数,结果是每一行都被分割成自己的行,行的一部分被分割成单独的元素。

books_file = spark_context.textFile("./BX-CSV-Dump/BX-Books.csv").map(lambda l: l.split(‘;’))
print("number of books = ",books_file.count())
print("First 3 rows are - \n",books_file.take(3))
接下来,让我们计算每本书得到的评价数量,并打印出评价数量最多的前10本书。因此,我们必须对BX-Book-Ratings.csv数据集进行聚合转换--计算每一个ISBN(国际标准书号)的出现次数,然后根据计算结果对数据集进行排序。为了在RDD上实现这些特殊的转换,我们需要将数据集转换成键值(K,V)对。(Spark只允许K,V对进行特殊转换。)在这里,我们只选取第二列(数组的第二个元素),并给每本书赋值为1,表示每本书(含ISBN)出现过一次。所以(K,V)=>(ISBN,1)。注意:如果你读过本系列的前一篇文章,这可能看起来很熟悉。(提示:MapReduce!)
#import the BX-Book-Ratings.csv file and split it into rows with individual elements
ratings_file = spark_context.textFile("./BX-CSV-Dump/BX-Book-Ratings.csv").map(lambda l: l.split('";"'))
print("First 3 rows are - \n",ratings_file.take(10))
print(" \n K,V pairs are - \n", ratings_file.map(lambda x: (x[1],1)).take(10))

我们使用filter()来删除包含列名(标题)的行。 filter()是一个转换函数,根据是否通过指定条件来挑选行。在这个代码片段中,我们检查'ISBN'是否出现在该行的第2列中,如果出现就过滤该行。

为了计算每个ISBN的出现次数,我们使用reduceByKey()转换函数。当reduceByKey在一个(K,V)对上被调用时,它根据传递给它的函数来聚合每个键的值。在这个例子中,x代表一个键k的聚合值,y是同一键k的新遇到的值。X和Y被添加并分配给X。
ratings_kv = ratings_file.filter(lambda x: x[1] != 'ISBN' ).map(lambda x: (x[1],1))
#print(ratings_kv.take(10))
ratings_count = ratings_kv.reduceByKey(lambda x, y: x + y)
print(ratings_count.take(10))

现在,我们交换键和值,使Count of occurrences成为键,而ISBN成为值。然后我们应用sortByKey(),它的作用与它的名字一样。默认情况下,它以升序排序,所以我们传递False来检索计数的降序。如果我们从这个排序的RDD的顶部输出10,我们就得到了我们的前10名

现在,我们只有ISBN,而且我们还不知道书名。因此,让我们结合Book-Ratings和Books数据集,检索出前10名的书名。
我们做一个小小的预处理,使ISBN字符串在两个数据集之间匹配。然后,我们使用filter()从books_file的RDD中只挑选出具有top_10列表中的ISBN的行。然后我们使用map只选择书名,并使用collect()动作在RDD中以列表形式返回所有结果值。
top_10 = []
for i in ratings_sorted.take(10):
top_10.append('"'+i[1]+'"')
print(books_file.filter(lambda x: x[0] in top_10).map(lambda x: x[1]).collect())

然后我们就完成了!我们成功地安装了Spark,并使用其核心编程概念,如RDD上的动作和转换,以快速从大型数据集中获得有用的洞察力。同样的例子,如果不使用Spark而反复运行,会使你的系统发热,并花费更多的时间!
在下一篇文章中,我们将探索Spark库,如Spark SQL和Dataframes以及MLLib,并将其用于同一个例子,以回答关于这个数据集的更多问题并提供书籍推荐。