Как проанализировать огромный JSON-файл как поток в Json.NET?

У меня есть очень, очень большой файл JSON (1000+ МБ) идентичных объектов 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);
    }
}

Это действительно удается. Тем не менее, это только несколько уменьшает проблему высокого использования оперативной памяти. В этом случае это выглядит так, как будто приложение десериализует элементы по одному, и поэтому не считывает весь файл JSON в ОЗУ, но мы по-прежнему имеем большой объем использования ОЗУ, поскольку объект C# List теперь содержит все данные из файла JSON в оперативной памяти. Это только сместило проблему.

Затем я решил просто попытаться удалить один символ из начала потока (чтобы устранить [) при выполнении sr.Read() прежде чем идти в петлю. Первый объект затем успешно читает, а последующие - нет, за исключением "неожиданного токена". Я предполагаю, что это запятая и пробел между объектами, отбрасывающими читателя.

Простое удаление квадратных скобок не сработает, поскольку объекты содержат собственный примитивный список, как вы можете видеть в примере. Даже пытаясь использовать }, как разделитель не будет работать, так как, как вы можете видеть, внутри объектов есть подобъекты.

Моя цель состоит в том, чтобы иметь возможность читать объекты из потока по одному. Прочитайте объект, сделайте что-нибудь с ним, затем удалите его из ОЗУ, прочитайте следующий объект и так далее. Это избавит от необходимости загружать либо всю строку JSON, либо все содержимое данных в ОЗУ как объекты C#.

Что мне не хватает?

6 ответов

Решение

Это должно решить вашу проблему. В основном это работает так же, как ваш исходный код, за исключением того, что это только десериализация объекта, когда читатель нажимает { символ в потоке, в противном случае он просто переходит к следующему, пока не найдет другой токен начального объекта.

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

Я думаю, что мы можем сделать лучше, чем принятый ответ, используя больше возможностей JsonReader сделать более обобщенное решение.

Как JsonReader потребляет токены из JSON, путь записывается в JsonReader.Path имущество.

Мы можем использовать это для точного выбора глубоко вложенных данных из файла 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);
            }
        }
    }
}

Данные, которые вас интересуют, лежат на путях:

[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 свойства из элементов в массиве.

Я нашел эту технику чрезвычайно полезной для извлечения определенных данных из огромных (100 ГиБ) JSON-дампов напрямую из HTTP с использованием сетевого потока (с низкими требованиями к памяти и без промежуточного хранилища).

.NET 6

Это легко сделать с помощью System.Text.Json.JsonSerializerв .NET 6:

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

Вы можете использовать простой пакет nuget, который имеет простые методы расширения, описанные выше. JStreamAsyncNet

Вот еще один простой способ проанализировать большой файл 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

Это то, что вы ищете? Нашел на предыдущем вопросе

Текущая версия 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);
    }
}

Документация: десериализация JSON из файлового потока

Другие вопросы по тегам