如何在Json.NET中将巨大的JSON文件解析为流?

fdm*_*ion 21 c# json json.net

我有一个非常非常大的JSON文件(1000+ MB)相同的JSON对象.例如:

[
    {
        "id": 1,
        "value": "hello",
        "another_value": "world",
        "value_obj": {
            "name": "obj1"
        },
        "value_list": [
            1,
            2,
            3
        ]
    },
    {
        "id": 2,
        "value": "foo",
        "another_value": "bar",
        "value_obj": {
            "name": "obj2"
        },
        "value_list": [
            4,
            5,
            6
        ]
    },
    {
        "id": 3,
        "value": "a",
        "another_value": "b",
        "value_obj": {
            "name": "obj3"
        },
        "value_list": [
            7,
            8,
            9
        ]

    },
    ...
]
Run Code Online (Sandbox Code Playgroud)

根JSON列表中的每个项都遵循相同的结构,因此可以单独反序列化.我已经编写了C#类来接收这些数据,并且反序列化包含单个对象的JSON文件而没有列表按预期工作.

起初,我试图在循环中直接反序列化我的对象:

JsonSerializer serializer = new JsonSerializer();
MyObject o;
using (FileStream s = File.Open("bigfile.json", FileMode.Open))
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
    while (!sr.EndOfStream)
    {
        o = serializer.Deserialize<MyObject>(reader);
    }
}
Run Code Online (Sandbox Code Playgroud)

这不起作用,抛出一个例外,明确指出一个对象是预期的,而不是一个列表.我的理解是这个命令只会读取包含在JSON文件根级别的单个对象,但由于我们有一个对象列表,这是一个无效的请求.

我的下一个想法是反序列化为C#对象列表:

JsonSerializer serializer = new JsonSerializer();
List<MyObject> o;
using (FileStream s = File.Open("bigfile.json", FileMode.Open))
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
    while (!sr.EndOfStream)
    {
        o = serializer.Deserialize<List<MyObject>>(reader);
    }
}
Run Code Online (Sandbox Code Playgroud)

这确实成功了.但是,它只是在一定程度上减少了高RAM使用率的问题.在这种情况下,它看起来像应用程序一次反序列化一个项目,因此不是将整个JSON文件读入RAM,但我们仍然使用大量的RAM,因为C#List对象现在包含所有的来自RAM中JSON文件的数据.这只能解决问题.

然后,我决定在进入循环之前尝试从流的开头取一个字符(以消除[)sr.Read().然后第一个对象成功读取,但后续的对象没有,但"意外令牌"除外.我猜这是抛出阅读器的物体之间的逗号和空格.

简单地删除方括号将不起作用,因为对象确实包含它们自己的原始列表,如示例中所示.即使尝试},用作分隔符也行不通,因为正如您所看到的,对象中有子对象.

我的目标是能够一次一个地从流中读取对象.读取一个对象,用它做一些事情,然后从RAM中丢弃它,然后读取下一个对象,依此类推.这样就不需要将整个JSON字符串或数据的全部内容作为C#对象加载到RAM中.

我错过了什么?

noc*_*ame 30

这应该可以解决您的问题.基本上它就像你的初始代码一样工作,除了它只是当读者点击{流中的字符时反序列化对象,否则它只是跳到下一个,直到找到另一个起始对象令牌.

JsonSerializer serializer = new JsonSerializer();
MyObject o;
using (FileStream s = File.Open("bigfile.json", FileMode.Open))
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
    while (reader.Read())
    {
        // deserialize only when there's "{" character in the stream
        if (reader.TokenType == JsonToken.StartObject)
        {
            o = serializer.Deserialize<MyObject>(reader);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 您不需要中间的`JObject`,只需执行`var o = serializer.Deserialize &lt;MyObject&gt;(reader);`。 (2认同)
  • 本质上,此方法向前跳过,直到看到对象的开始(跳过“开始数组”标记),然后读取该对象。大概您将在继续循环的下一次迭代之前对其进行处理。由于您不必一次将所有对象都保留在内存中,只需对一个对象进行操作(`o`),它的内存效率就更高。 (2认同)

Jos*_*hee 16

.NET 6

使用 .NET 6可以轻松完成此操作System.Text.Json.JsonSerializer

using (FileStream? fileStream = new FileStream("hugefile.json", FileMode.Open))
{
    IAsyncEnumerable<Person?> people = JsonSerializer.DeserializeAsyncEnumerable<Person?>(fileStream);
    await foreach (Person? person in people)
    {
        Console.WriteLine($"Hello, my name is {person.Name}!");
    }
}
Run Code Online (Sandbox Code Playgroud)


spe*_*der 5

我认为我们可以做的比接受的答案更好,可以使用的更多功能JsonReader来提供更通用的解决方案。

JsonReader从JSON消费令牌时,路径记录在JsonReader.Path属性中。

我们可以使用它来使用regex来从JSON文件中精确选择深层嵌套的数据,以确保我们走在正确的道路上。

因此,使用以下扩展方法:

public static class JsonReaderExtensions
{
    public static IEnumerable<T> SelectTokensWithRegex<T>(
        this JsonReader jsonReader, Regex regex)
    {
        JsonSerializer serializer = new JsonSerializer();
        while (jsonReader.Read())
        {
            if (regex.IsMatch(jsonReader.Path) 
                && jsonReader.TokenType != JsonToken.PropertyName)
            {
                yield return serializer.Deserialize<T>(jsonReader);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您关心的数据位于路径上:

[0]
[1]
[2]
... etc
Run Code Online (Sandbox Code Playgroud)

我们可以构造以下正则表达式来精确匹配此路径:

var regex = new Regex(@"^\[\d+\]$");
Run Code Online (Sandbox Code Playgroud)

现在可以从数据中流式传输对象(无需完全加载或解析整个JSON),如下所示

IEnumerable<MyObject> objects = jsonReader.SelectTokensWithRegex<MyObject>(regex);
Run Code Online (Sandbox Code Playgroud)

或者,如果我们想更深入地研究结构,则可以使用正则表达式更加精确

var regex = new Regex(@"^\[\d+\]\.value$");
IEnumerable<string> objects = jsonReader.SelectTokensWithRegex<string>(regex);
Run Code Online (Sandbox Code Playgroud)

value从数组中的项目中提取属性。

我发现该技术对于使用网络流(内存需求低且不需要中间存储)直接从HTTP直接从巨大的(100 GiB)JSON转储中提取特定数据非常有用。

  • 实际上我比我自己的答案更喜欢这个:P (5认同)