刚开始使用spark第一个功能就是合并两个文件,相比于python的pandas合并两个文件,spark在速度上快了不少,而且几乎不在乎文件大小,最大尝试过150G文件大小的merge,而对于pandas而言超过10G的文件已经就无法处理了,使用spark处理文件已经成了刚需。
使用spark合并两个文件比较简单,只不过使用过程中发现了不少的坑,主要分为以下几个步骤
- 读取文件
var df = spark.read.option("delimiter",delimiter).option("header",true).option("inferSchema", inferSchema).option("maxColumns",50000).csv(path=path)
spark 是sparkSession对象,option("maxColumns",50000)这里是其中一个坑,在使用过程中当数据文件列数少于20000时没有什么异常,但是当列数变多的时候,发现读取文件会报错,因此将上限提高为5w,因为读取操作比较常用,将该功能封装为公共函数
def read(spark:SparkSession,path:String,delimiter:String,inferSchema:String):DataFrame={
var df = spark.read.option("delimiter",delimiter).option("header",true).option("inferSchema", inferSchema).option("maxColumns",50000).csv(path=path)
println("rdd partions :"+df.rdd.partitions.length)
if(df.columns.contains("loan_dt")){
df=df.withColumn("loan_dt",Utils.formatLoan_dt(df("loan_dt")))
}
return df
}
def read(path:String,delimiter:String="\t",inferSchema:String="false"):DataFrame={
val spark = SparkEnv.getSession
return read(spark,path,delimiter=delimiter,inferSchema=inferSchema)
}
普通的文件这样处理就没什么问题了,如果dataframe的列名里面包含了"." 在进行select操作的时候就会报错,为了解决这个问题,在读取完文件后先将列名中的“.”替换为‘#’,等处理完成后再替换回来,columns是替换的列名
df = df.toDF(columns:_*)
- 合并文件
标准的spark的dataframe提供了join函数,通过该函数就能直接join,but这里有个坑存在,如果合并的列中存在空值,在最终合并的文件中会join不上,具体原因不详。这里提供了一种解决方案,先将待合并的两个文件中join的列里面的空值替换为“none”字符串,再进行合并,合并完成后再替换回来
val ts = udf((x:String)=>if(x==null)"None" else x)
val rts = udf((x:String)=>if(x=="None")null else x)
for(cols<-(usingCols.toSet)){
leftDfts=leftDfts.withColumn(cols,ts(leftDf(cols)))
rightDfts=rightDfts.withColumn(cols,ts(rightDfts(cols)))
}
var res = leftDfts.join(rightDfts,usingCols,joinType = joinType)
for(cols<-(usingCols.toSet)){
res=res.withColumn(cols,rts(res(cols)))
}
- 其他功能
merge操作在平时使用频率很高,本着解放生产力的原则对一些功能进行了封装,对常用参数进行了默认处理
package com.test.spark
object Merge {
def main(args: Array[String]): Unit = {
//val test = Array[String]("--input","test","test2","--out","test3","--joinType","inner")
val ps = Utils.getPs(args)
val out = ps.get("out").get(0)
val joinType = ps.get("joinType").getOrElse(List("inner"))(0)
var filepathlist = ps.get("input").get
var filelist = filepathlist.map(line=>Utils.read(line))
if(ps.contains("repartions")){
val repartions = ps.get("repartions").get(0).toInt
filelist = filelist.map(line=>line.repartition(repartions))
}
var res = filelist(0)
for(f<-filelist.drop(1))
{
res = Utils.join(res,f,List(),joinType)
}
res = Utils.rtsCols(res)
Utils.write(res,out)
}
}
其中 Utils.getPs(args) 是自己封装的一个函数,主要是将命令行调用传入的参数转换为 key-value的形式,这样调用命令行的时候参数的顺序和个数就可以任意比较灵活,代码如下
def getPs(args: Array[String],prex:String="--"):Map[String,List[String]]={
var params= Map[String,List[String]]()
var key = ""
for(item<-args){
if(item.length>=prex.length && prex==item.substring(0,prex.length)){
key = item.substring(prex.length)
params+=(key->List[String]())
}
else{
var value = params.get(key).get
value = value:+item
params+=(key->value)
}
}
params
}
def getParam(params:Map[String,List[String]],key:String):String={
var res = ""
res = if(params.contains(key)) params.get(key).get(0) else ""
res
}
将上述功能进行了封装,调用形式 spark_submit xx.jar --input file1 file2 file2 ... --out file_out --joinType inner