Miller (mlr) — 懂 CSV/JSON 表头的 awk
是什么
Miller(命令名 mlr)是 John Kerl 2015 年用 Go 写的命令行数据处理工具——把 awk / sed / cut / grep / join / sort 的能力,搬到 CSV / TSV / JSON / JSON-Lines 这些”有表头有结构”的数据上。
日常类比:
- awk 是不识字的工人——你给它一行
Alice,30,Beijing,它只知道”第 1 段、第 2 段、第 3 段”,要写$2才能拿年龄 - mlr 是识字的工人——同一行配上表头
name,age,city,它知道每段叫什么,写$age就行
一份 CSV 里加一列、按某列分组求平均、转成 JSON——三件事在 awk 里要写三段不同的脚本,在 mlr 里是三个 verb 拼成一行管道。
为什么重要
不是”awk 加糖”,是补一类工具空白:
- awk / sed / cut 不懂表头——脚本里全是
$3 $5 $7,列顺序一变全炸 - jq 只懂 JSON——CSV 还是得回 awk
- pandas 太重——为读一个 CSV 起 Python、装依赖、写 5 行脚本,对一次性探索性任务不划算
- mlr 在中间——单二进制零依赖、像 awk 一样 grep-able,但 schema-aware
数据团队、SRE 排查日志、Bug 单做统计、跨格式转换——这是高频场景,mlr 的位置是”awk 写不动 + 上 pandas 不值得”那一段。
核心要点
mlr 的设计可以拆成 3 件事:
-
字段名而非编号:DSL 里写
$age、$city、$total = $a + $b——读代码就知道在算什么。awk 的$2 $5在 6 个月后回看就没人能读懂。 -
格式正交:输入格式(
--icsv/--ijson/--itsv/--ijsonl)和输出格式(--ocsv/--ojson/--opprint对齐打印)独立指定。一个工具完成 CSV → JSON、JSON → TSV、TSV → 对齐表格。 -
链式 verb:50+ 内置动词,像 Unix 管道一样串:
mlr --icsv --ojson \filter '$age > 18' then \cut -f name,age then \sort -f name \people.csvthen把多个 verb 接成一条流式管道,每条记录依次经过。
实践案例
案例 1:CSV → JSON 一行命令
mlr --icsv --ojson cat people.csvcat 是默认 verb(不变换、只读出来)。--icsv 告诉 mlr 输入是 CSV,--ojson 是 JSON。一行命令搞定一份 CSV → JSON 转换;如果你只想要前几条还可以加 head -n 5 verb 在中间。
案例 2:按列分组求统计
mlr --icsv stats1 -a mean,p95 -f response_ms -g endpoint logs.csv读 logs.csv,按 endpoint 列分组,对每组的 response_ms 算 mean 和 95 分位。同样的事在 awk 里要 awk -F, ... | sort | uniq -c | ... 写 5 行。
案例 3:两个 CSV 按 key join
mlr --csv join -j user_id -f users.csv then sort -nr signup_date orders.csvusers.csv 和 orders.csv 按 user_id 内连接,再按 signup_date 倒序。--csv 是 --icsv --ocsv 的简写。POSIX join 命令要先两边各自 sort、参数难记;mlr 的 join verb 自动处理。
案例 4:加一列计算字段
mlr --csv put '$total = $price * $qty; $tax = $total * 0.13' orders.csvput verb 跑 DSL 表达式,对每条记录新增 total 和 tax 两列。多个赋值用 ; 分隔,全程不离开 mlr——awk 也能做但要写 BEGIN { FS="," } 和手动处理 header,mlr 一行写完。
踩过的坑
-
CSV 输入默认期望 header——没 header 的文件直接
mlr --icsv cat foo.csv会把第一行当列名。修法:加--implicit-csv-header,列名变成1,2,3,...让你后续rename改名。 -
字段访问是
$name不是$1——写mlr put '$1 = $2 + 1'会得到字面量字符串 “1”,不是第一个字段。mlr DSL 没有”按位置取列”语法,要么用名字、要么先--implicit-csv-header再用$1。 -
输入输出格式要分别声明——
mlr cat foo.csv默认按 DKVP(a=1,b=2key=value 行)解析,CSV 文件直接报”missing key”。养成总写--icsv --ojson的习惯。 -
sort/stats1/tac需要全量缓冲——mlr 多数 verb 是流式(一次过一条记录),但这几个排序/聚合/反转 verb 必须读完全文件才能输出。几 GB CSV 上这些 verb 内存会爆;要先head或拆文件。 -
嵌套 JSON 默认被压平——
{"user":{"id":1}}的user.id在--ijson下默认变成名为user.id的扁平字段。要保留嵌套结构得加--no-auto-flatten,否则mlr --ijson --ojson cat会”破坏”原始 JSON 形状。
历史
- 2015:John Kerl 在金融数据处理中受够 awk 写不动 CSV,用 Go 1.5 写了 mlr 第一版;当时 Go 模块系统还没成熟,单文件 main 包
- 2018 v5:DSL 引入 then 链式 verb、map / array 类型;从”awk 替代”升级成”小型 ETL 语言”
- 2020 v6:核心代码大重构,加入 JSON Lines、Markdown 表格、PPRINT 对齐输出;性能比 v5 快 2-3x
- 2023+:进入维护期,按周发布 patch;社区贡献者活跃但核心仍是 Kerl 一个人
- 2024–2025:Parquet / Arrow 实验性输入支持加入,但定位仍是”轻量结构化数据 CLI”,重 OLAP 场景明确推用户上 duckdb-2019
mlr 没有 ripgrep 那种”替换一类核心命令”的爆点,但在数据工程师 / SRE / 数据科学家圈子里口碑非常稳——9k+ star 大部分来自”用过就回不去”的私下推荐。一个特别有意思的现象:mlr 用户经常用它来生成 jq / awk 脚本(先用 mlr 探索结构、再写更紧的脚本进 cron),它本身就是个”数据探索 → 固化脚本”的中间台。
适用 vs 不适用场景
适用:
- 一次性数据探索——读 CSV、看分布、算统计、转 JSON 给前端
- CSV / TSV / JSON 之间互转——比写 Python 脚本快 10x
- 日志做按字段聚合分析——
stats1 -a count,mean -g status_code - 中等大小数据(几十 MB 到几 GB)——超过这个量上 duckdb-2019 或 pandas
不适用:
- 数据规模超过单机内存的 sort / stats——mlr 流式没问题但聚合 verb 会爆,上 DuckDB 或 Spark
- 复杂多表 join + 窗口函数——上 SQL(DuckDB / SQLite)
- 需要可视化 / 交互式探索——mlr 是 CLI,要图表去 pandas + Jupyter
- 不识 CSV / JSON 的二进制格式(Parquet 现在部分支持,但首选 DuckDB)
学到什么
- schema-aware 是关键差异——不是”性能更快”也不是”功能更多”,而是”读 6 个月前自己写的命令仍能秒懂”。字段名比编号在协作和回溯上是质变
- 格式正交比新增格式重要——mlr 把”输入格式”和”输出格式”拆成两个独立标志,而不是为每对格式做一个命令;新增一个格式就能和所有现有格式互转
- 流式 + 缓冲 verb 显式区分——多数 verb 流式(O(1) 内存),需要全量的 sort/stats 单独标出;让用户知道”什么时候不能用大文件”
- Unix 哲学还活着——50+ 单一职责 verb 用
then拼,没有”魔法配置”——和 awk / grep / sed 的设计同根 - 中间地带工具有市场——awk 太底层、pandas 太重,mlr 在中间这一段有稳定刚需。设计上别试图覆盖两端,找到自己的甜点
- DSL 的”够小”是优点——mlr 的 put/filter DSL 大概只有 awk 的 1/3 复杂度,但 90% 的数据探索任务用不到那 2/3。够用就停,是工具长期可维护的关键
延伸阅读
- 官方文档:miller.readthedocs.io(cookbook 很厚,几乎每个常见任务都有 1-2 行示例)
- 作者 John Kerl 的演讲:Miller: Awk for JSON(30 分钟把设计动机和 verb 链讲一遍)
- 实战对比:搜 “miller vs jq vs awk” 有大量博客,挑一篇 2022+ 的看,verb 语法稳定后写的更准
- 50+ verb 的速查表:
mlr --help列出所有 verb,配mlr <verb> --help看具体用法;建议第一次用先把 cut / cat / sort / filter / put / stats1 / join / rename 这 8 个吃透
关联
- bat —— 同代终端工具家族(Rust / Go 写的”现代 cat / grep / find”)
- ripgrep —— grep 替代;mlr 是它的”结构化数据”近邻
- fd —— find 替代;和 mlr 经常出现在同一份 dotfiles
- fzf —— 模糊查找;
fzf --preview 'mlr --icsv --opprint cat {}'做 CSV 预览很顺手 - sd —— 直觉语法的 sed 替代;和 mlr 互补(sd 处理纯文本、mlr 处理结构化)
- duckdb-2019 —— mlr 撞墙时的下一站:能在 CSV / Parquet 上直接跑 SQL
- jq —— JSON 专精的 mlr 邻居;mlr 的 JSON 处理简单场景够用,复杂深嵌套还是 jq
- eza —— 同样是”现代 ls”路线的 Rust 工具,和 mlr 一起构成数据探索工作流的”看目录 + 看内容”两步