yq group_by 操作符详解:按表达式对数组元素分组
2026/9/14 4:06:11 网站建设 项目流程

yq group_by 操作符详解:按表达式对数组元素分组

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

group_by是 yq 中用于按指定表达式对数组内元素进行分组的核心操作符,它将数组中具有相同表达式求值结果的元素聚拢到同一个子数组,适用于数据归类、去重统计、报表聚合等场景。读完本文,你将掌握group_by的完整语法、空值处理规则、分组顺序语义,并通过源码级剖析理解其底层实现原理与测试验证方式。

一、group_by 是什么

group_by操作符用于按一个表达式对数组中的条目进行分组(group items in an array by an expression)。它的使用形式为:

yq 'group_by(<表达式>)' <文件>
  • 输入必须是一个数组(Sequence),group_by会对数组中的每个元素求值<表达式>,将求值结果相同的元素归入同一组;
  • 输出是一个新的数组,其每个元素都是一个子数组,代表一个分组;
  • 分组的顺序遵循首次出现顺序:哪个 key 第一次出现,其分组就排在前面。

在 yq 操作符文档中,group_by的官方说明位于 pkg/yqlib/doc/operators/group-by.md,其行为语义与 jq 的group_by保持一致。

二、基础用法:按字段分组

2.1 准备数据

假设有一个sample.yml文件,内容为三个对象组成的数组,每个对象包含foobar两个字段:

- foo: 1 bar: 10 - foo: 3 bar: 100 - foo: 1 bar: 1

2.2 执行分组命令

yq 'group_by(.foo)' sample.yml

2.3 输出结果

- - foo: 1 bar: 10 - foo: 1 bar: 1 - - foo: 3 bar: 100

结果解读:

  • 所有foo == 1的元素({foo: 1, bar: 10}{foo: 1, bar: 1})被归入第一个子数组;
  • foo == 3的元素({foo: 3, bar: 100})单独构成第二个子数组;
  • 分组内部保留了元素在原始数组中的相对顺序(bar: 10bar: 1之前,与原数组顺序一致)。

三、null 值处理:字段缺失的元素如何分组

当数组中的某些元素不包含分组表达式中引用的字段时,这些元素的表达式求值结果为空,group_by会将它们统一归入以null为 key 的分组。

3.1 准备数据

- cat: dog - foo: 1 bar: 10 - foo: 3 bar: 100 - no: foo for you - foo: 1 bar: 1

这里{cat: dog}{no: foo for you}两个元素都没有foo字段。

3.2 执行同样的命令

yq 'group_by(.foo)' sample.yml

3.3 输出结果

- - cat: dog - no: foo for you - - foo: 1 bar: 10 - foo: 1 bar: 1 - - foo: 3 bar: 100

结果解读:

  • 两个缺少foo字段的元素被归入同一个 null 分组,且该分组排在输出最前面——因为它对应的 key 在遍历中首次出现;
  • foo: 1foo: 3的分组随后依次排列;
  • 分组 key 使用表达式求值的第一个匹配节点的值作为标识,缺字段时统一回落为字符串"null"

四、源码原理:分组到底是怎么实现的

group_by的完整实现位于 pkg/yqlib/operator_group_by.go,核心逻辑分为两层:入口的groupBy函数与逐元素分组的processIntoGroups函数。

4.1 入口检查:只支持数组

for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) if candidate.Kind != SequenceNode { return Context{}, fmt.Errorf("only arrays are supported for group by") } ... }

groupBy中,yq 会遍历所有匹配到的候选节点,一旦发现当前节点不是 Sequence(数组)类型,立即返回错误only arrays are supported for group by。这意味着对普通对象或标量执行group_by会直接失败,这是与 jq 行为对齐的语义。

4.2 逐元素求值分组

func processIntoGroups(d *dataTreeNavigator, context Context, rhsExp *ExpressionNode, node *CandidateNode) (*orderedmap.OrderedMap, error) { var newMatches = orderedmap.NewOrderedMap() for _, child := range node.Content { rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(child), rhsExp) ... keyValue := "null" if rhs.MatchingNodes.Len() > 0 { first := rhs.MatchingNodes.Front() keyCandidate := first.Value.(*CandidateNode) keyValue = keyCandidate.Value } groupList, exists := newMatches.Get(keyValue) if !exists { groupList = list.New() newMatches.Set(keyValue, groupList) } groupList.(*list.List).PushBack(child) } return newMatches, nil }

