大华的PySpark技术文档

写在开头:

本贴是博主学习自用,如有不对的地方,请各个大神指出。


如何在Linux上安装并配置Hive
##############
# HIVE 3.1.2 #
##############
	# 1、解压并重命名
	cd /opt/download
	tar -zxvf apache-hive-3.1.2-bin.tar.gz -C /opt/software/
	mv /opt/software/apache-hive-3.1.2-bin/ /opt/software/hive312
	cd /opt/software/hive312
	
	# 2、环境变量并激活
	vim /etc/profile.d/my.sh
	#-----------------------------------------
	# hive
	export HIVE_HOME=/opt/software/hive312
	export PATH=$PATH:$HIVE_HOME/bin
	#-----------------------------------------:
	source /etc/profile
	
	# 3、配置文件
	mv conf/hive-default.xml.template conf/hive-default.xml
	vim conf/hive-site.xml
	#-----------------------------------------
	<configuration>
	<!--hdfs仓库路径-->
    <property>
        <name>hive.metastore.warehouse.dir</name>
        <value>/hive312/warehouse</value>
    </property>
	<!--metastore(元)数据库类型-->
	<property>
		<name>hive.metastore.db.type</name>
		<value>mysql</value>
		<description>Expects one of [derby, oracle, mysql, mssql, postgres].</description>
	</property>
    <!--连接mysql字符串-->
    <property>
        <name>javax.jdo.option.ConnectionURL</name>mysql
        <value>jdbc:mysql://192.168.71.128:3306/hive312?createDatabaseIfNotExist=true</value>
    </property>
    <!--mysql连接驱动-->
    <property>
        <name>javax.jdo.option.ConnectionDriverName</name>
        <value>com.mysql.jdbc.Driver</value>
    </property>
    <!--mysql连接账号-->
    <property>
        <name>javax.jdo.option.ConnectionUserName</name>
        <value>root</value>
    </property>
    <!--mysql本地连接密码-->
    <property>
        <name>javax.jdo.option.ConnectionPassword</name>
        <value>kb16</value>
    </property>
	<!--关闭schema验证-->
	<property>
		<name>hive.metastore.schema.verification</name>
		<value>false</value>
	</property>
	<!--提示当前库名-->
	<property>
		<name>hive.cli.print.current.db</name>
		<value>true</value>
		<description>Whether to include the current database in the Hive prompt.</description>
	</property>
	<!--查询输出显示列名-->
	<property>
		<name>hive.cli.print.header</name>
		<value>true</value>
		<description>Whether to print the names of the columns in query output.</description>
	</property>
	</configuration>
	#-----------------------------------------
	${hive.exec.scratchdir} =DEFAULT=> /tmp/hive
		is still used for other temporary files, such as job plans
	
	#4、拷贝mysql驱动
	cp /opt/download/mysql-connector-java-5.1.47.jar /opt/software/hive312/lib/
	
	#5、更新guava包和hadoop一致
	ls lib/|grep guava
		# guava-19.0.jar
	rm -f lib/guava-19.0.jar
	find /opt/software/hadoop313/ -name guava*
		#/opt/software/hadoop313/share/hadoop/common/lib/guava-27.0-jre.jar
		#/opt/software/hadoop313/share/hadoop/hdfs/lib/guava-27.0-jre.jar
	cp /opt/software/hadoop313/share/hadoop/hdfs/lib/guava-27.0-jre.jar lib/
	
	#6、mysql授权
	grant all on *.* to root@master01 identified by 'kb16';
	flush privileges;
	
	#7、初始化
	bin目录下
	schematool -dbType mysql -initSchema
s
	#8、hive启动模式
		#首先启动元数据服务
		nohup hive --service metastore 1>/dev/null 2>&1 &
		
		#1、方法一 hive客户端
			hive
		#2、方法二 基于metastore和hiveserver2的beeline
			#启动hiveserver2服务
			nohup hive --service hiveserver2 1>/dev/null 2>&1 &
			beeline -u jdbc:hive2://localhost:10000
如何在hive中实现对手机号的加密
public class EncrypPhoneNumber extend UDF{
	public String evaluate(String phoNum){
	String encrypPhoneNum=null;
	//判断手机号不为空,并且为11位
	if(StringUtils.isNotEmpty(phoNum))&&phoNum.trim.length()==11{
		//判断数据满足中国大陆手机号码规范
		String regex ="^(1[3-9]\\d{9}$)";
		Pattern p=Pattern.compile(regex);
		Matcher m=p.matcher(phoNum);
		if(m.matcher()){//进入这里的都是符合手机号规则的
			//使用正则替换,返回加密后的数据
			encrypPhoneNum=phoNum.trim().replaceAll("(\\d{3}\\d{4}(\\d{4})","$1****$2");
		}else{
			//不符合手机号规则,数据直接原封不动返回
			encrypPhoneNum=phoNum;
		}
	}else{
		//不符合11位,数据直接原封不动返回
		encrypPhoneNum=phoNum;
	}
	return encrypPhoneNum;
	}
}


//建一个maven工程,贴两个依赖
//粘到IDEA里打jar包,拖到moba里(位置任意)
//用虚拟机执行:  add jar /root/hive-udf-1.0-SNAPSHOT.jar;(这个路径 要写对)
//注册成为临时函数,给UDF函数命名,格式为:create temporary function 函数名 as 'UDF类全路径';
//我改为create temporary  encrypt_phoNum as '粘->类的copy reference';

//使用这个函数,如select encrypt_phoNum();   括号里输你的参数

Part-1 基础入门

第一章

第一节 简单介绍
第二节 风雨十年
第三节 Spark和Hadoop框架的对比

image-20230813130220230

首先他们两都是分布式应用框架。

类型:Hadoop框架不仅有计算框架(MapReduce),还有存储和调度;而Spark仅做为一个计算框架。

场景:Hadoop主要应用于海量批数据处理,计算中间结果由磁盘迭代(Map先把计算结果写到HDFS硬盘之上,再有reduce算子把硬盘上的数据读取出来);Spark结果基于内存迭代,适用于批流一体数据的处理。

编程范式:Hadoop计算框架是mapReduce计算框架,该框架只有map和reduce两种算子,可实现的功能较单一,若要实现较复杂功能则需多次map或reduce算子转换;Spark算子非常多,实现相同功能比MapReduce用的算子更少。

运行:Hadoop中的Task任务以进程的方式进行,启动较慢,消耗资源较多;Spark是线程,线程之间快切,资源消耗较少。

第四节 四大特点

1、速度快

image-20230813132007104

2、使用简单

算子的用法都非常简单

image-20230813132051781

3、能力范围广,适用语言多

image-20230813132125894

SparkCore是Spark的核心,可以对非结构的数据处理,同时支持多种编程语言API进行开发。

sparkSql可以进行对结构化文件的处理

Streaming可以对流式文件的处理

MLib可以对进行机器学习任务的处理

GraphX可以进行图计算

4、兼容多种数据源和多种运行模式

image-20230813132507452

运行在云上(即容器中)。

数据源:可以从多种途径读写数据。

 速度快:比MapReduce快10倍
 使用简单:Python,Java,SQL都能用
 范围广:SparkSQL(结构化文件),SparkStreaming(流文件),MLib(机器学习),Graphx(图计算)
 兼容好:
   1. 支持多种运行方式:Hadoop,Mesos,Standlone,K8s
   2. 支持多种数据源:文件系统(HDFS,text,ORC,Json,csv,parquet)。数据库(Oracle,MySQL)。Nosql(HBase,Redis,ES)。消息对象(Kafka)

第五节 框架模块

image-20230813132920991

第六节 运行 模式

image-20230813133444278

  • StandAlone模式就是它的Master和Worker分布在多个Linux集群环境上

  • YARN模式就是Master和Worker分布在多个Yarn的容器内部,这些容器组成了集群环境

第七节 架构角色

Yarn角色回顾:

image-20230813134549965

Spark角色:

image-20230813134833469

和Yarn的角色基本功能基本一样,只不过叫法不一样。

特殊情况:只有在local模式下,Driver可以既管理,又干活。

第八节 服务器环境

image-20230813143109143

第二章

Local模式原理

image-20230813143200766

Local下的角色分布:

image-20230813143524928

Linux上安装Anaconda
Spark Local模式部署
  • pyspark程序启动

image-20231203171124841

 cd /export/server/spark/bin
 # 启动pyspark程序
 ./pyspark # 执行此命令前需先启动yarn,hadoop。# start-all.sh
 ​

image-20231203174210913

 # 开启下面spark环境后,Python代码和spark代码均可以执行

image-20231203174550202

image-20231203184019844

第三章 StandAlone

第一节 StandAlone运行原理

image-20230813144225495

第二节 部署
第三节 测试
第四节 Spark程序运行层次划分

image-20230813150208831

第四章 StandAlone HA运行原理和部署

StandAlone 的HA模式

为什么会有HA模式:因为单节点的Master会存在单点故障的问题,一旦Master宕机,整个Spark集群会陷入瘫痪。

image-20230810145554859

  • 解决方案:

image-20230810145438136

部署测试

第五章 Stand On Yarn(重点)

第一节 Stand On Yarn 运行原理和部署

image-20230814190524571

image-20230814191245715

第二节 部署和测试
第三节 两种部署模式的区别

分别是集群模式和客户端模式。

image-20230814182453343

集群模式图解:

image-20230814182823314

客户端模式图解:

image-20230814182909886

image-20230814183448992

第四节 两种部署模式的演示和总结

image-20230814185237105

image-20230814184911271

第五节 两种模式任务提交流程

客户端模式的提交流程:

image-20230814185742958

image-20230814185815979

集群模式的提交流程:

image-20230814190023523

第六章 框架,类库

框架和类库

类库:其实就是一堆别人写好得代码,可以导入进行使用,Numpy,Pandas就是Python的类库,但它自己不能单独运行,无法自己创建一个进程。Pandas就是一个类库,主要用于小规模数据梳理。

框架:可以独立运行,比如Spark,Hive,Hadoop,JavaScript都是框架,是提供编程结构的一种软件产品。Spark是一个独立的框架,主要用于大规模数据的处理。

PySpark类库和bin/pyspark的区别

一个是类库,有Spark官方提供;一个是客户端应用程序,用于写SparkApi。

PySpark类库介绍

image-20230810141635352

注意使用场景:以PySpark编写的程序,可以提交到Spark框架中去运行。

PySpark安装
  • 1.先进行Anaconda的安装:

image-20230810143559866

  • 再进行PySpark安装

image-20230810143734489

如果是在pycharm中,直接pip就行,如果是安装在Linux虚拟机上,需要先切换到root,再激活pyspark,最后执行安装命令。

image-20230810143954480

第八章

第一节 Spark运行角色回顾
第二节 分布式代码执行分析
第三节 Python On Spark执行原理

image-20230814194527407

简单图例版:

image-20230814194620551

Part-2 SparkCore

第一章 rdd介绍

什么是RDD

为什么需要RDD

首先,分布式计算需要:分区控制,shuffle控制,数据存储/序列化/发送,数据计算API等等。

image-20230331152913902

image-20230331153155806

image-20230331153826726

高容错的,可以在内存中,实现集群化计算的,弹性分布式数据集。

RDD五大特性-

前3大特性都是每个RDD具备的,后两个是可选的。

image-20230331155433073

特性1-RDD是有分区的(本地集合无)

image-20230331155618435

RDD是个抽象对象,他的物理实体是分区,每个物理的分区里存量物理的数据。每个分区的数据组合起来,就构成了抽象的RDD对象。

特性2-每个算子都会作用于RDD的每个分区上

image-20230331160206912

