UECジャーナル

開眼☆シェルスクリプト【12】メールを高速に振り分ける ―xargsで一気に処理する

はじめに

今回は電子メールを扱う。Maildirに溜まったメールを仕分ける処理を通じて、高速なシェルスクリプトを作成する方法を紹介する。

Mairdir

MTAとしてPostfixを使用しメールの保存形式にMaildirを使っているケースを考える。Maildirではメール1通が1つのファイルに保存される。Postfixであれば設定ファイルmail.cfに次のような設定をすればよい。

$ cat /pathto/main.cf
...
# DELIVERY TO MAILBOX
#
# The home_mailbox parameter specifies the optional pathname of a
# mailbox file relative to a user's home directory. The default
# mailbox file is /var/spool/mail/user or /var/mail/user.  Specify
# "Maildir/" for qmail-style delivery (the / is required).
#
#home_mailbox = Mailbox         ← mboxを使う
home_mailbox = Maildir/         ← Maildirを使う
...
$ 

Maildirディレクトリは通常、ユーザのホームディレクトリ直下に作成される。~/Maildirの下には次のようにcur、new、tmpという3つのディレクトリがある。

$ ls ~/Maildir
cur  new  tmp
$ 

新着メールはnewディレクトリ以下に保存されている。

$ ls ~/Maildir/new/ | head -n 3 
1339304183.Vfc03I46017dM943925.sakura1
1339305265.Vfc03I46062cM458553.sakura1
1339306807.Vfc03I4607c6M993984.sakura1
$ ls ~/Maildir/new/ | wc -l
25094    ← 2万5千件程度メールが入っている
$ 

ファイル名を眺める

ファイル名は重複せず一意になるように工夫されている。

1339304183.Vfc03I46017dM943925.sakura1

「1339304183」はUNIX時間(IEEE Std 1003.1-2001 ("POSIX.1")、time(3)など参照のこと)と呼ばれる表記で、1970年1月1日0時0分0秒からの積算秒数を表している。UNIX時間はBSD date(1)であれば次のように日付へ変更できる。

$ date -j -f "%s" 1339304183 
2012年 6月10日 日曜日 13時56分23秒 JST
$ 

次のように実行してもよい。

$ date -jr "%s" 1339304183 
2012年 6月10日 日曜日 13時56分23秒 JST
$ 

GNU Core Utilities date(1)を使う場合、次のように日付へ変更できる。

$ date -d @1339304183 
2012年  6月 10日 日曜日 13:56:23 JST
$ 

GNU Core Utilities date(1)にはフィルタモード(-fオプション)という機能があり、UNIX時間から日付への変換を次のようにまとめて実行できる。-fで指定するのは標準入力またはファイルとなる。

$ head -n 3 datefile 
@1339304183
@1339305265
@1339306807
$ head -n 3 datefile | date -f -
2012年  6月 10日 日曜日 13:56:23 JST
2012年  6月 10日 日曜日 14:14:25 JST
2012年  6月 10日 日曜日 14:40:07 JST
$ head -n 3 datefile | date -f - "+%Y%m%d %H%M%S"
20120610 135623
20120610 141425
20120610 144007
$ 

IEEE Std 1003.1 "POSIX.1"に記載されているdate(1)コマンドの規約はフォーマット指定と-uオプションのみで、ほかのオプションは規定されていない。このため、BSD date(1)GNU Core Utilities date(1)ではオプションが大きく異なっている。扱いの際はそれぞれの方法でオプションを指定する必要がある。

while構文を使わない振り分け

new以下のメールファイルを日付ごとに振り分ける。ホームディレクトリ以下にMAILというディレクトリを作り、サブディレクトリとして日付別のディレクトリを作成し、サブディレクトリ以下へメールをコピーする。

while構文を使ったスクリプトを次に示す。ファイルを1個ずつ日付のディレクトリに放り込んでいる。

$ cat DISTRIBUTE_BY_DATE.WHILE
#!/bin/sh 

sdir=/home/ueda/Maildir/new
ddir=/home/ueda/MAIL

tmp=/home/ueda/tmp/$$

cd $sdir || exit 1

######################################
#ファイルのリストを作る
echo *.*.*					|
tr ' ' '\n'					|
while read f ; do
	UNIXTIME="@"$(echo $f | awk -F. '{print $1}')
	DATE=$(date -d $UNIXTIME "+%Y%m%d")

	[ -e "$ddir/$DATE" ] || mkdir $ddir/$DATE
	cp -p $f $ddir/$DATE/
done
$ 

後ほどフィルタモードを使った処理を示したいので、ここではGNU Core Utilities date(1)の使用を前提とする。FreeBSDを使っている場合、dateを/compat/linux/bin/dateに置き換えるか、cf (sysutils/lbl-cf)などUNIX時間を時刻に変換するコマンドに差し替えてほしい。

echoはファイル名を空白区切りで出力してくれる。いつもls(1)を使っている人は、適当なディレクトリでecho *と打ってみてほしい。ファイルの一覧が取得できるはずだ。ファイル名が取得できたら、tr(1)で空白を改行に変換し、1つ1つwhile構文で回しながら処理する。

$ time ./DISTRIBUTE_BY_DATE.WHILE
real	7m21.673s
user	1m24.858s
sys	5m51.464s
$ 