这段代码揭示了三个关键实现细节:

  1. 求值每个元素:对数组中的每个子节点,以该节点为上下文求值分组表达式(rhsExp),拿到匹配结果;
  2. 取第一个匹配值作为分组 keyrhs.MatchingNodes.Front()即第一个匹配节点,其Value字符串被用作分组标识;若没有任何匹配(字段缺失),key 默认回落为字符串"null"——这正是第三节中缺字段元素被归入同一组的原因;
  3. 保持插入顺序:分组容器使用github.com/elliotchance/orderedmap的有序 Map,key 第一次出现的位置即决定了该分组在最终输出中的顺序,因此输出顺序严格遵循“首次出现顺序”而非排序顺序。

4.3 组装输出结构

resultNode := candidate.CreateReplacement(SequenceNode, "!!seq", "") for groupEl := newMatches.Front(); groupEl != nil; groupEl = groupEl.Next() { groupResultNode := &CandidateNode{Kind: SequenceNode, Tag: "!!seq"} groupList := groupEl.Value.(*list.List) for groupItem := groupList.Front(); groupItem != nil; groupItem = groupItem.Next() { groupResultNode.AddChild(groupItem.Value.(*CandidateNode)) } resultNode.AddChild(groupResultNode) }

groupBy会为每个分组创建一个!!seq类型的子数组节点,元素按原顺序PushBack进分组;再将这些分组依次挂到结果数组下,最终得到“数组的数组”这一输出形态。

五、进阶用法与技巧

5.1 分组后展开子数组

group_by的输出是嵌套数组,如需按组逐个处理,可以配合[](splat 操作符)展开。这一用法在 operator_group_by_test.go 的Group splat场景中有明确验证:

yq 'group_by(.foo)[]' sample.yml

输出会按组分别列出:

- foo: 1 bar: 10 - foo: 1 bar: 1 --- - foo: 3 bar: 100

5.2 分组表达式不限于字段

group_by的表达式参数可以是任意合法表达式,例如按tag(节点类型标签)、length(长度)、key(键名)甚至复合表达式分组,输出形态完全一致,只是分组依据不同。

5.3 与 unique_by 的对比

  • group_by:把相同 key 的元素聚成子数组,完整保留所有元素;
  • unique_by:只保留每组第一个元素,其余丢弃,用于去重。

两者在实现上高度相似:unique_by的实现(pkg/yqlib/operator_unique.go)同样遍历数组元素、求值表达式、以结果为 key 存入有序 Map,区别仅在于unique_by遇到重复 key 时不再追加元素。若想了解去重场景,可参考 pkg/yqlib/doc/operators/unique.md。

5.4 多文档输入

group_by会逐个处理上下文中的所有匹配节点,因此面对多文档(---分隔)输入时,会对每个文档中的数组分别执行分组,输出同样按文档顺序排列。

六、测试验证:行为由测试用例锚定

group_by的三种典型行为均有对应的单元测试,位于 pkg/yqlib/operator_group_by_test.go:

测试场景表达式验证点
Group by fieldgroup_by(.foo)按字段分组,组内保持原顺序,foo: 1组在前
Group splatgroup_by(.foo)[]展开后逐组输出,路径分别为P[0]P[1]
Group by field, with nullsgroup_by(.foo)缺字段元素统一落入 null 组,且排在首位

这些用例通过testScenario框架断言输出的节点路径(如D0, P[])、类型标签(!!seq)与精确的 YAML 内容,为group_by的行为提供了可回归验证的保障。

七、小结

group_by是 yq 处理数组分组任务的核心工具,掌握以下要点即可熟练使用:

  1. 语法group_by(<表达式>),输入必须是数组,输出是“数组的数组”;
  2. null 分组:表达式求值为空(字段缺失)的元素自动归入以null为 key 的分组;
  3. 顺序语义:分组与组内元素均保持首次出现顺序,不做排序;
  4. 组合用法:可配合[]展开分组,可按taglength等任意表达式分组;
  5. 实现原理:底层基于有序 Map 实现 key 聚合(operator_group_by.go),与unique_by共用同一套“按表达式求 key”的思想。

在实际的配置管理与数据处理流水线中,group_by常被用于按环境、按服务名、按状态字段聚合配置项,再配合其他操作符做进一步加工,是 yq 表达式体系中复用率极高的基础能力。

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询