特性3-RDD之间是有血缘(依赖)的

后一个RDD总是依赖于前一个RDD

image-20230331160850885

特性4-kv型RDD可以有分区器

kv型RDD:RDD内存储的是二元元祖

image-20230331163004616


特性5-RDD的分区规划会尽量靠近数据所在的服务器

image-20230331163516073

WordCount结合RDD特性进行执行分析

image-20230331175305587

rdd的创建(两种方式)

rdd的创建分为两步,第一步先要构造rdd入口对象sc。第二步通过sc入口对象构造rdd,构造rdd有两种方式:其一是并行化本地集合;其二是读取外部文件;其三还有个读取小文件专用方法wholeTextFile

image-20230327103746513

image-20230327103836719

image-20230327103955261

当在parallelize()中不写分区数的时候,默认分区数是根据CPU的核心数来定。

image-20230327104805618

image-20230327104925420

textFile的第二个参数(最小分区数)也可以省略,不写就按系统默认的分区数来。

默认分区数主要跟文件大小有关。如果是读取hdfs,主要跟它的block块有关。

image-20230327141802804

RDD的创建方式三:wholeTextFile

image-20230327141926063

第一个参数也可以读文件夹名称,只不过它通过collect()方法打印出来的是一个个的元祖,元祖里面是kv,k是文件路径,v是文件内容。

image-20230327142650915

要把它里面的文件提取出来可以通过map方法

image-20230327142831604

第二章 算子

算子概念和分类

image-20230327143053426

算子概念:

(Rdd是弹性分布式数据集合)算子其实就是各种封装好的方法,里面有计算逻辑;与方法不同的是,算子是作用于分布式集合(即分布于Rdd)上的,方法是作用于作用本地集合(list)上的,可以理解为方法是个体伤害,算子是群里伤害。

方法你可以理解为战士,数据(不管是本地集合还是分布式Rdd),你可以理解为敌人(攻击对象),方法是这节对数据进行操作,一刀一刀砍(直接伤害)。而算子可以理解为法器,它其实本身不能直接对数据造成伤害,它的参数是传入一个战士(方法),通过这个战士通过算子对数据rdd造成伤害(间接),与战士不同的是,多了这样一个间接嵌套,这个法师把对敌人的单个伤害变成了群体伤害。群体伤害是算子统一被动。

举个例子,filter这个算子,主动技能是对敌人进行过滤,但是它不能直接过滤,它的参数是接收一个满足过滤条件的语句,被动是实现了群体多分区(副本)过滤,大大提高了刷本效率。

image-20230327143136209

算子分类

转换算子和行动算子。

转换算子 顾名思义,即对数据(rdd)进行操作,是原rdd编程另一种rdd。需注意,转换算子是懒加载,即若没有行动算子配合,转换算子写再多也是执行的。

行动算子 :相当于比赛中发令枪的作用,前面的转换算子都是在构建对数据的执行计划,一步一步调用什么算子把什么数据变成什么样都是转换算子说了算,最后的执行算子才是决定这个计划是否被执行的关键。

算子如何区分:转换字段对数据操作后,返回的依旧是个rdd。行动算子操作后,返回的不是rdd。


collect算子

【收集法师】行动算子,用于将分布在集群中的数据收集到内存中,它以数组的形式返回。

注意:调用改算子后,所有分布在各个节点上的数据,都会被拉取到单个节点的内存中,所以处理特别大规模的数据时谨慎使用。

应用该算子的场景:就是需要以本地的方式处理所有数据时,比如数据本地落盘时。如果数据量较大,可以考虑先对数据进行聚合,分区或取样等操作,以减少对单节点内存的占用量。

map算子

【法师之神--转换法器】这个算子的作用非常强大(),他可以再和任何一个(即只允许接受一个参数)法器(方法或函数)配合打出非常多的效果,将战士砍杀后的结果返回一个新的rdd。但需注意:传入的敌人和他们处理返回后的敌人,类型必须相同。

image-20230320093809207

 from pyspark import Sparkconf, SparkContext
 ​
 if __name__ == '__main__':
   conf = SparkConf().setAppName('test').setMaster('local[*]')
   sc = SparkContext(conf=conf)
   
   rdd = sc.parallelize([1,2,3,4,5,6],3)
   
   # 方式一:定义方法,作为算子,传入函数体
   def add(data):
     return data * 10
   print(rdd.map(add).collect())
   
   # 方式二:直接匿名函数
   print(rdd.map(lambda x:x * 10).collect())

mapPartitions算子

和map法师处理的结果是一样的,只是过程有区别,map是一条一条处理数据,mapPartitions是一个分区一个分区处理数据。

image-20230327175520542

它和map的区别在于,map是的网络传输是一次传输分区内一条数据,它是一次性传输一整个分区的数据。在cpu执行层面,没有省下来任何东西,但在空间利用率(IO)上,有很大提升,综上,mapPartitino的性能,比map好很多。

image-20230327180233548


foreach算子

【法师之神mini--无返回值转换法师】

功能:对rdd的每个元素,执行你提供的逻辑操作(和map一个意思),但该算子没有返回值。可以认为它一个没有返回值的map方法。当你需要进行一些不需要返回值的操作时,可以使用该方法(因为他不跟driver汇报,直接在Executor上输出,效率会高一些)。

image-20230327102448057

image-20230327102526376


foreachPartition算子

【行动算子-foreach加强法师】

和foreach的作用一样,只不过foreach是一条一条处理数据,foreachePartition是一次处理一个分区的数据。

image-20230818094146641

 #coding:utf-8
 ​
 from pyspark.sql import SparkSession
 ​
 def map_func(data):
     result=[]
     for i in data:
         result.append(i*10)
     print(result)
 ​
 ​
 ​
 if __name__ == '__main__':
     spark=SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc=spark.sparkContext
 ​
     rdd= sc.parallelize([1,2,3,4,5,6,7,8],3)
     print(rdd.foreachPartition(map_func)) # 这里其实不用print了,因为foreachPartition没有返回值
 ​


flatMap算子

【法师之母--扁平法师】,她和map的作用有点类似,也是只和一个战士配合,返回一个处理后的rdd;只不过她比map多做一步的是,如果返回的这个rdd是二维的,它会把这个二维自动变成一维。即list嵌套list就是二维,她会把嵌套拆掉,变成只有一个list。

image-20230326211139735

image-20230326210938815


glom算子

【法目之敌--嵌套法师】

和flatMap的作用相反

 rdd.glom().flatMap(lambda x:x).collect() # 加嵌套,解嵌套,等于没变

image-20230327144026641

image-20230327144203239


mapValues算子

注意这个算子的限定条件,只针对二元元祖的rdd,而且这个二元元祖是里面是(k, v)结构。

假设一个个的二元元祖是刘禅、元歌类型的敌人,(k即本体,v即傀儡),这个法师的作用就是,当遇到一个个这种类型的敌人的时候,不管你来的是元歌还是刘禅,我统统忽视你的k,而只针对你的傀儡进行伤害,返回的依旧是一个列表里嵌套的二元元祖。

 rdd = sc.parallelize([('a',1), ('a',1), ('b',1), ('c',3)])
 ​
 rdd1 = rdd.mapValues(lambda value: value*10).collect().print()
 ​
 >>>
 [('a',10), ('a',10), ('b',10), ('c',30)]

image-20230326212419101

groupBy算子

【分组法师】

他的作用是对数据按照你指定的那个列进行分组,并返回一个重分组后的rdd,而且他对传入前和传入后rdd的类型不做限制,就是你的传参可以是数值类型,返回类型可以是字符串。

分组的那个列就是你lambda返回的后的值。

image-20230812175012422

image-20230812175708336

 # coding:utf-8
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc = spark.sparkContext
 ​
     rdd=sc.parallelize([('a',1),('b',1),('b',1),('c',1)])
     rdd1=rdd.groupBy(lambda x: x[0])
     print(rdd1.collect())
     # [('b', <pyspark.resultiterable.ResultIterable object at 0x000001C7C2D657F0>), ('c', <pyspark.resultiterable.ResultIterable object at 0x000001C7C2D65850>), ('a', <pyspark.resultiterable.ResultIterable object at 0x000001C7C2D65940>)]
     # 此时元祖的第二个元祖是一个Iterable的迭代对象,需要强转成list
     rdd2=rdd1.map(lambda x:(x[0],list(x[1])))
     print(rdd2.collect())
     # [('b', [('b', 1), ('b', 1)]), ('c', [('c', 1)]), ('a', [('a', 1)])]
     spark.stop()
groupByKey算子

【分组法师--按key分组法师】

功能:针对KV型的rdd,但它和groupBy算子不同的是,这个法师会自动按照Rdd的key分组,不需要你指定。

用法:rdd.groupByKey() 里面不需要有传入的参数

注意:和reduceByKey不同的是,reduce是分组聚合,而groupByKey只是单纯分组

image-20230327100719275


reduceByKey算子

【分组法师--按key分组聚合法师】

注意:reduceByKey中接收的函数,只负责聚合,不理会分组。这个算子会自动按照key分组。

image-20230326212007421

image-20230326211842923


面试题-groupByKey和reduceByKey的区别

它两本质的区别在于,groupByKey先shuffle,再聚合;reduceByKey是先组内聚合,再shuffle,大大减少了shuffle量,所以性能高。

image-20230327100909808

image-20230327100957970

reduceByKey在分区内会先做聚合,这样就能减少网络IO的次数

image-20230327101407553

如果业务要用的逻辑是:分组+聚合,想都不用想,直接用reduceByKey


filter算子

【过滤法师】

功能:返回的是一个布尔型,把为True的数据进行保留,为False的丢弃,返回一个新的rdd

image-20230327095010681

 from pyspark import SparkContext, SparkConf
 from pyspark.sql import SparkSession
 ​
 conf = SparkConf().setAppName("filter_demo").setMaster("local")
 sc = SparkContext(conf=conf)
 spark = SparkSession(sc)
 ​
 people_rdd = sc.parallelize([("Alice", 25), ("Bob", 30), ("Charlie", 35)])
 # 可以直接对rdd进行过滤算子操作
 people_rdd.filter(lambda x: x[1]>=30)
 ​
 # 也可以先转成DataFrame,在使用该算子进行过滤
 people_df = spark.createDataFrame(people_rdd, ["name", "age"])
 filtered_df = people_df.filter(people_df.age >= 30)
 ​
 rdd1 = sc.parallelize([1, 2, 3, 4, 5, 6])
 filtered_rdd = rdd1.filter(lambda x: x >= 4) # 满足 x>=4 的值保留,不满足的丢弃
 ​
 rdd2 = sc.parallelize(["apple", "banana", "orange", "peach"])
 filtered_rdd = rdd2.filter(lambda x: "a" in x)

distinct算子

【去重法师】

对相同rdd的数据进行去重,返回一个新的rdd。里面参数是分区数量,一般不填。

image-20230818092406403

 # coding:utf-8
 ​
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc = spark.sparkContext
 ​
     rdd = sc.parallelize([1, 2, 1, 23, 4, 23, 3, 5, 3, 1, 5])
     rdd1 = rdd.distinct()
     print(rdd1.collect())
 >>>
 [1, 2, 3, 4, 5, 23]

union算子

【合并法师】

将两个rdd合并是一个rdd返回一个新的rdd。rdd的类型不同也是可以合并的。

image-20230813170611068

join算子

【连接法师】

功能:对两个rdd执行join操作(可实现SQL的内、外连接)

注意:join算子只能用于kv型(二元元祖)的Rdd,关联条件就是rdd1的key去join(rdd2的key)

