技術研究所のまつけんです。

 

技術研究所 (技研) では、画像 (静止画) データや動画データを分析対象として扱います。その際、分析対象のデータを間違えて消してしまったり、書き換えてしまったりすると大変です。今回は、私が実践している「間違いが起きない仕組み」について紹介したいと思います。具体的には、

  • オリジナルファイルを入れたディレクトリは書き込み禁止にし、
  • その隣に作業用ディレクトリを作成し、
  • オリジナルと同じディレクトリ構造を作り、
  • オリジナルファイルへのシンボリック・リンクを作成

します。もちろん、手作業でも出来るのですが、オリジナルファイルが多数ある場合などには自動化しないと大変です。そこで本記事では、そのために作成した、複数のファイルやディレクトリを扱うシェルスクリプトと、その作り方も紹介します。

例えば、動画を分析するプロジェクト「movie_analyse」において、10月20日と10月31日に合計5本の動画を撮影したとします。それらを2019-10-20、2019-10-31の2つのディレクトリ (フォルダ) に格納します。findコマンドでアイテムの一覧を表示すると、以下のようになります:

$ find movie_analyse/
movie_analyse/
movie_analyse/2019-10-20
movie_analyse/2019-10-20/movie to analyse (3).avi
movie_analyse/2019-10-20/movie to analyse (1).avi
movie_analyse/2019-10-20/movie to analyse (2).avi
movie_analyse/2019-10-31
movie_analyse/2019-10-31/movie to analyse (4).avi
movie_analyse/2019-10-31/movie to analyse (5).avi

技研ではデータ分析や機械学習にUbuntu + Python 3 + Jupyter Notebookを利用しています。UNIX系OSにおいて、ファイルの書き換えはファイルのパーミッション設定、ファイルの削除はディレクトリのパーミッション設定で管理します。というわけで、

$ chmod -R a-w movie_analyse/

を実行して、ディレクトリとファイルの書き込み許可をオフにしてしまえば、ファイルの書き換えも削除もできなくなります。

しかし、これでは、分析結果や途中経過をmovie_analyse/2019-10-20/やmovie_analyse/2019-10-31/の中にファイルとして保存することも出来ません。ディレクトリを書き込み禁止にしているので、ファイルの作成も出来なくなってしまうからです。かと言って、ディレクトリごとコピーして、そこで作業をするとなると、ストレージが余分に消費されてしまいます。そこで、冒頭で述べた通り、

  • movie_analyseの隣にmovie_analyse.workというディレクトリを作成し、
  • そこにオリジナルと同じディレクトリ構造を作成し、
  • その中に分析対象ファイルへのシンボリック・リンクを作成

します。すなわち、

'movie_analyse.work/2019-10-20/movie to analyse (1).avi' -> 'movie_analyse/2019-10-20/movie to analyse (1).avi'
'movie_analyse.work/2019-10-20/movie to analyse (2).avi' -> 'movie_analyse/2019-10-20/movie to analyse (2).avi'
'movie_analyse.work/2019-10-20/movie to analyse (3).avi' -> 'movie_analyse/2019-10-20/movie to analyse (3).avi'
'movie_analyse.work/2019-10-31/movie to analyse (4).avi' -> 'movie_analyse/2019-10-31/movie to analyse (4).avi'
'movie_analyse.work/2019-10-31/movie to analyse (5).avi' -> 'movie_analyse/2019-10-31/movie to analyse (5).avi'

という状態です。
「movie_analyse.work/2019-10-20/movie to analyse (1).avi」は「movie_analyse/2019-10-20/movie to analyse (1).avi」を指すシンボリック・リンク、
「movie_analyse.work/2019-10-20/movie to analyse (2).avi」は「movie_analyse/2019-10-20/movie to analyse (2).avi」を指すシンボリック・リンク、
・・・・というわけです。これであれば、movie_analyse.work内のディレクトリを書き込み可に設定しておいても安心です。分析対象ファイルを書き込み不可にしておけば、間違えて書き換えてしまう心配はありませんし、シンボリック・リンクを消してしまっても、すぐに作成しなおすことが出来ます。

では早速、ディレクトリ構造のコピーから始めます (シェルはbashです)。movie_analyse内のディレクトリ一覧は「find movie_analyse -type d」で得られます:

$ find movie_analyse -type d
movie_analyse
movie_analyse/2019-10-20
movie_analyse/2019-10-31

それをsedで加工して「xargs mkdir -p」に流し込めば終わりです:

$ find movie_analyse -type d | sed -e 's#^movie_analyse#movie_analyse.work#' -e 's#.*#"�"#'
"movie_analyse.work"
"movie_analyse.work/2019-10-20"
"movie_analyse.work/2019-10-31"
$ find movie_analyse -type d | sed -e 's#^movie_analyse#movie_analyse.work#' -e 's#.*#"�"#' | xargs mkdir -p
$ find movie_analyse.work
movie_analyse.work
movie_analyse.work/2019-10-20
movie_analyse.work/2019-10-31

