我正在研究一个简单的ETL项目,该项目读取CSV文件,执行 对每列进行一些修改,然后将结果写为JSON。 我想要读取我的结果的下游流程 确信我的输出符合 商定的模式,但是我的问题是,即使我定义了 我所有字段的输入模式都为nullable = false,null可以潜行 进入并损坏我的输出文件,似乎没有(高效)的方法 让Spark对我的输入字段强制执行'not null'。
这似乎是一种功能,如以下《权威指南》 Spark中所述:
当您定义一个架构,其中所有列均声明为不具有 null值,Spark不会强制执行此操作,并且会很乐意让null 值进入该列。可为空的信号只是为了帮助Spark SQL优化处理该列。如果您有空值 不应该为空值的列,您可能会得到错误的结果 结果或看到难以调试的奇怪异常。
我编写了一个小检查工具来遍历数据帧的每一行,然后 如果在任何列中(在任何级别的 如果是字段或子字段(例如map,struct或array),则嵌套。)
我特别想知道:我是否已使用此检查实用程序重新插入车轮?是否有任何现有的库,或者 可以为我做到这一点的Spark技术(理想情况下比我实现的方法更好)?
下面将显示检查实用程序和管道的简化版本。如图所示, 检查实用程序已被注释掉。如果在未启用检查实用程序的情况下运行,则将在 /tmp/output.csv。
cat /tmp/output.json/*
(one + 1),(two + 1)
3,4
"",5
标题后的第二行应为数字,但为空字符串 (我猜这是spark写出null的方式。)此输出对于 读取我的ETL作业输出的下游组件:这些组件只需要整数。
现在,我可以通过取消注释行来启用检查
//checkNulls(inDf)
执行此操作时,出现异常,通知我无效的null值并打印 删除整个违规行,如下所示:
java.lang.RuntimeException: found null column value in row: [null,4]
Spark /权威指南中给出的一种可能的替代方法
Spark,权威指南提到了这样做的可能性:
<dataframe>.na.drop()
但这会(AFAIK)静默删除坏记录,而不是标记坏记录。 然后,我可以在删除前后对输入进行“设置减”,但这看起来像 严重影响性能以找出什么是null和什么不是。乍一看,我会 更喜欢我的方法。。。。但是我仍然想知道是否有更好的方法。 完整的代码如下。谢谢!
package org
import java.io.PrintWriter
import org.apache.spark.SparkConf
import org.apache.spark.sql._
import org.apache.spark.sql.types._
// before running, do; rm -rf /tmp/out* /tmp/foo*
object SchemaCheckFailsToExcludeInvalidNullValue extends App {
import NullCheckMethods._
//val input = "2,3\n\"xxx\",4" // this will be dropped as malformed
val input = "2,3\n,4" // BUT.. this will be let through
new PrintWriter("/tmp/foo.csv") { write(input); close }
lazy val sparkConf = new SparkConf()
.setAppName("Learn Spark")
.setMaster("local[*]")
lazy val sparkSession = SparkSession
.builder()
.config(sparkConf)
.getOrCreate()
val spark = sparkSession
val schema = new StructType(
Array(
StructField("one", IntegerType, nullable = false),
StructField("two", IntegerType, nullable = false)
)
)
val inDf: DataFrame =
spark.
read.
option("header", "false").
option("mode", "dropMalformed").
schema(schema).
csv("/tmp/foo.csv")
//checkNulls(inDf)
val plusOneDf = inDf.selectExpr("one+1", "two+1")
plusOneDf.show()
plusOneDf.
write.
option("header", "true").
csv("/tmp/output.csv")
}
object NullCheckMethods extends Serializable {
def checkNull(columnValue: Any): Unit = {
if (columnValue == null)
throw new RuntimeException("got null")
columnValue match {
case item: Seq[_] =>
item.foreach(checkNull)
case item: Map[_, _] =>
item.values.foreach(checkNull)
case item: Row =>
item.toSeq.foreach {
checkNull
}
case default =>
println(
s"bad object [ $default ] of type: ${default.getClass.getName}")
}
}
def checkNulls(row: Row): Unit = {
try {
row.toSeq.foreach {
checkNull
}
} catch {
case err: Throwable =>
throw new RuntimeException(
s"found null column value in row: ${row}")
}
}
def checkNulls(df: DataFrame): Unit = {
df.foreach { row => checkNulls(row) }
}
}
答案 0 :(得分:1)
您可以使用内置的Row方法anyNull拆分数据帧并以不同的方式处理这两种拆分:
val plusOneNoNulls = plusOneDf.filter(!_.anyNull)
val plusOneWithNulls = plusOneDf.filter(_.anyNull)
如果您不打算进行手动的空值处理过程,则使用内置的DataFrame.na方法会更简单,因为它已经实现了所有自动处理空值的常用方法(即使用默认值删除或填写空值)值)。