grep -v 100回実行するのはさすがに避けたい
いきなり問題
問題
今、約1万人の会員名簿(members.txt)と、諸藩の事情によりブラックリスト入りしてしまった約100人会員名一覧(blacklist.txt)がある。
会員名簿からブラックリストに登録されている会員のレコードを全て除去した、「キレイな会員名簿」を作りなさい。members.txtのデータフォーマット:
- スペース区切りのテキストファイル
- 1列目:会員ID、2列目:会員名
blacklist.txtのデータフォーマット:
- 1列のみで構成されたテキストファイル
- 1列目:会員名
さて、これをどう解くか?
joinコマンドを使うとスマートに解決できるよ
まさかcat members.txt | grep -v ブラックリスト会員1 | grep -v ブラックリスト会員2 | ……
なんてコード書くわけにもいかない。
普通に考えつくのは、blacklist.txtをwhile
ループで回して、ブラックリスト会員の数だけgrep -v ブラックリスト会員n
を実行するという方法だろうが、あまりにも効率が悪い。
こんな時は、join
コマンドを使うとカッコよく解けるよ、というのがこのTipsだ。
まずは、サンプルデータを作ろう
カッコよく解けることを実証するため、まずはデータを作る。
さすがに1万人分のサンプルネームを生成するのは大変だ。そこで/dev/urandom
を用いた4桁の16進数を便宜上の名前ということにして、そういう会員名簿を作ってみる。
こんなふうにしてワンライナーでサクッと作ろう。
# 註) 第1列に会員ID、第2列に(便宜上の)「名前」が入ったデータを作る
$ dd if=/dev/urandom bs=1 count=20000 |
> od -A n -t x2 |
> tr ' ' '\n' |
> grep -v '^$' |
> awk '{printf("ID%05d %s\n",NR,$0)}' > members.txt
# 註) ブラックリストの(便宜上の)「名前」が入ったデータを作る
$ dd if=/dev/urandom bs=1 count=200 |
> od -A n -t x2 |
> tr ' ' '\n' |
> grep -v '^$' > blacklist.txt
解答シェルスクリプト
先に解答シェルスクリプトを記す。
このシェルスクリプトを実行すれば、ブラックリスト会員行だけキレイに取り除かれて出力される。デフォルトだと結果は画面にだーっと表示されるので、結果は適宜ファイルにリダイレクトすること。
#! /bin/sh
# === joinに掛けられるようにブラックリストをソートしておく ==========
cat blacklist.txt |
sort -k 1,1 |
uniq > sorted_bl.txt # 列構成 1:会員名(ブラック)
# === ブラックリストを左(1)、会員名簿を右(2)としてRIGHT JOINする ====
cat members.txt |
# ここでの列構成 1:会員ID 2:会員名(members.txt由来)#
sort -k2,2 | # JOINするため、keyにする2列目で予めソート
join -1 1 -2 2 -a 2 -o 1.1,2.1,2.2 sorted_bl.txt - | # 会員名をキーにソート
# ここでの列構成 1:会員名(sorted_bl由来,あれば) 2:会員ID 3:会員名(members.txt由来)
grep '^ ' | # 結合失敗(=blacklistにない)行は1列目が空なのでそれのみ抽出
sed 's/^ *//' | # 行頭の空文字を除去
# ここでの列構成 1:会員ID 2:会員名(members.txt由来)#
sort -k1,1 # 当初の順(会員ID)に並べ替える
# === 後始末 ========================================================
rm -f sorted_bl.txt
exit 0
コードの解説
最初にこのコードのアイデアを述べておこう。一言で言うなら、「会員名簿とブラックリスト、それぞれをテーブルとみなして外部結合し、結合できなかった(=ブラックリストに無かった)行だけを抽出する」である。結合しているのがjoin
コマンドの部分で、抽出しているのがその次のgrep
コマンドの部分だ。
join
には-a
オプションを付けてあるが、これが外部結合のためには重要だ。-a
の次に指定してある2
というのは右表、つまりこの場合members.txtを指しており、結合に失敗した行であってもmembers.txtにある行は表示せよという意味だ。-o
オプションでは出力する列の構成を指定していて、一番最初に1(=左表、つまりブラックリスト)の会員名を出力するようにしているが、結合に失敗した場合には当然ここは空になるので、行頭には列区切り文字である半角スペースが来る。つまり、そのような行だけ抽出してやればよいわけだ。
抽出したら、sed
コマンド等で先頭の半角スペースを取り除き、再びsort
コマンドを使って順番を入れ替えた表を会員ID順に戻してやればよい。
SQLのSELECT文っぽいでしょ?
コードや解説を読んでいて、なんかやってることがSQLっぽく思えなかっただろうか。SQLのSELECT文で同じことをするとしたら、こんな感じに書けるでしょ?
SELECT
MEM."会員ID",
MEM."会員名"
FROM
blacklist AS MEM
RIGHT OUTER JOIN
members AS BL
ON BL."会員名" = MEM."会員名"
WHERE
BL."会員名" IS NOT NULL
ORDER BY
MEM."会員ID" ASC;
何が言いたいかというと、「SQLでできることはUNIXコマンド+シェルスクリプトでも大抵できるよ」ということだ。ついでに言うと、SELECT文でデータの流れを追うと、FROM句→(RIGHT OUTER JOIN句)→WHERE句→ORDER BY句→(最初に戻って)SELECTの直後、であるが、シェルスクリプトの場合はほぼ上から下へ一直線であるのが個人的には好きだ。