タブ区切りの key - value な複数のテキストファイルを事前のソートなしに join したい

# join コマンドは便利だけど、一度に２つのファイルしか join できないし、事前のソートも必要なのが少々めんどくさい...

たとえばこういうデータファイルがあったとします。（データの区切りはタブ区切り）

```bash
kinoue@kali:~/work$ cat list2.txt
key1    100

kinoue@kali:~/work$ cat list1.txt
key1    10
key2    5

kinoue@kali:~/work$ cat list3.txt
key3    20
key1    300
```

これをこんなふうにまとめたい。

| |list1.txt|list2.txt|list3.txt|
|:-:|:-:|:-:|:-:|
|key1|10|100|300
|key2|5|| |
|key3| | |20|

でも、これを join コマンドでやるのは案外めんどくさいです。join コマンドは一度に２つのファイルしか処理できないし、そもそも join の仕様として事前にソートしておかねばならない。対象のファイル数が多かったりすると、意外に手間取ります。

というわけで、事前のソートが不要かつ複数のデータファイルを読み込んで処理できるスクリプトを awk で書いてみました。

動作確認は Kali Linux と Windows10 1607 Anniversary Update の Bash on Windows (14.04LTS) で行っています。

# 実行例

```bash
# ニコイチの例。これは join コマンドでもできる。
kinoue@kali:~/work$ ./multijoin.awk list1.txt list2.txt
        list1.txt       list2.txt
key1    10      100
key2    5

# ３コイチの例。これは join コマンドでは一発ではできない。
kinoue@kali:~/work$ ./multijoin.awk list1.txt list2.txt list3.txt
        list1.txt       list2.txt       list3.txt
key1    10      100     300
key2    5
key3                    20
```

# 使用上の注意

大した注意事項は無いのですけど、下記３点が注意事項です。

- データファイルはタブ区切りで key[tab]value のように格納されていることが必要です。
- データファイルは事前のソートは不要です。
- 出力されるデータもタブ区切りテキストになりますが、Excel等で処理したい場合は必要に応じて改行コードや文字コード変換を実施するなどの別の作業が生じます。これには unix2dos とか nkf などをご利用いただければよろしいかと。

# 実装

```awk:multijoin.awk
#!/usr/bin/awk -f
#
# ソートされていない複数のファイルのデータを join して matrix にするスクリプト
#
# usage:
#   multijoin.awk [file1] [file2] ...
#
# 入力データ形式
#   key[tab]value

BEGIN {
    FS="\t"
    num_files=0
    current_filename=""
}

# 進捗の簡易表示
NR % 1000 == 0 {
        printf "%d:%s        \r", NR, FILENAME > "/dev/stderr"
}

{
    # 参照中のファイルが次のファイルに変わった場合は新しいファイル名を保持しておく
    if ( current_filename != FILENAME ) {
        current_filename=FILENAME
        filename_mapper[num_files]=current_filename
        num_files++
    }

    # ２次元配列への代入
    key=$1
    value=$2

    key_list[key]=1
    datamatrix[key][current_filename]=value
}

END {
    # 見出し行の出力
    for ( i = 0 ; i < num_files ; i++ ) {
        printf "\t%s",filename_mapper[i]
    }
    printf "\n"

    # データの出力
    for ( key in key_list ) {
        printf "%s", key
        for ( i = 0 ; i < num_files ; i++ ) {
            printf "\t%s",datamatrix[key][filename_mapper[i]]
        }
        printf "\n"
    }
}
```


# 処理時間

例えば、24個のデータファイル（それぞれ14万件以上のデータを含む）を処理するのに、自分の環境では20秒もかかりません。この例で用いたデータは、エンバカデロ・テクノロジーズの統合開発環境製品であるRAD Studio/Delphi/C++Builderの最新リリース10.2Tokyoのインストールオプションを変えてインストールを実施後にHDD上に存在するファイルおよびファイルサイズの一覧です。（ファイルの数がやたらと多いのは、これは c:¥Windows などのファイルも含んでいるためであり、RAD Studio/Delphi/C++Builderだけでこの数のファイルがインストールされるわけではありませんよ）

```bash
kinoue@kali:~/work$ wc -l *.txt
   190059 cpp_all.txt
   170835 cpp_Android.txt
   175285 cpp_iOS.txt
   168511 cpp_osx32.txt
   164419 cpp_win32.txt
   168592 cpp_win64.txt
   178218 delphi_all.txt
   158540 delphi_Android.txt
   164147 delphi_iOS.txt
   158138 delphi_Linux.txt
   156896 delphi_osx32.txt
   153207 delphi_win32.txt
   157810 delphi_win64.txt
   209082 dunitx.txt
   206450 helpfile.txt
   142957 ideonly.txt
   209469 interbase_desktop.txt
   209295 interbase_express.txt
   207399 intraweb.txt
   198426 languagepack-jpn.txt
   140495 preinstall_Tokyo.txt
   194796 radstudio_all.txt
   207409 sample-sourcecode.txt
   208212 teechart.txt
  4298647 total

kinoue@kali:~/work$ time ./multijoin.awk *.txt > result
real    0m20.553s
user    0m18.740s
sys     0m0.972s

kinoue@kali:~/work$ wc -l result
211758 result
```

しかし、これらの入力ファイルを join で処理するとしたら、事前準備としてのソートだけで30秒近く掛かってしまいます。

```bash
kinoue@kali:~/work$ time for file in *.txt ; do echo $file ; sort $file > .tmp\$file ; done
cpp_all.txt
cpp_Android.txt
cpp_iOS.txt
cpp_osx32.txt
cpp_win32.txt
cpp_win64.txt
delphi_all.txt
delphi_Android.txt
delphi_iOS.txt
delphi_Linux.txt
delphi_osx32.txt
delphi_win32.txt
delphi_win64.txt
dunitx.txt
helpfile.txt
ideonly.txt
interbase_desktop.txt
interbase_express.txt
intraweb.txt
languagepack-jpn.txt
preinstall_Tokyo.txt
radstudio_all.txt
sample-sourcecode.txt
teechart.txt

real    0m30.497s
user    0m27.248s
sys     0m0.712s
```