Perlでディレクトリの中のファイルにアクセスする
早速やっていきます。
いつも通り、コードと対象のディレクトリは同じところに置きます。ディレクトリとファイルの位置はこんな感じ。
. ├── read_dir.pl これから書くスクリプト └── test_dir ディレクトリ ├── test_file1.txt ディレクトリの中のファイル その1 └── test_file2.txt ディレクトリの中のファイル その2
- test_file1.txt の中身は
hoge
と1行だけ - test_file2.txt の中身は
fuga
と1行だけ
Perlでディレクトリの中のファイルにアクセスする方法として、ディレクトリハンドルを使う方法と、ファイルグロブを使う方法を書いていきます。
その1 ディレクトリハンドル
ファイルの中身を読んだり書き込んだりには ファイルハンドル を用いました。ディレクトリを扱うには ディレクトリハンドル を使います。
#!/usr/bin/env perl use strict; use warnings; my $directory_name = 'test_dir'; # ディレクトリ名 # ディレクトリハンドルの略称で $DH とした。 # ディレクトリ開けなかったらエラーを出して死ぬ(die) opendir my $DH, $directory_name or die; for my $content ( readdir $DH ) { print $content . "\n"; } closedir $DH;
コードの解説していきます。
opendir my $DH, $directory_path or die; # 中略 closedir $DH;
ファイルを開くときは open
を用いましたが、ディレクトリのときには opendir
を使います。ディレクトリの処理が終わった後にはclosedir
で閉じます。
ディレクトリハンドル $DH
が無事作成されると、この $DH
の中にディレクトリの中の情報が入ります。
ついでに説明ですが、末尾に or die
をつけていることで、ディレクトリが開けなかったらスクリプトが終了するようにしています。
これはよくある書き方、定型、お約束みたいなものですね。
for my $content ( readdir $DH ) {
次に、 readdir
ですが、これは $DH
の中身を1つ取り出す関数です。これが for
文の中にあるので、 $DH
の中身を一通り吐き出して終了、というわけです。
実行結果は以下の通りです。
. .. test_file2.txt test_file1.txt
ファイル以外にも表示されているものがあります。 .
と ..
です。
これはPerlが動いている環境( macOS や msys2 , Linux )において特別な意味を持つものです。
ターミナルで
$ ls . $ ls ..
と入力してみると分かりやすいと思います。
ディレクトリのファイル一覧をとるときにはこの .
と ..
を除外する書き方をすることも多いです。
.
と ..
の時は for のループを飛ばす、という処理です。こんな感じ。
for my $content ( readdir $DH ) { if ($content eq '.' or $content eq '..' ){ next } print $content . "\n"; }
もう少しスマートに、後置if文と正規表現を使うとこんな感じ。正規表現覚えてますか〜?
for my $content ( readdir $DH ) { next if $content =~ /^\.{1,2}$/; print $content . "\n"; } closedir $DH;
これで .
と ..
の場合には next
で次のループに行くので表示される事がなくなります。
その2 ファイルグロブ
ディレクトリにアクセスするもう一つの方法が、 ファイルグロブ を使う方法です。
#!/usr/bin/env perl use strict; use warnings; my $files_path = 'test_dir/*'; # ファイルのパスをワイルドカードで指定している my @contents = glob($files_path); # glob ファイルグロブを使っている for my $content (@contents) { print $content . "\n"; }
実行結果
test_dir/test_file1.txt test_dir/test_file2.txt
さっきのディレクトリハンドルを用いる方法とちょっと書き方や結果が異なります。
ただ、ディレクトリハンドルよりも直感的に書けたりするのでこちらが使われている場合も多い・・・気もします(個人の感想です)。
glob
ですが、これは引数のパスに該当するファイル名のリストを取得します。このとき、ワイルドカードや拡張子の指定をついでに行う事ができます。
ディレクトリハンドルはディレクトリ名を指定しましたが、glob
の場合はそのディレクトリの中身を指すパス になります。
ワイルドカードを使って、特定の拡張子のファイルのみを指定する事が可能です。
例えばこんな感じ。
my $files_path = 'test_dir/*.csv';
もちろん、ディレクトリハンドルでも同様のことは可能ですが、条件が単純な時はファイルロブを使った方が楽でしょう。
また、実行結果でもわかるようにパスの組み立ても可能です。これも条件が単純なときには便利です。
ディレクトリの中のファイルの中身を表示してみる
ディレクトリの中のファイルの中身を見たい時のスクリプトを例に、ディレクトリハンドル を用いたものと、ファイルグロブを使ったもの、と2つ書いてみました。
ディレクトリハンドルの中にあるのはファイル名のみなので、ファイルの中身にアクセスするときには ディレクトリ名/ファイル名
というようにパスを作ってあげる必要があります。
ディレクトリハンドル編
#!/usr/bin/env perl use strict; use warnings; my $directory_name = 'test_dir'; opendir my $DH, $directory_name or die; for my $content ( readdir $DH ) { next if $content =~ /^\.{1,2}$/; # ファイルのパスを作成する my $content_path = $directory_name . '/' . $content; # ファイルのパスからファイルハンドルを作成して中身を表示する open my $FH, '<', $content_path; for my $line (<$FH>) { print $line , "\n"; } close $FH; } closedir $DH;
実行結果
fuga hoge
ファイルグロブ編
ディレクトリハンドルより短くかけます。
#!/usr/bin/env perl use strict; use warnings; my $directory_path = 'test_dir/*'; my @contents = glob($directory_path); for my $content (@contents) { open my $FH, '<', $content; for my $line (<$FH>) { print $line . "\n"; } close $FH; }
実行結果
fuga hoge
ということで、これで Perl でファイルやディレクトリを扱う最低限のスクリプトの紹介はおしまいです。
Perl には File::Find
とか Path::Tiny
とか File::Spec
に Cwd
などのファイル・ディレクトリを扱う便利モジュールも数多くあります。
何かしようとして、うまくいかないとか、こういうことをがしたい!という方は Perl入学式の slack などで質問くださいませ。
長いおまけ: PerlでWindows上のファイル名を変更する
中には、Windows で msys2 などの環境を使わずに Perl を利用する人もいるかもしれません。
6年くらい前のうちみたいに・・・
そのとき、Perl から Windows 上の日本語ファイル名を扱えずに困り果てる・・・という事がありました。
試しに、ファイル名に特定の文字列をつけるスクリプトを解説付きで書いてみました。
Windows10 + ActivePerl 5.28 で動かしてます。
変数の中の文字列が cp932 なのか Perlで扱う utf8 なのかを変数名の先頭ににつけてみました。
・・・が、残念なことに分かりやすさに直結しなかったなぁという。
苦労したんだけどー
ディレクトリ構造です。
├── rename_file_win.pl これから書くスクリプト └── 日本語ディレクトリ ディレクトリ ├── 日本語ファイル1.txt ディレクトリの中のファイル その1 └── 日本語ファイル2.txt ディレクトリの中のファイル その2
Windows 上で日本語のファイル名を扱うときの注意点は、文字コードです。
これが基本です。
use strict; use warnings; use Encode qw/decode encode/; use utf8; my $utf8_dir_name = '日本語ディレクトリ'; # これはutf8のソースコード上に書かれた文字 # ディレクトリ名が書かれているのは utf8 上のソースコード # この utf8 で記されたディレクトリ名を Windows(cp932) が解釈できるよう変換する # スクリプトから見て「内->外」なのでエンコードする my $cp932_dir_name = encode('cp932', $utf8_dir_name); opendir my $DH, $cp932_dir_name or die; # Windows上のディレクトリを開く for my $cp932_content (readdir $DH){ # カレントディレクトリと上位ディレクトリは飛ばす next if $cp932_content =~ /^\.{1,2}$/; # $cp932_content は readdir が持ってきた Windows(cp932) のディレクトリ内のファイル名。 # これを Perl が解釈できるように変換する # スクリプトから見て「外->内」なのでデコードする my $utf8_content = decode('cp932', $cp932_content); # ファイル名の先頭に文字列を追加してみる # 追加する文字列。utf8 上のソースコードに書かれているのでこの文字列は utf8 my $utf8_add_string = '(変更済み)'; # 新しいファイル名の変数を用意 my $utf8_new_filename = $utf8_add_string . $utf8_content; # ファイル名を変更するために、元のファイル名と新しいファイル名それぞれに親ディレクトリをつけたパスを用意する # File::Spec を用いることで、OSごとのディレクトリとファイルの区切り文字をよしなに変換してくれる # ここで変換するのはあくまで区切り文字のみ。文字コードの変換ではない。 use File::Spec; my $utf8_old_file_path = File::Spec->catfile($utf8_dir_name, $utf8_content); my $utf8_new_file_path = File::Spec->catfile($utf8_dir_name, $utf8_new_filename); # Windows上で rename するので Windows(cp932) に変換する。 # スクリプトから見て「内->外」なのでエンコードする my $cp932_old_file_path = encode('cp932', $utf8_old_file_path); my $cp932_new_file_path = encode('cp932', $utf8_new_file_path); # rename 関数を使ってファイル名を変更する rename $cp932_old_file_path, $cp932_new_file_path or die; # 変換状況を表示する print "$cp932_old_file_path => $cp932_new_file_path\n"; } closedir $DH;