如何用Scala将大型JSON文件转为二进制并高效读取?
Scala处理大型JSON文件:转换为二进制格式并高效读取
方案一:使用Jackson Smile(二进制JSON)
Smile是Jackson提供的二进制JSON格式,兼容原有JSON结构,转换流程简单且性能优异,无需额外定义数据结构IDL。
步骤1:添加依赖
在build.sbt中加入以下依赖:
libraryDependencies ++= Seq( "com.fasterxml.jackson.core" % "jackson-core" % "2.15.2", "com.fasterxml.jackson.core" % "jackson-databind" % "2.15.2", "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.15.2", "com.fasterxml.jackson.dataformat" % "jackson-dataformat-smile" % "2.15.2" )
步骤2:定义数据模型
用Scala case class映射JSON结构:
case class DataRecord(a: Int, b: Int, c: String, d: Int, e: Int)
步骤3:JSON转Smile二进制(流式处理)
通过Jackson流式API逐行解析JSON,避免加载整个文件到内存:
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.smile.SmileFactory import com.fasterxml.jackson.module.scala.DefaultScalaModule import scala.util.Using object JsonToSmile { def main(args: Array[String]): Unit = { val jsonPath = "/path/to/your/large.json" val smilePath = "/path/to/output.smile" val jsonMapper = new ObjectMapper().registerModule(DefaultScalaModule) val smileMapper = new ObjectMapper(new SmileFactory()).registerModule(DefaultScalaModule) // 使用Using自动管理IO资源 Using(jsonMapper.createParser(new java.io.File(jsonPath))) { jsonParser => Using(smileMapper.createGenerator(new java.io.File(smilePath))) { smileGenerator => jsonParser.nextToken() // 跳过JSON数组开头的`[` // 逐记录解析并写入Smile格式 while (jsonParser.nextToken() != null) { val record = jsonMapper.readValue(jsonParser, classOf[DataRecord]) smileMapper.writeValue(smileGenerator, record) } } } } }
步骤4:高效读取Smile文件
同样采用流式方式读取,避免内存溢出:
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.smile.SmileFactory import com.fasterxml.jackson.module.scala.DefaultScalaModule import scala.util.Using object ReadSmile { def main(args: Array[String]): Unit = { val smilePath = "/path/to/output.smile" val smileMapper = new ObjectMapper(new SmileFactory()).registerModule(DefaultScalaModule) Using(smileMapper.createParser(new java.io.File(smilePath))) { smileParser => var record: DataRecord = null // 逐记录读取并处理 while ({record = smileMapper.readValue(smileParser, classOf[DataRecord]); record != null}) { // 替换为你的业务逻辑 println(s"处理记录: a=${record.a}, b=${record.b}, c=${record.c}") } } } }
方案二:使用Protocol Buffers(高效跨语言二进制格式)
Protobuf是Google推出的轻量高效二进制序列化格式,压缩率更高,适合长期存储和跨系统交互,需要提前定义数据结构IDL。
步骤1:配置Protobuf生成工具
在project/plugins.sbt中添加插件:
addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.6")
在build.sbt中添加依赖:
libraryDependencies += "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf"
步骤2:定义Protobuf IDL
在src/main/protobuf/data.proto中写入数据结构定义:
syntax = "proto3"; package com.example; message DataRecord { int32 a = 1; int32 b = 2; string c = 3; int32 d = 4; int32 e = 5; }
运行sbt compile会自动生成对应的Scala类。
步骤3:JSON转Protobuf二进制
流式解析JSON并转换为Protobuf对象,写入二进制文件(写入时记录每个对象的字节长度,方便读取):
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.example.DataRecord import com.google.protobuf.util.JsonFormat import scala.util.Using object JsonToProtobuf { def main(args: Array[String]): Unit = { val jsonPath = "/path/to/your/large.json" val protoPath = "/path/to/output.bin" val jsonMapper = new ObjectMapper().registerModule(DefaultScalaModule) val protoJsonParser = JsonFormat.parser() Using(jsonMapper.createParser(new java.io.File(jsonPath))) { jsonParser => Using(new java.io.FileOutputStream(protoPath)) { outputStream => jsonParser.nextToken() // 跳过JSON数组开头的`[` while (jsonParser.nextToken() != null) { val jsonNode = jsonMapper.readTree(jsonParser) // 将JSON转换为Protobuf对象 val recordBuilder = DataRecord.newBuilder() protoJsonParser.merge(jsonNode.toString, recordBuilder) val record = recordBuilder.build() // 先写入对象字节长度,再写入对象数据 val recordBytes = record.toByteArray outputStream.write(java.nio.ByteBuffer.allocate(4).putInt(recordBytes.length).array()) outputStream.write(recordBytes) } } } } }
步骤4:高效读取Protobuf二进制
按写入时的格式(长度+数据)流式读取:
import com.example.DataRecord import scala.util.Using object ReadProtobuf { def main(args: Array[String]): Unit = { val protoPath = "/path/to/output.bin" Using(new java.io.FileInputStream(protoPath)) { inputStream => val lengthBuffer = new Array[Byte](4) // 循环读取每个对象的长度和数据 while (inputStream.read(lengthBuffer) != -1) { val recordLength = java.nio.ByteBuffer.wrap(lengthBuffer).getInt() val recordBytes = new Array[Byte](recordLength) inputStream.read(recordBytes) // 反序列化为Protobuf对象并处理 val record = DataRecord.parseFrom(recordBytes) println(s"处理记录: a=${record.getA}, b=${record.getB}, c=${record.getC}") } } } }
关键优化点
- 流式处理:所有操作均采用流式API,避免一次性加载1.5GB文件到内存,防止内存溢出。
- 资源管理:使用
scala.util.Using自动关闭IO资源,避免资源泄漏。 - 格式选择:Smile适合快速转换、兼容JSON的场景;Protobuf适合高性能、跨语言、长期存储的场景。
内容的提问来源于stack exchange,提问作者Hima_93
相关产品推荐
相关产品推荐

