[unix] 複数キーでjoinしたい

2017年11月13日月曜日

bash unix シェルスクリプト

unixでRDBのテーブル結合のように複数のファイルを共通のフィールドで結合する場合はjoinコマンドを使う。

簡単な例

$ cat items
1001 2017-10-08 バナナ 100
1001 2017-10-09 バナナ 90
1002 2017-10-08 りんご 150
1002 2017-10-09 りんご 200
1003 2017-10-08 みかん 80
$ cat shops
1001 2017-10-08 蒲田
1001 2017-10-09 蒲田
1002 2017-10-08 横浜
1002 2017-10-09 横浜
1003 2017-10-08 品川
$ join -11 -21 items shops
1001 2017-10-08 バナナ 100 2017-10-08 蒲田
1001 2017-10-08 バナナ 100 2017-10-09 蒲田
1001 2017-10-09 バナナ 90 2017-10-08 蒲田
1001 2017-10-09 バナナ 90 2017-10-09 蒲田
1002 2017-10-08 りんご 150 2017-10-08 横浜
1002 2017-10-08 りんご 150 2017-10-09 横浜
1002 2017-10-09 りんご 200 2017-10-08 横浜
1002 2017-10-09 りんご 200 2017-10-09 横浜
1003 2017-10-08 みかん 80 2017-10-08 品川

上記の例の場合、両ファイル共にフィールド1で結合される。
※ファイルはあらかじめ、結合フィールドでソートされている必要がある。

しかし、joinはそれぞれのファイルに対して、結合フィールドとして1つのフィールドしか指定できない。
上記の例の場合、itemsとshopsで日付が異なるものが結合されてしまっている。
本当はフィールド1とフィールド2の両方で結合したいだろう。
こんな場合はどうすればいいのだろうか。

複数フィールドで結合

こんな時は結合条件にしたいフィールドのみを文字列結合して、1つのフィールドにしてしまえばいい。

$ awk '{ print $1 ":" $2, $1, $2, $3, $4 }' items |
  sort -k1 >items_with_key
$ cat items_with_key
1001:2017-10-08 1001 2017-10-08 バナナ 100
1001:2017-10-09 1001 2017-10-09 バナナ 90
1002:2017-10-08 1002 2017-10-08 りんご 150
1002:2017-10-09 1002 2017-10-09 りんご 200
1003:2017-10-08 1003 2017-10-08 みかん 80
$ awk '{ print $1 ":" $2, $1, $2, $3 }' shops |
  sort -k1 >shops_with_key
$ cat shops_with_key
1001:2017-10-08 1001 2017-10-08 蒲田
1001:2017-10-09 1001 2017-10-09 蒲田
1002:2017-10-08 1002 2017-10-08 横浜
1002:2017-10-09 1002 2017-10-09 横浜
1003:2017-10-08 1003 2017-10-08 品川

複数のキーの文字列長が固定でない場合を考慮すると、そのまま文字列結合するのではなく、登場しない文字をセパレータにして文字列結合したほうがいいだろう。
また、joinが要求するソート条件を満たすように文字列結合後にソートしておこう。

こうして作成したファイルの1フィールド目を結合条件として、joinすればいい。
出力フィールドも必要なもののみに絞ると、以下のようになる。

$ join -11 -21 -o1.2,1.3,1.4,2.4 items_with_key shops_with_key
1001 2017-10-08 バナナ 蒲田
1001 2017-10-09 バナナ 蒲田
1002 2017-10-08 りんご 横浜
1002 2017-10-09 りんご 横浜
1003 2017-10-08 みかん 品川

スクリプトにまとめると

以上をまとめると、こんなスクリプトになる。

プロセス置換を使えば、一時ファイルを使う必要がなくなるけど、この場合はシェバンをshではなく、bashにする必要がある。
#!/bin/bash
join -11 -21 -o1.2,1.3,1.4,2.4 \
  <(awk '{ print $1 ":" $2, $1, $2, $3, $4 }' items | sort -k1) \
  <(awk '{ print $1 ":" $2, $1, $2, $3 }' shops | sort -k1)