技研のまつけんです。
前回の記事では、複数のアーカイブを展開してファイルやフォルダの名前の傾向を調べる過程を紹介しました。今回は、その逆の「多数のファイルを分活してアーカイブする」ためのシェルスクリプトを紹介したいと思います (前回の「まとめ」では違う予告をしましたが、その前に今回の作業をする機会があったので、先に記事にしました)。
自分がデータクレンジングしたものを誰かに渡すことがあります。その際、メールで送るにしてもサーバに置くにしても、再びアーカイブ (+圧縮) すると便利なのですが、数ギガバイトのzipやtgzだと色々と不便です。そこで、分割してアーカイブしたくなります。それを実現するのに、真っ先に思いつく方法は、a、b、cという3つのフォルダがあるのであれば (そして、3つのフォルダ内のデータ量に偏りが無いのであれば)、
1 2 3 |
tar cfz a.tgz data/a* tar cfz b.tgz data/b* tar cfz c.tgz data/c* |
のように
- aで始まるものは、a.tgz
- bで始まるものは、b.tgz
- cで始まるものは、c.tgz
としてしまうことでしょう。
しかしながら、いつもこのように綺麗に分かれているとは限りません。そうすると、どのように分ければ良いか検討しなければなりません。これを手作業でするとなると意外に面倒です。そこで、今回は、最終的なアーカイブファイルの個数の上限を指定すると、分け方を考えてくれるスクリプトを作りました。こちらです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
TGT="$1" LIM=10 SKIP=1 find "$TGT" -type d | grep -v ^"$TGT"$ > list find "$TGT" -not -type d | grep -v ^"$TGT"$ > list_nd LEN=`cat list | sed -e 's#.#x#g' | uniq | sort | uniq | tail -n 1 | wc -c` declare NS for N in `seq $(($LEN - 1))` do M=`cat list | cut -c -$N | uniq | sort | uniq | wc -l` NS[$M]=$N if [ $LIM -lt $M ] then break fi NCH=$N NFL=$M done for M in "${!NS[@]}" do echo -n "${NS[$M]} chars: $M files" if [ ${NS[$M]} -eq $NCH ] then echo " <<< Selected <<<" else echo "" fi done while read PREFIX do ITEMS=`cat list_nd | grep ^"$PREFIX" | wc -l | sed -e 's#...$#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#,\+#,#g' -e 's#^,##'` echo "$PREFIX* -> "`basename "$PREFIX"`.tgz" ($ITEMS items)" done < <( cat list | cut -c -$NCH | uniq | sort | uniq ) echo -n "$(tput cuu $NFL)" while read PREFIX do ARC=`basename "$PREFIX"`.tgz ITEMS=`cat list_nd | grep ^"$PREFIX" | wc -l` ITEMS2=`echo $ITEMS | sed -e 's#...$#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#,\+#,#g' -e 's#^,##'` echo -n "$PREFIX* -> $ARC ($ITEMS2 items) " echo -n "$(tput sc)" if [ $SKIP -ne 0 -a -f "$ARC" ] then echo -n "<<< Comparing # of items <<<" ITEMS_ALL=`cat list list_nd | grep ^"$PREFIX" | wc -l` ITEMS_ARC=`(tar tfz $ARC | wc -l) 2> /dev/null` else ITEMS_ALL=-1 ITEMS_ARC=-2 fi echo -n "$(tput rc)" echo -n "$(tput el)" if [ $SKIP -ne 0 -a $ITEMS_ALL -eq $ITEMS_ARC ] then echo -n "Skipped " else echo -n "<<< Processing <<<" tar cfz "$ARC" "$PREFIX"* echo -n "$(tput rc)" echo -n "$(tput el)" echo -n "Done " fi SIZE=`wc -c < "$ARC" | sed -e 's#...$#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#,\+#,#g' -e 's#^,##'` echo "($SIZE bytes)" done < <( cat list | cut -c -$NCH | uniq | sort | uniq ) rm -f list list_nd |
今回のような処理を自動で行おうと考えた場合、もっとも簡単な方法は「先頭から何文字までを共通として扱えば目的を達成できるか」を調べることでしょう。例えば、前回の記事で扱った例とよく似たフォルダ構成:
1 2 3 4 5 6 7 8 9 10 |
data_000/20101121-090928 data_000/20110301-011826 data_000/20110502-195236 data_000/20110707-085504 data_000/20120407-180406 data_000/20120509-133753 data_000/20120513-204704 data_000/20120725-104143 data_000/20120824-024826 data_000/20121116-160047 |
のような場合、「data_000/20*」は上記の全ての項目を指しますが、先頭の12文字「data_000/201」までを共通として扱い、13文字目の違いを利用して
1 2 3 |
tar cf 2010.tar data_000/2010* tar cf 2011.tar data_000/2011* tar cf 2012.tar data_000/2012* |
とすれば、3つのファイルにわけてアーカイブすることが出来ます。13文字目までを共通として扱い、14文字目の違いを利用して
1 2 3 4 |
tar cf 20101.tar data_000/20101* tar cf 20110.tar data_000/20110* tar cf 20120.tar data_000/20120* tar cf 20121.tar data_000/20121* |
とすれば、4つに分かれます。
こちらに対して実行した結果です。そのときの実行中の画面表示がこちらです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
12 chars: 1 files 13 chars: 3 files 14 chars: 4 files 15 chars: 9 files 25 chars: 10 files <<< Selected <<< 26 chars: 28 files data_000/20101121-090928_* -> 20101121-090928_.tgz ( 7,091 items) Skipped (69,986,411 bytes) data_000/20110301-011826_* -> 20110301-011826_.tgz (10,856 items) Skipped (155,913,511 bytes) data_000/20110502-195236_* -> 20110502-195236_.tgz (12,824 items) Skipped (189,698,173 bytes) data_000/20110707-085504_* -> 20110707-085504_.tgz (12,912 items) Done (183,731,059 bytes) data_000/20120407-180406_* -> 20120407-180406_.tgz (12,160 items) Done (176,292,719 bytes) data_000/20120509-133753_* -> 20120509-133753_.tgz ( 1,672 items) Done (25,096,677 bytes) data_000/20120513-204704_* -> 20120513-204704_.tgz (10,080 items) Done (146,139,693 bytes) data_000/20120725-104143_* -> 20120725-104143_.tgz (10,160 items) <<< Processing <<< data_000/20120824-024826_* -> 20120824-024826_.tgz ( 3,896 items) data_000/20121116-160047_* -> 20121116-160047_.tgz (14,962 items) |
最初の部分は、何文字目まで共通にすると幾つのファイルが出来るか、のリストです。冒頭で、LIM=10としているので、10ファイル以下におさまる最大の値として、「25文字目まで共通」が選ばれています。実行中のステータスの表示などについては、カーソルを移動 [1][2] して上書きすることで実現しています。途中でctrl-cなどで中断すると、その位置にプロンプトが出てしまうので、trap [3] を使って、後始末をするようにしても良いかと思いますが、今回は省略しています。また、プログレスバー [4] を表示するのも良さそうです。
サマリー情報
上に述べたように分割したファイルを受け渡す場合は、受け取った側がこちらと同じフォルダ・ファイル構成を再現できたのか、確認する手段が欲しいところです。そこで、サマリー情報を送る方法を紹介します。
以下の通り、アイテムのリストと、それぞれのファイルのMD5を記録することが出来ます:
1 2 |
find result | sort | gzip > items.gz find result -type f | sort | xargs md5sum | gzip > file-sums.gz |
受け側は、分割されたアーカイブを受け取って展開したあと、
1 2 |
diff <( find result | sort ) <( zcat items.gz ) find result -type f | sort | xargs md5sum | gzip > file-sums.gz |
のようにすることで、全てのフォルダ・ファイルを正しく受け取ることが出来たか、確認することができます。
まとめ & Known Bug
如何でしたか? 今回は、大量のファイルを分割して送る方法、また、正しく受け渡せたのか確認する方法について紹介しました。よろしければ、活用してみてください。
さて、実は、今回のスクリプトにはバグがあります。実は、
1 2 3 4 5 6 7 8 9 10 |
./data/data_000/20101121-090928 ./data/data_000/20110301-011826 ./data/data_000/20110502-195236 ./data/data_000/20110707-085504 ./data/data_000/20120407-180406 ./data/data_001/20120509-133753 ./data/data_001/20120513-204704 ./data/data_001/20120725-104143 ./data/data_002/20120824-024826 ./data/data_002/20121116-160047 |
のようなディレクトリ構成の場合、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
12 chars: 1 files 13 chars: 3 files 17 chars: 6 files 18 chars: 8 files 19 chars: 9 files <<< Selected <<< 20 chars: 12 files data/data_000* -> data_000.tgz data/data_000/20101* -> 20101.tgz data/data_000/20110* -> 20110.tgz data/data_000/20120* -> 20120.tgz data/data_001* -> data_001.tgz data/data_001/20120* -> 20120.tgz data/data_002* -> data_002.tgz data/data_002/20120* -> 20120.tgz data/data_002/20121* -> 20121.tgz |
という動きをしてしまうのです。これでは、同じファイルが複数のアーカイブに入ってしまいます。これについては原因はわかっている (そして、それほど困っていない) ので、いずれ直したいと思いますが、読者の皆様もパズル感覚で修正案を考えていただければ、と思います。
参考文献
[1] Shellの出力でカーソルを移動させる
[2] Linux Command Line Adventure: tput
[3] shellのtrapについて覚え書き
[4] シェルスクリプトにプログレスバーを追加する方法