在.NET中解析大型JSON文件

38

到目前为止我一直使用Json.NET的"JsonConvert.Deserialize(json)"方法,这个方法非常好用,老实说,我不需要比这更多的东西。

我正在开发一个后台(控制台)应用程序,该应用程序不断从不同的URL下载JSON内容,然后将结果反序列化为.NET对象列表。

 using (WebClient client = new WebClient())
 {
      string json = client.DownloadString(stringUrl);

      var result = JsonConvert.DeserializeObject<List<Contact>>(json);

 }

上面的简单代码片段可能看起来并不完美,但它完成了工作。当文件很大(15,000个联系人 - 48 MB文件)时,JsonConvert.DeserializeObject不是解决方案,该行会抛出JsonReaderException异常类型。

下载的JSON内容是一个数组,这是样本的外观。Contact是反序列化JSON对象的容器类。

[
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  }
]

我的初步猜测是它运行时内存不足。出于好奇,我试图将其解析为JArray,结果也导致了同样的异常。

我已经开始深入研究Json.NET文档并阅读类似的主题讨论。由于我尚未成功地找到解决方案,所以我决定在这里发布一个问题。

更新:逐行反序列化时,我得到了相同的错误:“[. Path',第600003行,位置1。”因此我下载了其中两个并在Notepad++中检查了它们。我注意到,如果数组长度超过12,000,在第12000个元素之后,“[”就会关闭,另一个数组就会开始。换句话说,JSON看起来像这样:

[
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  }
]
[
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  },
  {
    "firstname": "sometext",
    "lastname": "sometext"
  }
]

7
异常信息是什么?是否有内部异常?异常信息是 JsonReaderException 类型。没有提到任何内部异常。 - Eser
3
你确定你的JSON格式是正确的吗? - Yuval Itzchakov
@Yavarski,正如您所看到的,这与json的大小无关。您的json末尾有一些额外的字符。 - Eser
格式有问题。 - Christo S. Christov
我正在使用第三方API,它生成一个包含联系人列表的链接(JSON数组)。 我得到的文件是一个JSON文件,并且构建如上所述。@YuvalItzchakov,我相信这是有效的JSON,因为我已经重复使用了100个不同的URL,并且从未遇到过问题。 但是,所有JSON数组中的联系人都少于10000个。 - Yavar Hasanov
显示剩余9条评论
4个回答

57

正如您在更新中正确诊断的那样,问题在于JSON具有一个闭合]紧接着一个开放[以开始下一组。当将整个JSON作为一个整体时,这种格式使得JSON无效,这就是为什么Json.NET抛出错误的原因。

幸运的是,这个问题似乎经常出现,Json.NET实际上有一个特殊设置来处理它。如果您直接使用JsonTextReader读取JSON,可以将SupportMultipleContent标志设置为true,然后使用循环逐个反序列化每个项目。

这应该允许您以记忆效率高的方式成功处理非标准的JSON,而不管有多少数组或每个数组中有多少项。

    using (WebClient client = new WebClient())
    using (Stream stream = client.OpenRead(stringUrl))
    using (StreamReader streamReader = new StreamReader(stream))
    using (JsonTextReader reader = new JsonTextReader(streamReader))
    {
        reader.SupportMultipleContent = true;

        var serializer = new JsonSerializer();
        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.StartObject)
            {
                Contact c = serializer.Deserialize<Contact>(reader);
                Console.WriteLine(c.FirstName + " " + c.LastName);
            }
        }
    }

完整演示在这里:https://dotnetfiddle.net/2TQa8p


我差点就要自己构建解析器了。这太棒了,谢谢Brian。 - Ibraheem Al-Saady

26

Json.NET支持直接从流中反序列化。以下是一种方法,使用StreamReader逐个读取JSON字符串的片段进行反序列化,而不是将整个JSON字符串加载到内存中。

using (WebClient client = new WebClient())
{
    using (StreamReader sr = new StreamReader(client.OpenRead(stringUrl)))
    {
        using (JsonReader reader = new JsonTextReader(sr))
        {
            JsonSerializer serializer = new JsonSerializer();

            // read the json from a stream
            // json size doesn't matter because only a small piece is read at a time from the HTTP request
            IList<Contact> result = serializer.Deserialize<List<Contact>>(reader);
        }
    }
}

参考资料:JSON.NET 性能技巧


15
这段代码可能不会将整个流加载到内存中,但肯定会将所有联系人的列表加载到内存中。除非“联系人”对象从流中丢弃了大量数据,否则您只是把内存问题推到了更深的地方。 - John Bledsoe

6

我曾经在Python中处理过一个5 GB大小的文件类似的事情。我将文件下载到某个临时位置,并逐行读取它,以形成一个类似SAX工作方式的JSON对象。

对于使用Json.NET的C#,您可以下载文件,使用流阅读器读取文件,并将该流传递给JsonTextReader并使用JTokens.ReadFrom(your JSonTextReader object)解析为JObject。


没问题,我会尝试并在这里发布更新。非常感谢。 - Yavar Hasanov
请查看下面“Kristian”的答案。他有代码实现,与我上面解释的概念非常相似,但我更喜欢“Kristian”的方法 :) - nixdaemon

0

对于一些人来说,这可能仍然是相关的,因为“新”的System.Text.Json已经发布。

await using FileStream file = File.OpenRead("files/data.json");
var options = new JsonSerializerOptions {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

// Switch the JsonNode type with one of your own if
// you have a specific type you want to deserialize to.
IAsyncEnumerable<JsonNode?> enumerable = JsonSerializer.DeserializeAsyncEnumerable<JsonNode>(file, options);

await foreach (JsonNode? obj in enumerable) {
    var firstname = obj?["firstname"]?.GetValue<string>();
}

如果你对更多内容感兴趣,比如如何解析压缩的JSON数据,可以看看我写的这篇博客文章:使用.NET流解析60GB JSON文件


这是从Medium复制/粘贴的内容。 - pregmatch
1
@pregmatch 当然是的,我确实写了那篇中文文章。 - Millard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接