sedコマンドのセパレータは/や#や!などが使えますが、pathを扱う場合は/が出てきますので/以外を使います。#は他の文字と比べて目立つので、私のオススメは#です。sedコマンドに2つの正規表現置換を与えていますが、前者は元の名前に.workを追加するためのもの、後者は行頭と行末にダブルクォーテーションを追加するためのものです。

最終的にシェルスクリプトにするので、2つのディレクトリ名movie_analyse、movie_analyse.workは変数に格納しておきます。

ORG=movie_analyse
WRK=$ORG.work
find $ORG -type d | sed -e "s#^$ORG#$WRK#" -e 's#.*#"�"#' | xargs mkdir -p

sedの1つ目の引数をダブルクォーテーションに変更したのはシングルクォーテーションだと$XXXを展開してくれないからです。

続いて、シンボリック・リンクを作成してみます。手動で1つずつ作るには、

$ cd $WRK/2019-10-20/
$ ln -s "../../$ORG/2019-10-20/movie to analyse (1).avi"
$ ln -s "../../$ORG/2019-10-20/movie to analyse (2).avi"
$ ln -s "../../$ORG/2019-10-20/movie to analyse (3).avi"
$ cd ../2019-10-31/
$ ln -s "../../$ORG/2019-10-31/movie to analyse (4).avi"
$ ln -s "../../$ORG/2019-10-31/movie to analyse (5).avi"

のように手打ちします。これをそのままシェルスクリプトで実現しようとすると「cd ../2019-10-31」のような戻る処理が面倒です。そのような場合、( )を使うことで簡単にできます:

$ ( cd $WRK/2019-10-20/ ; ln -s "../../$ORG/2019-10-20/movie to analyse (1).avi" )
$ ( cd $WRK/2019-10-20/ ; ln -s "../../$ORG/2019-10-20/movie to analyse (2).avi" )
$ ( cd $WRK/2019-10-20/ ; ln -s "../../$ORG/2019-10-20/movie to analyse (3).avi" )
$ ( cd $WRK/2019-10-31/ ; ln -s "../../$ORG/2019-10-31/movie to analyse (4).avi" )
$ ( cd $WRK/2019-10-31/ ; ln -s "../../$ORG/2019-10-31/movie to analyse (5).avi" )

では、これをシェルスクリプトにしてみましょう。ファイル一覧を取得して、それらについて1つずつ処理するには、

find $ORG -type f | while read FILE
do
処理1
処理2
処理3
done

という方法を使います。1行で書く場合は、

find $ORG -type f | while read FILE; do 処理1 ; 処理2 ; 処理3 ; done

となります (doの後ろにセミコロンを入れるとエラーになるので注意)。もう1つ、forを使って、

for FILE in `find $ORG -type f`
do
処理1
処理2
処理3
done

というふうに書く方法もありますが、こちらの場合、検索結果に半角スペースが含まれいていると、うまく動きません。以下のように、スペースで分割されたものがFILEに格納されてしまうからです:

$ find $ORG -type f | while read FILE ; do echo $FILE ; done
movie_analyse/2019-10-20/movie to analyse (3).avi
movie_analyse/2019-10-20/movie to analyse (1).avi
movie_analyse/2019-10-20/movie to analyse (2).avi
movie_analyse/2019-10-31/movie to analyse (4).avi
movie_analyse/2019-10-31/movie to analyse (5).avi
$ for FILE in `find $ORG -type f` ; do echo $FILE ; done
movie_analyse/2019-10-20/movie
to
analyse
(3).avi
movie_analyse/2019-10-20/movie
to
analyse
(1).avi
movie_analyse/2019-10-20/movie
to
analyse
(2).avi
movie_analyse/2019-10-31/movie
to
analyse
(4).avi
movie_analyse/2019-10-31/movie
to
analyse
(5).avi

ちなみに、「while read」を知る前は、こんな感じ:

SPACE=Ko3LIeH4ty089L # ランダムな文字列
for FILE in `find $ORG -type f | sed -e "s# #$SPACE#g"`
do
FILE=`echo "$FILE" | sed -e "s#$SPACE# #g"`
echo $FILE
done

で一旦、スペースをランダムな文字列に置き換えてから戻すということをやっていました。

さて、話を本題に戻しますと、シンボリック・リンクの作成をスクリプトにしたものは、以下の通りとなります:

find $ORG -type f | while read FILE
do
(
DIR=`dirname "$FILE" | sed -e "s#$ORG#$WRK#"`
cd "$DIR"
N=`echo "$DIR" | sed -e 's#[^/]##g' | wc -c`
for K in `seq $N`
do
PREFIX=../$PREFIX
done
ln -s$FORCE "$PREFIX$FILE"
)
done

