mlr(Miller)は、CSV や JSON などの構造化データをコマンドラインで軽快に扱うためのツールです。 Unix の cut や sort、grep のような使い勝手でありながら、より強力なデータ処理能力を備えています。
様々なファイル形式への対応
mlr は様々なファイル形式を透過的に扱えます。
入力・出力形式の指定方法
入力と出力の形式は、それぞれ以下のフラグで個別に指定できます。
--i{format}: 入力形式の指定 (e.g. --icsv, --ijson)
--o{format}: 出力形式の指定 (e.g. --ocsv, --ojson)
入力と出力で同じ形式を使用する場合は、以下のフラグでまとめて指定できます。
--{format}: 入出力両方の形式 (e.g. --csv, --json)
また、--icsv --ojson を --c2j のように短いフラグでまとめて指定できる便利なショートカットもあります。c(CSV)、t(TSV)、j(JSON)、l(JSONLines)、y(YAML)、p(PPRINT)、m(Markdown)などの文字を組み合わせます。
例: CSV を読み込んで JSON Lines で出力する
$ cat <<'EOS' | mlr --c2l cat
name,value
foo,10
bar,20
EOS
主な対応形式
CSV / TSV (Comma / Tab-Separated Values)
カンマ(CSV)またはタブ(TSV)で区切られた形式です。RFC 4180 に準拠したダブルクォートの扱いなどに対応しています。
- 入力フラグ:
--icsv / --itsv
- 出力フラグ:
--ocsv / --otsv
- 両方指定:
--csv / --tsv
サンプル:
$ cat <<'EOS' | mlr --c2l cat
name,value,notes
foo,10,"This is a note."
bar,20,"Note with comma, and
a new line."
EOS
csvlite / tsvlite
RFC 4180 に厳密に準拠しない、少し緩やかな CSV/TSV 形式です。例えば、クォートで囲まれていないフィールド内にクォート文字が現れることを許容します。
- 入力フラグ:
--icsvlite / --itsvlite
- 出力フラグ:
--ocsvlite / --otsvlite
- 両方指定:
--csvlite / --tsvlite
csv ではパースエラーになりますが、csvlite なら読み込めるデータの例です。
$ cat <<'EOS' | mlr --icsv --ojsonl cat
name,value
foo,I "love" miller
EOS
$ cat <<'EOS' | mlr --icsvlite --ojsonl cat
name,value
foo,I "love" miller
EOS
JSON
JSON の配列形式です。
- 入力フラグ:
--ijson
- 出力フラグ:
--ojson
- 両方指定:
--json
サンプル:
[
{"name": "foo", "value": 10},
{"name": "bar", "value": 20}
]
JSON Lines
1 行に 1 つの JSON オブジェクトが並ぶ形式です。ログファイルなどでよく使われます。
- 入力フラグ:
--ijsonl
- 出力フラグ:
--ojsonl
- 両方指定:
--jsonl
サンプル:
{"name": "foo", "value": 10}
{"name": "bar", "value": 20}
YAML
YAML 形式です。Miller 6 から正式にサポートされました。
- 入力フラグ:
--iyaml
- 出力フラグ:
--oyaml
- 両方指定:
--yaml
サンプル:
$ cat <<'EOS' | mlr --iyaml --ojsonl cat
- name: foo
value: 10
- name: bar
value: 20
EOS
PPRINT (Pretty-Printed)
等幅スペースで綺麗に整列された表形式です。
- 入力フラグ:
--ipprint
- 出力フラグ:
--opprint
- 両方指定:
--pprint
サンプル:
name value
foo 10
bar 20
Markdown
Markdown のテーブル形式です。
- 入力フラグ:
--imd
- 出力フラグ:
--omd
- 両方指定:
--md
サンプル:
| name | value |
| --- | --- |
| foo | 10 |
| bar | 20 |
DKVP (Delimited Key-Value Pairs)
キー=値 のペアがカンマなどのデリミタで区切られたフォーマットです。
- 入力フラグ:
--idkvp
- 出力フラグ:
--odkvp
- 両方指定:
--dkvp
サンプル:
name=foo,value=10
name=bar,value=20
NIDX (Numerically Indexed)
値だけがスペースやタブなどで区切られた形式です。キーは自動的に数値(1, 2, 3...)が割り当てられます。awk などの Unix 標準のテキスト処理ツールでよく扱われるフォーマットです。
- 入力フラグ:
--inidx
- 出力フラグ:
--onidx
- 両方指定:
--nidx
サンプル:
foo 10
bar 20
XTAB (Transposed Tabular)
キーと値が縦に並んだ形式です。横に長いレコードを縦に表示して見やすくしたい場合に便利です。
- 入力フラグ:
--ixtab
- 出力フラグ:
--oxtab
- 両方指定:
--xtab
サンプル:
name foo
value 10
name bar
value 20
便利な Verb
mlr の操作は Verb と呼ばれるサブコマンドを組み合わせて行います。
cat : 基本中の基本
cat は入力レコードをそのまま出力します。主にファイル形式の変換に使用します。
$ cat <<'EOS' | mlr --c2l cat
name,value
foo,10
bar,20
EOS
また、Unix 標準の cat コマンドと同様に、-n オプションで行番号を付与できます。
-n: n という名前のフィールドを先頭に追加し、1 から始まる行番号を付与します。
-N {label}: 指定した {label} という名前のフィールドを先頭に追加し、1 から始まる行番号を付与します。
$ cat <<'EOS' | mlr --c2l cat -N no
name,value
foo,10
bar,20
EOS
label : ヘッダーを付与する
label はフィールドに名前を付与します。ヘッダーのないファイルに名前を指定したい場合に便利です。
-N オプションを指定してヘッダーなし CSV として読み込むと、まず 1、 2、 3... といった連番が一時的なラベルとして割り当てられます。その上で label Verb を使用すると、それらの連番を分かりやすい名前に置き換えることができます。
$ cat <<'EOS' | mlr --c2l -N cat
foo,10
bar,20
EOS
$ cat <<'EOS' | mlr --c2l -N label name,value
foo,10
bar,20
EOS
join : データを結合する
join は、SQL の JOIN のように、2 つのファイルを特定のキーで結合します。
-f: 左側のファイル(メモリにすべて読み込まれる側)を指定します。
-j: 結合キーとなるフィールドを指定します。
--ul: ペアにならなかった左側ファイルのレコードも出力します( SQL の LEFT JOIN 相当)。
--ur: ペアにならなかった右側ファイルのレコードも出力します( SQL の RIGHT JOIN 相当)。
--lp {text}: 左側ファイルの競合フィールドに付与するプレフィックスを指定します。
--rp {text}: 右側ファイルの競合フィールドに付与するプレフィックスを指定します。
デフォルト(--ul や --ur の指定なし)では、両方に存在するレコードのみが出力されます(SQL の INNER JOIN 相当)。
$ cat <<'EOS' > data1.jsonl
{"id": 1, "name": "foo", "age": 20}
{"id": 2, "name": "bar", "age": 30}
{"id": 3, "name": "only_left", "age": 40}
EOS
$ cat <<'EOS' > data2.jsonl
{"id": 1, "value": 100, "age": 25}
{"id": 2, "value": 200, "age": 35}
{"id": 4, "value": 400, "age": 45}
EOS
$ mlr --jsonl join -f data1.jsonl -j id data2.jsonl
$ mlr --jsonl join -f data1.jsonl -j id --lp left_ --rp right_ data2.jsonl
$ mlr --jsonl join --ul -f data1.jsonl -j id data2.jsonl
$ mlr --jsonl join --ur -f data1.jsonl -j id data2.jsonl
cut : フィールドを選択・除外する
cut は特定のフィールドのみを選択、または除外します。
$ echo '{"id": 1, "name": "foo"}' | mlr --jsonl cut -f name
$ echo '{"id": 1, "name": "foo"}' | mlr --jsonl cut -x -f name
filter : レコードを絞り込む
filter は SQL の WHERE 句のように、条件に一致するレコードを抽出します。
$ cat <<'EOS' | mlr --c2l filter '$value > 15'
name,value
foo,10
bar,20
EOS
put : フィールドを加工・追加する
put は新しいフィールドを計算して追加したり、既存のフィールドを加工したりできます。
$ echo '{"name": "foo", "value": 10}' | mlr --jsonl put '$new_value = $value * 10'
sort : レコードを並べ替える
sort はレコードを並べ替えます。数値順(-n)や辞書順(-f)、降順(-r)などを指定できます。
$ cat <<'EOS' | mlr --c2l sort -nr value
name,value
foo,10
bar,20
baz,5
EOS
uniq : 重複を排除する
uniq は指定したフィールドの値に基づいて、重複のない(ユニークな)レコードを抽出します。
-g {フィールド名} (または -f): 重複排除の基準とするフィールドを指定します(複数ある場合はカンマ区切り)。
-c: 重複している数をカウントして一緒に出力します。
-o {名前} : -c で出力されるカウント結果のフィールド名を指定します(デフォルトは "count" )。
-a: 特定のフィールドだけでなく、レコード全体として完全に重複しているレコードを 1 つにまとめます( -g とは併用できません)。
-n: ユニークな値の「種類数(件数)」のみを出力します。
例: 特定のフィールドでユニークにする
$ cat <<'EOS' | mlr --c2l uniq -g name
name,category
foo,A
bar,B
foo,A
baz,A
bar,B
EOS
例: 重複レコードのカウントも一緒に出力する
$ cat <<'EOS' | mlr --c2l uniq -g name -c
name,category
foo,A
bar,B
foo,A
baz,A
bar,B
EOS
例: レコード全体で重複を排除する(-a オプション)
$ cat <<'EOS' | mlr --c2l uniq -a -c
name,category
foo,A
bar,B
foo,A
baz,A
bar,B
EOS
count : レコード数をカウントする
count はレコードの件数をカウントします。
-g {フィールド名}: 指定したフィールドの値ごとにグループ化してカウントします。
-o {名前}: カウント結果のフィールド名を指定します(デフォルトは "count" )。
-n: グループ化したときのユニークな値の数(グループの総数)のみを出力します(-g と一緒に使用します)。
例: 全体のレコード数をカウントする
$ cat <<'EOS' | mlr --c2l count
name,category
foo,A
bar,B
foo,A
baz,A
bar,B
EOS
例: グループごとにカウントする
$ cat <<'EOS' | mlr --c2l count -g category
name,category
foo,A
bar,B
foo,A
baz,A
bar,B
EOS
split : 特定のキーでファイルを分割する
split を使用すると、特定のフィールド(キー)の値ごとにデータを自動的に分割して、別々のファイルに出力できます。
$ cat <<'EOS' | mlr --jsonl split -g category --prefix split --suffix jsonl
{"category": "A", "val": 10}
{"category": "B", "val": 20}
{"category": "A", "val": 30}
EOS
$ cat split_A.jsonl
$ cat split_B.jsonl
flatten : ネストした構造を平坦化する
flatten は、JSON などのネストされた(階層構造を持つ)フィールドを、1 つのフラットなフィールドに展開します。
-s {string}: 平坦化する際のキーの区切り文字を指定します。デフォルトは . です。
-f {fields}: 平坦化する特定のフィールド名を指定します(カンマ区切り)。指定しない場合はすべてのフィールドが対象となります。
例: ネストした JSON をフラットにする
$ echo '{"id": 1, "a": {"name": "foo", "age": 20}}' | mlr --jsonl flatten
unflatten : 平坦なキーをネストした構造に戻す
unflatten は flatten の逆の操作を行います。区切り文字(ドットなど)で表現されたフラットなフィールド名を、ネストされた階層構造(オブジェクト)へと復元します。
-s {string}: 階層化する際のキーの区切り文字を指定します。デフォルトは . です。
-f {fields}: 階層化する特定のフィールド名を指定します。
例: ドット区切りのキーをネストした JSON に戻す
$ echo '{"id": 1, "a.name": "foo", "a.age": 20}' | mlr --jsonl unflatten
join コマンドで左右のデータのフィールド名の重複を避けるために --lp(左プレフィックス)や --rp(右プレフィックス)を使用することがありますが、そのプレフィックスに a. や b. のようなドット区切りの名前を指定しておき、結合したあとに unflatten を適用すると、結合元のデータをそれぞれのオブジェクトとして綺麗にネストした JSON に整形できます。
$ cat <<'EOS' > data1.jsonl
{"id": 1, "name": "foo", "age": 20}
EOS
$ cat <<'EOS' > data2.jsonl
{"id": 1, "score": 100, "age": 25}
EOS
$ mlr --jsonl join -f data1.jsonl -j id --lp a. --rp b. then unflatten data2.jsonl
Verb の組み合わせ
then を使用すると、複数の Verb をつなげて実行できます。
$ cat <<'EOS' | mlr --c2l filter '$value > 15' then sort -n value then cut -f name
name,value
foo,10
bar,20
baz,5
EOS
その他の便利なオプション
コメント行の扱い
--skip-comments フラグを使用すると、# で始まる行を無視できます。
$ cat <<'EOS' | mlr --c2l --skip-comments cat
# test data
name,value
foo,10
#bar,20
baz,30
EOS
--pass-comments フラグを使用すると、コメント行は解析されずにそのまま即時出力されます。
$ cat <<'EOS' | mlr --c2l --pass-comments cat
# test data
name,value
foo,10
#bar,20
baz,30
EOS
元ファイルのコメント行を維持したいときに便利ですが、そのまま出力されるだけなので、出力のファイル形式でそれがコメントとして扱われるかどうかは別問題です。
ヘッダー行の有無
--implicit-csv-header (--hi): 入力ファイルにヘッダーがないものとして扱います。フィールド名は 1、 2、 3... となります。
--headerless-csv-output (--ho): 出力にヘッダー行を含めません。
-N: --implicit-csv-header と --headerless-csv-output を両方指定した状態と同様になります。
実務においては最終的な出力を CSV/TSV にすることはそれほど多くなく、JSON や JSON Lines にすることが多いため、出力側のヘッダー有無は関係ないことがほとんどです。
そのため、ヘッダーなしの CSV を読み込みたい場合は、両方指定となる -N オプションを使用するのが最も簡単です。
$ cat <<'EOS' | mlr --icsv -N --ojsonl label name,value
foo,10
bar,20
EOS
フィールド区切り文字(デリミタ)のカスタマイズ
CSV や TSV 以外の、特殊な文字で区切られたデータを扱う場合に便利です。
--ifs: 入力フィールドの区切り文字を指定します。
--ofs: 出力フィールドの区切り文字を指定します。
--fs: 入出力両方のフィールド区切り文字を指定します。
$ echo 'foo;10' | mlr --csv -N --ifs ";" --ofs ":" cat
列数が揃っていない不揃いな CSV を許容する
--ragged(または --allow-ragged-csv-input):
データ行の列数がヘッダー行より少ない場合、空文字で埋めます。多い場合は数値インデックスを自動で割り当てます。このオプションがないとエラーになります。
$ cat <<'EOS' | mlr --c2l --ragged cat
name,value,extra
foo,10
bar,20,yes,unwanted
baz
EOS
cat <<'EOS' | mlr --c2l cat
name,value,extra
foo,10
bar,20,yes,unwanted
baz
EOS
類似のツール
mlr のようにコマンドラインで構造化データ(特に CSV / TSV )を扱うための類似ツールをいくつか紹介します。ほとんど使ったことは無いので説明には誤りが含まれているかもしれません。
xsv の後継として開発されている、Rust 製の高速なデータ処理ツールキットです。
- 特徴: 動作が極めて高速です。CSV だけでなく、TSV などのデリミタ付きテキストはもちろん、JSON や JSON Lines、Excel(XLSX)、Parquet、SQLite、PostgreSQL など、多彩なフォーマットの相互変換やインポート・エクスポートをサポートしています。
- 使い所: 巨大な CSV や Parquet などのデータを高速にフィルタリング・集計したい場合や、各種データフォーマットをシームレスに相互変換したい場合。
YAML や JSON、XML、CSV、TOML などの複数フォーマットをシームレスに扱える、Go 製の構造化データ処理ツールです。
- 特徴:
jq に似たシンプルなセレクタ構文を使い、フォーマットの違いを意識せずにデータの抽出や更新が行えます。XML や TOML にも対応しているのが大きな強みです。
- 使い所: JSON や YAML だけでなく、XML や TOML など多様なフォーマットのデータを単一のツールで手軽に操作・相互変換したい場合。
インメモリで高速に動作する、分析用途に特化したデータベースです。コマンドラインツールも提供されています。
- 特徴: CSV や Parquet、JSON といったファイルを直接、高速な SQL でクエリできます。Pandas との連携もスムーズです。
- 使い所:巨大な CSV や Parquet ファイルに対して、より高度で複雑な SQL を使ったデータ分析や集計を行いたい場合。
まとめ
mlr は非常に多機能であり、公式リファレンス( https://miller.readthedocs.io/en/latest/reference-verbs/ )を確認するとわかるように、本稿では紹介しきれない多数の Verb が存在します。
しかし、filter や put、sort などのレコード操作は、一度 JSON で出力したうえで jq にパイプして処理すれば十分なことも多く、mlr の独自の DSL を苦労して覚える必要性はそれほど高くありません。
一方で、以下のような処理は jq では行うのが難しく、mlr が真価を発揮する領域です。
jq(に限らず普段使いの他のツール)単体では記述が困難な、ちょっと面倒な前処理やフォーマット変換を補完する形で mlr を組み合わせて使用するのが、スマートな使い方と言えるでしょうか。