今日からperlプログラマー
《第3回》どこまでもフォルダを降りる−−再帰的プログラミング
「ファイル操作をしたい。でも、再帰的階層の下り方が分からない!」

 そろそろ本格的なプログラムを作りましょう。「ファイル操作をしたい。でも、再帰
的階層の下り方が分からない!」――そこで今回は、"定番"なのに分かりにくい、「
再帰的プログラミング」の方法です。

 

自信作、いまお届け?!
 
 のっけから恐縮ですが、HP「今日からPerlプログラマー*」に、多数のアクセ
ス、ありがとうございます。日本全国、南は九州まで。感激です!今後も、本連載のス
クリプトや、補足、記事の訂正を掲載してゆきます。過去のスクリプトもここでゲット
できます。

*URL
http://www.geocities.co.jp/SiliconValle
y-Cupertino/6103/

 さて、今回のスクリプトは、「指定フォルダ以下をごっそりリネームしていく」とい
うものです。使えるスクリプトですよ!


「新規テキスト文書.txt」はダサい?
 
 いったい、どんなプログラムでしょうか。
 何かファイルを作ったとき、OSやアプリがデフォルトで決めるファイル名って、ダ
サくありませんか?
 「新規テキスト文書(3).txt」とか、「名称未設定 1.jpg」とか、見覚
えありますよね? 
 重要なファイルなら簡単ですが、それほど重要でないファイルは、いちいち名前を決
めるのも面倒です。でも、テキストを保存するたびに、「新規テキスト文書.txt」
としていたら、HDDは、「新規テキスト文書.txt」であふれてしまいます。

 それから、こんなこともありませんか?
 好きなアニメの画像をどんどんダウンロードしていると、「rapi_01.jpg
」「rapi_05.jpg」とか、重複したファイル名のファイルが、あちこちに保
存されていた。なんか探しにくいなあ。という悩み。

 他にも、ただひたすら大量に画像スキャンばかりしている人、デジカメ写真が半年で
2500枚も溜まってしまう人など、「一括で整理できたらいいのに…」という切なる
なげき。

 データがどんどん溜まってしまう人は、ファイル名をいちいち入力して整理するのが
とてもおっくうです。
 そんな需要を満たすべく、「リネーム・ツール」は、フリーウェアでも、たくさん出
回っています。

 今回は、そうした「リネーム・ツール」を発表します。
 日付・時間・秒で、勝手にファイルをリネームしますが、
@ 再帰的にリネームする
A 別のディレクトリのファイルも調査し、リネーム後のファイル名が重複しないよう
にする
B リネームのログが完全に残る
などが、特徴です。


詳しい仕様
 
 本当は、リネーム後の形式なども自由にカスタマイズできるほうがよいのですが、今
回は、以下の固定方式にします。必要なら、スクリプトを好きに書き換えて使ってくだ
さい。

(1) 指定ディレクトリ以下を「2001-06-24-015308.jpg」の
形式で、ファイルの更新時刻に従って、「日付時刻秒+拡張子」にします。

(2) リネーム後のファイル名が重複する場合は、「2001-06-24-115
328-001.txt」「2001-06-24-115328-002.txt」
「2001-06-24-115328-003.txt」のように、連番で対応しま
す。
 デジカメの高速連続写真などでは、ファイル作成秒まで一致する場合がありますから
、こうした場合に重宝する機能です。

(3) リネーム・チェックは、ディレクトリをまたいで、再帰的に調査するので、別
のディレクトリも含め、重複しないようにリネームされます。

(4) メンテ楽ちん。すでにリネームしたファイルは無視します。

  指定フォルダ以下に、どんどんファイルを溜めた場合、以前リネームしたファイル
と混在します。
 この状態で再び起動すると、既にリネームした、 「2001-06-24-115
328-001.txt」を、さらにまた  「2001-06-24-115328
-002.txt」に、リネームしようとしてしまいます。これを判断して、一度リネ
ームしたファイルは、  リネームしないようにします。
 ただし、別のフォルダに、同じ名前の  「2001-06-24-115328-