语法:

 rdd.join(other_rdd) # 内连接
 rdd.leftOuterJoin(other_rdd) # 左外
 rdd.rightOuterJoin(other_rdd) # 右外

image-20230327095703822

注意:join算子必然产生shuffle,导致性能降低。

intersection算子

【取交集法师】

image-20230818121019504

 # coding:utf-8
 ​
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc = spark.sparkContext
 ​
     rdd1 = sc.parallelize([('a', 1), ('b', 1)])
     rdd2 = sc.parallelize([('a', 1), ('c', 1)])
 ​
     res=rdd1.intersection(rdd2).collect()
     print(res)
 >>>
 [('a', 1)]

sortBy算子

【排序法师】

sortByKey算子

【按key排序法师】

案例-提交到Yarn执行

image-20230327145040657

image-20230327151325461

image-20230327151452181

countByKey算子

【统计key法师】行动算子,返回的不是rdd。

image-20230812195235453

 # coding:utf-8
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc = spark.sparkContext
 ​
     rdd = sc.textFile('../Data/Input/words.txt')
     result = rdd.flatMap(lambda x: x.split(' ')).map(lambda x: (x, 1)).countByKey()
     print(result)
 >>>
 defaultdict(<class 'int'>, {'hadoop': 3, 'flink': 1, 'spark': 3, 'java': 2, 'sql': 1})

reduce算子
fold算子

takeSample算子

【抽样法师】行动算子,最外层返回的是一个列表。

随机从现有rdd中抽取你指定条数的数据

 print(rdd1.takeSample(True, 3)) 
 >>>>
 [('SCOTT', 1), ('FORD', 1), ('TURNER', 1)]
takeOrdered算子

【排序法师】

第一个参数是要几个数据,第二个参数是对排序的数据修改,但不会改变数据本身

image-20230818093019194

 #coding:utf-8
 ​
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark=SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc=spark.sparkContext
 ​
     rdd=sc.parallelize([1,3,2,4,7,9,6],1)
 ​
     print(rdd.takeOrdered(3))
 ​
     print(rdd.takeOrdered(3,lambda x:-x)) # 相当于倒序排列
 >>>>
 [1, 2, 3]
 [9, 7, 6]

saveAsTextFile算子

【落盘法师】行动算子

功能:将rdd的数据写入文本文件中,支持本地写出,hdfs等文件系统。注意,这个算子写出文件的时候,rdd有几个分区,它就写出成几个文件。

image-20230327101702975

image-20230327102146522

partitionBy算子

【指定分区内容法师】

image-20230818105119242

 #coding:utf-8
 ​
 from pyspark.sql import SparkSession
 ​
 def process(k):
     if 'hadoop'==k or 'hello'==k:return 1
     if 'scala'==k or 'spark'==k:return 2
     return 3
 ​
 ​
 ​
 if __name__ == '__main__':
     spark=SparkSession.builder.master('test').master('local[*]').getOrCreate()
     sc=spark.sparkContext
 ​
     rdd = sc.parallelize([('hadoop',1),('hadoop',1),('spark',1),('spark',1),('hello',1),('flink',1),('scala',1),('java',2)])
     print(rdd.partitionBy(3, process).glom().collect())
     
 >>>>>>>
 [[('flink', 1), ('java', 2)], [('hadoop', 1), ('hadoop', 1), ('hello', 1)], [('spark', 1), ('spark', 1), ('scala', 1)]]

repartition、coalesce算子

【修改分区数量法师】

这两个算子尽量少用!!!就算真要用,也尽量减少分区(比如为了做排序,设置为1),而不是增加分区。 因为它会极大可能导致内存迭代管道增加,从而导致shuffle。这两个api本质上是一回事,但建议用coalesce,因为它的第二个参数是个安全阀,当你把shuffle=True时,才会真正执行你要增加的分区数。

image-20230327180526081

image-20230327181123983

first算子

【行动算子--取首级法师】行动算子

取出rdd的第一个元素,元素类型本身是啥,取出来就是啥。

take算子

【取前N元素法师】行动算子

取出rdd中前N个元素,返回一个list给你

image-20230818100242587

top算子

【降序取前N法师】行动算子

对rdd的数据按降序排序,取最大的前N个元素

image-20230818100337274

count算子

【计数法师】行动算子

计算rdd有多少条数据,返回一个数字给你。

image-20230818100447846

第三章 rdd缓存

RDD的数据是过程数据

image-20230328092111580

RDD的缓存

用法:很简单,只需要调用cache()这个api,当前这个rdd就被保存起来了(内存)。

image-20230328092857872

建议:一般把从文件读出来的RDD就可以做一步持久化的操作了。

image-20230328094351947

image-20230328094247115

image-20230328094958176

image-20230328095055434

RDD的CheckPoint

image-20230328095438938

image-20230328095507667

image-20230328095639869

image-20230328100110488

总结

image-20230328101033078

第四章 rdd案例

jieba库简单使用

jieba库是一个十分优秀的中文分词第三方库(需要手动下载),它的分词分为3种模式:

  1. 精确模式:jieba.cut():把文本精确的分开,不存在冗余的单词

  2. 全模式:jieba.cut(s, cut_all=True):把文本中所有可能的词语都扫描出来,有冗余

  3. 搜索引擎模式:jieba.cut_for_search(s):在精确模式的基础上,对长词再次切分,有冗余

generator

generator是一个可迭代对象,可以通过list()包裹,将其强转为一个列表,也可以通过for循环,将其里面的元素进行遍历。

image-20230328103042263

image-20230328103254446

image-20230328103435794

image-20230328104153632

 # -*- coding:utf-8 -*-
 import jieba
 ​
 if __name__ == '__main__':
     content='小明毕业于中国科学院计算研究所,后在清华大学深造'
 ​
     result = jieba.cut(content,cut_all=True)
     result1= jieba.cut(content)
     result2= jieba.cut_for_search(content)
     print(list(result))
     print(list(result1))
     print(list(result2))
     
 >>>
 ['小', '明', '毕业', '于', '中国', '中国科学院', '科学', '科学院', '学院', '计算', '研究', '研究所', ',', '后', '在', '清华', '清华大学', '华大', '大学', '深造']
 ['小明', '毕业', '于', '中国科学院', '计算', '研究所', ',', '后', '在', '清华大学', '深造']
 ['小明', '毕业', '于', '中国', '科学', '学院', '科学院', '中国科学院', '计算', '研究', '研究所', ',', '后', '在', '清华', '华大', '大学', '清华大学', '深造']
需求1开发

将高频词汇进行统计,打印前5名

 \t 的意思是 :水平制表符。将当前位置移到下一个tab位置。
 \r 的意思是: 回车。将当前位置移到本行的开头。
 \n 的意思是:回车换行。将当前位置移到下一行的开头。
 \f 的意思是:换页。将当前位置移到下一页的开头
 # coding:utf-8
 ​
 from pyspark.sql import SparkSession
 from pyspark.storagelevel import StorageLevel
 import jieba
 ​
 def context_jieba(data):
     res = jieba.cut_for_search(data)
     l=[]
     for i in res:
         l.append(i)
     return l
 ​
 def filter_words(data):
     return data not in ['帮','谷','客']
 ​
 def convert_words(data):
     if data == '传智播': data='传智播客'
     if data == '院校': data='院校帮'
     if data == '博学': data='博学谷'
     return (data,1)
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     sc = spark.sparkContext
 ​
     file_rdd = sc.textFile('../../Data/Input/SogouQ.txt')
     split_rdd = file_rdd.map(lambda x: x.split('\t'))
 ​
     split_rdd.persist(StorageLevel.MEMORY_ONLY)
     print(split_rdd.takeSample(True, 3))
     # 从这开始才是需求1的开发,前面都是公共代码
     content_rdd = split_rdd.map(lambda x: x[2])
 ​
     words_rdd = content_rdd.flatMap(context_jieba)
 ​
     filter_rdd = words_rdd.filter(filter_words)
 ​
     result_rdd = filter_rdd.map(convert_words)
 ​
     print(result_rdd.reduceByKey(lambda a, b: a + b).sortBy(lambda x: x[1], ascending=False).take(5))

需求2开发

image-20230328123138549

image-20230328122934358

需求3开发

image-20230328141255925

代码:

 # -*- coding:utf-8 -*-
 import jieba
 ​
 ​
 def context_jieba(data):
     seg = jieba.cut_for_search(data)
     l = list()
     for word in seg:
         l.append(word)
     return l
 ​
 ​
 def filter_words(data):
     return data not in ('谷', '帮', '客')
 ​
 ​
 def append_words(data):
     if data == '传智播': data = '传智播客'
     if data == '院校': data = '院校帮'
     if data == '博学': data = '博学谷'
     return (data, 1)
 ​
 ​
 def extract_user_and_word(data):
     user_id = data[0]
     content = data[1]
     words = context_jieba(content)
     return_list = list()
     for word in words:
         if filter_words(word):
             return_list.append(user_id + '_' + append_words(word)[0], 1)
     return return_list
 ​

 # -*- coding:utf-8 -*-
 from numpy import add
 from pyspark import SparkContext, SparkConf, StorageLevel
 from defs import context_jieba, filter_words, append_words, extract_user_and_word
 ​
 if __name__ == '__main__':
     conf = SparkConf().setAppName('need_1').setMaster('local[*]')
     sc = SparkContext(conf=conf)
 ​
     file_rdd = sc.textFile('SogouQ.txt')
     split_rdd = file_rdd.map(lambda x: x.split('\t'))
     print(type(split_rdd))
     print(split_rdd.collect())
     # 因为要对split_rdd多次使用
     split_rdd.persist(StorageLevel.MEMORY_AND_DISK)
 ​
     # TODO 需求1:对用户搜索的关键词进行分析
     context_rdd = split_rdd.map(lambda x: x[2])
     words_rdd = context_rdd.flatMap(context_jieba)
 ​
     filter_rdd = words_rdd.filter(filter_words)
     append_rdd = filter_rdd.map(append_words)
 ​
     result1 = append_rdd.reduceByKey(lambda a, b: a + b). \
         sortBy(lambda x: x[1], ascending=False, numPartitions=1). \
         take(5)
 ​
     print('需求1的结果是:', result1)
 ​
     # TODO 需求2:用户和关键词组分析
     # 1,我喜欢传智播客
     # 1+我 1+喜欢 1+传智播客
     user_content_rdd = split_rdd.map(lambda x: (x[1], x[2]))
     user_content_with_one_rdd = user_content_rdd.flatMap(extract_user_and_word)
     # 对内容进行分组,聚合,求前5
     result2 = user_content_with_one_rdd.reduceByKey(lambda a, b: a + b). \
         sortBy(lambda x: x[1], ascending=False, numPartitions=1).take(5)
 ​
     print('需求2结果:', result2)
 ​
     # TODO 需求3:热门搜索时段分析
     time_rdd = split_rdd.map(lambda x: x[0])
     # 对时间进行处理,只保留小时进度即可
     hour_with_one_rdd = time_rdd.map(lambda x: (x.split(':')[0], 1))
     # 分组,聚合,排序
     result3 = hour_with_one_rdd.reduceByKey(add). \
         sortBy(lambda x: x[1], ascending=False, numPartitions=1). \
         collect()
     print('需求3结果:', result3)
     split_rdd.unpersist()
 ​

提交到Yarn集群运行

把上述代码修改的地方:

  1. 删掉 conf = SparkConf().setAppName('need_1').setMaster('local[*]')中的.setMaster(...),因为在命令行提交模式下,需要指定master为yarn模式,如下图:

  2. 将本地文件路径改成hdfs上的路径

    file_rdd = sc.textFile('SogouQ.txt') --> file_rdd = sc.textFile('hdfs://node1:8020/Input/SogouQ.txt')。因为集群模式下,无法读到本地的文件路径。

  • 删掉setMaster,因为提交到集群的时候可以通过参数来指定。

  • 把sc.textFile读取文件的位置改成hdfs对应的位置

  • 给所有Linux节点安装jieba库

