c#中如何对这个json数据进行筛选?
3 个回答
Working with JSON
JSON 已成为 XML 的流行替代品。虽然它缺少 XML 的高级功能(例如名称空间、前缀和模式),但它的好处是简单和整洁,其格式类似于将 JavaScript 对象转换为字符串所获得的格式。
过去,.NET 没有对 JSON 的内置支持,您必须依赖第三方库——主要是 Json.NET 。尽管情况已不再如此,但 Json.NET 库仍然很受欢迎,原因有很多:
- 自 2011 年以来一直存在。
- 相同的 API 也可以在较旧的 .NET 平台上运行。
- 它被认为比 Microsoft JSON API 更实用(至少在过去如此)。
Microsoft JSON API 的优势是从头开始设计为简单且极其高效。此外,从 .NET 6 开始,它们的功能已变得非常接近 Json.NET 。
在本节中,我们将介绍以下内容:
- 只进读取器和写入器(Utf8JsonReader 和 Utf8JsonWriter)
- JsonDocument 只读 DOM 读取器
- JsonNode 读/写 DOM 读取器/写入器
在线补充 http://www. albahari.com/nutshell 的 Serialization 一节,我们介绍了 JsonSerializer,它自动将 JSON 序列化和反序列化为类。
Utf8JsonReader
System.Text.Json.Utf8JsonReader 是针对 UTF-8 编码的 JSON 文本的优化的只读阅读器。从概念上讲,它类似于本章前面介绍的 XmlReader,并且使用方式也大致相同。
考虑以下名为 people.json 的 JSON 文件:
{
"FirstName":"Sara",
"LastName":"Wells",
"Age":35,
"Friends":["Dylan","Ian"]
}
大括号表示一个 JSON 对象(其中包含“名字”和“姓氏”等属性),而方括号表示一个 JSON 数组(其中包含重复元素)。在这种情况下,重复元素是字符串,但它们可以是对象(或其他数组)。
以下代码通过枚举文件的 JSON 标记来解析文件。标记是对象的开头或结尾、数组的开头或结尾、属性的名称或数组或属性值(字符串、数字、真、假或空):
byte[] data = File.ReadAllBytes ("people.json");
Utf8JsonReader reader = new Utf8JsonReader (data);
while (reader.Read())
switch (reader.TokenType)
case JsonTokenType.StartObject:
Console.WriteLine ($"Start of object");
break;
case JsonTokenType.EndObject:
Console.WriteLine ($"End of object");
break;
case JsonTokenType.StartArray:
Console.WriteLine();
Console.WriteLine ($"Start of array");
break;
case JsonTokenType.EndArray:
Console.WriteLine ($"End of array");
break;
case JsonTokenType.PropertyName:
Console.Write ($"Property: {reader.GetString()}");
break;
case JsonTokenType.String:
Console.WriteLine ($" Value: {reader.GetString()}");
break;
case JsonTokenType.Number:
Console.WriteLine ($" Value: {reader.GetInt32()}");
break;
default:
Console.WriteLine ($"No support for {reader.TokenType}");
break;
这是输出:
Start of object
Property: FirstName Value: Sara
Property: LastName Value: Wells
Property: Age Value: 35
Property: Friends
Start of array
Value: Dylan
Value: Ian
End of array
End of object
因为 Utf8JsonReader直接与 UTF-8 一起工作,它无需首先将输入转换为 UTF-16(.NET 字符串的格式)即可遍历标记。仅当您调用诸如 GetString() 之类的方法时,才会转换为 UTF-16。
有趣的是,Utf8JsonReader 的构造函数不接受字节数组,而是接受 ReadOnlySpan<byte>(因此,Utf8JsonReader 被定义为 ref 结构)。您可以传入字节数组,因为存在从 T[] 到 ReadOnlySpan<T> 的隐式转换。在第 23 章中,我们描述了 span 是如何工作的,以及如何使用它们通过最小化内存分配来提高性能。
JsonReaderOptions
默认情况下,Utf8JsonReader 要求 JSON 严格符合 JSON RFC 8259 标准。您可以通过传递一个JsonReaderOptions 的实例到 Utf8JsonReader 构造函数指示 Reader 更宽容 。选项允许以下内容:
C-Style comments
默认情况下,JSON 中的注释会导致抛出 JsonException。设置 JsonCommentHandling.Skip 的 CommentHandling 属性导致注释
comments
被忽略,而 JsonCommentHandling.Allow 会导致Reader 识别它们并在它们出现时发出 JsonTokenType.Comment 标记
token
。评论不能出现在其他标记的中间。
Trailing commas
根据标准,对象的最后一个属性和数组的最后一个元素不得有尾随逗号。将AllowTrailingCommas 属性设置为
e
放宽了这个限制。
Control over the maximum nesting depth
默认情况下,对象和数组可以嵌套到 64 层。将 MaxDepth 设置为不同的数字会覆盖此设置。
Utf8JsonWriter
System.Text.Json.Utf8JsonWriter 是一个只进的 JSON 编写器。它支持以下类型:
- String 和 DateTime(格式为 JSON 字符串)
- 数值类型 Int32、UInt32、Int64、UInt64、Single、Double 和 Decimal(格式为 JSON 数字
- bool(格式为JSON true/false 文字)
- JSON null
- 数组
您可以根据 JSON 标准将这些数据类型组织到对象中。它还允许您编写注释,这些注释不是 JSON 标准的一部分,但在实践中通常由 JSON 解析器支持。
下面的代码演示了它的使用:
var options = new JsonWriterOptions { Indented = true };
using (var stream = File.Create ("MyFile.json"))
using (var writer = new Utf8JsonWriter (stream, options))
writer.WriteStartObject();
// Property name and value specified in one call
writer.WriteString ("FirstName", "Dylan");
writer.WriteString ("LastName", "Lockwood");
// Property name and value specified in separate calls
writer.WritePropertyName ("Age");
writer.WriteNumberValue (46);
writer.WriteCommentValue ("This is a (non-standard) comment");
writer.WriteEndObject();
这会生成以下输出文件:
{
"FirstName": "Dylan",
"LastName": "Lockwood",
"Age": 46
/*This is a (non-standard) comment*/
}
从 .NET 6 开始,Utf8JsonWriter 有一个 WriteRawValue 方法,可以将字符串或字节数组直接发送到 JSON 流中。这在特殊情况下很有用——例如,如果您希望写入的数字始终包含小数点(1.0 而不是 1)。
在此示例中,我们将 JsonWriterOptions 上的 Indented 属性设置为 true 以提高可读性。如果我们不这样做,输出将如下所示:
{"FirstName":"Dylan","LastName":"Lockwood","Age":46...}
JsonWriterOptions 还有一个 Encoder 属性来控制字符串的转义,以及一个 SkipValidation 属性来允许绕过结构验证检查(允许发出无效输出 JSON)。
JsonDocument
System.Text.Json.JsonDocument 将 JSON 数据解析为由按需生成的 JsonElement 实例组成的只读 DOM。与 Utf8JsonReader 不同,JsonDocument 允许您随机访问元素。
JsonDocument 是用于处理 JSON 的两个基于 DOM 的 API 之一,另一个是 JsonNode(我们将在下一节中介绍)。 JsonNode是在.NET 6中引入的,主要是为了满足可写DOM的需求。然而,它也适用于只读场景,并公开了一个更流畅的接口,由使用 JSON 值、数组和对象类的传统 DOM 支持。相比之下,JsonDocument 非常轻量级,仅包含一个注释类(JsonDocument)和两个按需解析底层数据的轻量级结构体(JsonElement 和 JsonProperty)。区别如图 11-1 所示。
<aside> JsonDocument 通过使用池化内存来最大限度地减少垃圾收集,进一步提高了效率。这意味着您必须在使用后处理 JsonDocument;否则,它的内存将不会返回到池中。因此,当类在字段中存储 JsonDocument 时,它还必须实现 IDisposable。如果这很麻烦,请考虑改用 JsonNode。
</aside>
静态 Parse 方法从流、字符串或内存缓冲区实例化 JsonDocument:
using JsonDocument document = JsonDocument.Parse (jsonString);
调用 Parse 时,您可以选择提供 JsonDocumentOptions 对象来控制尾随逗号、注释和最大嵌套深度的处理(有关这些如何实现的讨论选项起作用,请参阅第 546 页的“JsonReaderOptions”)。
从那里,您可以通过 RootElement 属性访问 DOM:
using JsonDocument document = JsonDocument.Parse ("123");
JsonElement root = document.RootElement;
Console.WriteLine (root.ValueKind); // Number
JsonElement 可以表示 JSON 值(字符串、数字、true/false、null)、数组或对象; 通过ValueKind 属性指示它们。
如果元素不是预期的类型,我们在以下部分中描述的方法将抛出异常。如果您不确定 JSON 文件的架构,您可以通过首先检查 ValueKind(或使用 TryGet* 方法)来避免此类异常。
JsonElement 还提供了两种适用于任何类型元素的方法:GetRawText() 返回内部 JSON,WriteTo 将该元素写入 Utf8JsonWriter。
Reading simple values
如果元素表示一个 JSON 值,则可以通过调用 GetString、GetInt32、GetBoolean 等获取其值:
using JsonDocument document = JsonDocument.Parse ("123");
int number = document.RootElement.GetInt32();
JsonElement 还提供了将 JSON 字符串解析为其他常用 CLR 类型的方法,例如 DateTime(甚至是 base-64 二进制) )。还有一些 TryGet* 版本可以避免在解析失败时抛出异常。
Reading JSON arrays
如果 JsonElement 表示一个数组,您可以调用以下方法:
EnumerateArray()
枚举 JSON 数组的所有子项(作为 JsonElements)。
GetArrayLength()
返回数组中元素的数量。
您还可以使用索引器返回特定位置的元素:
using JsonDocument document = JsonDocument.Parse (@"[1, 2, 3, 4, 5]");
int length = document.RootElement.GetArrayLength(); // 5
int value = document.RootElement[3].GetInt32(); // 4
Reading JSON objects
如果元素表示 JSON 对象,则可以调用以下方法:
EnumerateObject()
枚举对象的所有属性名称和值。
GetProperty (string propertyName)
按名称获取属性(返回另一个 JsonElement)。如果名称不存在则抛出异常。
TryGetProperty (string propertyName, out JsonElement value)
返回对象的属性(如果存在)。
例如:
using JsonDocument document = JsonDocument.Parse (@"{ ""Age"": 32}");
JsonElement root = document.RootElement;
int age = root.GetProperty ("Age").GetInt32();
以下是我们如何“发现”Age 属性:
JsonProperty ageProp = root.EnumerateObject().First();
string name = ageProp.Name; // Age
JsonElement value = ageProp.Value;
Console.WriteLine (value.ValueKind); // Number
Console.WriteLine (value.GetInt32()); // 32
JsonDocument and LINQ
JsonDocument 非常适合 LINQ。给定以下 JSON 文件:
[
"FirstName":"Sara",
"LastName":"Wells",
"Age":35,
"Friends":["Ian"]
"FirstName":"Ian",
"LastName":"Weems",
"Age":42,
"Friends":["Joe","Eric","Li"]
"FirstName":"Dylan",
"LastName":"Lockwood",
"Age":46,
"Friends":["Sara","Ian"]
]
我们可以使用 JsonDocument 通过 LINQ 查询它,如下所示:
using var stream = File.OpenRead (jsonPath);
using JsonDocument document = JsonDocument.Parse (json);
var query =
from person in document.RootElement.EnumerateArray()
select new
FirstName = person.GetProperty ("FirstName").GetString(),
Age = person.GetProperty ("Age").GetInt32(),
Friends =
from friend in person.GetProperty ("Friends").EnumerateArray()
select friend.GetString()
因为 LINQ 查询是惰性计算的,所以在文档超出范围之前枚举查询很重要,并且 JsonDocument 通过 using 语句隐式处理。
Making updates with a JSON writer
尽管 JsonDocument 是只读的,但您可以使用 WriteTo 方法将 JsonElement 的内容发送到 Utf8JsonWriter。这提供了一种用于发出 JSON 的修改版本的机制。以下是我们如何从前面的示例中获取 JSON 并将其写入一个新的 JSON 文件,该文件只包含有两个或更多朋友的人:
using var json = File.OpenRead (jsonPath);
using JsonDocument document = JsonDocument.Parse (json);
var options = new JsonWriterOptions { Indented = true };
using (var outputStream = File.Create ("NewFile.json"))
using (var writer = new Utf8JsonWriter (outputStream, options))
writer.WriteStartArray();
foreach (var person in document.RootElement.EnumerateArray())
int friendCount = person.GetProperty ("Friends").GetArrayLength();
if (friendCount >= 2)
person.WriteTo(writer);
但是,如果您需要更新 DOM 的能力,JsonNode 是更好的解决方案。
JsonNode
JsonNode(在 System.Text.Json.Nodes 中)是在 .NET 6 中引入的,主要是为了满足对可写 DOM 的需求。然而,它也适用于只读场景,并公开了一个更流畅的接口,由使用 JSON 值、数组和对象类的传统 DOM 支持(见图 11-1)。作为类,它们会产生垃圾收集成本,但这在大多数真实场景中可能可以忽略不计。 JsonNode 仍然是高度优化的,当重复读取相同的节点时实际上可以比 JsonDocument 更快(因为 JsonNode 虽然懒惰,但会缓存解析结果)。
静态 Parse 方法从流、字符串、内存缓冲区或 Utf8JsonReader 创建 JsonNode:
JsonNode node = JsonNode.Parse (jsonString);
调用 Parse 时,您可以选择提供 JsonDocumentOptions 对象来控制尾随逗号、注释和最大嵌套深度的处理(有关讨论这些选项如何工作,请参阅第 546 页的“JsonReaderOptions”)。与 JsonDocument 不同,JsonNode 不需要处置
disposal
。
在 JsonNode 上调用 ToString() 会返回人类可读(缩进)的 JSON 字符串。还有一个 ToJsonString() 方法,它返回一个紧凑的 JSON 字符串。
Parse 返回 JsonNode 的子类型,它将是 JsonValue、JsonObject 或 JsonArray。为了避免向下转型的混乱,JsonNode 提供了名为 AsValue()、AsObject() 和 AsArray() 的辅助方法:
var node = JsonNode.Parse ("123"); // Parses to a JsonValue
int number = node.AsValue().GetValue<int>();
// Shortcut for ((JsonValue)node).GetValue<int>();
但是,您通常不需要调用这些方法,因为最常用的成员都暴露在 JsonNode 上类本身:
var node = JsonNode.Parse ("123");
int number = node.GetValue<int>();
// Shortcut for node.AsValue().GetValue<int>();
Reading simple values
我们刚刚看到您可以通过使用类型参数调用 GetValue 来提取或解析一个简单的值。为了使这更容易,JsonNode 重载了 C# 的显式强制转换运算符,启用了以下快捷方式:
var node = JsonNode.Parse ("123");
int number = (int) node;
适用于此的类型包括标准数字类型:char、bool、DateTime、DateTimeOffset 和 Guid(及其可为 null 的版本),以及 string 。
如果不确定解析是否成功,则需要如下代码:
if (node.AsValue().TryGetValue<int>(out var number))
Console.WriteLine (number);
从 JSON 文本解析的节点在内部由 JsonElement(JsonDocument 只读 JSON API 的一部分)支持。您可以提取底层 JsonElement,如下所示: JsonElement je = node.GetValue<JsonElement>(); 但是,当节点被显式实例化时,这不起作用(当我们更新 DOM 时就是这种情况)。此类节点不是由 JsonElement 支持,而是由实际解析值支持(请参阅第 554 页的“使用 JsonNode 进行更新”)。
Reading JSON arrays
表示 JSON 数组的 JsonNode 将是 JsonArray 类型。
JsonArray 实现了 IList<JsonNode>,因此您可以枚举它并像访问数组或列表一样访问元素:
var node = JsonNode.Parse (@"[1, 2, 3, 4, 5]");
Console.WriteLine (node.AsArray().Count); // 5
foreach (JsonNode child in node.AsArray())
{ ... }
作为快捷方式,您可以直接从 JsonNode 类访问索引器:
Console.WriteLine ((int)node[0]); // 1
Reading JSON objects
表示 JSON 对象的 JsonNode 将是类型 JsonObject。
JsonObject 实现 IDictionary<string, JsonNode>,因此您可以通过索引器访问成员,以及枚举字典的键/值对。
与 JsonArray 一样,您可以直接从 JsonNode 类访问索引器:
var node = JsonNode.Parse (@"{ ""Name"":""Alice"", ""Age"": 32}");
string name = (string) node ["Name"]; // Alice
int age = (int) node ["Age"]; // 32
以下是我们如何“发现” Name 和 Age 属性:
// Enumerate over the dictionary’s key/value pairs:
foreach (KeyValuePair<string,JsonNode> keyValuePair in node.AsObject())
string propertyName = keyValuePair.Key; // "Name" (then "Age")
JsonNode value = keyValuePair.Value;
如果您不确定是否已定义属性,以下模式也适用:
if (node.AsObject().TryGetPropertyValue("Name", out JsonNode nameNode))
{ ... }
Fluent traversal and LINQ
您可以仅使用索引器深入到层次结构。例如,给定以下 JSON 文件:
[
"FirstName":"Sara",
"LastName":"Wells",
"Age":35,
"Friends":["Ian"]
"FirstName":"Ian",
"LastName":"Weems",
"Age":42,
"Friends":["Joe","Eric","Li"]
"FirstName":"Dylan",
"LastName":"Lockwood",
"Age":46,
"Friends":["Sara","Ian"]
]
我们可以提取第二个人的第三个朋友,如下所示:
string li = (string) node[1]["Friends"][2];
这样的文件也很容易通过 LINQ 查询:
JsonNode node = JsonNode.Parse (File.ReadAllText (jsonPath));
var query =
from person in node.AsArray()
select new
FirstName = (string) person ["FirstName"],
Age = (int) person ["Age"],
Friends = from friend in person ["Friends"].AsArray()
select (string) friend
与 JsonDocument 不同,JsonNode 不是一次性的,因此我们不必担心潜在的用于惰性枚举期间的处置
disposal
。
Making updates with JsonNode
JsonObject 和 JsonArray 是可变的,因此您可以更新它们的内容。
向 JsonObject 替换或添加属性的最简单方法是通过索引器。在下面的示例中,我们将 Color 属性的值从“Red”更改为“White”并添加一个名为“Valid”的新属性:
var node = JsonNode.Parse ("{ \\"Color\\": \\"Red\\" }");
node ["Color"] = "White";
node ["Valid"] = true;
Console.WriteLine (node.ToJsonString()); // {"Color":"White","Valid":true}
该示例中的第二行是以下内容的快捷方式:
node ["Color"] = JsonValue.Create ("White");
而不是为属性分配一个简单的值,您可以为它分配一个 JsonArray 或 JsonObject。 (我们将在下一节演示如何构造 JsonArray 和 JsonObject 实例。)
要删除一个属性,首先转换为 JsonObject(或调用 AsObject),然后调用 Remove 方法:
node.AsObject().Remove ("Valid");
(JsonObject 还暴露了一个 Add 方法,该方法会抛出异常如果该属性已存在。)
JsonArray 还允许您使用索引器替换项目:
var node = JsonNode.Parse ("[1, 2, 3]");
node[0] = 10;
调用 AsArray 公开 Add/Insert/Remove/RemoveAt 方法。在下面的示例中,我们删除数组中的第一个元素并在末尾添加一个:
var arrayNode = JsonNode.Parse ("[1, 2, 3]");
arrayNode.AsArray().RemoveAt(0);
arrayNode.AsArray().Add (4);
Console.WriteLine (arrayNode.ToJsonString()); // [2,3,4]
Constructing a JsonNode DOM programmatically
JsonArray 和 JsonObject 具有支持对象初始化语法的构造函数,这允许您在一个表达式中构建整个 JsonNode DOM:
var node = new JsonArray
new JsonObject {
["Name"] = "Tracy",
["Age"] = 30,
["Friends"] = new JsonArray ("Lisa", "Joe")
new JsonObject {
["Name"] = "Jordyn",
["Age"] = 25,
["Friends"] = new JsonArray ("Tracy", "Li")
这将计算为以下 JSON:
[
"Name": "Tracy",
"Age": 30,
"Friends": ["Lisa", "Joe"]