Miller 全字段操作实战:批量重命名、全局搜索替换与$*记录重排
【免费下载链接】millerMiller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON项目地址: https://gitcode.com/gh_mirrors/mi/miller
本指南聚焦 Miller 中一类高频且易混淆的需求:不再针对单个字段名写死操作,而是批量作用于当前记录的全部字段——包括把空格批量替换为下划线、清洗带换行符的字段名、对每个字段做搜索替换、以及用映射字面量一次性重命名并重排整个记录。你将掌握两条互补的技术路线:rename动词(带-g/-r选项的正则批量重命名),以及基于 Miller 编程语言(DSL)的put脚本(配合$*、$[k]、gsub与 out-of-stream 变量@),并理解各自的适用场景与底层实现。
一切围绕$*:Miller 的“全字段”视角
Miller 处理的是 name-indexed 数据(如 CSV、TSV、DKVP 等),每条记录在 DSL 中是一个键值对集合。在put/filter的 DSL 语境下,三个关键符号构成了全字段操作的基础:
$*:当前整条记录本身,作为一个 map(映射)参与表达式运算,可整体读取、整体赋值;$[k]:以计算出的键k(变量或表达式)动态索引字段值,这是“遍历所有字段”循环体的核心;@name:out-of-stream 变量,跨记录持续存在,常用于累加器等需要跨行状态的场景。
因此,“对全部字段做 X”在 Miller 中通常有两种实现:一是命令行rename等动词直接提供批量语义;二是用 DSL 的for (k in $*) { ... }循环显式遍历。下面依次展开。
批量重命名字段:rename -g -r
假设列名中含有空格,希望把列名里的空格替换为下划线。示例数据 docs/src/data/spaces.csv 内容如下:
column 1,column 2,column 3 apple,ball,cat dale egg,fish,gale最简单的做法是使用mlr rename,配合-g与-r两个选项:
mlr --csv rename -g -r ' ,_' data/spaces.csv输出:
column_1,column_2,column_3 apple,ball,cat dale egg,fish,gale这里-r表示按正则模式匹配字段名(而不是显式逐列指定 old,new 对),-g表示在每个字段名内部做全局替换(而非只替换首次出现)。注意上例中替换模式是' ,_',即“把空格换成下划线”。
也可以换用 pprint 输出格式查看效果更直观:
mlr --csv --opprint rename -g -r ' ,_' data/spaces.csv输出:
column_1 column_2 column_3 apple ball cat dale egg fish gale-g与-r的语义与源码实现
从源码 pkg/transformers/rename.go 可以看到这两个选项的精确定义:
-r:把旧字段名当作正则表达式处理。例如"ab"、"a.*b"会分别匹配所有含子串ab或匹配a.*b的字段名,也可用"^ab$"、"^a.*b$"形式的锚定;新字段名可以是普通字符串,也可以包含\1到\9的捕获组。若用双引号包裹正则并在其后跟i,可表示大小写不敏感匹配(如'"name"i')。-g:在每个字段名内做全局替换(等价于 Go 的ReplaceAllString),而非首次匹配替换。
源码中一个容易被忽略的细节是(见 rename.go):
if doGsub { doRegexes = true }即-g会隐式开启正则模式(因为全局替换必然基于模式匹配实现)。在实际执行时(transformWithRegexes),-g走regex.ReplaceAllString(oldName, replacement)对所有匹配处替换;不带-g时则走lib.RegexCompiledSub,只替换每个字段名中的首个匹配。
rename动词的参数形式是逗号分隔的old1,new1,old2,new2,...对,个数必须为偶数,否则解析器会报names string must have even length(见 rename.go)。此外-r还支持捕获组重排,例如官方用法中列出的:
mlr rename -r 'Date_([0-9]+).*,\1' # 把 Date_20151015xxx 这类字段重命名为 20151015 mlr rename -r '"name"i,Name' # 把 name/Name/NAME 等统一改为 Name用 DSL for 循环实现同样的重命名
rename动词虽然直接,但只覆盖“改名”这一种变换。当你想在改名之外再叠加其他逻辑时,可以用put脚本遍历$*。官方示例脚本 docs/src/data/bulk-rename-for-loop.mlr 如下:
map newrec = {}; for (oldk, v in $*) { newrec[gsub(oldk, " ", "_")] = v; } $* = newrec执行:
mlr --icsv --opprint put -f data/bulk-rename-for-loop.mlr data/spaces.csv输出与上一种方式完全一致:
column_1 column_2 column_3 apple ball cat dale egg fish gale这段脚本的核心思路是:先声明一个空 mapnewrec,遍历$*(键为oldk、值为v),用gsub(oldk, " ", "_")生成新键,再把原值放入newrec,最后整体赋值$* = newrec。对比rename动词,这种写法把“字段名变换”完全交给了 DSL 的字符串函数,后续可自由扩展(例如同时修改值、过滤字段等)。
处理含换行符的字段名:先清洗再赋值
上一节的方案并不足以应付字段名本身包含换行符(carriage return / line feed)的情形——CSV 字段名被引号包裹且内部含\n时,rename -r的正则对\n的处理比较棘手。此时应回到 Miller 编程语言,用put完成“取记录 → 清洗键 → 重建记录”三步。
示例数据 docs/src/data/header-lf.csv(第一个列名是"field\nA",即引号内带换行):
"field A",field B 1,2 3,3 6,6执行:
mlr --csv --from data/header-lf.csv put ' map inrec = $*; $* = {}; for (oldkey, value in inrec) { newkey = clean_whitespace(gsub(oldkey, "\n", " ")); $[newkey] = value; } '输出:
field A,field B 1,2 3,3 6,6要点拆解:
map inrec = $*;:先把当前记录整体快照到一个局部 map,避免在循环中边遍历边修改$*;$* = {};:清空当前记录,准备重建;for (oldkey, value in inrec) { ... }:遍历快照中的每一对键值;newkey = clean_whitespace(gsub(oldkey, "\n", " "));:先用gsub把字段名里的换行符替换为空格,再用clean_whitespace收敛空白(trim 与折叠多余空白),得到干净的新键名;$[newkey] = value;:以计算出的新键写入当前记录,这正是$[k]动态下标能力的典型用法。
全字段搜索替换:$[k]动态下标
把$name = gsub($name, "old", "new")推广到所有字段,本质就是上节循环思想的复用——只不过这次变换作用在值上,且不需要重建记录(直接改写原键对应的值即可)。
示例数据 docs/src/data/sar.csv:
a,b,c the quick,brown fox,jumped over,the,lazy dogs官方脚本 docs/src/data/sar.mlr 只有三行:
for (k in $*) { $[k] = gsub($[k], "e", "X"); }执行:
mlr --csv put -f data/sar.mlr data/sar.csv输出:
a,b,c thX quick,brown fox,jumpXd ovXr,thX,lazy dogs这里for (k in $*)遍历的是当前记录的键,循环体内$[k] = gsub($[k], "e", "X")同时用到动态读($[k]取值)与动态写($[k]赋值)。注意for (k in $*)与for (k, v in $*)的差异:前者只遍历键,适合此类“键已确定、只改值”的场景,写法更简洁。
整体重命名与重排:$* = 映射字面量
借助 Miller 5.0.0 引入的 map 字面量语法与$*整体赋值,你可以完全泛化rename、reorder等动词——不止改名、不止换序,甚至可以同时重排、改名、删字段、增字段,并让新字段依赖其他字段的值与跨记录的 out-of-stream 状态。
示例数据 docs/src/data/small(DKVP 格式):
a=pan,b=pan,i=1,x=0.346791,y=0.726802 a=eks,b=pan,i=2,x=0.758679,y=0.522151 a=wye,b=wye,i=3,x=0.204603,y=0.338318 a=eks,b=wye,i=4,x=0.381399,y=0.134188 a=wye,b=pan,i=5,x=0.573288,y=0.863624执行:
mlr put ' begin { @i_cumu = 0; } @i_cumu += $i; $* = { "z": $x + y, "KEYFIELD": $a, "i": @i_cumu, "b": $b, "y": $x, "x": $y, }; ' data/small输出:
z=0.346791,KEYFIELD=pan,i=1,b=pan,y=0.346791,x=0.726802 z=0.758679,KEYFIELD=eks,i=3,b=pan,y=0.758679,x=0.522151 z=0.204603,KEYFIELD=wye,i=6,b=wye,y=0.204603,x=0.338318 z=0.381399,KEYFIELD=eks,i=10,b=wye,y=0.381399,x=0.134188 z=0.573288,KEYFIELD=wye,i=15,b=pan,y=0.573288,x=0.863624这个示例同时演示了四类能力:
begin块初始化 out-of-stream 变量:@i_cumu = 0在读取第一条记录前执行一次;- 跨记录累计:
@i_cumu += $i把每条记录的i累加起来,因此输出中i列是 1、3、6、10、15 的累计序列; - map 字面量整体重建记录:
$* = { ... }定义了输出记录的字段及其顺序。与reorder动词(见 reference-verbs.md 中reorder条目)相比,这里可自由决定新记录里出现哪些字段、以何种顺序出现、每个字段的取值表达式; - 字段交叉引用:新字段
z取$x + y(注意y是未加$前缀的裸名——在 DSL 中put的表达式语境里它被视为当前记录的字段,与$y等价),而y与x字段做了交换:"y": $x, "x": $y。
由于$*赋值发生在@i_cumu += $i之后,新的i字段拿到的是累计值;而原i字段被彻底覆盖。这正是“全字段重命名与重排”最彻底的形态——旧记录被整体替换为一张新 map。
小结与进一步阅读
- 纯批量改名:优先用
mlr rename -g -r,-g隐式开启正则模式并做全局替换,支持捕获组\1~\9与"regex"i大小写不敏感写法,实现见 pkg/transformers/rename.go;其行为有对应的回归测试目录 test/cases/verb-rename-no-regex/ 与 test/cases/verb-rename-regex/ 可对照验证。 - 改名之外还要加工:用
put+for (k in $*)或for (oldk, v in $*)循环,配合gsub、clean_whitespace等字符串函数,动态读写$[k]。 - 整体重建记录:用 map 字面量整体赋值
$* = { ... },可同时完成重命名、重排、增删字段与跨记录累计,是rename/reorder动词的 DSL 泛化形态。 - 三种技巧覆盖了字段名清洗、全字段值替换、记录级重组三大类场景,是 Miller DSL 中复用率最高的模式;DSL 的完整语法与内置函数可继续阅读 miller-programming-language.md、reference-verbs.md 与 reference-dsl-builtin-functions.md。
【免费下载链接】millerMiller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON项目地址: https://gitcode.com/gh_mirrors/mi/miller
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考