image-20230817144046984

普通提交:

image-20230328141535528

image-20230328142113104

总结

image-20230328142857847

第五章 共享变量

广播变量

应用场景:

 本地集合和分布式对象(rdd)进行关联时,且本地集合数据量不是很大的情况下,可以将本地集合对象封装为广播变量。
 这样可节省:
 1、网络IO的次数
 2、Executor的内存占用
 备注:本地集合跑的是driver,rdd跑的是Executor
 ​
 只要封装成广播变量以后,driver的本地集合就会给分布式的Executor的每个进程都同步一份,而每个Executor进程的资源又都是共享的,在Executor里可能会有多个分区,每个分区是一个线程,线程就可以共享driver同步过来的这个集合。如果不是广播变量,那driver就需要给每个分区同步一次,增加了网络IO,造成内存资源的浪费。

image-20230329122426498

image-20230329122648659

解决方案:

image-20230817150200882

image-20230329123628895

用法

 1.将本地集合listA,用sc.broadcast(listA)套一下,返回的是一个广播变量broadCastA(本质是一个集合)
 2.用这个广播变量的时候,只需要broadCast.value方法,即可取出这个集合。

image-20230329124824060

 # -*- coding:utf-8 -*-
 from pyspark import SparkContext, SparkConf
 ​
 def map_func(data):
         id = data[0]
         name = ''
 ​
         for stu_info in broadcast.value:
             stu_id = stu_info[0]  # stu_id是广播变量
             if id == stu_id:  # ID是rdd
                 name = stu_info[1]
             return (name, data[1], data[2])
 ​
 ​
 if __name__ == '__main__':
     conf = SparkConf().setAppName('broadcast').setMaster('local[*]')
     sc = SparkContext(conf=conf)
 ​
     stu_info_list = [(1, '张大仙', 11), (2, '王晓晓', 13), (3, '张甜甜', 11), (4, '王大力', 11)]
     # 将本地集合转为广播变量
     broadcast = sc.broadcast(stu_info_list)
 ​
     score_info_rdd = sc.parallelize(
         [(1, '语文', 99), (2, '数学', 99), (3, '英语', 99), (4, '编程', 99), (1, '语文', 99),
          (2, '编程', 99), (3, '语文', 99), (4, '英语', 99), (1, '语文', 99), (3, '英语', 99), (2, '编程', 99)
          ])
 ​
     print(score_info_rdd.map(map_func).collect())
 ​
 >>>
 [('张大仙', '语文', 99), ('王晓晓', '数学', 99), ('张甜甜', '英语', 99), ('王大力', '编程', 99), ('张大仙', '语文', 99), ('王晓晓', '编程', 99), ('张甜甜', '语文', 99), ('王大力', '英语', 99), ('张大仙', '语文', 99), ('张甜甜', '英语', 99), ('王晓晓', '编程', 99)]

image-20230817152942633

适用场景:本地集合对象/rdd本身很小 和另一个RDD进行关联的时候,可以将本地集合对象或这个小的RDD分装成广播变量。

因为rdd和rdd关联是用的join算子,比产生shuffle,倒不如把这个小的rdd直接全部分到这个大的RDD的分区上,这样能省去大量网络IO

累加器

用法:acmlt = sc.accumulator(0) 0表示初始变量设为0

image-20230329141626197

使用了累加器后的代码演示:最终的结果为10

image-20230329171906876

 from pyspark import SparkContext, SparkConf
 ​
 if __name__ == '__main__':
     conf = SparkConf().setAppName('accumulator').setMaster('local[*]')
     sc = SparkContext(conf=conf)
 ​
     rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2)
     acmlt = sc.accumulator(0)
 ​
     def map_func(data):
         global acmlt
         acmlt += 1
         print(acmlt)
 ​
     rdd.map(map_func).collect()
     print(acmlt)

image-20230329172120921

这段代码说明,当生成rdd3的时候,上面这个rdd2已经collect过,失效了(因为rdd是过程数据),此时rdd2需要重新溯源,继续调用一遍map_func方法,而此时的map_func里acmlt已经累加过一遍变成10了,所以会在10的基础上继续累加,所以rdd3的结果为20。

上述问题的解决办法:

加个cache()

image-20230817161020385

总结:在生产环境中,用到累加器的时候,一定要注意上述问题,会不会出现重复累加的现象。

广播变量累加器综合案例

image-20230329172830731

image-20230329174359307

image-20230329174323023

总结

image-20230329174522331

第六章 DAG

本章内容是重点理解内容,也是面试常问内容,体现了对Spark底层的理解

DAG

image-20230331100547741

DAG:有向无环图,有方向,无闭环。说白了,这个DAG就是Spark给RDD做的一个工作计划,告诉RDD接下来每一步要做什么。

通过查看Spark的4040端口,可以看到Spark每个任务的DAG,效果如下所示:

image-20230331102228902

Job和Action的关系

一个Spark任务在运行的时候,会分成好几个Job。Job就是子任务。

image-20230331103817277

Action是执行链条的开关(RDD迭代链条的开关)。上图中一个Action会产生一个Job,一个Action对应一个Job,每个Job都有各自的DAG图。

image-20230331104659804

image-20230331104920113

宽窄依赖和阶段划分

image-20230331105101759

窄依赖

不管子分区接收多少个父分区,只要父分区的数据全部只给一个子分区,就叫窄依赖。

image-20230331105208650

宽依赖(shuffle)

image-20230331105545095

阶段划分

image-20230331105742542

内存迭代计算

image-20230331110022830

不要轻易改分区计算管道,即不要随意使用重分区算子,因为重分区就以为是shuffle,增加一次shuffle导致内存计算管道变短,性能下降一次。

image-20230331112706259

上图总结:

横向上:一个task可以贯穿很多个RDD进行处理,但只能RDD中的一个分区。纵向上:一个RDD有几个分区,就会被几个task进行处理。

image-20230331113349021

image-20230331113730560

简而言之就是,我算子多,窄依赖之间依靠内存迭代。就这两点速度就很快。

Spark并行度

image-20230331114141790

注意:并行度不是设定RDD分区,而是先设定task的数量,然后才根据并行度的需要,所以RDD被构造和task一样的分区数。总结:先有3个并行度,才有3个分区。一个分区只会被一个task去处理。一个task是可以处理多个RDD的。但是一个task只能处理一个RDD的一个分区的数据。

image-20230331114452511

全局并行度

image-20230331114608603

image-20230331114740032

image-20230331114806569

Spark任务调度

image-20230331115614635

总结:

image-20230331120119382

image-20230331120644158

image-20230331124043424

task1和task4,他两走的是网络(本地回环网络)。原因:虽然他两都在一台机器上,但是他两是在两个Executor上,而每个Executor是一个进程,进程和进程之间通讯是无法直接通过内存直接交流的。如果task1和task4是在两台不同的服务器上,那它两走的是交换机网络。

综上,一个服务器,开一个Executor就够了,开多了,他们还是走的网络交互,而不是内存。如果集群有100台服务器,那设置Executor参数的时候,设定为100就够了。

如果服务器1有16个CPU,那只要task有16个,那每个CPU都能用得上。

Task调度器

它主要监控任务的执行情况,定时做汇报的。相当于监工,DAG调度器相当于管家。

Spark运行概念名词解释和层级梳理

image-20230331144924370

总结

image-20230331145116519

Part-3 SparkSql

第一章

  • 目标

  1. 了解SparkSql框架的基础概念和发展历史

  2. 掌握SparkSQL DataFrame Api开发

  3. 掌握SparkSQL运行流程

  4. 掌握SparkSQL和hive集成

spark快速入门
  • 什么是SparkSQL

SparkSql是Spark用于处理结构化数据的一个Spark的模块。SparkSQL只适用于处理结构化数据,而RDD可以处理非结构化数据

 半结构化数据:日志文件
 结构化数据:数据库数据
 ​
 半结构化数据和结构化数据的区别就是有无 元数据 
 结构化数据里的表头,就是元数据

Spark应用对堆内存的计算效率比MapReduce对堆内存的计算效率快很多。MapReduce内存完全依赖于Java的垃圾回收机制。就是即使你给MapReduce大的内存,他也有可能没法充分利用。

sparkSQL概述
  • 为什么学

  1. SparkSQL本身十分优秀,支持SQL,性能强,可自动优化,API简单,兼容hive

  2. 企业大面积使用其处理业务数据(离线开发,数仓搭建,科学计算,数据分析)

  • 特点

  1. 融合性:可以无缝融合在代码中,随时使用SQL处理数据

  2. 统一数据访问:一套API可以读写不同数据源(csv,json,txt等)

  3. hive兼容:可以使用sparkSQL直接计算并生成hive数据表

  4. 标准化连接:可以通过jdbc或odbc连接各种各样的数据库进行数据交互

  • 发展历史 - 前身shark框架

2014年诞生

2016年成熟

2019年发布3.0版本

第二章

sparkSQL与hive异同和SparkSQL的数据抽象

image-20230315174514545

  • SparkSQL发展

DataFrame数据抽象

image-20230315175447725

image-20230315175019983

image-20230315174813971

image-20230315175152024

SparkSession执行入口环境

image-20230315175740153

构造出SparkSession这个环境入口对象以后,可以进行SparkSQL编程,也可以进行Rdd编程,后面统一用这个,不用SparkContext对象了

 # coding:utf8
 ​
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark = SparkSession. \
         builder.appName('create'). \
         master('local[*]'). \
         config('spark.sql.shuffle.partitions', 1). \
         getOrCreate()
     sc = spark.sparkContext
 ​
     df = spark.read.csv('../Data/Input/scores.csv', sep=',',header=False)
     df2=df.toDF('id','subject','score')
     df2.printSchema()
     df2.createTempView('scores')
 ​
     spark.sql('''
         select *
         from scores 
         where subject='math'
     ''').show()
     df2.show()

第三章 创建DataFrame

创建DataFrame的schema

DataFrame其实就是个二位表结构的数据,而Schema就是二维表结构的信息。这节说的就是我们创建这个表时要先指定这个表结构中的信息,即创建schema。

创建schema有两种方法:不管那种都要使用StructType()这个API

法一:直接通过add方法,一列一列添加

 df = StructType(). \
         add('colName1',StringType(),nullable=False). \
         add('colName2',IntegerType(),nullable=True). \
         ...

法二:通过StructField

 df = StructType([
   StructField('colName1', StringType(), True),
   StructField('colName2', DoubleType(), False),
   StructField('colName3', IntegerType(), False),
   ...
 ])

image-20230315204900946

利用Rdd的toDF()构建DataFrame

toDF()这个API的功能是,把rdd转成DataFrame。他有两种写法

  • 一种是传入一个完成的schema,schema中含有列名,列类型和列是否可以为空;

 rdd.toDF(['colName1', 'colName2', 'colName3'])
  • 一种是只传列名,列类型依据自动推断。

 schema = StructType()....
 rdd.toDF(schema)

image-20230315205433129

构建的DataFrame怎么再转成RDD

只需要将dataFrame.rdd即可转成rdd,后面再可以跟rdd的算子了。

image-20230817151219792

用pandas的DataFrame构建DataFrame

依靠spark.createDataFrame(PDF),其返回值是Spakr的DataFrame,其中PDF为pandas的DataFrame。

image-20230315205631817

读取text文件创建DataFrame

