使用Apache Spark对比两个JSON文件输出细粒度键值变更及对应路径的方法
问题根因
你当前使用的except逻辑,是将加载后的嵌套struct作为独立行做全量匹配——两个JSON加载完成后,employee是一个复合struct类型,只要struct内部任意字段值发生变化,Spark就会判定整行不匹配,直接返回整个struct对象,完全不会拆解内部字段定位具体变更位置,因此无法实现精准定位字段变更路径的需求。
正确实现思路
核心逻辑为先将嵌套JSON拆解为带完整访问路径的叶子键值对,再在键值对粒度做对比,最后映射为标准变更操作,具体流程分为三步:
- 递归拍平嵌套结构
遍历JSON的Schema,逐层展开所有嵌套的struct类型,为每个基础类型的叶子字段生成完整访问路径(例如/employee/salary),让每个叶子字段成为独立的对比单元,不再受上层嵌套结构的影响。 - 路径粒度全外连接对比
将两个JSON拍平后的「路径-字段值」数据集,按路径做全外连接,自动识别三类变更:- 仅在第一个文件存在的路径:对应删除操作
- 仅在第二个文件存在的路径:对应新增操作
- 两边路径都存在但字段值不相等:对应修改操作
- 映射为目标输出格式
根据对比结果,为每条变更记录匹配对应操作类型op,关联变更路径path和新值value,整理为符合要求的JSON数组结构。
可直接适配场景的示例代码
import org.apache.spark.sql.DataFrame import org.apache.spark.sql.types._ import org.apache.spark.sql.functions._ // 递归拍平嵌套struct,生成带完整访问路径的列 def flattenJson(df: DataFrame, prefix: String = ""): DataFrame = { val flatCols = df.schema.flatMap { case StructField(name, struct: StructType, _, _) => val currentPrefix = if (prefix.isEmpty) s"/$name" else s"$prefix/$name" flattenJson( df.select(struct.fields.map(f => col(s"`$name`.${f.name}").as(f.name)): _*), currentPrefix ).columns.map(col(_)) case StructField(name, _, _, _) => val colPath = if (prefix.isEmpty) s"/$name" else s"$prefix/$name" col(name).as(colPath) :: Nil } df.select(flatCols: _*) } // 初始化SQL上下文,加载两个JSON文件 val sqlContext = new org.apache.spark.sql.SQLContext(sc) val jsonData1 = sqlContext.read.json(sc.wholeTextFiles("D:\\File_1.json").values) val jsonData2 = sqlContext.read.json(sc.wholeTextFiles("D:\\File_2.json").values) // 对两个数据集做扁平化处理 val flat1 = flattenJson(jsonData1) val flat2 = flattenJson(jsonData2) // 将宽表转成「路径-字段值」的长表结构,方便对比 def toLongFormat(flatDf: DataFrame): DataFrame = { val colCount = flatDf.columns.length val stackArgs = flatDf.columns.map(c => s"'$c', `$c`").mkString(", ") flatDf.selectExpr(s"stack($colCount, $stackArgs) as (path, value)") } val long1 = toLongFormat(flat1) val long2 = toLongFormat(flat2) // 全外连接对比,识别变更类型 val diffResult = long1.join(long2, "path", "full_outer") .select( col("path"), when(long1("value").isNull, lit("add")) .when(long2("value").isNull, lit("remove")) .when(long1("value") =!= long2("value"), lit("replace")) .otherwise(lit("same")) .as("op"), coalesce(long2("value"), long1("value")).as("value") ) .filter("op != 'same'") // 输出为目标格式的JSON文件 diffResult.write.mode("overwrite").json("./json_diff_output")
针对你提供的测试样本,上述代码运行后会生成完全匹配预期的输出:
{"op":"replace","path":"/employee/salary","value":58000}
内容的提问来源于stack exchange,提问作者prem
相关产品推荐
相关产品推荐

