jqのむだづかいーTSV・CSV篇

[JSON](https://datatracker.ietf.org/doc/html/rfc8259)処理のコマンドラインツールである[jq](https://stedolan.github.io/jq/)は、JSONテキストを1行でちゃくっと解析、変換するときに使うものです。複雑な分岐や制御が絡んできたら、`sed`や`awk`など他の文字列処理ユーティリティと組み合わせるのが通例です。単体で複雑なことはしません。

とは言え、`jq`にも変数定義、`for`や`if`などの制御構造、関数定義などプログラミング言語らしき機能が備わっているので、「やればできんじゃねぇ」と野望を募らせてしまうこともあります。

ここではTSV（Tab-Separated Values）およびCSV（Comma-Separated values）データの解析に挑戦します。具体的には、ファイル先頭行のヘッダ（見出し）をキーに、以降のデータ行をそれぞれ値としたオブジェクトを生成します。

たとえば、次のTSVファイル

```
X	Y	Z
0001	0002	0003
0004	0005	0006
```

を、次のようなオブジェクトの配列にします。

```
[
	{"X": "0001", "Y":"0002", "Z":"0003"},
	{"X": "0004", "Y":"0005", "Z":"0006"},
]
```

これで行位置とキー名から一意に値を取得できるようになります。たとえば、（0からカウントして）1行目のXの値は次の要領で取得します。

```bash
$ echo '[{"X":"0004", "Y":0005, "Z":0006}, {"X":"0004", "Y":0005, "Z":0006}]' | jq '.[1]["X"]'
"0004"
```

オリジナルのお題は[arc279](https://qiita.com/arc279)氏の「[tsv を 1行1件ずつの json にする](https://qiita.com/arc279/items/2515272a7050c0a13cbf)」です。彼の解法は、ヘッダ部分を読むのに`bash`のビルトインコマンド`read`を使い、得られたヘッダを`$ENV`を用いて`jq`内で参照します。`Unix`らしいツールの連携です。

> TSV（タブ区切り）がCSV（カンマ区切り）という以外、本ページとほとんど同じことは[shigekimono](https://qiita.com/shigekimono)氏が「[[jq] jqでcsvをjsonへ変換](https://qiita.com/shigekimono/items/d80eb4f626baff9e9236)」で1年前にやっていました…。というわけで、タブでもカンマでも区切れるように、`split`で正規表現を用いるよう変更しました。

しかし、ここは`jq`をむだづかいする場所です。強引は承知ですべてを`jq`で記述します。


### コード

コード（フィルタファイル）は`tsv.jq`で、次の[Github](https://github.com/stoyosawa/jqDoc-public/tree/main/filters)から取得できます（短いのでコピペでも十分ですが）。

```https://github.com/stoyosawa/jqDoc-public/```

フィルタファイルの改行はUnixスタイルの`LF`だけでなければならないので、保存時に注意してください。Windows流に`CRLF`だと次のエラーが報告されます。

```
jq: error: syntax error, unexpected INVALID_CHARACTER (Unix shell quoting issues?)
  at <top-level>, line 1:
```


### フィルタファイルの使い方

これらファイルをjqから呼び出すときは、次のオプションを指定します。

- `-f`オプション（`--from-file`）ーフィルタファイルを読み込みます。
- `-R`オプション（`--raw-input`）ー入力行をJSONテキストではなく、単一の文字列として読み込みます。
- `-s`オプション（`-slurp`）ー入力JSONテキストをまとめて処理します。これを`-R`と組み合わせることで、入力が大きな1文字列として扱われます。

まとめると、次のようになります。

```bash
$ jq -sRf tsv.jq
```

### TSVのオブジェクト配列化

入力データはオリジナルのお題に倣って、`printf`と`seq`と`paste`から生成します。

```bash
$ { printf "c1\nc2\nc3\n"; seq -f '%04g' 30; } | paste - - -
c1      c2      c3
0001    0002    0003
0004    0005    0006
0007    0008    0009
⋮
```
> CSVにするには、`paste`に`-d,`オプションを加えます。

コードは次の通りです。

```
split("\n") |
map(select(. != "")) |
map(split("[\\t,]"; "g")) |
.[0] as $header |
[
	.[1:][] |
	[$header, .] |
	transpose |
	map({(.[0]):.[1]}) |
	add
]
```

行順に説明します（以下、TSVデータはファイル`a`に収容されているものとします）。

0 `-sR`オプションで入力されたTSVは次のような1文字列になります。ひとまとめにしないと、個々の行がそれぞれ独立したJSONテキストとして扱われてしまうからです（行をまたいだ構造化がされない）。

```bash
$ cat a | jq -sR '.'
"c1\tc2\tc3\n0001\t0002\t0003\n0004\t0005\t0006\n0007\t0008\t0009\n...
```

1 `split`を用いて行単位に分解し、それぞれ要素とした配列を生成します。

```bash
$ cat a | jq -sR 'split("\n")'
[
  "c1\tc2\tc3",
  "0001\t0002\t0003",
  "0004\t0005\t0006",
  ⋮
  ""
]
``` 

2 余計な空行があるので、それを`select`で削除します。配列が入力されるので、その個々の要素を順次処理するのに`map`を用います。

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != ""))'
[
  "c1\tc2\tc3",
  "0001\t0002\t0003",
  "0004\t0005\t0006",
  ⋮
  "0028\t0029\t0030"
]
```

3 タブまたはカンマ区切りを、正規表現を用いた`split`でばらします。ここも配列要素単位の処理なので、`map`を併用します。

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("[\\t,]"; "g"))'
[
  [
    "c1",
    "c2",
    "c3"
  ],
  [
    "0001",
    "0002",
    "0003"
  ],
  ⋮
]
```

4 ヘッダは上記配列の0番目の要素なので、`.[0]`に格納されています。これを変数`$header`に退避します。変数定義の`... as $xxx`はパイプライン処理に影響を与えません。

5 以降で出力されるオブジェクト（たとえば`{"c1":"0001", "c2":"0002", "c3":"0003"}`）を配列に収容するために、配列化の`[]`で処理全体をくくります。

6 配列スライス`.[1:]`で1行目以降のデータを取り出します。続く`[]`でそれぞれの要素をイテレートします。

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |  
	.[0] as $header | [ .[1:][] ]'
[
  [
    "0001",
    "0002",
    "0003"
  ],
  [
    "0004",
    "0005",
    "0006"
  ],
  ⋮
]
```

7 `$header`に退避したヘッダ行（の配列）とその時点のデータ要素（`.`）で構成された配列の配列を生成します。

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) | 
	.[0] as $header | [ .[1:][] | [$header, .] ]'
[
  [
    [
      "c1",
      "c2",
      "c3"
    ],
    [
      "0001",
      "0002",
      "0003"
    ]
  ],
  ⋮
]
```

8 `transpose`を用いて、2行3列の配列の配列を3行2列に変換します（これは考え付かなかった!!）

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) | 
	.[0] as $header | [ .[1:][] | [$header, .] | transpose]'
[
  [
    [
      "c1",
      "0001"
    ],
    [
      "c2",
      "0002"
    ],
    [
      "c3",
      "0003"
    ]
  ],
  ⋮
]
```

9 `map`を使って、[ヘッダ, 値]の配列からオブジェクトを生成します。外側の`{}`がオブジェクト化を意味します。キー側の`.[0]`はカッコ（`()`）でくくらなければなりません。

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
	.[0] as $header | [ .[1:][] | [$header, .] | transpose | map({(.[0]):.[1]}) ]'
[
  [
    {
      "c1": "0001"
    },
    {
      "c2": "0002"
    },
    {
      "c3": "0003"
    }
  ],
  ⋮
]
```

10 あとは、これらオブジェクトをひとつにまとめます。

```bash
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
	.[0] as $header | [ .[1:][] | [$header, .] | transpose | map({(.[0]):.[1]}) | add ]'
[
  {
    "c1": "0001",
    "c2": "0002",
    "c3": "0003"
  },
  {
    "c1": "0004",
    "c2": "0005",
    "c3": "0006"
  },
  ⋮
]
```

できあがりです。全体を流します。

```bash
$ { printf "c1\nc2\nc3\n"; seq -f '%04g' 30; } | paste - - - | jq -sRf tsv.jq
[
  {
    "c1": "0001",
    "c2": "0002",
    "c3": "0003"
  },
  {
    "c1": "0004",
    "c2": "0005",
    "c3": "0006"
  },
  {
    "c1": "0007",
    "c2": "0008",
    "c3": "0009"
  },
  {
    "c1": "0010",
    "c2": "0011",
    "c3": "0012"
  },
  {
    "c1": "0013",
    "c2": "0014",
    "c3": "0015"
  },
  {
    "c1": "0016",
    "c2": "0017",
    "c3": "0018"
  },
  {
    "c1": "0019",
    "c2": "0020",
    "c3": "0021"
  },
  {
    "c1": "0022",
    "c2": "0023",
    "c3": "0024"
  },
  {
    "c1": "0025",
    "c2": "0026",
    "c3": "0027"
  },
  {
    "c1": "0028",
    "c2": "0029",
    "c3": "0030"
  }
]
```

### おわりに

Unixユーティリティを使えば、コードはもっと見やすくなります（分割統治というやつです）。でも、`jq`魂がうずいてしまったものは仕方ありません。よいお題を提供してくださった[arc279](https://qiita.com/arc279)氏に感謝。


### 参考

- [tsv を 1行1件ずつの json にする](https://qiita.com/arc279/items/2515272a7050c0a13cbf) - オリジナルのお題。
- [[jq] jqでcsvをjsonへ変換](https://qiita.com/shigekimono/items/d80eb4f626baff9e9236) - このページと同じことは、1年前に実装されていました…。
- [./jq](https://stedolan.github.io/jq/) - `jq`のオフィシャルサイトです（英文）。
- [Qiita jq tag](https://qiita.com/tags/jq) - Qiita掲載の`jq`関連の記事一覧です。
- [jqマニュアル（開発バージョン）](https://yujiorama.github.io/unofficial-translations/jq/l10n/jp/manual/) - Yuji Okazawa氏が（非公式に）訳しておられる`jq`マニュアルの日本語版です。和訳マニュアルは他にもあるので、検索してください。
- [The JavaScript Object Notation (JSON) Data Interchange Format（RFC 8259）](https://www.rfc-editor.org/rfc/rfc8259.txt) - JSONの仕様です（英文）。しばしば[RFC 7159](https://www.rfc-editor.org/rfc/rfc7159.txt)が参照されますが、そちらはobsoleteになりました。
- [JSON(JavaScript Object Notation)データ交換フォーマット](https://www.asahi-net.or.jp/~ax2s-kmtn/internet/rfc8259j.html) - 上記の非公式の和訳です。
- [jqハンドブックーNetOps/DevOps必携のJSONパーザ](http://www.cutt.co.jp/book/978-4-87783-491-3.html) -  ここで用いた`--slurp`コマンドラインオプションや`transpose`などの関数の詳細を説明した書籍です。ご購入はこちらから: 【[出版社](http://www.cutt.co.jp/book/978-4-87783-491-3.html) | [honto](https://honto.jp/netstore/pd-book_31037910.html) | [amazon.co.jp](https://www.amazon.co.jp/jq/dp/4877834915) | [ヨドバシカメラ](https://www.yodobashi.com/product/100000009003450000/)】。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712175/20272c85-f084-557b-189f-aa7128e283ab.png)