image-20230315210338676

读取json文件创建DataFrame

image-20230315210609355

读取csv文件创建DataFrame

image-20230315210927162

读取parquet文件创建DataFrame

image-20230315213359252

image-20230315213159089

从HDFS读文件创建DataFrame

HDFS上的文件都在hive上,读取HDFS文件,首先就要将Spark和Hive进行集成,其次由SparkSession的spark对象需要用.enableHiveSupport()这个API,然后写spark.sql('''sql语句''')即可。

 from pyspark.sql import SparkSession
 ​
 spark = SparkSession().Builder().appName('test').master('local[*]').enableHiveSupport().getOrCreate()
 ...
 hive_df=spark.sql('''
   select *
   from table2
 ''')
从数据库读数据创建DataFrame

利用数据库读到的表文件创建dataFrame,首先要将Spark和MySQL或Oracle进行集成,再利用spark的read的API

 db_df = spark.read.format('jdbc'). \
           option('url', 'jdbc:mysql://localhost:3306/DatabaseName?useSSL=false&useUnicode=true'). \
           option('user', 'root'). \
           option('password', '123456'). \
           option('dbtable', 'tableName'). \
           load()
将DataFrame注册是临时表
 df2.createTempView('scores')

SQL语法风格

image-20230315214723061

image-20230315215157563

WordCount案例 sql.functions

image-20230316093433774

image-20230316094559722

电影评分数据案例(DSL语法)
  • 构建入口环境

 if __name__ == '__main__':
   spark =  SparkSession.builder
     .appName('test')
     .master('local[*]')
     .config('spark.sql.shuffle.partitions',2) # local模式下没必要设置那么多分区,2个就可以了(默认200个)
     .getOrCreate()
   sc = spark.sparkContext

image-20230317145729497

  • 读取文件

 schema = StructType().add('user_id',StringType(),nullable=True)
   .add('movie_id',IntegerType(),nullable=False)
   .add('rank',IntegerType(),nullable=False)
   .add('ts',StringType(),nullable=False)
 df = spark.read.format('csv')
   .option('sep','\t')
   .option('header',False)
   .option('encoding','utf-8')
   .schema(schema=schema)
   .load('../data/input/sql/u.data')

  • 用户平均分

 df.groupBy('user_id')
   .avg('rank')
   .withColumnRenamed('avg(rank)','avg_rank')
   .withColumn('avg_rank',F.round('avg_rank',2))
   .orderBy('avg_rank',ascending=False)
   .show()

  • 电影的平均分查询

  • 查询大于平均分的电影的数量

  • 查询高分电影(>3)打分次数最多的用户,此人打分的平均分

  • 查询每个用户的平均打分,最低打分,最高打分

agg函数可以再里面一次完成多个聚合函数的编写

 df.groupBy('user_id').
   agg(
     F.round(F.avg('rank'),2).alias('avg_rank'),
     F.min('rank').alias('min_rank'),
     F.max('rank').alias('max_rank')
   ).show()
  • 查询评分超过100次的电影的平均分,排名前10

image-20230316165212944

image-20230317105943436

image-20230317113411371

shuffer 阶段分区参数设定

image-20230317113336132

image-20230317145743011

异常数据清洗相关的API
  • 数据去重

dropDuplicates

image-20230317145825561

image-20230317150815663

 df.dropDuplicates().show() # 无差别去重
 df.dropDuplicates(['age','job']).show() # 针对age和job两列去重

  • 缺失值去重

subset指定列

image-20230317151702553

 df.dropna().show() # 无差别删除,只要这行有空,就删除整行
 df.dropna(thresh=3).show() # 该行至少有3个有效列,否则删除该行
 df.dropna(thresh=2,subset=['name','age']).show() # 只对name和age这两列去重,并且有效列满足2列

  • 缺失值填充

image-20230317152229695

 df.fillna('data_lose').show() # 将空值无差别填充为data_lose
 df.fillna('N/A',subset=['job']).show() # 将job列的空值填充为'N/A'
 df.fillna({'name':'未知姓名','age':1,'job':'worker'}) # 指定每列空值的填充内容
DataFrame数据写出到本地、HDFS

image-20230317153114283

image-20230317171016021

 # coding:utf-8
 from pyspark.sql import SparkSession,functions as F
 from pyspark.sql.types import StructType, StringType
 import pymysql
 ​
 ​
 from pyspark import SparkConf
 ​
 if __name__ == '__main__':
     spark = SparkSession. \
         builder. \
         appName('test'). \
         master('local[*]'). \
         config('spark.sql.shuffle.partitions', 2). \
         getOrCreate()
     sc = spark.sparkContext
 ​
     # 先读csv文件构造一个df
     schema = StructType(). \
         add('user_id', StringType(), nullable=True). \
         add('subject', StringType(), nullable=True). \
         add('score', StringType(), nullable=True)
 ​
     df = spark.read.format('csv'). \
         option('sep', ','). \
         option('header', False). \
         option('encoding', 'utf-8'). \
         schema(schema=schema). \
         load('../Data/Input/scores.csv')
 ​
     # 以text格式写出到本地文件
     # 注意:text格式写出时只能以一列的写出,所以如果文件中本来有多列,要把这多列拼接起来
     df.select(F.concat_ws('--','user_id','subject','score')).\
         write.mode('overwrite'). \
         format('text'). \
         save('../Data/Input/write_text')
 ​
     # 以csv格式写出
     df.write.mode('overwrite').format('csv').\
         option('sep',';').\
         option('header',True).\
         save('../Data/Input/write_csv')
 ​
     # 以json格式写出
     df.write.mode('overwrite').format('json'). \
         save('../Data/Input/write_json')
 ​
     # 以parquet格式写出
     df.write.mode('overwrite').format('parquet'). \
         save('../Data/Input/write_parquet')

DataFrame读写到数据库(mysql)

image-20230317171445141

image-20230317171912248

image-20230317172700600

第四章

在Python中的spark sql中,只能定义UDF函数,不能定义UDAF和UDTF函数。如果非要用到UDAF和UDTF这两个函数,可以用MapPartitions算子和字典或数组类型来代替实现。

UDF创建演示

image-20230317172807031

image-20230317173051019

image-20230319121844618

image-20230319122408516

 if __name__ == '__main__':
   # 构建执行环境入口对象SparkSesssion
   spark = SparkSession.builder.
     appName('test').
     master('local[*]').
     config('spark.sql.shuffle.partitions',2).
     getOrCreate()
   sc = spark.sparkContext
   
   # 构建一个rdd
   rdd=sc.parallelize([1,2,3,4,5,6,7]).map(lambda x:[x])
   df= rdd.toDF(['num'])
   
   # TODO 1:方式1 sparksession.udf.register(), DSL和SQL风格都可以使用
   # UDF的处理函数
   def num_ride_10(num):
     return num*10
   
   udf2 = spark.register('udf1', num_ride_10, IntegerType())
   # 参数一:注册udf函数的名称,上文的udf1,仅适用于SQL风格,udf2适用于DSL风格
   # 参数二:udf的处理逻辑,是一个单独的方法
   # 参数三:udf的返回值类型,必须有
   # 返回值的对象(udf2),这是一个udf对象,仅使用于DSL风格
   
   # SQL风格中的使用
   df.selectExpr('udf1(num)').show()
   
   # DSL风格中的使用
   # 返回值udf对象(即:本文中的udf2)作为方法使用,它里面传入的参数必须是一个Column对象,但它返回后的列名还是你当时register时的列名(即:udf1)
   df.select(udf2(df['num'])).show()
   
   # TODO 2:方式2注册,仅适用于DSL风格
   # 通过导入的functions包中的udf()这个API
   udf3 = F.udf(num_ride_10, IntegerType())
   df.select(udf3(df['num'])).show() # DSL风
   
   df.selectExpr('udf3(num)').show() # SQL风
注册返回值是数组类型的UDF

image-20230319124255764

image-20230320095923996

 if __name__ == '__main__':
   spark = SparkSession.builder
     .appName('test')
     .master('local[*]')
     .config('spark.sql.shuffle.partitions',2)
     .getOrCreate()
   sc = spark.sparkContext
   
   # 构建RDD
   rdd = sc.paralize(['hadoop spark flink'],['hadoop spark flink'])
   df = rdd.toDF('line')
   
   # 注册UDF
   def splie_line(data):
     return data.split(' ')
   # TODO 1
   udf2 = spark.udf.register('udf1', split_line, ArrayType(StringType())) # ArrayType里面的数据是什么类型的,里面还要再写需要参数的类型,这里是字符串类型
   
   # DLS风格
   df.select(udf2(df['line'])).show()
   
   # SQL风格
   df.createTepmView('lines')
   spark.sql('select udf1(line) from lines').show(truncate=False) # 表示不截断显示,有多长展示多长
   
   # TODO 2
   udf3 = F.udf(split_line, ArrayType(StringType()))
   df.select(udf3(df['line'])).show(truncate=False)

返回字典类型的UDF定义

字典类型的返回值,没法直接用register来定义,它的源码里面没有定义字典类型,因此可以用StructType来接收。

image-20230320104221467

image-20230320104201100

 if __name__ = '__main__':
   spark = SparkSession.builder
     .appName('test')
     .master('local[*]')
     .config('spark.sql.shuffle.partitions',2)
     .getOrCreate()
   sc = spark.sparkContext
   
   # 需求:传入1,返回{'num':1, 'letters':'a'}
   rdd = sc.parallelize([1],[2],[3])
   df = rdd.toDF(['num'])
   
   def process(data):
     return {'num':data, 'letters':string.ascii_letters[data]}
   
   udf1 = spark.udf.register('udf1', process, StructType().add('num', IntegerType(), nullable=True)
                     .add('letters', StringType(), nullable=True))
   
   df.selectExpr('udf1(num)').show()
   df.select(udf1(df['num'])).show()

通过RDD代码模拟UDAF效果

image-20230320110408306

 if __name__ = '__main__':
   spark = SparkSession.builder
     .appName('test')
     .master('local[*]')
     .config('spark.sql.shuffle.partitions',2)
     .getOrCreate()
   sc = spark.sparkContext
   
   rdd = sc.parallelize([1,2,3,4,5], 3)
   df = rdd.map(lambda x:[x]).toDF(['num'])
   
   # 要在Python中实现UDAF的效果,可以使用rdd中的mapPartitions算子完成聚合操作
   rdd1 = df.rdd.repartition(1) # 使用mapPartitions算子只能使用单分区
   
   def process(iter):
     sum = 0
     for row in iter:
       sum += row['num']
     return [sum] # 一定要嵌套list,因为mapPartitions方法需求的返回值是list对象
   
   print(rdd1.mapPartitions(process).collect())
窗口函数的演示

image-20230320110429098

image-20230320142119630

