ファイルの検索を行う find

UNIX 系の環境で開発を行っている方は、おそらく find という便利なコマンドを使っていることでしょう。私も毎日使っています!

find は例えば、名前の記憶のあやふやなファイルを探す
find /path/to/projects -name "*some_name*";

○日以上前のファイルを探して消す
find /var/dbdump -name "*.dump" -mtime +10 -exec rm -f {} \;

などの目的で様々に使えます。(各 UNIX 環境で文法が微妙に異なりますので、上記でエラーが出る場合はお使いの環境で man find して下さい。)

File::Find でできること

このように便利な find コマンドですが、ファイル検索の目的によっては力不足の点もあります。例えば、以下のような点です。
  • 余り複雑な検索はできない
  • ファイルの中身までは調べられない
  • 抽出したファイルの一覧をそのまま何かに利用するのは難しい (整形が必要)
例えば Webサイト構築中に使うのであれば、以下のような処理は find コマンドでは難しいのです。
  • 名前に日本語が付いてしまっているファイルを探す
  • ファイルの文字コードが違ってしまっているファイルを探す
  • 色々なディレクトリに散らばってしまっている画像の一覧を作成する
このような場合に使えるのが Perl の File::Find モジュールです。このモジュールはディレクトリ階層を巡ってファイルを探して来てくれます。プログラムでは、ファイルを開くもよし、削除するもよし、煮て喰うなり焼いて喰うなり、好きなファイル操作ができます。

また、あまり使い勝手の良くない検索ツールしか備えていない Windows 環境で、コマンドラインからファイル検索を行ったり、ファイルパスの一覧が欲しいといった場合にも応用できます。

今回は練習のため、ファイル名に日本語が付いてしまっているファイルを探すプログラムを作ることにします。

File::Find の使い方

File::Find の基本的な使い方は以下の通りです。
#!/usr/bin/perl --

#File::Find のデモ

use strict;
use warnings;
use File::Find;
use Encode::Guess qw/iso-2022-jp-1 shiftjis cp932 MacJapanese euc-jp utf8 ascii/;
# Encode::Guess の詳細 http://allabout.co.jp/gm/gc/455808/
use Encode qw(decode);
use utf8;
#use Smart::Comments;

binmode STDOUT, 'encoding(utf8)'; #Windows の場合は utf8 -> cp932

#プログラム実行の引数として、ディレクトリ名を受け付け
my $dir = shift @ARGV;

#ディレクトリ名指定のエラー処理
defined $dir and length $dir or die "usage: perl_find.pl DIR\n";
-d $dir or die "$dir does not exists.\n";

#実行!
find ({wanted => \&checkdir, no_chdir => 0}, $dir);

#ここで詳細処理を指定
sub checkdir{
	# 下記には上から、指定した $dir を含むパス、ディレクトリ名、ファイル名が入る (この代入が、File::Find のお仕事)。
	### $File::Find::name
	### $File::Find::dir
	### $_
	
	-f $_ or return; #ディレクトリは出力しないので終了
	$_ =~ m/^[ !"#+,;=0-9a-zA-Z~._-]+$/ and return; #一般に考えられる英数字ファイル名だった時も終了
	
	#ファイルシステムから取得できるファイル名は、
	#use utf8 しているかしていないかに関わらず生文字表現である事に注意。
	#おまけに、ファイル名がどの文字コードになっているかは分からない。
	
	#ファイル名の文字コードを判別
	my $genc = guess_encoding($_);
	
	#ファイルパスを出力
	if (ref $genc){ #判別可能だったら
		$genc->name eq 'ascii' and return; #ascii 判定が出たら終了
		printf ('%s, %s', $genc->decode($File::Find::name), $genc->name);
	} else {
		#判別できない時にどうするかは御意。Encode::Guess の判別し損ねも多い。
		printf ('%s, %s', decode('utf8', $File::Find::name), 'unknown');
	}
	print "\n";	
}

前半部分は処理対象のディレクトリ名受け入れやエラー処理です。

「find ({wanted => \&checkdir, no_chdir => 0}, $dir);」の行が File::Find モジュールの機能を使っている部分です。カッコ内の引数「{wanted => \&checkdir, no_chdir => 0}」は Perl で関数のオプションを指定する時によく行う形式で、「 => 」でつないで値のペアを示す仕組みはハッシュと一緒です。

どのようなキーが指定できるか (指定しなければならないか) は「perldoc File::Find」で調べることができます。最低限必要なのは「wanted」というオプションで指定するサブルーチンで、そこに、ファイルをどのように処理するかを指定します。今回は &checkdir というサブルーチンを指定しています。

ファイル名を出力する部分は &checkdir 内で指定していますが、このサブルーチン内から参照できる変数 $File::Find::name、$File::Find::dir、$_ にそれぞれ以下のように値を代入してくれるのが File::Find の働きです。プログラマーは、渡されたファイル情報をどのように料理するかだけを指定すればよいのです。

File::Find で使える変数名

$File::Find::name	find の第2引数で指定したディレクトリを含むファイルのパス名
$File::Find::dir  そのパスのディレクトリ名
$_         ファイル名単体 (=上位階層までのディレクトリ名を除いたもの) 

今回は「日本語の名前が付いてしまっているファイル」を探しますので、正規表現でファイル名をチェックして、該当したものを出力しています。

no_chdir オプションについて


find の実行部分に「no_chdir => 0」という設定がありますが、これは、プログラム内部的にディレクトリを変えながら進むか? という指定です。この指定を今回のように「no_chdir => 0」にすると、ファイルのあるディレクトリに移動しながら進みます。

「no_chdir => 1」にすると、プログラムを起動したディレクトリにとどまったまま処理を続けます。この時、変数「$_」には $File::Find::name と同じ値が設定されます。

言い換えると、「$_」には常に、プログラムからファイルへのパスが格納されています。

どちらのオプションを使うか、処理によってやりやすいこととやりにくいことがありますが、今回はファイル名単体がすぐに利用できる「no_chdir => 0」を使いました。

ファイルを移動させる処理を行う場合や、セキュリティの問題でディレクトリを移動するのは不適切な場合は「no_chdir => 1」の方が処理が簡単になります。

様々な使い方

File::Find を使うと、ファイルに関する様々な処理が行えます。
  • 2つのディレクトリを統合する (バックアップをまとめるのに便利)
  • ディレクトリを比較して、更新されているファイルを抽出する
  • *.html ファイルを探して文字コードを変換する (ファイル内のメタタグも!)
色々工夫してみて下さい!


※記事内容は執筆時点のものです。最新の内容をご確認ください。
※OSやアプリ、ソフトのバージョンによっては画面表示、操作方法が異なる可能性があります。