まず、シンボリック・リンクを作成するべきディレクトリを生成し、DIRに格納します。dirnameで元ファイルの親ディレクトリの名前を得て、.workを挿入しています。続いて、そのディレクトリへのpathに含まれるスラッシュの数をカウントし、その数の分だけ../を繋げた文字列をPREFIXに格納します。なお、「sed -e ‘s#[^/]##g’」でスラッシュ以外の文字を除去していますが、「tr -cd ‘/’」でも同じ結果になります。最後にlnコマンドでシンボリック・リンクを作成します。このとき、FORCEにfを代入しておけば、既にシンボリック・リンクが存在しても強制的に作成できます。

では、実行結果を確認してみましょう:

$ find movie_analyse.work/ -not -type d | sed -e 's#.*#"�"#' | xargs ls -l
lrwxrwxrwx 1 matsken matsken 55 11月 14 17:20 'movie_analyse.work/2019-10-20/movie to analyse (1).avi' -> '../../movie_analyse/2019-10-20/movie to analyse (1).avi'
lrwxrwxrwx 1 matsken matsken 55 11月 14 17:20 'movie_analyse.work/2019-10-20/movie to analyse (2).avi' -> '../../movie_analyse/2019-10-20/movie to analyse (2).avi'
lrwxrwxrwx 1 matsken matsken 55 11月 14 17:20 'movie_analyse.work/2019-10-20/movie to analyse (3).avi' -> '../../movie_analyse/2019-10-20/movie to analyse (3).avi'
lrwxrwxrwx 1 matsken matsken 55 11月 14 17:20 'movie_analyse.work/2019-10-31/movie to analyse (4).avi' -> '../../movie_analyse/2019-10-31/movie to analyse (4).avi'
lrwxrwxrwx 1 matsken matsken 55 11月 14 17:20 'movie_analyse.work/2019-10-31/movie to analyse (5).avi' -> '../../movie_analyse/2019-10-31/movie to analyse (5).avi'

目論見通りになっています。「chmod -R a-w $ORG」も実行して、その結果を確認します:

$ find movie_analyse/ -type d | sed -e 's#.*#"�"#' | xargs ls -l
movie_analyse/:
合計 8
dr-xr-xr-x 2 matsken matsken 4096 11月 14 16:31 2019-10-20
dr-xr-xr-x 2 matsken matsken 4096 11月 14 16:31 2019-10-31
movie_analyse/2019-10-20:
合計 0
-r--r--r-- 1 matsken matsken 0 11月 14 16:31 'movie to analyse (1).avi'
-r--r--r-- 1 matsken matsken 0 11月 14 16:31 'movie to analyse (2).avi'
-r--r--r-- 1 matsken matsken 0 11月 14 16:31 'movie to analyse (3).avi'
movie_analyse/2019-10-31:
合計 0
-r--r--r-- 1 matsken matsken 0 11月 14 16:31 'movie to analyse (4).avi'
-r--r--r-- 1 matsken matsken 0 11月 14 16:31 'movie to analyse (5).avi'

これで、movie_analyse.work内で何をしても安心です。試しにシンボリック・リンクに対して上書きを試みてみます:

$ cat > 'movie_analyse.work/2019-10-20/movie to analyse (1).avi'
-bash: movie_analyse.work/2019-10-20/movie to analyse (1).avi: 許可がありません

ちゃんと拒否されました。

では、最後に最終版のシェルスクリプトmk_links.shを掲載します:

FORCE=""
for ARG
do
if [ $ARG = "-f" ]
then
FORCE=f
else
ORG=`echo $ARG | sed -e 's#/+$##'`
fi
done
WRK=`echo $ORG | sed -e 's#.org$##'`.work
chmod -R a-w $ORG
find $ORG -type d | sed -e "s#^$ORG#$WRK#" -e 's#.*#"�"#' | xargs mkdir -p
find $ORG -type f | while read FILE
do
(
DIR=`dirname "$FILE" | sed -e "s#$ORG#$WRK#"`
cd "$DIR"
N=`echo "$DIR" | sed -e 's#[^/]##g' | wc -c`
for K in `seq $N`
do
PREFIX=../$PREFIX
done
ln -s$FORCE "$PREFIX$FILE"
)
done

元ファイルを格納するディレクトリをmovie_analyse.orgにして作業ディレクトリをmovie_analyse.workにしたい場合のために、WRK=の行に「sed -e ‘s#.org$##’」を追加しています。

使い方は以下の3通りです:

$ ./mk_links movie_analyse
$ ./mk_links movie_analyse -f
$ ./mk_links -f movie_analyse

引数の数のチェックなどは入れていませんが、必要であれば、ファイルの先頭に

if [ $# = 0 ]
then
echo 'Too few argument(s).'
fi
if [ $# -gt 2 ]
then
echo 'Too many argument(s).'
fi

を追加することで実現できます。

如何でしたか? これで明日から元データを損なうことを心配せずにデータ分析が出来ますね。