【问题标题】:Reading json file with corrupt_record in spark java在 spark java 中读取带有 corrupt_record 的 json 文件
【发布时间】:2023-01-12 13:29:27
【问题描述】:

我正在使用 spark 2.7 版的 spark java 应用程序。我正在尝试根据我的模式加载可能包含损坏记录的多行 JSON 文件。我在加载它时传递了一个模式,但问题是它拒绝整个文件作为一个损坏的记录,即使有一个 JSON 对象不满足我提供的模式。

我的 Json 文件看起来像-

[
{Json_object},
{Json_object},
{Json_object}
]

我为它手动创建了模式(StructType)并加载它 -

Dataset<Row> df = spark.read().option("multiline", "true").option("mode","PERMISSIVE").option("columnNameOfCorruptRecord","_corrupt_record").schema(schema).json("filepath");

问题是,即使一个 JSON 对象不遵循模式,例如,如果我的模式中的 attribute1 具有整数类型,并且它是 json 对象之一的字符串形式,那么 json 对象应该进入 corrupted_record,insted I'我得到类似的东西-

+------------+---------------+---------------+
| attribute1 |   attribute2  |_corrupt_record|
+------------+---------------+---------------+
|    null    |     null      |             [{|
|            |               | all_json_obj  |
|            |               |          ...  |
|            |               |         }]    |
+------------+---------------+---------------+

它在典型的单行 json 对象中工作得非常好,其中换行符 '\n' 被用作分隔符,没有任何问题,并且结果理想。有人能告诉我我在这里想念什么吗?

PS:问题不限于spark java,scala和python的行为也是一样的。

