You need to enable JavaScript to run this app. 使用Apache Spark对比两个JSON文件输出细粒度键值变更及对应路径的方法
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.25 18:24:02