我有一个非常非常大的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文件根级别包含的单个对象,但由于我们有一个 list 对象,这是一个无效的请求。
我的下一个想法是反序列化为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对象现在包含所有的来自RAM中JSON文件的数据。这只能解决问题。
然后我决定在进入循环之前通过执行[
来尝试从流的开头取一个字符(以消除sr.Read()
)。然后第一个对象成功读取,但后续的对象不会,“意外令牌”除外。我的猜测是这是抛出阅读器的物体之间的逗号和空格。
简单地删除方括号将不起作用,因为对象确实包含它们自己的基元列表,如示例中所示。即使尝试使用},
作为分隔符也行不通,因为您可以看到,对象中有子对象。
我的目标是能够一次一个地从流中读取对象。读取一个对象,用它做一些事情,然后从RAM中丢弃它,然后读取下一个对象,依此类推。这样就不需要将整个JSON字符串或数据的全部内容作为C#对象加载到RAM中。
我错过了什么?
答案 0 :(得分:21)
这应该可以解决您的问题。基本上它就像你的初始代码一样,除了它只是当读者点击流中的{
字符时反序列化对象,否则它只是跳到下一个,直到它找到另一个起始对象令牌。
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);
}
}
}
答案 1 :(得分:0)
我认为我们可以做的比接受的答案更好,可以使用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);
}
}
}
}
您关注的数据位于路径上
[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转储中提取特定数据非常有用。
答案 2 :(得分:-2)
这是你正在寻找的吗? Found on a previous question
当前版本的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);
}
}