DataFrame详解
DataFrame与RDD之间的差异
DataFrame的特性
在使用Python进行数据分析时,一般都是使用pandas这个类库来对数据进行包装的,而其中的数据都是以DataFrame这种对象的形式统一呈现出来。同样的SparkSQL中也提供了DataFrame这种类来对数据进行封装并为我们的编程提供了统一的API。
DataFrame是一个类似于2维表的数据类型,其有着行和列的概念,每一列都有其自己的数据类型和名称,而每一行就相当于一条记录,这么说来DataFrame在数据结构层面上就为我们提供了对于元素层面更深入的信息(也就是Schema信息),所以DataFrame就是一种结构化数据。由于它可以显示出更多的信息,因此也会有更多的API可用,这就会让我们的编程过程会更轻松一点。
具体的体现
在使用textFile来读取一个文本文件的时候,我们得到的返回值都是RDD[String]这种类型,就是每一个元素都是一个String的字符串。然后我们再更具里面具体的内容进行数据的转化操作,如:map(_.split("\t")).map((x._1,x._2))…。如果每一次编程时里面的元素都不是同一种类型的话我们将要不停地进行新的代码开发。
如果是DataFrame的话,我们在构建之初就对数据的schema进行设定,在后面的开发过程之中我们就可以使用统一的API进行编程了,如:select("…").filter("…"),show。我们甚至可以直接使用SQL进行行过滤和列裁剪来得到我们想要的数据。
底层的区别
在使用RDD编程的过程中,使用不同的编程语言将会得到不同的执行性能,Java比Scala快,Scala比Python快。但是当使用DataFrame编程的时候,无论我们使用的是Java,Scala还是Python都是想调用DataFrame的API来编程,其底层会将我们的代码转换为一种logisticalPlan(逻辑执行计划),然后在构建一种physicalExecution(物理执行引擎)来执行的。这样无论我们使用什么语言最终的执行性能都一样的。甚至我们知道即使要得到相同的结构但是使用不同的SQL,这个过程的执行性能也是差很远的,但这在SparkSQL里面是不存在的,因为我们的执行逻辑将会由Spark的catalyst优化器来提升执行效率的。
DataFrame的构建
吹了这么就的DataFrame,现在我们来了解一下构建DataFrame的几个途径:
- RDD转DataFrame
- HiveTable
- External Data Source
下面我们分别对这几种方式进行详细介绍。
HiveTable
如果我们要想读取到Hive上的表的时候,我们首先就是要能访问得到Hive的metastore地址,所以我们需要一个hive-site.xml的配置文件并将其放到$SPARK_HOME/conf目录下。然后我们就可以直接访问Hive上面的表了。
具体操作:
//我们读取了Hive中doudou这个数据库里面的text这个表,其返回了一个DataFrame对象给我们
val df = spark.table("doudou.text")
//这里是两种方式将DF重新保存到Hive中,后一种是通过一个option的方法重新配置了要表格的数据实际是存储在HDFS上的路径
df.write.saveAsTable("t")
df.write.option("path","/some/path").saveAsTable("t")
RDD转DataFrame
我们也可以使用已有的RDD来构建DF的
reflaction infer 反射推导
这种方式是我们需要在RDD中提前构建好schema信息的,下面是一个完整的例子;
//首先要定义一个case Class来为RDD表明字段的名称和类型
case Clasa Person(id:Int,name:String,age:Int)
//将这个case Class作用到RDD上面,返回一个RDD[Person]的带泛型参数的RDD
val rdd1 = rdd.map(x => Person(x(0),x(1),x(2))
//调用RDD的toDF方法来构建DataFrame
val df = rdd1.toDF()
在Spark1.x版本里面这种方法只能构建20来个字段,太多就不行了。但是,在2.x版本里面就没有限制了。生产上面其实我们很少使用到这种方法的。
编程定义DataFrame
下面这个例子是使用编程的方式来构建RDD的:
//这个方式是固定写法,可能和官网上的有一点不同,但生产上就是这么写,反而官网上的不是很推荐
val struct = StructType(Array(
StructField("id",IntType,nullable = True),
StructField("name",StringType,nullable = True),
StructField("age",IntType,nullable = True)
)
)
//构建一个RDD[Row],过程中记得对RDD中的数据进行转换
val rdd1= rdd.map(x => Row(x(0).toInt,x(1),x(2).toInt)
//使用SparkSession的createDataFrame方法来构建DF
val df = spark.createDataFrame(struct,rdd1)
使用外部数据源来构建DataFrame
其实就是从不同的文件系统中读取不同的格式的文件,包括jdbc
如果是使用外部数据源来构建的话,我们使用以下几种方式:
读取
//标准型,format定义了输入类型,load定义了输入的路径
val df = spark.read.format("...").load(".....")
//简写型,以json格式的文件为例子,后面括号里填的是路径
val df1 = spark.read.json("......")
//更简版,但这种方式默认只能读取parquet文件,想要更改这种默认设置可以在spark-default.xml中配置spark.sql.sources.default这个参数
val df2 = spark.read.load("...")
format这个方法里面可以传入各种内建的文件类型,有:json,csv,parquet,orc,jdbc,text(文本)…
写入
//标准型
df.write.format("...").save("....")
//如果文件已存在可以选择重写或者追加,甚至报错
df.write.format("...").mode("overwrite").save("...")
import org.apache.spark.sql.SaveMode
df.write.format("....").mode(SaveMode.Overwrite).save("....")
使用option方法来设置,这个一般我们在读取csv和jdbc时候用得多
//这里三个option分别定义了分隔符使用逗号,是否推到schema信息和是否保留header
val df = spark.read.format("text").option("sep",";").option("inferSchema","true").option("header","true).load("....")
//如果是使用jdbc读入数据的话,你就需要有url,username等用户信息,以及库和表的名称才能准确定位
val df = spark.read.format("jdbc")
.option(“url","jdbc:mysql://....")
.option("usrname","doudou")
.option("password","123456")
.option("dbtable","xxxx.xxx").load()
DataFrame的使用
常用的方法是:
//select和filter里面是可以传入字符串或者Column对象的·
df.select("name").filter("age > 18").show()
//传入的是Column对象的情况
df.select($"name").filter($"age" > 18).show()
df.select('name).filter('age> 18).show()
以上就是完成了行过滤和列裁剪的过程,当然还有什么grouBy等使用
//传统的写法,这种只能用一个聚合函数,实在不方便
df.groupBy("name").sum("age")
//这种就可以用多个了
df.groupBy("name").agg(max("age),avg("age"))
注册方法
//首先定义好你要使用的函数
def func1(ip:String):(String,String) = {
val array = ip.split("@")
var start = ""
var end = ""
if(array(0)=="186"){
start = "nanjing"
}else{
start = "beijing"
}
if(array(1)=="0.0"){
end = "liantong"
}else{
end = "dianying"
}
(start,end)
}
//注册方法,里面传入方法名和一个lambda表达式,式子里面放你定义的方法
udf.register("parseIP",(ip:String) => { func1(ip) })
//在sql中使用,下起博文将接介绍withColumns这个方法的使用
spark.sql("select ip,parseIP(ip)(0) as city,parseIP(ip)(1) as carrier from table1").show