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文件,内容为三个对象组成的数组,每个对象包含foo和bar两个字段:
- foo: 1 bar: 10 - foo: 3 bar: 100 - foo: 1 bar: 12.2 执行分组命令
yq 'group_by(.foo)' sample.yml2.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: 10在bar: 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.yml3.3 输出结果
- - cat: dog - no: foo for you - - foo: 1 bar: 10 - foo: 1 bar: 1 - - foo: 3 bar: 100结果解读:
- 两个缺少
foo字段的元素被归入同一个 null 分组,且该分组排在输出最前面——因为它对应的 key 在遍历中首次出现; foo: 1和foo: 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 }这段代码揭示了三个关键实现细节:
- 求值每个元素:对数组中的每个子节点,以该节点为上下文求值分组表达式(
rhsExp),拿到匹配结果; - 取第一个匹配值作为分组 key:
rhs.MatchingNodes.Front()即第一个匹配节点,其Value字符串被用作分组标识;若没有任何匹配(字段缺失),key 默认回落为字符串"null"——这正是第三节中缺字段元素被归入同一组的原因; - 保持插入顺序:分组容器使用
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: 1005.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 field | group_by(.foo) | 按字段分组,组内保持原顺序,foo: 1组在前 |
Group splat | group_by(.foo)[] | 展开后逐组输出,路径分别为P[0]、P[1] |
Group by field, with nulls | group_by(.foo) | 缺字段元素统一落入 null 组,且排在首位 |
这些用例通过testScenario框架断言输出的节点路径(如D0, P[])、类型标签(!!seq)与精确的 YAML 内容,为group_by的行为提供了可回归验证的保障。
七、小结
group_by是 yq 处理数组分组任务的核心工具,掌握以下要点即可熟练使用:
- 语法:
group_by(<表达式>),输入必须是数组,输出是“数组的数组”; - null 分组:表达式求值为空(字段缺失)的元素自动归入以
null为 key 的分组; - 顺序语义:分组与组内元素均保持首次出现顺序,不做排序;
- 组合用法:可配合
[]展开分组,可按tag、length等任意表达式分组; - 实现原理:底层基于有序 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),仅供参考