テキストファイルをソートするときに頻繁に使うUnixコマンド
@nokuno さんの Unix コマンドに関する記事を読みました。
- テキストファイルを処理するときのUnixコマンドまとめ
-- http://d.hatena.ne.jp/nokuno/20120121/1327139192
実務では何でもかんでも難しい手法や複数台の計算機で処理することが正義ではなく「同じ処理を実現する最も簡単で効果的な手法」で処理することが正義なので、この手の記事はいくつあっても良いものだ、と思いました。
便乗して、僕が中小規模なデータをソートする際に頻繁に使っている Unix コマンド(と Perl のワンライナー)も書いてみます。
サンプルのTSVファイル
特に意味はないファイルですが2列目は値段です。どうでもいいですけど割と適正な価格が付いていると思います。カラムはタプ文字で区切っています。
■ test_imput.txt
cat test_imput.txt コーヒー 270 E B ミルク 300 F A ココア 350 D B 抹茶 350 C A オレンジジュース 300 B B ウーロン茶 270 A A
以下ではこのファイルをソートしてみます。
列全体をキーにしてソート
sort するときは LC_ALL=C sort を使わないと、環境によって結果が変わってしまう場合があるし、一定量以上のファイルをsortする際には、sort処理自体の速度もLC_ALL=Cありの方が速いです。
LC_ALL=C sort < test_imput.txt ウーロン茶 270 A A オレンジジュース 300 B B ココア 350 D B コーヒー 270 E B ミルク 300 F A 抹茶 350 C A
任意の列だけを比較して sort したい
上記は行ごとに比較をしているけど、以下のようにタブで区切った任意の値の値でソートすることもできます。もちろん値の境界文字も任意で指定できます。
3列目の値をキーにしてソート
LC_ALL=C sort -t $'\t' -k 3,3 < test_imput.txt ウーロン茶 270 A A オレンジジュース 300 B B 抹茶 350 C A ココア 350 D B コーヒー 270 E B ミルク 300 F A
2列目の値を一般的な数値のキーとしてソート
もちろん、以下のようにタブで区切った2列目の値の値でソートすることもできます。その時に-gオプションを付けると、一般的な数値として比較することができます。他にもsort方法のオプションはあるのでman sortを見てみましょう。
LC_ALL=C sort -t $'\t' -k 2,2 -n < test_imput.txt ウーロン茶 270 A A コーヒー 270 E B オレンジジュース 300 B B ミルク 300 F A ココア 350 D B 抹茶 350 C A
■man sortの結果の一部
-g, --general-numeric-sort
compare according to general numerical value
-i, --ignore-nonprinting
consider only printable characters
-M, --month-sort
compare (unknown) < `JAN' < ... < `DEC'
-h, --human-numeric-sort
compare human readable numbers (e.g., 2K 1G)
-n, --numeric-sort
compare according to string numerical value
-R, --random-sort
sort by random hash of keys
2〜3列目の値をキーにしてソート
さらに、2カラム目から3カラム目までを考慮することもできます。
LC_ALL=C sort -t $'\t' -k 2,3 < test_imput.txt ウーロン茶 270 A A コーヒー 270 E B オレンジジュース 300 B B ミルク 300 F A 抹茶 350 C A ココア 350 D B
任意の列を任意の順に組み合わせたキーでソートしたい
ここで4列目と2列目を取り出して、sortのキーを「4列目2列目」としたい場合には、僕はいったんPerlスクリプトで別のファイルを生成したりしますが、Unix コマンドだけでもやればできます。
cutコマンドは各行から任意のカラムをカラム番号の昇順に列を抜き出します。今回は2より4を先にしたいのでcutコマンドを2回実行します。オプションがsortと違うのか紛らわしいです。
■2列目と4列目をそれぞれ切り出す
cut -d $'\t' -f 2 < test_imput.txt > test_imput_cut2.txt cat test_imput_cut2.txt 270 300 350 350 300 270 cut -d $'\t' -f 4 < test_imput.txt > test_imput_cut4.txt cat test_imput_cut4.txt B A B A B A
ちなみにcutは複数のカラムを同時に指定できるのですが、上にも書いたとうり指定した番号を昇順にしてしまいます。そのため以下のようにしても、2列目4列目、と出力されるので無駄です。
cut -d $'\t' -f 4,2 < test_imput.txt 270 B 300 A 350 B 350 A 300 B 270 A
出力したら paste コマンドでマージします。
■切り出したファイルと元のファイルを結合
paste test_imput_cut4.txt test_imput_cut2.txt test_imput.txt > test_imput_paste.txt cat test_imput_paste.txt B 270 コーヒー 270 E B A 300 ミルク 300 F A B 350 ココア 350 D B A 350 抹茶 350 C A B 300 オレンジジュース 300 B B A 270 ウーロン茶 270 A A
最後に1列目と2列目でソートしたあと、3,4,5,6列目をcutで取り出します。
LC_ALL=C sort -t $'\t' -k 1,2 < test_imput_paste.txt > test_imput_paste_sort.txt cat test_imput_paste_sort.txt A 270 ウーロン茶 270 A A A 300 ミルク 300 F A A 350 抹茶 350 C A B 270 コーヒー 270 E B B 300 オレンジジュース 300 B B B 350 ココア 350 D B cut -d $'\t' -f 3,4,5,6 < test_imput_paste_sort.txt ウーロン茶 270 A A ミルク 300 F A 抹茶 350 C A コーヒー 270 E B オレンジジュース 300 B B ココア 350 D B
できましたね。
Perlのワンライナーを使って良いなら以下のようにもできます。
perl -ne '$l=$_;$l=~s|\n||;@a=split /\t/,$l;print "$a[3]\t$a[1]\t$l\n";' < test_imput.txt > test_imput_paste.txt sort -t $'\t' -k 1,2 < test_imput_paste.txt > test_imput_paste_sort.txt cut -d $'\t' -f 3,4,5,6 < test_imput_paste_sort.txt ウーロン茶 270 A A ミルク 300 F A 抹茶 350 C A コーヒー 270 E B オレンジジュース 300 B B ココア 350 D B
3行で済むのはとても楽ですね。
おわりに
大体の場合、こんな感じで整形をしたあと、任意のカラムを使ってキーに対する値を合計したり、キーをuniqにしたりします。
近いうちにキーの頻度カウント時によく使う Unix コマンドについて書こうと思います。