yq 的 unique 与 unique_by 操作符:数组去重、保持原序与按字段去重的完整实践
yq 的 unique 与 unique_by 操作符数组去重、保持原序与按字段去重的完整实践【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq本文聚焦 yq 项目中unique与unique_by两个操作符讲解如何对 YAML 数组按值、按结构、按指定字段进行去重并结合 operator_unique.go 的源码实现与 operator_unique_test.go 的测试场景说明其保持原始顺序、处理 null 差异、保留注释等关键行为背后的机制读完即可在数据清洗、配置合并等场景中熟练运用这两个操作符。一、操作符总览unique 与 unique_byunique用于过滤数组中的重复项原始数组的顺序会被保持。与之配套的是unique_by(表达式)它按给定表达式计算出的“键”对元素去重而不是按元素本身的值。从操作符注册表可以确认二者的定义见 operation.govar uniqueOpType operationType{Type: UNIQUE, NumArgs: 0, Precedence: 52, Handler: unique, CheckForPostTraverse: true} var uniqueByOpType operationType{Type: UNIQUE_BY, NumArgs: 1, Precedence: 52, Handler: uniqueBy, CheckForPostTraverse: true}两者都在词法分析层被识别为关键字见 lexer_participle.gosimpleOp(unique_?by, uniqueByOpType), simpleOp(unique, uniqueOpType),几个使用前提需要明确操作符只支持数组sequence作为输入。源码中显式校验若匹配到的节点不是SequenceNode会直接报错only arrays are supported for unique见 operator_unique.go。unique实际上是unique_by的语法糖其去重键就是元素自身。去重时保留的是首次出现的元素后续重复项被丢弃。二、对标量数组去重给定sample.yml- 2 - 1 - 3 - 2执行yq unique sample.yml输出- 2 - 1 - 3注意结果仍按原始顺序输出2, 1, 3而不是排序后的1, 2, 3——unique只做去重、不排序。这一点在测试用例uniqueOperatorScenarios的第一个场景中同样得到验证文档[2,1,3,2]经unique后期望值为D0, P[], (!!seq)::[2, 1, 3]见 operator_unique_test.go。三、null 的特殊行为按值去重 vs 按 tag 去重这是unique最容易踩坑的地方。unique工作在节点的文本值上因此同一逻辑 null 的不同字面表示~与null会被视为不同的元素给定sample.yml- ~ - null - ~ - null执行yq unique sample.yml输出- ~ - null两种写法各保留了一个看起来像是“没去干净”。此时可以用tag操作符作为去重键~与null的 YAML tag 都是!!null而 tag 是解析层而非文本层的信息因此可以“一次性去重所有 null”yq unique_by(tag) sample.yml输出- ~tag操作符本身的行为读取节点的!!map、!!str、!!int、!!null等标签见 tag.md 与 operator_tag.go 中的getTagOperator它直接把候选节点的candidate.Tag包装成一个字符串标量节点返回供unique_by取用。四、对对象数组与嵌套数组去重unique同样适用于对象和嵌套数组。对于非标量节点去重键不再是文本值而是把该子树序列化为 YAML 字符串后比较。给定sample.yml- name: harry pet: cat - name: billy pet: dog - name: harry pet: cat执行yq unique sample.yml输出- name: harry pet: cat - name: billy pet: dog嵌套数组同理- - cat - dog - - cat - sheep - - cat - dog执行yq unique sample.yml输出- - cat - dog - - cat - sheep从源码看这一行为由 getUniqueKeyValue 实现func getUniqueKeyValue(rhs Context) (string, error) { keyValue : null ... if rhs.MatchingNodes.Len() 0 { first : rhs.MatchingNodes.Front() keyCandidate : first.Value.(*CandidateNode) keyValue keyCandidate.Value if keyCandidate.Kind ! ScalarNode { keyValue, err encodeToString(keyCandidate, encoderPreferences{YamlFormat, 0}) } } return keyValue, err }要点标量节点直接取节点的文本值keyCandidate.Value——这正是“~和null值不同”的根源非标量节点对象、数组调用 encodeToString 以YamlFormat、缩进 0 序列化整个子树作为键因此对象去重等价于“两个子树渲染出的 YAML 文本相同”表达式无匹配结果时键默认取字符串null这条默认路径解释了下一节的字段缺失场景。五、unique_by按指定字段去重当只需要依据某个字段判定重复时用unique_by(表达式)。给定sample.yml- name: harry pet: cat - name: billy pet: dog - name: harry pet: dog执行yq unique_by(.name) sample.yml输出- name: harry pet: cat - name: billy pet: dog第三项虽然pet不同但name与第一项重复因此被丢弃——保留的始终是首个出现的元素。字段缺失时的行为测试中还有两个值得注意的边界场景均标记了skipDoc说明属于实现细节而非文档承诺见 operator_unique_test.go// {name: harry, pet: fish} 没有 name 字段与 {name: harry, pet: dog} 的键不同吗 // 答案没有 .name 时求值无匹配键统一退化为 null因此 {pet: fish} 会被当作 // 与任何“缺失字段”的元素同键此处它先于第二个 harry 出现所以保留 document: [{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}], expression: unique_by(.name), expected: []string{D0, P[], (!!seq)::[{name: harry, pet: cat}, {pet: fish}]\n}, // 对不存在的深层路径 .cat.dog 求值三个元素都无匹配、键均为 null只剩第一个 document: [{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}], expression: unique_by(.cat.dog), expected: []string{D0, P[], (!!seq)::[{name: harry, pet: cat}]\n},即求值不到值的元素会共享同一个null键彼此之间互为“重复”。使用unique_by时要确认所选表达式在每个元素上都能取到期望的值。与其他操作符组合unique/unique_by的结果仍是数组因此可以直接串联 splat[]或map等后续操作。测试中验证了两个组合写法operator_unique_test.goyq unique[] # 对 [2,1,2] 去重后展开为逐行输出 2、1 yq unique_by(.name)[] # 按字段去重后把各对象展开为独立结果此外去重时会保留序列节点上的注释。测试场景表明源文档首尾的注释# abc/# xyz会原样保留在去重结果两侧operator_unique_test.go。其实现对应 operator_unique.go 中构造结果节点时调用的candidate.CreateReplacementWithComments(SequenceNode, !!seq, candidate.Style)——新建的序列节点继承了原序列的样式与注释再把orderedmap中按首次出现顺序排列的子节点逐个挂接回去。六、实现原理unique 如何保证顺序与去重把 operator_unique.go 的核心逻辑串起来看语法糖展开unique的处理函数unique构造了一个以自引用表达式self对应 selfReferenceOpType为右操作数的虚拟unique_by表达式节点直接转入uniqueBy。也就是说unique与unique_by走的是完全相同的主流程区别仅在“键表达式”是self还是用户给定的表达式。逐子节点求键uniqueBy遍历数组candidate.Content中的每个子节点在context.SingleReadonlyChildContext(child)的上下文中对键表达式求值再由getUniqueKeyValue把求值结果归约为一个字符串键标量取文本值、复合节点取 YAML 序列化文本、无匹配取null。有序去重使用github.com/elliotchance/orderedmap维护key - 子节点的映射键不存在时newMatches.Set(keyValue, child)已存在则跳过。由于 orderedmap 按插入顺序遍历最终结果天然保持“首次出现”的原始顺序。重建序列节点以原序列的 tag!!seq与样式创建替换节点按 map 的遍历顺序挂接子节点作为新的匹配结果返回给上下文。该实现有两个从源码结构可以直接看出的工程取舍去重键的归一化依赖 YAML 序列化文本因此键比较发生在“渲染后的文本”层面——对嵌套结构足够直观结构相同即视为相同但也意味着两个对象若键值相同、键顺序不同其序列化文本不同从实现看会被判定为不重复这一点仓库文档未作明确承诺可视为从源码结构推断的行为。七、适用场景小结场景推荐写法说明标量/对象/嵌套数组按内容去重unique保留首次出现顺序复合节点按 YAML 序列化文本比较按某字段去重unique_by(.field)字段缺失的元素共享null键注意前置过滤归一化所有 null 表示unique_by(tag)~、null、~的 tag 均为!!null可一次去重去重后继续逐元素处理unique[]/unique_by(.field)[]结果仍为数组可串联 splat、map 等相关文档与代码unique 操作符文档、tag 操作符文档、group_by 文档同族操作符、实现源码、测试场景。【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考