iter.json:在 Go 中迭代和操作 JSON 的强大而有效的方法

🌾
前言: 您是否曾经需要在 Go 中修改非结构化 JSON 数据?也许你不得不删除密码和所有列入黑名单的字段,将键从 camelCase 重命名为 snake_case,或者将所有数字 ID 转换为字符串,因为 JavaScript 不喜欢 int64?如果你的解决方案是使用 encoding/json 将所有内容解组到 map[string]any 中,然后将其封组回来......好吧,让我们面对现实吧,这远非高效!
如果您可以遍历 JSON 数据,获取每个项目的路径,并动态决定如何处理它,那会怎么样?
是的!我有个好消息!借助 Go 1.23 中新的迭代器功能,有更好的方法来迭代和操作 JSON。认识 ezpkg.io/iter.json — 您在 Go 中使用 JSON 的强大而高效的伙伴。

世界您好!

ezpkg.io/iter.json 是一个 Go 包,它提供了一种简单而有效的方法来迭代 JSON 数据。它允许您遍历 JSON 对象、数组和值,并对它们执行各种操作,而无需完全解析数据。
它的核心是一个 Parse() 函数和一个 Builder 类型。该函数返回一个迭代器,该迭代器生成 JSON 数据中的每个项目,而该类型允许您动态构建新的 JSON 数据。
让我们看一些如何使用 iter.json 在 Go 中迭代、构建、格式化、过滤和编辑 JSON 数据的示例。

1. 迭代 JSON

假设我们有一个 alice.json 文件:
首先,让我们使用 Parse() 遍历 JSON 文件,然后打印每个项目的路径、键、令牌和级别。参见 examples/01.iterfor range
代码将输出:

2. 构建 JSON

用于构建 JSON 数据。它接受用于缩进的可选参数。请参阅 examples/02.builderBuilder
  • 使用 创建新 .BuilderNewBuilder(prefix, indent string)
  • Builder.AddRaw(key RawToken, token RawToken)
    • 将原始令牌添加到 JSON 数据中。
  • Builder.Add(key any, token any)
    • 将键值对添加到 JSON 数据中。
  • Builder.Bytes()
    • 以字节切片的形式返回 JSON 数据。
  • 它接受各种类型,包括 、 等。stringintstruct[]byte
这将输出带有缩进的 JSON:

3. 格式化 JSON

您可以通过将 JSON 数据的键和值发送到 .参见 examples/03.reformatBuilder
第一个示例缩小 JSON,而第二个示例在每行使用前缀 “👉” 对其进行格式化。

4. 添加行号

在此示例中,我们通过在调用前添加 a 来将行号添加到 JSON 输出中。参见 examples/04.line_numberb.WriteNewline()fmt.Fprintf()
这将输出:

5. 添加评论

通过在 和 之间放置 ,您可以在每行的末尾添加注释。参见 examples/05.comment
fmt.Fprintf(comment)b.WriteComma()b.WriteNewline()
这将输出:

6. 筛选 JSON 并提取值

有 和 来获取当前项目的路径。您可以使用它们来筛选 JSON 数据。参见 examples/06.filter_printitem.GetPathString()item.GetRawPath()
带有 和 的示例 :item.GetPathString()regexp
带有 和 的示例 :item.GetRawPath()path.Match()
这两个示例都将输出:

7. 筛选 JSON 并返回新的 JSON

通过将 与 选项和筛选逻辑结合使用,您可以筛选 JSON 数据并返回新的 JSON。查看示例/07.filter_jsonBuilderSetSkipEmptyStructures(false)
此示例将返回仅包含筛选字段的新 JSON:

8. 编辑值

这是编辑 JSON 数据中的值的示例。假设我们为 API 使用数字 ID。ID 太大,JavaScript 无法处理它们。我们需要将它们转换为字符串。请参阅 examples/08.number_id 和 order.json
迭代 JSON 数据,找到所有字段并将数字 ID 转换为字符串:_id
这将向数字 ID 添加引号:

它如何解析 JSON 数据

这是得益于 Go 1.23 中强大的迭代器,ezpkg.io/iter.json 能够以最少的代码行和较低的开销处理 JSON 数据。
核心解析器逻辑包含在 2 个文件中:scanner.go 和 parser.go。以下是其工作原理的简要概述:
  • Parse() 是一个带有堆栈的状态机。它从 Importing 中提取下一个 token,然后根据当前状态对其进行处理。

NextToken() 从输入中提取下一个 Token

以下是 NextToken() 函数 (scanner.go) 的核心逻辑:

Scan() 在单个循环中的所有标记

Scan() 函数本质上是一个循环,每次都从输入中提取下一个标记。

Parse() 是一个带有堆栈的状态机

这是解析器 (parse.go) 的核心逻辑。
  • 它使用堆栈来跟踪 JSON 数据的当前状态(路径、级别)。
  • 它从输入中提取下一个令牌,并根据当前状态对其进行处理:
    • 如果是 或 ,则将当前状态推送到堆栈。[{
    • 如果是 或 ,则从堆栈中弹出状态。]}
    • 否则,它会根据当前状态解析 “value” 或 “key: value”。
以下是它初始化堆栈的方式:
通过 PathItem 的实现:
和 , , 辅助函数:push()pop()advance()
核心状态机代码如下。老实说,在这种情况下使用非常有趣:goto

如何动态构建 JSON 数据

基本上只使用单个方法实现 Builder 生成有效的 JSON 也是一个有趣的挑战!Add(key any, value any)

从 RawToken 重建 JSON