image-20230320142042760

 if __name__ = '__main__':
   spark = SparkSession.builder
     .appName('test')
     .master('local[*]')
     .config('spark.sql.shuffle.partitions',2)
     .getOrCreate()
   sc = spark.sparkContext
   
   rdd = sc.parallelize([
     ('zhangsan','class_1',99),
     ('lisi','class_1',100),
     ('wangwu','class_2',80),
     ('zhaoliu','class_2',70),
     ('maqi','class_3',60
     ('yangba','class_3',55)
   ])
     
   schema = StructType()
     .add('name', StringType())
     .add('class', StringType())
     .add('score', IntegerType())
   df = rdd.toDF(schema)
     
   df.createTempView('stu')
   
   # 聚合窗口的演示
   spark.sql(r'''
     select *,AVG(score) as avg_score
     from stu
   ''').show()
   
   # 排序相关的窗口函数计算
   spark.sql(r'''
     select *,
       row_number() over(order by score desc) as rn,
       dense_rank() over(partition by class order by score desc) as dr,
       rank() over(order by score) as r
     from stu
   ''').show()
     
   # 均分函数 ntile
   spark.sql(r'''
     select *,ntile(6) over(order by score desc) as n
     from stu
   ''').show()

总结

image-20230320142411423

第五章

Catalyst优化器

SparkRDD的执行流程回顾

image-20230322142142424

SparkSQL自动优化

image-20230322142220185

  • 原因

image-20230322142326637

Catalyst优化器

image-20230322142439831

image-20230322143238507

image-20230322143810440

image-20230322151204913

image-20230322152853191

断言下推就是把行变少

列值裁剪就是把列变少

SparkSql执行流程

SparkSQL执行流程

image-20230322164617785

第六章

SparkOnHive原理及配置
原理

image-20230322170433545

image-20230322170457618

配置

image-20230322170558693

  • 步骤一:

image-20230322170718837

  • 步骤二:

image-20230322171119931

  • 步骤四:

image-20230323095648756

在hive中集成

image-20230323100105397

这个student表在hive中,只要把上面的hive配置好了,就可以直接使用spark.sql来查。

第七章

分布式SQL的执行引擎原理和配置
  • 概念

image-20230323101336296

image-20230323101419672

  • 客户端连接工具

image-20230323101459841

  • 代码JDBC连接

image-20230323102602093

image-20230323102815069

part-4 案例

写一个Spark需求的步骤

  • 指定编码

 #coding:utf-8
  • 导包

 from pyspark.sql import functions as F,SparkSession
 from pyspark.sql.types import StructType,DoubleType,IntegerType
  • main方法中构建spark入口对象

 if __name__ == '__main__':
     ...
     spark = SparkSession. \
         builder.appName('create'). \
         master('local[*]'). \
         config('spark.sql.shuffle.partitions', 1). \
         getOrCreate()
     sc = spark.sparkContext
  • 如果在集群运行,main方法中指定集群参数(本地则略过该步骤)

 if __name__ == '__main__':
   
     ...(构建spark,sc对象)
     
     # 初始化数据库
     spark.sql('use ${database}')
     # 设置日志级别
     sc.setLogLevel('WARN')
 ​
     spark.sql(r'''set hive.merge.mapfiles=true''')
     spark.sql(r'''set hive.merge.mapredfiles=true''')
     spark.sql(r'''set hive.merge.size.per.task=512000000''')
     spark.sql(r'''set hive.merge.smallfiles.avgsize=512000000''')
     spark.sql(r'''set hive.exec.dynamic.partition.mode=nonstrict''')
     spark.conf.set("spark.sql.crossJoin.enabled", "true")
     
     main() # 业务逻辑
  • 读取数据,根据spark创建Rdd对象或DataFrame对象

    • Rdd可以直接通过sc对象读取得来,也可由spark对象得来。

    • DataFrame对象可以从本地文件(spark.read)读取,可以从HDFS(spark.sql)读取,也可有rdd((rdd.toDF)转换得来,也可有pandas(spark.createDataFrame)获得

  • 根据具体业务场景,写对应业务逻辑

    • main()方法中写业务逻辑

    • 保存数据

  • main方法中关闭spark对象

 spark.stop()

spark集群中参数解读

     # 初始化数据库
     spark.sql('use ${database}') # 这段代码是在 Spark SQL 中切换当前使用的数据库,其语法为:spark.sql('USE database_name')。
     # 需要注意的是,使用该语句切换数据库时,需要确保指定的数据库已经存在,并且用户拥有访问该数据库的权限。如果指定的数据库不存在或用户没有访问权限,将会抛出异常。
 ​
     # 设置日志级别
     sc.setLogLevel('WARN')
     # 这段代码是用来设置 Spark 日志级别的。Spark 中的日志级别分为五个级别,分别是:
     # ALL:最低级别,记录所有日志信息;
     # DEBUG:记录详细的调试信息;
     # INFO:记录常规的信息,如启动信息等;
     # WARN:记录警告信息,表示出现了一些问题,但不会影响程序的正常运行;
     # ERROR:记录错误信息,表示出现了一些错误,程序可能无法正常运行。
 ​
     spark.sql(r'''set hive.merge.mapfiles=true''') 
     # 启用小文件合并功能,将多个小文件合并为一个大文件,从而减少查询时的文件数量,提高查询性能。
     
     spark.sql(r'''set hive.merge.mapredfiles=true''')
     # 这个参数的作用与上一个参数类似,都是启用小文件合并功能,将多个小文件合并为一个大文件,从而提高查询性能。不同的是,这个参数使用的是 MapReduce 的方式进行文件合并
     
     
     spark.sql(r'''set hive.merge.size.per.task=512000000''')
     # 这个参数用于控制小文件合并的最大文件大小,以字节为单位。
     # 如果设置的 hive.merge.size.per.task 值较小,则 Hive 将更倾向于将多个小文件合并为一个大文件,从而减少文件数量;如果设置的值较大,则 Hive 可能会保留更多的小文件,以避免产生过大的中间结果文件。一般来说,该参数的设置需要根据具体的数据量和计算资源进行调整。
     
     spark.sql(r'''set hive.merge.smallfiles.avgsize=512000000''')
     # 这个参数用于控制小文件合并的平均文件大小,以字节为单位。
     
     spark.sql(r'''set hive.exec.dynamic.partition.mode=nonstrict''')
     # 这个参数用于控制 Hive 动态分区功能的模式。
     # 在 Hive 中,动态分区是指在 INSERT INTO 语句中,根据数据中的某些列动态地生成分区,并将数据插入到对应的分区中。动态分区可以大大简化分区管理的工作,同时也能提高查询性能。主要有以下几种:
     
     # strict:严格模式。在这种模式下,必须使用静态分区或者完整的分区列表才能进行插入操作,否则会报错。
     # nonstrict:非严格模式。在这种模式下,可以使用静态分区或者部分分区列表进行插入操作。如果插入的数据中包含了没有指定分区的字段,Hive 会自动根据数据中的字段值生成对应的分区。
     # strict_all:严格全部模式。在这种模式下,必须使用完整的分区列表才能进行插入操作,否则会报错。
     
     spark.conf.set("spark.sql.crossJoin.enabled", "true")
     # 这个参数用于控制 Spark SQL 中是否允许进行笛卡尔积(Cross Join)操作。笛卡尔积是一种常见的数据处理操作,它可以将两个表中的每一行都和另一个表中的每一行组合,从而生成一个新的表。但是,笛卡尔积操作的开销较大,可能会导致性能问题,因此在默认情况下,Spark SQL 是禁止进行笛卡尔积操作的。
     # 需要注意的是,在进行笛卡尔积操作时,需要特别小心,避免由于数据量过大导致的性能问题或内存溢出问题。

案例背景

image-20230323103458439

image-20230323103524937

image-20230323103637809

  • 原始数据格式

image-20230323105039219

  • 第一步:准备数据

image-20230323105121223

需求1开发

image-20230323110037024

 # coding:utf-8
 ​
 from pyspark.sql import SparkSession
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder.appName('test').master('local[*]').getOrCreate()
     df = spark.read.format('json').load('../../Data/Input/mini.json')
 ​
     df.createTempView('details')
 ​
     spark.sql('''
         select storeProvince,sum(receivable) as money
         from details
         where receivable < 10000 and storeProvince is not null
         group by storeProvince 
         order by sum(receivable) desc
     ''').show()
     
 >>>
 +--------------+------------------+
 | storeProvince|             money|
 +--------------+------------------+
 |        广东省|     1713207.92334|
 |        湖南省|1701303.5340000002|
 |广西壮族自治区|          37828.22|
 |        北京市|10926.909999999998|
 |        上海市|            7358.5|
 |        江苏省|            6357.9|
 |        浙江省|            4568.1|
 |        山东省|             664.0|
 |        江西省|             553.5|
 |          null|              20.0|
 +--------------+------------------+

需求2开发

image-20230323112224468

需求3开发

image-20230323112654117

需求4开发

image-20230323141849424

 # coding:utf8
 from pyspark.sql import SparkSession
 from pyspark.sql import functions as F
 from pyspark.sql.types import StringType
 from pyspark.storagelevel import StorageLevel
 ​
 '''
 receivable:订单金额
 storeProvince:店铺省份
 dataTs:订单销售日期
 payType:支付类型
 storeID:店铺ID
 '''
 ​
 # 准备数据
 ​
 if __name__ == '__main__':
     spark = SparkSession.builder. \
         appName('SparkSql_sale'). \
         master('local[*]'). \
         config('spark.sql.shuffle.partitions', '2'). \
         enableHiveSupport(). \
         getOrCreate()
 ​
     # 读取数据
     # 过滤省份字典中空值的该行数据
     # 过滤省份字段中,内容为‘null’的
     # 过滤订单金额大于10000的
     # 列值裁剪
     df = spark.read.format('json').load('../../data/input/mini.json'). \
         dropna(thresh=1, subset=['storeProvince']). \
         filter('storeProvince != "null"'). \
         filter('receivable < 10000'). \
         select('storeProvince', 'storeID', 'receivable', 'dateTS', 'payType')
 ​
     # TODO:需求一:每个省份的销售额统计
     province_sale_df = df.groupby('storeProvince').sum('receivable'). \
         withColumnRename('sum(receivable)', 'money'). \
         withColumn('money', F.round('money', 2)). \
         orderBy('money', ascending=False)
     province_sale_df.show()
 ​
     # 写入MySQL
     province_sale_df.write.mode('overwrite'). \
         format('jdbc'). \
         option('url', 'jdbc://node1:3306/bigdata?useSSL=false&useUnicode=true&characterEncoding=utf8'). \
         option('dbtable', 'province_sale'). \
         option('user', 'root'). \
         option('password', '123456'). \
         option('encoding', 'utf-8'). \
         save()
     # 写入Hive
     province_sale_df.write.mode('overwrite'). \
         saveAsTable('default.province_save', 'parquet')
 ​
     # TODO:需求二:销售额前3的省份中,有多少销售额到达日销售额1000+
     top3_province_df = province_sale_df.limit(3).select('storeProvince'). \
         withColumnRename('storeProvince', 'top3_Province')
     top3_province_df_joined = df.join(top3_province_df, df['storeProvince'] == top3_province_df['top3_Province'],
                                       'inner')
     top3_province_df_joined.persist(StorageLevel.MEMORY_AND_DISK)
 ​
     province_hot_store_cnt = top3_province_df_joined.groupBy('storeProvince', 'storeID',
                                                              F.from_unixtime(df['dateTS'].substr(0, 10),
                                                                              'yyyy-MM-dd').alias('day')). \
         sum('receivable').withColumnRename('sum(receivable)', 'money'). \
         filter('money>1000'). \
         dropDuplicates(subset=['storeID']). \
         groupBy('storeProvince').count()
 ​
     # 写出MySQL,省略
     # 写出hive,省略
 ​
     # TODO:需求三:销售额前3的省份中,各省的平均订单价格
     top3_province_order_df = top3_province_df_joined.groupBy('storeProvince'). \
         avg('receivable'). \
         withColumnRename('avg(receivable)', 'money'). \
         withColumn('money', F.round('money', 2)). \
         orderBy('money', ascending=False)
     top3_province_order_df.show(truncate=False)
     # 写出MySQL,省略
     # 写出hive,省略
 ​
     # TODO:需求四:销售额前3的省份中,各省支付方式的比例
     # 用SQL方式实现
     top3_province_df_joined.createTempView('province_pay')
 ​
     def udf_concat(percent):
         return str(round(percent * 100, 2)) + '%'
     my_udf = F.udf(udf_concat,StringType())
 ​
     pay_type = spark.sql('''
         select storeProvince,payType,(count(payType)/total) as percent from
         (SELECT storeProvince,payType,count(1) over(PARTITION BY storeProvince) as total
         FROM province_pay) as c
         group by storeProvince,payType,total
     ''').withColumn('percent',my_udf('percent'))
 ​
     pay_type.show()
     # 写出MySQL,省略
     # 写出hive,省略

Part-5 Spark新特性及核心回顾

spark里面也是有Map和Reduce过程的,但并不是Map和Reduce框架,而是指Map阶段和

Reduce阶段。并且shuffle(磁盘交互)过程是无时无刻都存在的。shuffle过程是造成数据慢的主要原因。在spark中有两种shuffle,一种是HashShuffle,另一种是SortShuffle。

HashShuffleManager

image-20230326115655338

在Map端,每个分区的数据先按HASH进行分组到内存中,然后再由内存落盘到磁盘上,再由磁盘文件通过网络,按照hash分组的原则,找到下一阶段对应的Reduce端,将数据发送到Redue端的内存上。

image-20230326115930770

下图是HashShuffle优化后的逻辑

image-20230326120403456

优化逻辑:先在map端的内存中,将不同task端(分区)的数据先按Hash的原则,将相同内容的数据合并。结果就是在给Reduce端进行网络传输的文件数量(即网络IO的次数)大大减少了。

SortShuffleManager

image-20230818145213278

image-20230326124012201

总结两种shuffle

image-20230326123828948

3.0新特性-AOE

image-20230326140806572

image-20230326141924678

image-20230326141842537

image-20230326133447148

image-20230326133514704

新特性-动态分区裁剪 (DPP)

这个不需要我们做什么操作,只是在执行的过程中,系统自动识别,如果满足执行动态分区裁剪的条件,系统就会自动执行。

image-20230326205940594

新特性-koalas库

实际用到的Spark代码解读

取数

 #!/usr/bin/env python
 # encoding: ${encoding}
 # @Time: 2020/7/22 11:24
 # @Auther: Jack
 from pyspark.sql import SparkSession
 import math
 ​
 # r'''...''' 是 Python 中的原始字符串表示法,可以防止字符串中的转义字符被解释。
 ​
 def main():
     spark.sql(r'''
 create table tab_comm_user_yj_fb_202302 as
 select  * from tab_comm_user_yj_fb where month = 202302
     ''')
 # 这一步相当于建临时视图
 ​
 if __name__ == '__main__':
     # 初始化spark
     spark = SparkSession.Builder()
       .appName('spark_qs')
       .enableHiveSupport() # 设置 Hive 参数需要并开启 Hive 支持
       .getOrCreate()
     sc = spark.sparkContext
     # 这段代码是在 Spark 中创建一个 SparkContext 对象,它是连接 SparkRDD编程入口点。在 Spark 2.x 版本及以上,可以通过 SparkSession 对象直接访问 SparkContext
     
     # 初始化数据库
     spark.sql('use ${database}') # 这段代码是在 Spark SQL 中切换当前使用的数据库,其语法为:spark.sql('USE database_name')。
     # 需要注意的是,使用该语句切换数据库时,需要确保指定的数据库已经存在,并且用户拥有访问该数据库的权限。如果指定的数据库不存在或用户没有访问权限,将会抛出异常。
 ​
     # 设置日志级别
     sc.setLogLevel('WARN')
     # 这段代码是用来设置 Spark 日志级别的。Spark 中的日志级别分为五个级别,分别是:
     # ALL:最低级别,记录所有日志信息;
     # DEBUG:记录详细的调试信息;
     # INFO:记录常规的信息,如启动信息等;
     # WARN:记录警告信息,表示出现了一些问题,但不会影响程序的正常运行;
     # ERROR:记录错误信息,表示出现了一些错误,程序可能无法正常运行。
 ​
     spark.sql(r'''set hive.merge.mapfiles=true''') 
     # 启用小文件合并功能,将多个小文件合并为一个大文件,从而减少查询时的文件数量,提高查询性能。
     
     spark.sql(r'''set hive.merge.mapredfiles=true''')
     # 这个参数的作用与上一个参数类似,都是启用小文件合并功能,将多个小文件合并为一个大文件,从而提高查询性能。不同的是,这个参数使用的是 MapReduce 的方式进行文件合并
     
     
     spark.sql(r'''set hive.merge.size.per.task=512000000''')
     # 这个参数用于控制小文件合并的最大文件大小,以字节为单位。
     # 如果设置的 hive.merge.size.per.task 值较小,则 Hive 将更倾向于将多个小文件合并为一个大文件,从而减少文件数量;如果设置的值较大,则 Hive 可能会保留更多的小文件,以避免产生过大的中间结果文件。一般来说,该参数的设置需要根据具体的数据量和计算资源进行调整。
     
     spark.sql(r'''set hive.merge.smallfiles.avgsize=512000000''')
     # 这个参数用于控制小文件合并的平均文件大小,以字节为单位。
     
     spark.sql(r'''set hive.exec.dynamic.partition.mode=nonstrict''')
     # 这个参数用于控制 Hive 动态分区功能的模式。
     # 在 Hive 中,动态分区是指在 INSERT INTO 语句中,根据数据中的某些列动态地生成分区,并将数据插入到对应的分区中。动态分区可以大大简化分区管理的工作,同时也能提高查询性能。主要有以下几种:
     
     # strict:严格模式。在这种模式下,必须使用静态分区或者完整的分区列表才能进行插入操作,否则会报错。
     # nonstrict:非严格模式。在这种模式下,可以使用静态分区或者部分分区列表进行插入操作。如果插入的数据中包含了没有指定分区的字段,Hive 会自动根据数据中的字段值生成对应的分区。
     # strict_all:严格全部模式。在这种模式下,必须使用完整的分区列表才能进行插入操作,否则会报错。
     
     spark.conf.set("spark.sql.crossJoin.enabled", "true")
     # 这个参数用于控制 Spark SQL 中是否允许进行笛卡尔积(Cross Join)操作。笛卡尔积是一种常见的数据处理操作,它可以将两个表中的每一行都和另一个表中的每一行组合,从而生成一个新的表。但是,笛卡尔积操作的开销较大,可能会导致性能问题,因此在默认情况下,Spark SQL 是禁止进行笛卡尔积操作的。
     # 需要注意的是,在进行笛卡尔积操作时,需要特别小心,避免由于数据量过大导致的性能问题或内存溢出问题。
     
     main()
 ​
     spark.stop()
     # 用于停止SparkContext,即访问Spark功能的入口点。当调用spark.stop()方法时,它会停止所有的Spark执行器并释放SparkContext所使用的所有资源。
     # 如果不调用该方法可能会有以下问题:
     '''
     1.Spark应用程序会一直运行,而不会停止。这将导致占用系统资源,可能会影响其他应用程序的性能。
     2.SparkContext占用的资源(例如内存)将不会被释放,这可能会导致内存泄漏和资源浪费。
     3.如果尝试重新创建SparkContext,可能会出现错误,因为之前的SparkContext并未完全停止。
     '''
 ​

高携出小区落边界

 #!/usr/bin/env python
 # encoding: ${encoding}
 # @Time: 2020/7/22 11:24
 # @Auther: Jack
 from pyspark.sql import SparkSession
 import math
 ​
 x_pi = 3.14159265358979324 * 3000.0 / 180.0
 pi = 3.1415926535897932384626  # π
 a = 6378245.0  # 长半轴
 ee = 0.00669342162296594323  # 偏心率平方
 ​
 ​
 # String, Int, Double, Double, Double, Double, Array[Double], Array[Double]
 def initPolygon(row):
     shape = str(row[1]).strip()
     if shape[-1:] == ';':
         shape = shape[:-1]
     # 这段代码用于判断字符串shape的最后一个字符是否为分号(;),如果是,则通过切片操作去掉该字符。
     # 具体来说,shape[-1:]表示取字符串shape的最后一个字符,shape[:-1]表示取字符串shape的第一个字符到倒数第二个字符(不包括最后一个字符),即去掉最后一个字符。   
     
     polyBounds = shape.split('\n')[0].split(';')
     # 该代码首先使用split('\n')方法将多边形边界数据按照换行符分割成多个行(列表中的元素),然后使用[0]取第一个行(多边形的轮廓线),最后使用split(';')方法将轮廓线按照分号分割成多个点(列表中的元素)。
 ​
     pointNum = len(polyBounds)
     pointLng = [0] * (pointNum + 1)
     # 该代码使用了列表复制的技巧,即使用[0] * n的方式来初始化一个长度为n的列表,其中每个元素的值都是0。在这里,pointNum+1表示需要初始化的列表的长度,该代码的作用是为存储多边形各个点的经度坐标初始化一个列表,并将该列表的每个元素初始化为0
     pointLat = [0] * (pointNum + 1)
     for i in range(0, pointNum):
         temp = polyBounds[i].split(',')
         
         pointLng[i] = float(temp[0])
         pointLat[i] = float(temp[1])
         
 ​
     pointLng[pointNum] = pointLng[0]
     pointLat[pointNum] = pointLat[0]
     # 将列表pointLng和pointLat中的最后一个元素设置为第一个元素,以便将多边形闭合。
     
     return [str(row[0]), pointNum + 1, float(max(pointLng)), float(min(pointLng)), float(max(pointLat)),float(min(pointLat)), pointLng, pointLat]
     # 将多边形的相关信息以列表形式返回,包括多边形的ID、点的数量、经度最大值、最小值、纬度最大值、最小值以及点的经度坐标和纬度坐标。
 ​
 # PNPoly算法实现
 def pointInPolygon(points, lng, lat, boundsLng, boundsLat):
     flag = False
     j = 0
     for i in range(0, points):
         if i == 0:
             j = points - 1
         else:
             j = i - 1
 ​
         if (((boundsLat[i] > lat) != (boundsLat[j] > lat)) and (
                 lng < (boundsLng[j] - boundsLng[i]) * (lat - boundsLat[i]) /
                 (boundsLat[j] - boundsLat[i]) + boundsLng[i])):
             flag = not flag
     return flag
 ​
 ​
 # 主要计算函数,使用的poly数据结构为
 # poly: Array[(String, Int, Double, Double, Double, Double, Array[Double], Array[Double])],
 # row: (String, Double, Double)
 def compute(poly, row):
     result = []
     for i in range(0, len(poly)):
         # if float(str(poly[i][3])) <= float(str(row[1])) <= float(str(poly[i][2])) and float(str(poly[i][5])) <= float(str(row[2])) <= float(str(poly[i][4])):
         if poly[i][3] <= row[1] <= poly[i][2] and poly[i][5] <= row[2] <= poly[i][4]:
             if pointInPolygon(poly[i][1], row[1], row[2], poly[i][6], poly[i][7]):
                 result.append(row[0] + '|' + poly[i][0])
     return result
 ​
 ​
 def bx_zsmr_region2grid_200_d():
     # 获取经纬度边界,这个地方放高携出小区的经纬度
     polygon_df = spark.sql(r'''                         
     select REGION_FLAG as id,ORIGIN_LAT_LON as shape from qs_wgg_gxc_yjsp where ORIGIN_LAT_LON is not null
     ''')
     print('小区边界读取完毕')
     polygon_df = sc.broadcast(polygon_df.rdd.map(lambda x: initPolygon(x)).collect())
     print('小区边界读取完毕1')
     # 获取经纬度数据 要求经纬度为数字格式并不为空
     # 这里放沿街商铺的表,要把表抽到hive库,因为这个程序是在HIVe库跑
     print('开始经纬度读取完毕')
 ​
     grid_df = spark.sql(r'''
       select  custid as id,cast(longitude as double) as lng,cast(latitude as double) as lat
       from qs_wg_ql_20221227 
       where CUSTID is not null
       and longitude is not null 
       and latitude is not null                  
         ''').repartition(1000)
     print('经纬度读取完毕')
 ​
     res_rdd = grid_df.rdd.flatMap(lambda x: compute(polygon_df.value, x)).map(
         lambda x: [x.split('|')[0], x.split('|')[1]])
     # 这行代码的作用是对经纬度数据表中的每个记录计算其所属的小区边界,并将结果转化为一个RDD,其中每个元素都是一个列表,包含两个元素:第一个元素是经纬度记录的id,第二个元素是其所属的小区边界的id。具体来说,这行代码首先调用了grid_df的rdd方法将数据转化为RDD,然后使用flatMap方法对每个经纬度记录调用compute函数,计算其所属的小区边界,最后使用map方法将结果转化为所需格式的RDD。
     
     res_df = spark.createDataFrame(res_rdd, ['id', 'region_id']).repartition(100)
     # 这一行代码将上一行代码生成的RDD转换为DataFrame,其中'id'和'region_id'分别为DataFrame的两列,并将数据分为100个分区(即100个文件)。
     
     res_df.createOrReplaceTempView('temp_changjing_result') 
     # 这行代码将DataFrame注册为一个临时表,可以在Spark SQL中使用该表进行后续的查询和操作。在这里,临时表的名称为 temp_changjing_result。
 ​
     print('step result_1 is OK')
 ​
     spark.sql(r'''drop table if exists temp_changjing_${taskid}_result''')
 ​
     spark.sql(r'''
     create table temp_changjing_${taskid}_result
     select *
     from temp_changjing_result
     ''')
     # 这段代码是创建一个名为temp_changjing_${taskid}_result的Hive表,并将temp_changjing_result中的数据插入到该表中。这段代码的作用是将前面处理得到的结果保存到Hive表中,以便后续使用或查询。
 ​
 def main():
     bx_zsmr_region2grid_200_d()
 # main函数是程序的主入口,通常会调用其他函数或模块来完成具体的任务,是程序的逻辑枢纽。在这段代码中,main函数只调用了一个函数bx_zsmr_region2grid_200_d()来执行具体的计算任务,其作用是将小区边界和经纬度数据读入,并计算经纬度数据所在的小区。
 ​
 ​
 if __name__ == '__main__':
     # 初始化spark
     spark = SparkSession.Builder().appName('jwd_bj_qs_wgg_gxc_yjsp').enableHiveSupport().getOrCreate()
     sc = spark.sparkContext
     # 初始化数据库
     spark.sql('use ${database}')
     # 设置日志级别
     sc.setLogLevel('WARN')
 ​
     spark.sql(r'''set hive.merge.mapfiles=true''')
     spark.sql(r'''set hive.merge.mapredfiles=true''')
     spark.sql(r'''set hive.merge.size.per.task=512000000''')
     spark.sql(r'''set hive.merge.smallfiles.avgsize=512000000''')
     spark.sql(r'''set hive.exec.dynamic.partition.mode=nonstrict''')
     spark.conf.set("spark.sql.crossJoin.enabled", "true")
 ​
     main()
 ​
     spark.stop()
 ​

百度经纬度转WGS84经纬度

 #!/usr/bin/env python
 # encoding: ${encoding}
 # @Time: 2023/3/14 11:22
 # @Auther: Dahua
 ​
 # -*- coding: utf-8 -*-
 from pyspark.sql import SparkSession
 from pyspark.sql.types import StructType, StructField, StringType, DoubleType
 import json
 import urllib
 import math
 # import numpy as np
 ​
 x_pi = 3.14159265358979324 * 3000.0 / 180.0
 pi = 3.1415926535897932384626  # π
 a = 6378245.0  # 长半轴
 ee = 0.00669342162296594323  # 偏心率平方
 ​
 ​
 '''
 输入(经度,维度)
 '''
 def bd09_to_gcj02(bd_lon, bd_lat):
     """
     百度坐标系(BD-09)转火星坐标系(GCJ-02)
     百度——>谷歌、高德
     :param bd_lat:百度坐标纬度
     :param bd_lon:百度坐标经度
     :return:转换后的坐标列表形式
     """
     x = bd_lon - 0.0065
     y = bd_lat - 0.006
     z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi)
     theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
     gg_lng = z * math.cos(theta)
     gg_lat = z * math.sin(theta)
     return [gg_lng, gg_lat]
 def gcj02_to_wgs84(lng, lat):
     """
     GCJ02(火星坐标系)转GPS84
     :param lng:火星坐标系的经度
     :param lat:火星坐标系纬度
     :return:
     """
     if out_of_china(lng, lat):
         return [lng, lat]
     dlat = _transformlat(lng - 105.0, lat - 35.0)
     dlng = _transformlng(lng - 105.0, lat - 35.0)
     radlat = lat / 180.0 * pi
     magic = math.sin(radlat)
     magic = 1 - ee * magic * magic
     sqrtmagic = math.sqrt(magic)
     dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
     dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
     mglat = lat + dlat
     mglng = lng + dlng
     return [lng * 2 - mglng, lat * 2 - mglat]
 ​
 def gcj02_to_bd09(lng, lat):
     """
     火星坐标系(GCJ-02)转百度坐标系(BD-09)
     谷歌、高德——>百度
     :param lng:火星坐标经度
     :param lat:火星坐标纬度
     :return:
     """
     z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_pi)
     theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi)
     bd_lng = z * math.cos(theta) + 0.0065
     bd_lat = z * math.sin(theta) + 0.006
     return [bd_lng, bd_lat]
 def wgs84_to_gcj02(lng, lat):
     """
     WGS84转GCJ02(火星坐标系)
     :param lng:WGS84坐标系的经度
     :param lat:WGS84坐标系的纬度
     :return:
     """
     if out_of_china(lng, lat):  # 判断是否在国内
         return [lng, lat]
     dlat = _transformlat(lng - 105.0, lat - 35.0)
     dlng = _transformlng(lng - 105.0, lat - 35.0)
     radlat = lat / 180.0 * pi
     magic = math.sin(radlat)
     magic = 1 - ee * magic * magic
     sqrtmagic = math.sqrt(magic)
     dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
     dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
     mglat = lat + dlat
     mglng = lng + dlng
     return [mglng, mglat]
 def wgs84_to_bd09(lon, lat):
     lon, lat = wgs84_to_gcj02(lon, lat)
     return gcj02_to_bd09(lon, lat)
 ​
 def out_of_china(lng, lat):
     """
     判断是否在国内,不在国内不做偏移
     :param lng:
     :param lat:
     :return:
     """
     return not (lng > 73.66 and lng < 135.05 and lat > 3.86 and lat < 53.55)
 ​
 def _transformlng(lng, lat):
     ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \
           0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng))
     ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
             math.sin(2.0 * lng * pi)) * 2.0 / 3.0
     ret += (20.0 * math.sin(lng * pi) + 40.0 *
             math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
     ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 *
             math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
     return ret
 def _transformlat(lng, lat):
     ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + \
           0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng))
     ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
             math.sin(2.0 * lng * pi)) * 2.0 / 3.0
     ret += (20.0 * math.sin(lat * pi) + 40.0 *
             math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
     ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 *
             math.sin(lat * pi / 30.0)) * 2.0 / 3.0
     return ret
   
 def bd09_to_wgs84(row):
     lon, lat = bd09_to_gcj02(row[1],row[2])
     lonwgs,latwgs=gcj02_to_wgs84(lon, lat)
     return [row[0],row[1],row[2],row[3],row[4],row[5],row[6],row[7],row[8],row[9],row[10],lonwgs,latwgs]   
 ​
 def main():
     grid_df = spark.sql(r'''
            select         
              zydmc,
               cast(bdjd as double) as bdjd,
               cast(bdwe as double) as bdwe,
               sfjzldtkcj,
               tkcjmc,
               fgs,
               wg,
               jd,
               large_category,
               medium_category,
               small_category
           from 
               temp_zyd_jjl_20230228
             ''').repartition(1000).cache() # DataFrame对象将被缓存到内存中,以便在后续操作中更快地访问数据。
 ​
     res_rdd = grid_df.rdd.map(lambda row : bd09_to_wgs84(row))
     
     schema = StructType([
     StructField("zydmc", StringType(), True),
     StructField("bdjd", DoubleType(), True),
     StructField("bdwe", DoubleType(), True),
     StructField("sfjzldtkcj", StringType(), True),
     StructField("tkcjmc", StringType(), True),
     StructField("fgs", StringType(), True),
     StructField("wg", StringType(), True),
     StructField("jd", StringType(), True),
     StructField("large_category", StringType(), True),
     StructField("medium_category", StringType(), True),
     StructField("small_category", StringType(), True),
     StructField("lonwgs", DoubleType(), True),
     StructField("latwgs", DoubleType(), True)
     ])
     
     res_df = spark.createDataFrame(res_rdd, schema)
     # 该方法用来创建DataFrame对象,res_rdd表示输入数据,schema是DataFrame的模式,如果不指定,则会尝试自动推断。
     res_df.coalesce(10).write.mode("overwrite").saveAsTable("temp_zyd_jjl_20230228_1")
 ​
 if __name__ == '__main__':
     # 初始化spark
     spark = SparkSession.Builder().appName('jwd_bd09_to_wgs84_1').enableHiveSupport().getOrCreate()
     sc = spark.sparkContext
     # 初始化数据库
     spark.sql('use ${database}')
     # 设置日志级别
     sc.setLogLevel('WARN')
 ​
     main()
 ​
     spark.stop()
 ​