001.txt」があった場合は、片方は、  リネームされます。この辺り  の判
定は、少し凝りました。

(5) 無視する拡張子を指定できます。
 「〜.com」「〜.exe」「〜.html」などの拡張子のファイルは、リネー
ム対象から外します。この部分は、最初の変数で簡単にカスタマイズできます。

(6) 追加式のログを残すので、リネームの記録が残ります。リネームしなかったフ
ァイルも残ります。
  現在時刻  2001-06-29-12:06:30

E:/テスト/pt/md/PIC0002.JPG  ---> 2001-
06-23-182606.jpg
  
E:/テスト/pt/md/2001-06-23-1 82544.jpg---&
gt; 既にリネーム
  
E:/テスト/pt/PH4.COM  ---> 無視する拡張子
のような感じです。


使用方法
 
 リスト1、「autoren.pl」がスクリプトです。内容のわりには行数が少な
く、いつもPerlの実力には、驚くばかりです。つくづく優れた言語だと思います。


 最初の、
 |$dir = "d:/photo/ren"; |@notrename = (exe,com,pif,html,htm,sue);
の変数を変えて、自分の環境に合わせてください。
 「$dir」以下のフォルダが全部対象になり、「@notrename」で指定し
た拡張子のファイルのみ、対象から外れます。

 autoren.plを起動すると、すぐにリネームが始まり、その様子は、スクリ
プトのあるフォルダに「log.txt」として残されます。「log.txt」は、
以前のログに追加されて行く方式ですが、起動のたびに、現在時刻を残すので、いつの
ログなのか判読できます。


「再帰的」とは?

 用語の説明をしないできてしまいましたが、「再帰的」の意味は、「指定フォルダ以
下、下の階層まですっかり全部を対象にする」という意味です。
 これは、簡単そうですが、ちょっとやっかいです。


X┬A┬B┬B1─B2
 │ │ │  ┌B4─B5
 │ │ ├B3┼B6┌B8
 │ │ └B10└B7┴B9
 │ └C─C1−C2
  └Y


 対象フォルダは、A以下だとします。
 A→B→B1→B2へと処理を進めたあと、B3に戻って→B4→B5と行ったあと
、B6に戻って行く……階層を降りて行くだけなら簡単なのですが、降りきったあと、
途中まで戻って処理を続けるアルゴリズムは、どのように考えたらよいのでしょうか。


 では、実際のプログラムで見てみましょう。

