【问题标题】:how to parse a huge JSON file without loading it in memory如何解析巨大的 JSON 文件而不将其加载到内存中
【发布时间】:2019-02-21 23:46:49
【问题描述】:

我有一个包含大约 80000 行的大型 JSON 文件 (2.5MB)。

看起来像这样:

{
  "a": 123,
  "b": 0.26,
  "c": [HUGE irrelevant object],
  "d": 32
}

我只想要为键 abd 存储整数值并忽略 JSON 的其余部分(即忽略 c 值中的任何内容)。

我无法修改原始 JSON,因为它是由我从其服务器下载的第 3 方服务创建的。

如何在不将整个文件加载到内存的情况下执行此操作?

我尝试使用 gson 库并像这样创建了 bean:

public class MyJsonBean {
  @SerializedName("a")
  @Expose
  public Integer a;

  @SerializedName("b")
  @Expose
  public Double b;

  @SerializedName("d")
  @Expose
  public Integer d;
}

但即便如此,为了使用 Gson 对其进行反序列化,我需要先下载 + 读取内存中的整个文件,然后将其作为字符串传递给 Gson?

File myFile = new File(<FILENAME>);
myFile.createNewFile();

URL url = new URL(<URL>);
OutputStream out = new BufferedOutputStream(new FileOutputStream(myFile));
URLConnection conn = url.openConnection();

HttpURLConnection httpConn = (HttpURLConnection) conn;

InputStream in = conn.getInputStream();
byte[] buffer = new byte[1024];

int numRead;
while ((numRead = in.read(buffer)) != -1) {
  out.write(buffer, 0, numRead);
}

FileInputStream fis = new FileInputStream(myFile);
byte[] data = new byte[(int) myFile.length()];
fis.read(data);
String str = new String(data, "UTF-8");

Gson gson = new Gson();
MyJsonBean response = gson.fromJson(str, MyJsonBean.class);

System.out.println("a: " + response.a + "" + response.b + "" + response.d);

有什么方法可以避免加载整个文件而只获取我需要的相关值?

【问题讨论】:

  • 我觉得您将不得不下载整个文件并将其转换为字符串,但如果您没有关联的对象,您至少不会有任何不必要的对象。也许如果数据是静态的,你可以在两者之间做一个层,一个小的服务器来获取数据,修改它,然后你可以从那里获取。
  • 2.5MB 不算大。
  • with jackson:将字段排除在外并使用@JsonIgnoreProperties(ignoreUnknown = true)进行注释

标签: java json gson


【解决方案1】:

您绝对应该检查不同的方法和库。如果您真的关心性能检查:GsonJacksonJsonPath 库来执行此操作并选择最快的一个。当然,您必须在本地磁盘上加载整个 JSON 文件,可能是 TMP 文件夹,然后解析它。

简单的JsonPath 解决方案如下所示:

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;

import java.io.File;

public class JsonPathApp {
    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        DocumentContext documentContext = JsonPath.parse(jsonFile);
        System.out.println("" + documentContext.read("$.a"));
        System.out.println("" + documentContext.read("$.b"));
        System.out.println("" + documentContext.read("$.d"));
    }
}

注意,我没有创建任何POJO,只是使用类似于XPathJSONPath 功能读取给定值。你可以用Jackson做同样的事情:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;

public class JsonPathApp {
    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        JsonNode root = mapper.readTree(jsonFile);
        System.out.println(root.get("a"));
        System.out.println(root.get("b"));
        System.out.println(root.get("d"));
    }
}

我们不需要JSONPath,因为我们需要的值直接在root 节点中。如您所见,API 看起来几乎相同。我们也可以创建POJO结构:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.math.BigDecimal;

public class JsonPathApp {
    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        Pojo pojo = mapper.readValue(jsonFile, Pojo.class);
        System.out.println(pojo);
    }
}

@JsonIgnoreProperties(ignoreUnknown = true)
class Pojo {
    private Integer a;
    private BigDecimal b;
    private Integer d;

    // getters, setters
}

即便如此,这两个库都允许直接从URL 读取JSON 有效负载,我建议使用您能找到的最佳方法在另一个步骤中下载它。欲了解更多信息,请阅读这篇文章:Download a File From an URL in Java

【讨论】:

    【解决方案2】:

    有一些优秀的库可以用最少的资源解析大型 JSON 文件。一种是流行的GSON library。它与将文件解析为流和对象的效果相同。它会在每条记录通过时对其进行处理,然后丢弃流,从而保持较低的内存使用率。

    如果您对使用 GSON 方法感兴趣,这里有一个很好的教程。 Detailed Tutorial

    【讨论】:

      【解决方案3】:

      我只希望为键 a、b 和 d 存储整数值并忽略 JSON 的其余部分(即忽略 c 值中的任何内容)。 ...如何在不将整个文件加载到内存的情况下执行此操作?

      一种方法是使用 所谓的流解析器,使用--stream 选项调用。这正是您想要的,但需要在空间和时间之间进行权衡,并且使用流解析器通常更困难。

      在目前的情况下,例如,使用非流式(即默认)解析器,可以简单地编写:

      jq '.a, .b, .d' big.json
      

      使用流解析器,您必须编写如下内容:

      jq --stream 'select(length==2 and .[0][-1] == ("a","b","c"))[1]' big.json
      

      或者如果您愿意:

      jq -c --stream '["a","b","d"] as $keys | select(length==2 and (.[0][-1] | IN($keys[])))[1]' big.json
      

      Java 和 jq 的注意事项

      虽然有 jq 的 Java 绑定(请参阅 jq FAQ 中的“?:Java 可以使用哪些语言绑定?”),但我不知道任何与 --stream 选项一起使用的方法。

      但是,由于 2.5MB 对于 jq 来说很小,因此您可以使用可用的 Java-jq 绑定之一,而无需使用流解析器。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-09-22
        • 2017-09-30
        • 1970-01-01
        • 2022-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多