备注:

 if __name__ == "__main__":
     main()
 这段代码的意思是,如果当前脚本被作为主程序执行,那就调用main()函数。如果当前脚本被作为模块导入到其他脚本,那么main()函数不会被执行。
 使用if __name__ == "__main__"语句判断当前脚本是否作为主程序,如果是,就调用main()函数

Spark SQL核心编程

核心对象是SparkSession,其实就是SparkCore中SparkContext的升级版本。

DataFrame的创建(SparkSQL语法)

 //读取Json文件创建DataFrame
 val df = spark.read.json("data/user.json")
 //对DataFrame创建一个临时表
 df.createOrReplaceTempView("people")  //这个视图是不能跨越Session的,如果像全局有效,可以像下面创建全局临时表。
 //通过SQL语句实现全表查询
 val sqlDF = spark.sql("select * from people")
 sqlDF.show
 //对于DataFrame创建一个全局表
 df.createGlobeTempView("people")
 //通过sql语句实现全表查询
 spark.sql("select * from global_temp.people").show()  //使用全局临时表需要全路径访问,如:global_temp.people

DSL语法

 //用法与RDD中用法非常类似,因为DataFrame有数据和结构信息,可以直接当元数据(类似RDD)来用
 //创建一个DataFrame
 val df = spark.read.json("data/user.json")
 //查看DataFrame的Schema信息
 df.printSchema
 //只查看“username”列数据
 df.select("username").show()
 //查看“username”列数据以及“age+1”数据
 df.select($"username",$"age"+1).show  //写法一,$表示引用
 df.select(`username,`age + 1 as "newage").show()  //写法二,`也表示引用
 //查看“age"大于30的数据
 df.filter($"age">30).show
 //按照age分组,查看数据条数
 df.groupBy("age").count.show

RDD,DataFrame,DataSet三者的转换

image-20220522160106120

结尾

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值