sub saiki {local($dir) = @_;
  local *DIR;        #注3
  opendir(DIR,$dir);
  local ($file,$dirfile);  #注2
  while($_ = readdir(DIR)){
 next if $_ =~ m/^\.\.$/;  #注4
 next if $_ =~ m/^\.$/;
    $file = $_;
    $dirfile = "$dir/$file";
    if( -d $dirfile){       #判定部分
       処理A;
       &saiki($dirfile);    #注1
    }
    else{
       処理B;
    }
  closedir(DIR);
  }

 「#判定部分」によって、現在注目しているのが「ディレクトリ」か「ファイル」か
に分岐されます。
 「処理A」には、ディレクトリに対して行ないたい処理を、「処理B」には、ファイ
ルに対して行ないたい処理を書きます。

 ミソは、「注1」の部分です。つまり、対象がディレクトリなら、自分自身のサブル
ーチンをもう一度呼び出すことで処理するのです。このように、フォルダを下るたびに
、サブルーチンを呼び出し、それ以上、フォルダを降りることができなくなったら、サ
ブルーチンを抜け、元の処理の続きに戻ります。

 このため、「注2」では、「$file」と「$dirfile」は、localに
よって、局所的な変数として、宣言します。さもないと、呼び出し先のサブルーチンで
、変数が書き換わって、戻ってきても、スクリプトがどういう風に処理を続けてよいの
か分からなくなってしまいます。
 しかし、「local」で宣言すれば、サブルーチンごとに変数を宣言するため、見
た目には同じ名前の変数でも、変数の中身が、別のサブルーチンからの干渉を受けませ
ん。
 なお、Perl5以上では、「local」ではなく、「my」のほうがさらに有利
だそうです。

 同じように、「注3」の「*DIR」は、大変重要です。
 これは、「型ブロブ」という、ちょっと分かりにくい手法なのですが、要するに、こ
う宣言することで、新たなファイルハンドルを生成してくれます。
 つまり、サブルーチンごとに、同じファイル・ハンドルで処理される「DIR」は、
それぞれ、別の値をもてるのです。
 反対に、このようなファイルハンドルの生成ができない限り、再帰的にディレクトリ
を降りていくプログラムは組めません。

 「注4」も、意外な落とし穴です。こうすることで、ファイル名が、「.」(起点フ
ォルダ自身を示す特殊記号)と「..」(上位フォルダ)の場合は、無条件に無視でき
ます。

HTMLカタログ
 
 『I/O』2001年6月号では、画像ファイルをファイル名順にHTMLでサムネ
イル化し、クリックで大きく表示したり、ページをめくることで閲覧できる「カタログ
化スクリプト」(photo4-8jpg.pl)を発表しました。
 本スクリプトは、ファイル名が衝突しませんから、画像ファイルを、一か所に集め、
6月号のスクリプトで、画像カタログを作ることもできます。

 保存してある画像って、年間ですぐに一万枚の大台になってしまうんですよね(^ 
^;)。画像カタログが簡単にできるのは、重宝です。
 また、スクリプトをいじれば、動作は、無限にカスタマイズ可能。もう、フリーウェ
アなんて、使う気がしないですよね!まだの人は、さっそく、Perlをインストール
してください。
 (インストールなどの補足情報は、冒頭に書いたHPを参照してください)。
 なお、データはバックアップを取ってから使ってください。
(辻 豊史)


リスト1 ファイル名をリネームするスクリプト
$dir = "d:/photo/ren";#←最後は/で終わらないこと!
@notrename = (exe,com,pif,ini,dll,vxd,ocx,drv,scr,bat,pl,hlp,html,htm,sue);
open (logout, ">> log.txt");

($sec,$min,$hour,$mday,$mon,$year,@rest) = localtime(time);&timehosei;
print logout "\n現在時刻  $year-$mon-$mday-$hour\:$min\:$sec\n";

###main
&saiki_filelist($dir);#再帰的に降りて行き、ファイルリストを作成する
for ($j = 0; $j < (@flist); $j++) {$kekka = 0;
  $kekka = (&kakuchocheck ($dflist[$j])); #無視する拡張子のチェック
  ($kekka == 1) && print logout "$dflist[$j]---\> 無視する拡張子\n";
  next if $kekka == 1; #無視なら次の処理
  $kouhofname = (&newfname ($j));#$dflist[$j]の新候補fname
  $kekka = (&sumicheck ($j,$kouhofname));#$dflist[$j]が既に新fname?
  ($kekka == 1) && print logout "$dflist[$j]---\> 既にリネーム\n";
  next if $kekka == 1; #既にリネーム済みなら次の処理
  $hantei = (&icchicheck ("$kouhofname"));
  ($hantei == 1) && ($kouhofname = (&newfnameNO ($kouhofname)));
  rename ($dflist[$j],$kouhofname); #実際のリネーム
  splice (@flist,$j,1,$kouhofname); #リストもリネーム後のファイル名にする
  print logout "$dflist[$j]---\> $kouhofname\n";
}
print logout "\n\n";close logout;

#↓@file=filename(小文字)  @dirfile=dir/file
sub saiki_filelist {local($dir) = @_;
  local *DIR; #サブルーチンでファイルハンドルを使用する場合、こうする
  opendir(DIR,$dir) or die $!; #開かなかったらエラーで戻る
  local ($file,$dirfile) ;
  while($_ = readdir(DIR)){next if $_ =~ m/^\.\.$/;next if $_ =~ m/^\.$/;
     #↑ ./. と ./.. の場合は、無視する
    $file = $_;$dirfile = "$dir/$file";$file =~ y/A-Z/a-z/;
    next if ( !-w $dirfile ); #リネーム不可なら対象としない
    if( -d $dirfile){ &saiki_filelist($dirfile); }
      # ↑ディレクトリなら再帰的にサブルーチン呼び出し
    else{ push (@dflist,$dirfile);push (@flist,$file); }
  }
  closedir(DIR);}

sub newfname {local($i) = @_;local $kakucho,$newfname;
  ($mtime) = ((stat($dflist[$i]))[9]);
  ($sec,$min,$hour,$mday,$mon,$year,@rest) = localtime($mtime);&timehosei;
  $newfname = "$year-$mon-$mday-$hour$min$sec";
  if (index($flist[$i] , "\.") > 0)  {
    #↑拡張子なしのファイルに対応
    $kakucho = $flist[$i];$kakucho =~ s/^.*\.([^.]*)$/$1/;
    $newfname = "$newfname\.$kakucho";}
  return ($newfname);}

sub kakuchocheck { local ($kakucho,$i,$icchi) = (@_,0,0);
  $kakucho =~ s/^.*\.([^.]*)$/$1/;$kakucho =~ y/A-Z/a-z/;
  foreach $i (@notrename) { 
    ($i eq $kakucho) && ($icchi = 1);last if ($icchi == 1); }
  return ($icchi);}

sub sumicheck { local($i,$newfname,$icchi,$c1,$c2) = (@_,0,0,0);
  foreach $k (@flist) { ($k eq $newfname) && ($c1++);
    #↑候補ファイル名と同じファイルがないか
    ($flist[$i] eq $k) && ($c2++);}
    #↑対象ファイルと同じファイル名がないか

  ($c1 == 1) && ($flist[$i] eq $newfname) && return (1);
  #↑リストの中の一致したファイル名が候補の名前と完全に一致

  local ($bodyflist,$bodynewfname,$lengnfnm);
  $bodynewfname = $newfname;$bodynewfname =~ s/^(.*)\.[^.]*$/$1/;
  $lengnfnm = length ($bodynewfname);
  $bodyflist = (substr ($flist[$i],0,$lengnfnm));
  ($c2 == 1) && ($bodyflist eq $bodynewfname) && return (1);
  #リストの中に同じファイル名がなく、候補の名前+xの場合、一致とみなす
  return (0);}

sub icchicheck { local ($newfname,$icchi) = (@_,0);local $i;
  foreach $i (@flist) { ($i eq $newfname) && ($icchi = 1);
    last if ($icchi == 1); }
  return ($icchi);}

sub newfnameNO { local ($newfname) = @_;
  local ($kakucho,$body,$no,$icchi);
  $kakucho = $newfname;$kakucho =~ s/^.*\.([^.]*)$/$1/;
  $body = $newfname;$body =~ s/^(.*)\.[^.]*$/$1/;
  for ($i = 1; $i <= 999; $i++) {
    $no = "00$i";$no = substr($no,-3); #←1桁でも強制的に3桁にする
    $newfname = "$body-$no\.$kakucho";

    foreach $h (@flist) { $icchi = 0; #候補名が既にないか調べる
      ($h eq $newfname) && ($icchi = 1); 
      last if ($icchi == 1); }

    last if ($icchi == 0);}
  return ($newfname);}

sub timehosei { local ($n); $year = 1900 + $year;$mon++; $n = "0$mon";
  $mon = substr($n,-2);$n = "0$mday";$mday = substr($n,-2);
  $n = "0$hour";$hour = substr($n,-2);$n = "0$min";$min = substr($n,-2);
  $n = "0$sec";$sec = substr($n,-2); #←↑1桁でも強制的に2桁にする
  }