Как проанализировать огромный 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 из файлового потока