首先,让我们看看如何在不使用 .这是 Reconstruct() 最简单的实现,它生成一个缩小的 JSON:RawTokenBuilder
  • 它迭代结果以检索密钥和令牌。Parse()
  • 令牌可以是 、 或 值。请注意,和 不是由 返回的。[{]},:Parse()
  • 它将密钥和令牌写入缓冲区,并在必要时添加逗号。
  • 要在令牌之间正确添加逗号,它需要跟踪最后一个令牌类型并调用 .ShouldAddComma()
函数:ShouldAddComma()
  • 跳过逗号,最后一个标记是 、 、 或 ,或者下一个标记是 或 。[{,:]}
  • 否则,请添加逗号。

如何支持缩进

为了支持缩进,我们需要跟踪当前级别并在每行前添加适当数量的空格。以下是我们如何修改函数以支持缩进:Reconstruct()
  • Add 和 arguments 添加到函数中。prefixindent
  • 在每行之前添加 。prefix
  • 添加 for each level 的缩进。indent
  • 使用 from the result 确定缩进级别。LevelParse()
或者,我们可以通过为每个 [{ 和 ]} 递增和递减一个计数器来跟踪级别。我们还可以使用 stack 来跟踪级别和当前路径。
下面是修改后的函数,支持缩进,如 Reformat():

Builder 的早期实施

所以你明白了 / 函数是如何工作的。现在,让我们看看 是如何实现的。Reconstruct()Reformat()Builder
它从向 JSON 数据添加原始令牌的方法开始。以下是早期的实现AddRaw()
代码与 Code 基本相同,但有一些不同:Reformat()
  • 它跟踪最后一个标记类型、当前级别和 和 的堆栈。[{
  • 要跟踪级别,它需要打开或关闭令牌以更新堆栈和级别。
    • 而不是像 .Key.IsValue()Reformat()
  • 它将密钥和令牌写入缓冲区,并在必要时添加逗号。

扭曲代码以支持更多用例Builder

随着更多用例的添加,代码会随着时间的推移而发展并变得更加复杂。Builder
WriteNewline() 被公开以控制 prefix 的位置。
示例添加行号中,请注意我们有一个 b.WriteNewLine(item.Token.Type()) 调用。fmt.Fprintf()
这是因为我们需要控制行号的位置:
  • 所以行号可以在逗号和换行符后添加,
  • 但在 key 和 token 之前
WriteNewline()是可选的。
  • 如果您不包含它,则 next 将自动调用它。b.Add()
  • 如果你这样做了,它将被记住并跳过电话。b.Add()
它也很聪明。如果 没有配置缩进,则不会添加任何换行符。它不会添加第一个换行符,也不会添加两个换行符。Builder
这样,API 变得更加灵活:易于使用,同时仍允许更高级的用例
和 WriteComma() 进行注释。
这同样适用于 .它在 添加注释 示例中用于控制注释的位置。WriteComma()
  • 与 , 一样,是可选的且很智能。它只会在必要时添加逗号,以始终生成有效的 JSON。WriteNewline()WriteComma()
  • 因此,如果你调用 then ,它实际上不会添加任何逗号,否则 JSON 将变得无效。Add("", TokenObjectOpen)WriteComma()
SetSkipEmptyStructures(true) 忽略空结构。
在 Filtering JSON and returning a new JSON 示例中,我们用于忽略空结构。SetSkipEmptyStructures(true)
如果没有此选项,则会将空 or 添加到输出 JSON 中。尝试删除它,输出将变为:Builder{}[]
请注意该字段的空值。但它是如何工作的呢?[]scores
  • 要使其正常工作,不应在收到令牌时立即写入 the,因为它不知道里面是否有任何项目。Builder"scores": [[
  • 相反,它会写入备用缓冲区。并保存当前状态的快照。
  • 下次编辑新令牌时,如果它为空,则将清除备用缓冲区,恢复上一个快照,并跳过空字段。Add]"scores"
  • 否则,它会切换回主缓冲区,包括备用缓冲区的内容。
  • 这样,就可以以最小的开销跳过空结构。Builder
下面是使用 和 的方法的更新实现add()switchAltBuf()restore(snapshot)

所有测试均通过

该软件包以良好的测试开始:
这是第一个版本,因此仍有改进的空间,例如模糊测试或基准测试。但是这些测试是确保 package 的核心 logic 按预期工作的良好起点。

缺失的功能和未来的工作

这仅仅是个开始。还有更多功能和改进可以添加到包中:
  • 查询复杂值:如对象数组、嵌套对象等。
  • 支持读取器/写入器:处理大型 JSON 数据。
  • 支持 JSONL:处理以行分隔的 JSON。
  • 支持 ProtoBuf JSON:处理来自 ProtoBuf 的 JSON 数据。
  • 易于使用的 API:用于处理筛选、转换等常见用例。
  • 更多示例:演示如何在实际场景中使用该包。
  • Optimize, benchmark, and fuzz: 确保软件包高效可靠。
  • 还有更多...
如果您有任何想法或建议,请随时打开问题拉取请求。我很想听听您的反馈并帮助支持您的使用案例!

结论

ezpkg.io/iter.json 软件包使 Go 开发人员能够精确高效地处理 JSON 数据。无论您是需要迭代复杂的 JSON 结构、动态构建新的 JSON 对象、格式化或缩小数据、筛选特定字段,还是转换值,iter.json 都能提供灵活而强大的解决方案。
 
🌾
来自:Oliver NguyenOliver Nguyeniter.json: A Powerful and Efficient Way to Iterate and Manipulate JSON in Go
 
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

Previous

结构化提示词到底是什么?Deepseek V3.1

Next

三阶段学习即兴表达