- CLI
- 数据分析
【免费下载链接】miller
Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON
本篇技术指南以 Miller 官方文档 reference-main-compressed-data.md 为主体,系统讲解 Miller 6 起内建支持的 GZIP、BZIP2、ZLIB、ZSTD 四种压缩格式的读取方式:按文件扩展名自动检测、用--gzin/--bz2in/--zin/--zstdin手动指定、以及通过--prepipe/--prepipex调用任意外部解压程序的通用方案,并覆盖管道、tee语句和-I原地模式下的压缩输出。读完本文,你将掌握在不解压落盘的前提下直接对.gz、.bz2、.z、.zst文件执行mlr数据处理全流程的技能,并理解这些能力在源码层面的实现原理与安全边界。
压缩数据支持总览:Miller 6 的内建解压能力
从 Miller 6 开始,Miller 在进程内部(in-process)直接支持读取以下四种压缩格式,无需外部解压工具:
| 压缩格式 | 文件扩展名 | 内建解压方式 |
|---|---|---|
| GZIP | .gz | Go 标准库compress/gzip |
| BZIP2 | .bz2 | Go 标准库compress/bzip2 |
| ZLIB | .z | Go 标准库compress/zlib |
| ZSTD | .zst | 第三方库github.com/klauspost/compress/zstd |
这四种格式的读取统一由 pkg/lib/file_readers.go 中的TFileInputEncoding枚举(FileInputEncodingDefault、FileInputEncodingBzip2、FileInputEncodingGzip、FileInputEncodingZlib、FileInputEncodingZstd)标识,并被所有输入格式的记录读取器(CSV、JSON、XTAB、DKVP、NIDX、PPRINT、DCF 等)共享,因此无论你使用哪种文件格式,压缩处理机制都是同一套。
此外,Miller 6 之前就已存在更通用的--prepipe选项,用于调用$PATH中任意的外部解压程序。两条路径并存:内建解压器(进程内)与外部解压器(进程外)。这两种方式的选择逻辑在 pkg/lib/file_readers.go 的OpenFileForRead中清晰可见:prepipe非空时优先走外部管道(openPrepipedHandleForRead),否则再按内建编码打开。
输入自动检测:按文件扩展名识别压缩格式
如果你的输入文件以.gz、.bz2、.z或.zst结尾,Miller 会自动按扩展名判断压缩格式并即时解压。文档中的完整示例(数据文件为仓库内 docs/src/gz-example.csv.gz):
file gz-example.csv.gzgz-example.csv.gz: gzip compressed data, was "gz-example.csv", last modified: Mon Aug 23 02:04:34 2021, from Unix, original size modulo 2^32 429mlr --csv sort -f color gz-example.csv.gzcolor,shape,flag,k,index,quantity,rate purple,triangle,false,5,51,81.2290,8.5910 purple,triangle,false,7,65,80.1405,5.8240 purple,square,false,10,91,72.3735,8.2430 red,square,true,2,15,79.2778,0.0130 red,circle,true,3,16,13.8103,2.9010 red,square,false,4,48,77.5542,7.4670 red,square,false,6,64,77.1991,9.5310 yellow,triangle,true,1,11,43.6498,9.8870 yellow,circle,true,8,73,63.9785,4.2370 yellow,circle,true,9,87,63.5058,8.3350注意,mlr --csv sort -f color gz-example.csv.gz与mlr --csv sort -f color gz-example.csv的输出完全一致——解压发生在内存/流中,磁盘上的压缩文件保持原样、不会被修改。这样做的价值在于:压缩数据在磁盘上节省空间,代价是运行时需要额外 CPU 开销完成解压。对于 GB 级的大文件,这是一种典型的"以 CPU 换磁盘"的取舍。
自动检测的实现位于 pkg/lib/file_readers.go 的FindInputEncoding与 pkg/lib/file_readers.go 的openEncodedHandleForRead:
- 若用户没有通过标志显式指定编码,则依次检查后缀:
.bz2→ bzip2、.gz→ gzip、.z→ zlib、.zst→ zstd; - 匹配到后缀后,
openEncodedHandleForRead将原始文件句柄包上一层对应的解压 Reader。
一个值得注意的实现细节:Go 标准库的bzip2.NewReader与klauspost/compress的zstd.NewReader都只返回io.Reader而不实现io.ReadCloser,因此 Miller 在 pkg/lib/file_readers.go 中分别定义了BZip2ReadCloser与ZstdReadCloser包装类型,把底层文件句柄的关闭职责一并代理,保证解压流可以正常释放资源。
手动指定压缩格式:--gzin/--bz2in/--zin/--zstdin
当输入文件的扩展名不在.gz、.bz2、.z、.zst之列(例如被重命名为.bin、.dat或从流式 stdin 读取),Miller 无法自动判断压缩格式。此时可用以下四个标志显式告知:
mlr --csv --gzin sort -f color myfile.bin # myfile.bin 的内容是 gzip 压缩的四个标志与格式的对应关系:
| 标志 | 含义 | 对应自动检测后缀 |
|---|---|---|
--gzin | 按 gzip 解压 | .gz |
--bz2in | 按 bzip2 解压 | .bz2 |
--zin | 按 zlib 解压 | .z |
--zstdin | 按 zstd 解压 | .zst |
这些标志在 pkg/cli/option_parse.go 的CompressedDataFlagSection中定义,每个标志的作用都是把options.ReaderOptions.FileInputEncoding设置为对应的枚举值。--gzin的帮助文本明确写着"Done by default if file ends in.gz",即显式标志与扩展名自动检测的效果等价。
与 stdin 配合使用
压缩数据同样可以通过标准输入管道进入 Miller。由于管道没有文件名,扩展名自动检测无从谈起,此时--gzin等标志几乎是唯一的内建解压途径。仓库测试用例 test/cases/io-compressed-input/0008、0009、0010、0015 分别验证了:
mlr --bz2in count -g a < test/input/medium.bz2 mlr --gzin count -g a < test/input/medium.gz mlr --zin count -g a < test/input/medium.z mlr --zstdin count -g a < test/input/medium.zst对应地,0005、0006、0007、0014 验证了同样的输入文件不带标志、仅靠扩展名自动检测即可读取。这组两两对照的用例(文件方式 vs stdin 方式、自动检测 vs 手动标志)完整覆盖了内建解压的四种格式、两条路径,是理解本主题的最佳测试标本。
外部解压器:--prepipe与--prepipex
基本用法:--prepipe
--prepipe接受$PATH中任意可执行程序的名称,Miller 会对每个输入文件运行一次该程序,把该程序的标准输出接到 Miller 的标准输入上。效果等价于传统的 shell 管道,例如单文件场景下你完全可以自己写:
gunzip < gz-example.csv.gz | mlr --csv sort -f colorcolor,shape,flag,k,index,quantity,rate purple,triangle,false,5,51,81.2290,8.5910 purple,triangle,false,7,65,80.1405,5.8240 purple,square,false,10,91,72.3735,8.2430 red,square,true,2,15,79.2778,0.0130 red,circle,true,3,16,13.8103,2.9010 red,square,false,4,48,77.5542,7.4670 red,square,false,6,64,77.1991,9.5310 yellow,triangle,true,1,11,43.6498,9.8870 yellow,circle,true,8,73,63.9785,4.2370 yellow,circle,true,9,87,63.5058,8.3350而--prepipe的收益在于:Miller 会按文件边界,对每个输入文件分别启动一次指定程序,多文件处理时天然保留文件边界,这是手工管道难以优雅实现的:
mlr --prepipe gunzip --csv sort -f color a.csv.gz b.csv.gz--prepipe的命令可以是任何"从标准输入读取、向标准输出产出 Miller 可接受数据"的程序,因此名义上你可以按需选用系统里安装的各种解压工具,且支持逐文件差异配置:
mlr --prepipe 'zcat -cf' --csv cat data1.zst mlr --prepipe 'xz -cd' --csv cat data2.xz如果命令自身带参数(flag),必须用引号整体包裹,例如mlr --prepipe 'zcat -cf'。这一点在 pkg/cli/option_parse.go 的实现中也能印证:--prepipe会把紧随其后的整个参数原样存入options.ReaderOptions.Prepipe,参数内的空格不会被重新分词。
该功能非常通用,并不局限于解压工具——你可以用它做任意逐文件过滤器,例如只取每个文件的前 10 行:
mlr --prepipe 'head -n 10' ...--prepipe与--prepipex的区别
两者都接受解压命令,区别在于运行时如何拼接命令行(见 pkg/lib/file_readers.go 的openPrepipedHandleForRead):
--prepipe:按命令 < 文件名方式调用,适用于从标准输入读取的程序,如gunzip、zcat -cf、xz -cd;--prepipex:按命令 文件名方式调用,不插入<重定向,适用于必须以文件名为参数的程序,如unzip -qc。
对应地,pkg/cli/option_parse.go 中--prepipex的解析器会把PrepipeIsRaw置为true,从而在拼接时走prepipe + " " + escapedFilename分支;而--prepipe的PrepipeIsRaw为false,走prepipe + " < " + escapedFilename分支。
优先级:--prepipe会覆盖自动检测
需要特别留意两条优先级规则(文档明确说明,pkg/cli/option_parse.go 的帮助文本也复述了同样的语义):
- 只要命令行指定了
--prepipe或--prepipex,它就替换掉一切基于文件扩展名的自动检测决定——即使文件叫foo.gz,也会按你指定的外部命令来处理; - 同理,若同时指定了
--prepipe/--prepipex,则--gzin/--bz2in/--zin/--zstdin四个内建标志全部被忽略。
原因显而易见:从 pkg/lib/file_readers.go 的OpenFileForRead可以看到,prepipe非空时直接返回外部管道句柄,根本不会进入内建编码的分支。
安全边界:.mlrrc中的限制
由于--prepipe/--prepipex会执行任意外部命令,存在被恶意利用执行意外代码的风险,因此这两个标志不允许出现在.mlrrc配置文件中。实现位于 pkg/climain/mlrcli_mlrrc.go 的handleMlrrcLine:遇到--prepipe、--prepipex(以及同样危险的--load、--mload)时直接返回"未识别",该行被静默忽略。
不过,.mlrrc中可以使用四个固定等价物(白名单式替代,见 pkg/cli/option_parse.go):
可在.mlrrc中使用的标志 | 等价于 |
|---|---|
--prepipe-gunzip | --prepipe gunzip |
--prepipe-zcat | --prepipe zcat |
--prepipe-zstdcat | --prepipe zstdcat |
--prepipe-bz2 | --prepipe bz2 |
这组标志的命令是写死在代码里的常量,不接受用户输入,因此不构成任意代码执行风险。关于.mlrrc的完整用法参见 customization.md。
另一个安全细节:在把文件名拼入 shell 命令前,Miller 会调用 pkg/lib/file_readers.go 的escapeFileNameForPopen做转义——对文件名中的单引号、双引号分别用单引号包裹,对含空白的文件名整体加单引号,从而避免 shell 注入;外部命令本身最终通过 pkg/lib/halfpipe.go 的OpenInboundHalfPipe以/bin/sh -c "..."方式启动(Windows 上为cmd /c),这也是为什么--prepipe天然支持管道符和重定向等 shell 语法。
压缩输出:三种写入压缩文件的方式
以上全部内容围绕压缩输入展开。压缩输出则有三种典型路径:
1. 标准输出管道(最常用)
Miller 默认把结果写到 stdout,因此直接接管道即可:
mlr sort -n quantity foo.csv | gzip > sorted.csv.gz这是最简单、最通用的做法,任何压缩/加密/传输工具都能以这种方式参与。
2.tee语句内重定向
当使用 DSL 的tee语句 把输出写到文件(而非 stdout)时,可以用tee的重定向语法指定压缩命令。官方示例将example.csv按color拆分成三个 gzip 文件:
mlr --from example.csv --csv put -q ' filename = $color.".csv.gz"; tee | "gzip > ".filename, $* '生成的文件确认为 gzip 格式:
file red.csv.gz purple.csv.gz yellow.csv.gzred.csv.gz: gzip compressed data, last modified: Mon Aug 23 02:34:05 2021, from Unix, original size modulo 2^32 185 purple.csv.gz: gzip compressed data, last modified: Mon Aug 23 02:34:05 2021, from Unix, original size modulo 2^32 164 yellow.csv.gz: gzip compressed data, last modified: Mon Aug 23 02:34:05 2021, from Unix, original size modulo 2^32 158这些压缩输出之后仍可被 Miller 直接读回(自动检测再次生效):
mlr --csv cat yellow.csv.gzcolor,shape,flag,k,index,quantity,rate yellow,triangle,true,1,11,43.6498,9.8870 yellow,circle,true,8,73,63.9785,4.2370 yellow,circle,true,9,87,63.5058,8.3350tee | "gzip > ..."的重定向底层由 pkg/lib/halfpipe.go 的OpenOutboundHalfPipe实现:Miller 把数据写入管道写端,子进程的 stdout/stderr 直接透传,gzip在另一端完成压缩落盘。
3. 原地模式-I:自动重压缩
使用 原地模式 的-I标志时,被覆盖写回的文件在可能的情况下会自动重新压缩:即输入是 gzip 压缩的,输出依然是 gzip 压缩的。这一点由 pkg/lib/file_readers.go 的WrapOutputHandle保证——gzip 与 zlib 都支持写端压缩包装。
但有两个必须知道的限制(详见 reference-main-in-place-processing.md):
- bzip2 不支持原地重压缩:受 Go 标准库所限(
compress/bzip2只读不写),WrapOutputHandle对FileInputEncodingBzip2会直接返回bzip2 is not currently supported for in-place mode错误。换言之,对.bz2文件使用-I会失败;GZIP 和 ZLIB 则可以正常重压缩; - gzip 压缩级别不保留:
mlr -I ... yourfile.gz会以 gzip 默认级别重新压缩,Miller 不尝试探测或模仿原文件的压缩级别; --prepipe/--prepipex输入不可原地更新:IsUpdateableInPlace(pkg/lib/file_readers.go)明确拒绝带外部管道的输入,因为 Miller 无从知道如何反转该外部命令;同理,http://、https://、file:// 等 URL 输入也不可原地更新。
源码级实现链路小结
把整条压缩数据流串起来看:
- 参数解析:pkg/cli/option_parse.go 的
CompressedDataFlagSection统一注册全部压缩相关标志,并把选择写入ReaderOptions.Prepipe/PrepipeIsRaw/FileInputEncoding; - 打开输入:pkg/lib/file_readers.go 的
OpenFileForRead按"prepipe 优先 → 内建编码 → 扩展名推断"三级决策返回解压后的读取句柄; - 内建解压:
openEncodedHandleForRead按编码分别包装 gzip/zlib/bzip2/zstd Reader(pkg/lib/file_readers.go); - 外部解压:
openPrepipedHandleForRead拼接命令(区分<与直接传参),OpenInboundHalfPipe以 shell 子进程方式启动(pkg/lib/halfpipe.go); - 压缩输出:stdout 管道、
tee | "命令"重定向(OpenOutboundHalfPipe)、-I原地模式的WrapOutputHandle; - 安全护栏:
.mlrrc拒绝自由命令(pkg/climain/mlrcli_mlrrc.go)、文件名 shell 转义(escapeFileNameForPopen)。
完整的端到端验证可参考仓库测试目录 test/cases/io-compressed-input,其 16 个用例覆盖了四种格式 ×(自动检测/手动标志)×(文件/标准输入)的全部组合,以及--prepipe与文件/标准输入的配合;测试数据文件位于 test/input/medium.gz、test/input/medium.bz2、test/input/medium.z、test/input/medium.zst。
使用建议与注意事项汇总
- 优先用内建解压:
.gz/.bz2/.z/.zst文件直接传文件名即可,零配置、跨平台一致(纯 Go 实现,无外部依赖); - stdin 场景必须手动声明格式:管道没有扩展名,用
--gzin/--bz2in/--zin/--zstdin; - 奇特的压缩格式或自定义过滤器:用
--prepipe(读 stdin 的程序)或--prepipex(读文件名的程序),命令带参记得整体加引号; .mlrrc中只能用白名单四件套:--prepipe-gunzip、--prepipe-zcat、--prepipe-zstdcat、--prepipe-bz2;-I原地模式:gzip/zlib 输入可自动重压缩,bzip2 与 prepipe 输入不行;- 文档生成说明:本文所依据的 reference-main-compressed-data.md 由同目录的
.md.in源文件经 docs/src/genmd-filter(Ruby 脚本,处理GENMD-RUN-COMMAND等实时执行宏)生成;其中tee压缩示例特意采用非实时呈现,因为 gzip 默认在头部写入时间戳,每次重新生成都会产生文件差异,不利于版本控制——这也是实际使用tee | "gzip > ..."时可能遇到的现象,若希望输出可复现,可自行在命令中加入gzip -n之类的时间戳抑制参数。
- CLI
- 数据分析
【免费下载链接】miller
Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON
相关推荐
SpoofDPI数据压缩:gzip压缩与传输优化
SpoofDPI数据压缩:gzip压缩与传输优化 还在为网络限速和流量监控烦恼吗?SpoofDPI作为专业的反审查工具,不仅提供深度包检测(Deep Packe
网络安全3分钟快速上手:drawio-desktop开源流程图工具完整指南
3分钟快速上手:drawio desktop开源流程图工具完整指南 还在为复杂的流程图工具烦恼吗?🤔 每次打开浏览器才能画图,担心数据安全,或者被各种付费订阅
桌面应用图形学Rose/verdict-classifier未来路线图:多模态事实核查和实时检测的终极指南
Rose/verdict classifier未来路线图:多模态事实核查和实时检测的终极指南 在当今信息爆炸的时代, Rose/verdict classifi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考