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

vbopmzt1  于 2023-02-01  发布在  .NET
关注(0)|答案(5)|浏览(168)

我有一个非常非常大的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
        ]

    },
    ...
]

根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);
    }
}

这不起作用,抛出了一个异常,明确指出需要的是一个对象,而不是一个列表。我的理解是,这个命令只会读取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);
    }
}

这确实成功了。但是,它只是在一定程度上减少了高RAM使用率的问题。在这种情况下,它看起来确实像是应用程序一次反序列化一个项目,因此没有将整个JSON文件读取到RAM中,但我们仍然使用了大量RAM,因为C# List对象现在将JSON文件中的所有数据包含在RAM中。这只是取代了问题。
然后我决定在进入循环之前执行sr.Read(),从流的开头去掉一个字符(以消除[)。然后,第一个对象成功读取,但随后的对象没有成功读取,除了“unexpected token”。我猜这是对象之间的逗号和空格导致读取器出错。
简单地去掉方括号是不起作用的,因为对象确实包含它们自己的原语列表,正如您在示例中所看到的,即使尝试使用},作为分隔符也是不起作用的,因为对象中有子对象,正如您所看到的。
我的目标是,能够一次从流中读取一个对象。读取一个对象,对它执行一些操作,然后从RAM中丢弃它,并读取下一个对象,以此类推。这将消除将整个JSON字符串或整个数据内容作为C#对象加载到RAM中的需要。
我错过了什么?

kr98yfug

kr98yfug1#

这应该可以解决你的问题,基本上它和你的初始代码一样工作,除了当读取器命中流中的{字符时,它只反序列化对象,否则它只是跳到下一个,直到找到另一个开始对象令牌。

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);
        }
    }
}
piok6c0g

piok6c0g2#

我认为我们可以做得比公认的答案更好,使用JsonReader的更多特性来做出更通用的解决方案。
JsonReader使用JSON中的令牌时,路径记录在JsonReader.Path属性中。
我们可以使用它从JSON文件中精确地选择深度嵌套的数据,使用regex确保我们走的是正确的路径。
因此,使用以下扩展方法:

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);
            }
        }
    }
}

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

[0]
[1]
[2]
... etc

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

var regex = new Regex(@"^\[\d+\]$");

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

IEnumerable<MyObject> objects = jsonReader.SelectTokensWithRegex<MyObject>(regex);

或者,如果我们想更深入地挖掘结构,我们可以使用正则表达式来实现更精确的结果

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

以仅从数组中的项目提取value属性。
我发现这种技术对于使用网络流直接从HTTP(内存需求低,不需要中间存储)从巨大的(100 GiB)JSON转储中提取特定数据非常有用。

jtw3ybtb

jtw3ybtb3#

.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}!");
    }
}
epfja78i

epfja78i4#

下面是使用Cinchoo ETL解析大型JSON文件的另一种简单方法,Cinchoo ETL是一个开源库(在底层使用JSON.NET以流方式解析JSON)

using (var r = ChoJSONReader<MyObject>.LoadText(json)
       )
{
    foreach (var rec in r)
        Console.WriteLine(rec.Dump());
}

样品小提琴:https://dotnetfiddle.net/i5qJ5R

bcs8qyzn

bcs8qyzn5#

这是您要找的吗?Found on a previous question
www.example.com的当前版本Json.net不允许您使用可接受的答案代码。当前的替代方法是:

public static object DeserializeFromStream(Stream stream)
{
    var serializer = new JsonSerializer();

    using (var sr = new StreamReader(stream))
    using (var jsonTextReader = new JsonTextReader(sr))
    {
        return serializer.Deserialize(jsonTextReader);
    }
}

文件:Deserialize JSON from a file stream

相关问题