【问题讨论】:

    标签: apache-spark pyspark apache-spark-sql


    【解决方案1】:

    恐怕这行不通,至少对于当前版本的 Spark 是行不通的。

    我不是 Spark 提交者,但我进行了调查,这就是我的发现。我不确定这是 100% 正确的,但也许它对你有用(至少作为进一步调查的良好起点)

    我深入研究了 Spark 代码,发现多行文件和标准文件之间存在很大差异:

    • 将 multiline 设置为 false Spark 正在使用 TextInputJsonDataSource 读取此文件,在这里您可以在代码 Spark Source Code 中看到读取操作的样子:

      override def readFile(
          conf: Configuration,
          file: PartitionedFile,
          parser: JacksonParser,
          schema: StructType): Iterator[InternalRow] = {
        val linesReader = new HadoopFileLinesReader(file, parser.options.lineSeparatorInRead, conf)
        Option(TaskContext.get()).foreach(_.addTaskCompletionListener[Unit](_ => linesReader.close()))
        val textParser = parser.options.encoding
          .map(enc => CreateJacksonParser.text(enc, _: JsonFactory, _: Text))
          .getOrElse(CreateJacksonParser.text(_: JsonFactory, _: Text))
      
        val safeParser = new FailureSafeParser[Text](
          input => parser.parse(input, textParser, textToUTF8String),
          parser.options.parseMode,
          schema,
          parser.options.columnNameOfCorruptRecord)
        linesReader.flatMap(safeParser.parse)
      }
      

    在这里我们可以看到 Spark 正在逐行读取文件,然后调用 flatMap 来用解析器处理每一行,这样以后很容易找到格式错误的记录并为它们生成 _corrupt_record

    当您将 multiline 选项设置为 true 时,Spark 将使用 MultiLineJsonDataSource(剧透 - 它以前称为 WholeFileJsonDataSource)。在这里你可以找到读取数据的功能:Spark source code

      override def readFile(
          conf: Configuration,
          file: PartitionedFile,
          parser: JacksonParser,
          schema: StructType): Iterator[InternalRow] = {
        def partitionedFileString(ignored: Any): UTF8String = {
          Utils.tryWithResource {
            CodecStreams.createInputStreamWithCloseResource(conf, new Path(new URI(file.filePath)))
          } { inputStream =>
            UTF8String.fromBytes(ByteStreams.toByteArray(inputStream))
          }
        }
        val streamParser = parser.options.encoding
          .map(enc => CreateJacksonParser.inputStream(enc, _: JsonFactory, _: InputStream))
          .getOrElse(CreateJacksonParser.inputStream(_: JsonFactory, _: InputStream))
    
        val safeParser = new FailureSafeParser[InputStream](
          input => parser.parse[InputStream](input, streamParser, partitionedFileString),
          parser.options.parseMode,
          schema,
          parser.options.columnNameOfCorruptRecord)
    
        safeParser.parse(
          CodecStreams.createInputStreamWithCloseResource(conf, new Path(new URI(file.filePath))))
      }
    

    现在让我们看一下 JsonParser 及其通用函数解析:Spark source code

      def parse[T](
          record: T,
          createParser: (JsonFactory, T) => JsonParser,
          recordLiteral: T => UTF8String): Iterable[InternalRow] = {
        try {
          Utils.tryWithResource(createParser(factory, record)) { parser =>
            // a null first token is equivalent to testing for input.trim.isEmpty
            // but it works on any token stream and not just strings
            parser.nextToken() match {
              case null => None
              case _ => rootConverter.apply(parser) match {
                case null => throw QueryExecutionErrors.rootConverterReturnNullError()
                case rows => rows.toSeq
              }
            }
          }
        } catch {
          case e: SparkUpgradeException => throw e
          case e @ (_: RuntimeException | _: JsonProcessingException | _: MalformedInputException) =>
            // JSON parser currently doesnt support partial results for corrupted records.
            // For such records, all fields other than the field configured by
            // `columnNameOfCorruptRecord` are set to `null`
            throw BadRecordException(() => recordLiteral(record), () => None, e)
          case e: CharConversionException if options.encoding.isEmpty =>
            val msg =
              """JSON parser cannot handle a character in its input.
                |Specifying encoding as an input option explicitly might help to resolve the issue.
                |""".stripMargin + e.getMessage
            val wrappedCharException = new CharConversionException(msg)
            wrappedCharException.initCause(e)
            throw BadRecordException(() => recordLiteral(record), () => None, wrappedCharException)
          case PartialResultException(row, cause) =>
            throw BadRecordException(
              record = () => recordLiteral(record),
              partialResult = () => Some(row),
              cause)
        }
      }
    

    在这里您可以看到 Json 没有生成 PartialResultException,但可能是这两个异常中的一个:JsonProcessingException |畸形输入异常

    由于此代码抛出此异常:BadRecordException(() => recordLiteral(record), () => None, e) 其中 record = our stream = whole file。

    此异常稍后由 FailureSafeParser 解释,它为您生成输出行,并且只是将数据复制到 _corrupt_record

    总的来说,我试图在提交和 Jira 中找到信息,但我认为这个主题真是一团糟。我找到了初始提交,它使用此消息添加了此功能:

    [SPARK-18352][SQL] Support parsing multiline json files
    
    ## What changes were proposed in this pull request?
    
    If a new option `wholeFile` is set to `true` the JSON reader will parse each file (instead of a single line) as a value. This is done with Jackson streaming and it should be capable of parsing very large documents, assuming the row will fit in memory.
    
    Because the file is not buffered in memory the corrupt record handling is also slightly different when `wholeFile` is enabled: the corrupt column will contain the filename instead of the literal JSON if there is a parsing failure. It would be easy to extend this to add the parser location (line, column and byte offsets) to the output if desired.
    

    “如果存在解析失败,损坏的列将包含文件名而不是文字 JSON”- 看起来后来有所更改(实际上您在此列中有文字 Json),但我认为一般方法是相同的。

    所以回到问题:“我想知道这是预期的行为还是只是一个错误!” - 我认为这不是一个错误,也不是预期的行为,而是 Jackson 解析器最初实现方式的结果,目前我们必须忍受这个

    【讨论】:

    • 谢谢你这么详细的回答。另外我认为他们一定在努力,因为它破坏了读取多行 json 文件的全部意义。之后我将开始自己查看源代码。一旦堆栈溢出允许我,将奖励赏金声誉。
    【解决方案2】:

    通过查看您的输出,我将在此处复制:

    +------------+---------------+---------------+
    | attribute1 |   attribute2  |_corrupt_record|
    +------------+---------------+---------------+
    |    null    |     null      |             [{|
    |            |               | all_json_obj  |
    |            |               |          ...  |
    |            |               |         }]    |
    +------------+---------------+---------------+
    

    如果您查看第一行和最后一行,您会看到 corrupt_records 是 [{}]。这让我觉得那些 {} 字符可能不应该存在。您的 json 文件是否可能实际上是这样的:

    [{
    {Json_object},
    {Json_object},
    {Json_object}
    }]
    

    如果是这种情况,那么那些位于最高级别 [] 方括号之间的 {} 花括号将使最高级别的数组看起来只包含 1 个对象,具有错误的模式。如果是这种情况,您可以尝试删除数组方括号之间的花括号吗?

    只是为了给你一个功能示例,请考虑以下 json 文件:

    [
        {
            "id": 1,
            "object": {
                "val1": "thisValue",
                "val2": "otherValue"
            }
        },
        {
            "id": 2,
            "object": {
                "val1": "hehe",
                "val2": "test"
            }
        },
        {
            "id": 3,
            "object": {
                "val1": "yes",
                "val2": "no"
            }
        }
    ]
    

    使用以下命令在 spark-shell(spark 版本 2.4.5)中读取该 json 文件:

    val df = spark.read.option("multiline", "true").json("test.json")

    给我以下输出:

    scala> df.show(false)
    +---+-----------------------+
    |id |object                 |
    +---+-----------------------+
    |1  |[thisValue, otherValue]|
    |2  |[hehe, test]           |
    |3  |[yes, no]              |
    +---+-----------------------+
    
    
    scala> df.printSchema
    root
     |-- id: long (nullable = true)
     |-- object: struct (nullable = true)
     |    |-- val1: string (nullable = true)
     |    |-- val2: string (nullable = true)
    

    这只是一个小例子,可以为您提供一些功能。

    但是请务必查看损坏数据框中的 [{}] 行!

    希望能帮助到你 :)

    【讨论】:

    • 感谢您的帮助,但没有。我在数组内的根级别没有额外的大括号,这会使它成为单个对象。我有与功能示例中所示类似的 json 文件。
    • 哦,真的,这很有趣!我从 M_S 的回答中学到了一些东西 :) 也许你可以尝试避免多行输出?您可以使用“紧凑输出”转换您的 json(例如使用 jq:programminghistorian.org/en/lessons/json-and-jq)并在没有多行选项的情况下读入您的 json?
    猜你喜欢
    • 2016-12-18
    • 2019-10-18
    • 2016-10-15
    • 2020-03-26
    • 2019-07-31
    • 1970-01-01
    • 2022-01-16
    • 2019-10-29
    相关资源
    最近更新 更多