引子
變長參數對於我們來說並不陌生,在Java裏我們這麼寫
public void varArgs(String... args)
在Scala裏我們這麼寫
def varArgs(cols: String*): String
而在Spark裏,很多時候我們有自己的業務邏輯,現成的functions滿足不了我們的需求,而當我們需要處理同一行的多個列,將其經過我們自己的邏輯合併爲一個列時,變長參數及其變種實現可以給我們提供幫助。
但是在Spark UDF裏我們是 無法使用變長參數傳值 的,但之所以本文以變長參數開頭,是因爲需求起於它,而通過對它進行變換,我們可以使用變長參數或Seq類型來接收參數。
下面通過Spark-Shell來做演示,以下三種方法都可以做到多列傳參,分別是
變長參數類型的UDF
定義UDF方法
def myConcatVarargs(sep: String, cols: String*): String = cols.filter(_ != null).mkString(sep)
註冊UDF函數
由於變長參數只能通過方法定義,所以這裏使用部分應用函數來轉換
val myConcatVarargsUDF = udf(myConcatVarargs _)
可以看到該UDF的定義如下
UserDefinedFunction(<function2>,StringType,List(StringType, ArrayType(StringType,true)))
也即變長參數轉換爲了ArrayType,而且函數是隻包括兩個參數,所以變長參數列表由此也可看出無法使用的。
變長參數列表傳值
我們構造一個DataFrame如下
val df = sc.parallelize(Array(("aa", "bb", "cc"),("dd","ee","ff"))).toDF("A", "B", "C")
然後直接傳入多個String類型的列到myConcatVarargsUDF
df.select(myConcatVarargsUDF(lit("-"), col("A"), col("B"), col("C"))).show
結果出現如下報錯
java.lang.ClassCastException: anonfun$1 cannot be cast to scala.Function4
由此可以看出,使用變長參數列表的方式Spark是不支持的,它會被識別爲四個參數的函數,而UDF確是被定義爲兩個參數而不是四個參數的函數!
變換:使用array()轉換做第二個參數
我們使用Spark提供的array() function來轉換參數爲Array類型
df.select(myConcatVarargsUDF(lit("-"), array(col("A"), col("B"), col("C")))).show
結果如下
+-------------------+
|UDF(-,array(A,B,C))|
+-------------------+
| aa-bb-cc|
| dd-ee-ff|
+-------------------+
由此可以看出,使用變長參數構造的UDF方法,可以通過構造Array的方式傳參,來達到多列合併的目的。
使用Seq類型參數的UDF
上面提到,變長參數最後被轉爲ArrayType,那不禁要想我們爲嘛不使用Array或List類型呢?
實際上在UDF裏,類型並不是我們可以隨意定義的,比如使用List和Array就是不行的,我們自己定義的類型也是不行的,因爲這涉及到數據的序列化和反序列化。
以Array/List爲示例的錯誤
下面以Array類型爲示例
定義函數
val myConcatArray = (cols: Array[String], sep: String) => cols.filter(_ != null).mkString(sep)
註冊UDF
val myConcatArrayUDF = udf(myConcatArray)
可以看到給出的UDF簽名是
UserDefinedFunction(<function2>,StringType,List())
應用UDF
df.select(myConcatArrayUDF(array(col("A"), col("B"), col("C")), lit("-"))).show
會發現報錯
scala.collection.mutable.WrappedArray$ofRef cannot be cast to [Ljava.lang.String
同樣List作爲參數類型也會報錯,因爲反序列化的時候無法構建對象,所以List和Array是無法直接作爲UDF的參數類型的
以Seq做參數類型
定義調用如下
val myConcatSeq = (cols: Seq[Any], sep: String) => cols.filter(_ != null).mkString(sep)
val myConcatSeqUDF = udf(myConcatSeq)
df.select(myConcatSeqUDF(array(col("A"), col("B"), col("C")), lit("-"))).show
結果如下
+-------------------+
|UDF(array(A,B,C),-)|
+-------------------+
| aa-bb-cc|
| dd-ee-ff|
+-------------------+
使用Row類型參數的UDF
我們可以使用Spark functions裏struct方法構造結構體類型傳參,然後用Row類型接UDF的參數,以達到多列傳值的目的。
def myConcatRow: ((Row, String) => String) = (row, sep) => row.toSeq.filter(_ != null).mkString(sep)
val myConcatRowUDF = udf(myConcatRow)
df.select(myConcatRowUDF(struct(col("A"), col("B"), col("C")), lit("-"))).show
可以看到UDF的簽名如下
UserDefinedFunction(<function2>,StringType,List())
結果如下
+--------------------+
|UDF(struct(A,B,C),-)|
+--------------------+
| aa-bb-cc|
| dd-ee-ff|
+--------------------+
使用Row類型還可以使用模式提取
,用起來會更方便
row match {
case Row(aa:String, bb:Int) =>
}
最後
對於上面三種方法,變長參數和Seq類型參數都需要array的函數包裝爲ArrayType,而使用Row類型的話,則需要struct函數構建結構體類型,其實都是爲了數據的序列化和反序列化。三種方法中,Row的方式更靈活可靠,而且支持不同類型並且可以明確使用模式提取,用起來相當方便。
而由此我們也可以看出,UDF不支持List和Array類型的參數,同時 自定義參數類型 如果沒有混合Spark的特質實現序列化和反序列化,那麼在UDF裏也是 無法用作參數類型 的。當然,Seq類型是可以 的,可以接多列的數組傳值。
此外,我們也可以使用柯里化
來達到多列傳參的目的,只是不同參數個數需要定義不同的UDF了。
歡迎閱讀轉載,轉載請註明出處:https://my.oschina.net/u/2539801/blog/1154536