次にwhile構文を使わない場合のスクリプトを示す。

$ cat DISTRIBUTE_BY_DATE
#!/bin/sh 

sdir=/home/ueda/Maildir/new
ddir=/home/ueda/MAIL
tmp=/home/ueda/tmp/$$

cd $sdir || exit 1

######################################
#ファイルのリストを作る
echo *.*.*                      |
tr ' ' '\n'                     |       
#1:ファイル名
awk -F. '{print "@" $1,$0}'     > $tmp-files
#1:UNIX時間 2:ファイル名

# $tmp-filesの例:
#@1348117807 1348117807.Vfc03I4670eaM254446.www5276ue.sakura.ne.jp

######################################
#ファイルのリストに年月日をくっつける
self 1 $tmp-files       |
date -f - "+%Y%m%d"     |
#1:年月日
ycat - $tmp-files       |
#1:年月日 2:UNIX時間 3:ファイル名
delf 2 > $tmp-ymd-file
#1:年月日 2:ファイル名

# $tmp-ymd-fileの例
#20120920 1348116008.Vfc03I4670ecM186337.www5276ue.sakura.ne.jp

cd $ddir || exit 1

######################################
#日別のディレクトリを作る
self 1 $tmp-ymd-file    |
uniq                    |
xargs -i_ mkdir -p _

cat $tmp-ymd-file       |
awk -v sd="$sdir" '{print sd "/" $2, "./" $1 "/"}'      |
#コピー元、コピー先を読み込んでcpに渡す。
xargs -n 2 cp -p

rm -f $tmp-*
exit 0
$ 

while構文を使わない版の実行速度は次のようになる。10倍ほど高速だ。

 $ time ./DISTRIBUTE_BY_DATE
real	0m43.866s
user	0m16.774s
sys	0m43.599s
$ 

while構文を使わない版のスクリプトでは、1個1個ファイルを処理するのではなく、作るディレクトリのリストとコピーするファイルのリストを作成し、xargs(1)で一気に作成している。date(1)やawk(1)、mkdir(1)をwhile構文で何回も呼ぶ必要がなくなっていることが実行速度の高速化につながっている。また、シェルのwhileパース処理の負担がなくなることも高速化に寄与している。

ycat(1)Open usp Tukubaiのコマンドだ。「横キャット」と発音する。横にファイルをくっつける。例を次に示す。Open usp Tukubaiの詳細は、Open usp Tukubaiについてをご覧いただきたい。

$ cat file1 
1
2
3
$ cat file2 
a
b
c
$ ycat file1 file2
1 a
2 b
3 c
$ 

date(1)で作った日付が、もとの$tmp-filesのレコードにくっつくことになる。

日付のディレクトリを作りその中にファイルをコピーする。

xargs mkdir -p

上の処理で日付を次々に受け取って、mkdir(1)を実行している。mkdir(1)の-pオプションは、すでにディレクトリがあってもエラーにならないように指定している。

xargs -n 2 cp -p

上の処理には、次のようなテキストが流れ込む。

/home/ueda/Maildir/new/1339308608.Vfc03I4609ebM178619.sakura1 ./20120610/
/home/ueda/Maildir/new/1339308909.Vfc03I4609ecM601364.sakura1 ./20120610/
/home/ueda/Maildir/new/1339309208.Vfc03I4609edM55303.sakura1 ./20120610/
...
$ 

コピー元のファイルとコピー先のディレクトリがxargs(1)に流れ込む。xargs(1)に-n 2 というオプションがついているが、これは「2個ずつ文字列を読み込む」という意味になる。行ごとに空白やタブで区切られた文字列を2つ取ってきて、cp -pの後ろに引数として配置してからcp(1)が実行されることになる。

cp(1)の-pオプションは、ファイルの時刻や持ち主などをなるべく変えずにコピーしたいときに使う。

今回の例で大事なことは、while構文を使わずに、処理すべき内容が書いてあるテキストを作ってから一気に処理するを実行した方が速度やデバッグの点で有利になることだ。特に今回のようにコピーなどの具体的なファイル移動が絡むと、スクリプトを書いて動作確認して・・・という作業はとても面倒だ。

./DISTRIBUTE_BY_DATE を実行して、MAILディレクトリの下に日付のディレクトリができ、各日付のディレクトリ下にメールのファイルが配られていることを確認する。

$ ls
20120610  20120624  20120708  20120722  20120805  20120819  20120902  20120916
20120611  20120625  20120709  20120723  20120806  20120820  20120903  20120917
$ ls 20120920 | head -n 3
1348066810.Vfc03I467066M309422.www5276ue.sakura.ne.jp
1348067409.Vfc03I467067M503001.www5276ue.sakura.ne.jp
1348068009.Vfc03I467068M641721.www5276ue.sakura.ne.jp
$ 

おわりに

今回はMaildirのメールを振り分ける処理を例に取り上げ、高速で実用的なシェルスクリプトを作成する方法を紹介した。今回のポイントを要約すると次のとおり。

コマンドを呼ぶ回数を減らすxargsを活用する

Software Design 2012年12月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【12】メールを高速に振り分ける ―xargsで一気に処理する―」より加筆修正後転載

last modified: 2014-03-17 20:03:17