sironekotoroの日記

Perl で楽をしたい

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::SpecCwd などのファイル・ディレクトリを扱う便利モジュールも数多くあります。

何かしようとして、うまくいかないとか、こういうことをがしたい!という方は Perl入学式の slack などで質問くださいませ。

docs.google.com

長いおまけ: PerlWindows上のファイル名を変更する

中には、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;