今回は電子メールを扱う。Maildirに溜まったメールを仕分ける処理を通じて、高速なシェルスクリプトを作成する方法を紹介する。
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)ではオプションが大きく異なっている。扱いの際はそれぞれの方法でオプションを指定する必要がある。
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で一気に処理する―」より加筆修正後転載