第6章 ファイル操作とファイルハンドル

6-1. 標準入出力

Perlでは、ファイルの入出力にファイルハンドルというものを使います。 ファイルハンドルとは、今どのファイルを処理しているかを管理するための名前のことです。
ファイルの読み込みや書き込みをするには、まずファイルを開いてファイルハンドルに関連付けし、そのファイルハンドルを操作するのが基本手順になります。
Perlでは、特別なファイルハンドルとして、以下の3つが用意されています。
ファイルハンドル 意味 内容
STDIN 標準入力 パイプ ( *1 ) やリダイレクト ( *2 ) 時にデータを取り込む。
コマンドライン時にはキーボード。
STDOUT 標準出力 パイプやリダイレクト時にデータを出力する。コマンドライン時にはモニター。
STDERR 標準エラー エラー時に出力される。
*1 : 標準出力と標準入力を経由してデータを受け渡しする仕組みで、あるプログラムが標準出力に出力した結果を、他のプログラムの標準入力に渡す仕組みをいいます。
*2 : 標準出力をそのままのテキストファイルなどに書き出す方法をいいます。
たとえば、コマンドラインから操作する場合には、以下のようなスクリプトを実行することができます。
use strict;

# 質問
print "お名前は? \n";

# 標準入力
my $name = <STDIN>;

# 出力
print "こんにちは、$nameさん\n"; 
>  お名前は?
>  TARO (← キーボードから入力)
>  こんにちは、TAROさん
このように、キーボード入力を標準入力であるファイルハンドルSTDINから変数$nameにセットし、文字列を操作・表示させることができます。
pagetop

6-2. open関数とファイルハンドル

前節ではあらかじめ用意された標準のファイルハンドルを説明しましたが、それ以外のファイルハンドル以外を使う場合には、open関数を使用します。
open関数を使ってファイルを読み込む場合の構文は次のようになります。
1. ファイルを読み込みモードでオープンする場合 (open命令)
open ファイルハンドル, "ファイル名";
2. ファイルを読み込む処理 (read命令)
配列 = <ファイルハンドル>
3. ファイルのオープンを閉じる処理 (close命令)
close ファイルハンドル
たとえばこの一連の処理として、記録ファイルを読み込んで表示するには、次のように記述します。
open(IN,"data.txt");  # 記録ファイルを、INというファイルハンドルで読み込む。
@data = <IN>;         # 読み込んだ記録ファイルを配列@dataにセットする。
close(IN);            # 読み込み処理を閉じる。

foreach (@data) {
	print;
}
ただし、読み出す記録ファイルが大量データの場合には、上記のように一気に読み込むと危険なケースがあります。
その場合には、次のようにwhile文を使って、1行ずつ読み出します。
open(IN,"data.txt");
while(<IN>) {
	print;
}
close(IN);
pagetop

6-3. ファイルの読み書き

ファイルに書き込み操作を行う場合にも、open関数とファイルハンドルを使います。
open関数によるファイルの読み書き操作としては、次のような構文が用意されています。
構文 内容
open (FH, "file")
open (FH, "< file") 
読み込みモード
open (FH, "> file")
書き込みモード
open (FH, ">> file")
追加書き込みモード
open (FH, "+< file")
open (FH, "+> file")
open (FH, "+>> file")
読み書き両用モード
open (FH, "| command")
パイプ出力用
open (FH, "command |")
パイプ入力用
コード-1. ファイルの読み込み
open(FH,"< data.txt");
print <FH>;
close(FH);
data.txtを読み込み中身を読みこみます。読むだけですので、data.txtの中身に影響はありません。
< を省略して、open(FH, "data.txt") と書いても意味は同じです。
コード-2. ファイルの上書き
open(FH,"> data.txt");
print FH "DEF";
close(FH); 
data.txtに「DEF」という文字列を上書きします。
上書きですから、data.txtの中身をいったん消して新たに書き込むことになります。 仮に、当初data.txt の中に「ABC」という文字列があった場合、実行後の中身は「DEF」となります。
コード-3. 追加上書き
open(FH,">> data.txt");
print FH "DEF";
close(FH);
data.txtに「DEF」という文字列を追加書き込みします。
追加書き込みですから、data.txtの中身は消さずに追加で上書きすることになります。 仮に、当初data.txt の中に「ABC」という文字列があった場合、実行後の中身は「ABCDEF」となります。
コード-4. 読み書き両用モード-1 ( +< )
open(FH,"+< data.txt");
$log = <FH>;
if ($log eq 'A') { print FH 'B'; } # 条件判断して書くことができる
close(FH);
読んでから、書くことができます。読むのみで、書かなくてもOKです。
コード-5. 読み書き両用モード-2 ( +> )
open(FH,"+> data.txt");
print FH "ABC";
seek(FH, 0, 0); # ポインタを先頭へ移動
$data = <FH>;
close(FH);

print "$data\n";
>  ABC
書いてから、読むことができます。書くのみで、読まなくてもOKです。
コード-6. 読み書き両用モード-3 ( +>> )
open(FH,"+>> data.txt"); # 当初の中身が「ABC」とします。
print FH "DEF";          # 追加上書き
seek(FH, 0, 0);          # ポインタを先頭へ移動
$data = <FH>;
close(FH);

print "$data\n";
>  ABCDEF
追加上書きしてから、読むことができます。追加上書きのみで、読まなくてもOKです。
コード-7. コマンドからパイプ入力用にオープン
open(IN,"ls -l |");
@cmd = <IN>;
close(IN);

foreach $cmd (@cmd) {
	print "$cmd\n";
}
ls -l (カレントディレクトリ内のファイル名を取得) というコマンドを実行し、その結果を配列@cmdに格納します。
コード-8. コマンドへパイプ出力用にオープン
open(OUT,"| /usr/sbin/sendmail");
print OUT "HELO\n";
close(OUT);
sendmailという外部にあるソフトウェアへHELOコマンドを出力します。
pagetop

6-4. ファイルの削除と変更

その他にファイル操作のための関数として、次のようなものがあります。
内容 構文
ファイルの削除 unlink ファイル名
ファイル名の変更 rename 変更前のファイル名, 変更後のファイル名
パーミッションの変更 chmod モード, ファイル1, ファイル2, ... ファイルn
コード例-1. unlink関数
# file.txtを削除する
unlink("file.txt");
コード例-2. rename関数
# file.txtをfile.datにリネームする
rename("file.txt", "file.dat");
コード例-3. chmod関数
# file.txtのパーミッションを666に変更する
chmod(0666, "file.txt");
chmod関数の第1引数で指定する「モード」は、上記のように先頭に0を付けます。 これは指定するパーミッション値を10進法で記述していますという意味になります。
また、chmod関数は一度に複数のファイルのパーミッションを変更することもできます。 その場合、次のように第2引数以下に変更したいファイル名を複数指定します。
コード例-4. chmod関数(複数ファイルの指定)
# 複数ファイルのパーミッションを666に変更する
chmod(0666, "file.txt", "log.txt", "data.txt", "bbs.dat");
pagetop

6-5. ファイルのコピーと移動

ファイルをコピーする場合は、Perlには関数が用意されていません。
そのため、open関数を使う方法とPerlモジュールを使う方法の2種類があります。
まずは、open関数式の場合です。
コード例-1. open関数式
use strict;

my $old = 'old.txt';
my $new = 'new.txt';

open(OLD,"$old");
open(NEW,"> $new");
while(<OLD>) {
	print NEW $_;
}
close(OLD);
close(NEW);

print "OK\n";
ちょっとしたデータファイルであれば、上記の方法で十分です。
しかしながら、画像などの容量の大きなファイルをコピーする場合のことを考慮すると、一定サイズ毎に読み込んで書き込む方式のほうがいいでしょう(その方がサーバへの負荷が柔しい )。
以下のコード例は、read関数を併用した改良版です。 1024バイト毎に読み込みながらコピーします。
なお、binmode関数(8,9行目)は、Windows環境でバイナリーファイルを扱う場合を考慮しています。
コード例-2. open関数式:改良版
use strict;

my $old = 'old.jpg';
my $new = 'new.jpg';

open(OLD,"$old");
open(NEW,"> $new");
binmode(OLD);
binmode(NEW);
while( read( OLD, my $buf, 1024 ) ) {
	print NEW $buf;
}
close(OLD);
close(NEW);

print "OK\n";
次に、モジュールを利用した例です。
Perlモジュールとしては、File::Copyモジュールが用意されています。 比較的新しいPerl環境では、標準モジュールとして組み込まれています。
コード例-3. File::Copyモジュール
use strict;
use File::Copy;

my $old = 'old.jpg';
my $new = 'new.jpg';

copy($old, $new);

print "OK\n";
File::Copyモジュールは、ファイルの「移動」も可能です。 その場合、move関数を使用します。
コード例-4. File::Copyモジュール「移動」
use strict;
use File::Copy;

my $old = 'old.jpg';
my $new = 'new.jpg';

move($old, $new);

print "OK\n";
File::Copyモジュールが利用できるサーバ環境では、こちらを利用したほうが手軽です。
それに対して、open関数式は汎用的な局面で利用することができます。
pagetop

6-6. ディレクトリを再帰的にコピー又は削除

前節では、単純にファイルをコピーする方法を紹介しました。
それでは、ディレクトリ内にあるファイルやディレクトリを再帰的にコピーするには、どうしたらいいでしょうか。
opendir + File::Copy の組み合わせでもできないことはありませんが、少し面倒です。
そのような用途のために、File::Copy::Recursive モジュールがあるので、ご紹介しておきます。
ただし、残念ながら標準モジュールではないようですので、ご自分でホームディレクトリ配下などへインストールする必要があります。
基本的な構文例は、次のとおりです。
コード例-1. File::Copy::Recursiveモジュール(フォルダ配下を再帰的にコピーする)
use strict;
use File::Copy::Recursive qw(rcopy);

my $old = './old'; # コピー元のディレクトリ
my $new = './new'; # コピー先のディレクトリ

rcopy($old, $new);

print "OK\n";
上記の rcopy により、oldディレクトリ内にある全てのファイルとディレクトリが、newディレクトリ内にそのままコピーされます。 さらには、各ディレクトリとファイルのパーミッションも忠実にコピーしてくれます。
次に今度は、ディレクトリ内のファイルやディレクトリを、「再帰的に削除」する場合にはどうしたらいいでしょうか?
ディレクトリを削除するための関数では、rmdir がありますが、これはディレクトリの中身が空っぽのときだけしか使用できません。ですので、opendir関数等で中身のディレクトリやファイルを確認しながら、rmdir + unlink で1つ1つを削除していくしかありませんが、これも面倒です。
このような用途のために、File::Path モジュールが用意されています。このモジュールは標準モジュールですので、手軽に利用することができます。
コード例-2. File::Pathモジュール(フォルダ配下を再帰的に削除する)
use strict;
use File::Path;

my $dir = './dir'; # 削除対象のディレクトリ

rmtree($dir);

print "OK\n";
上記の rtree により、dirディレクトリ内にある全てのファイルとディレクトリが、再帰的に削除されます。
さらには、rmtreeを右辺に置くと、「返り値」に削除したファイルとディレクトリの個数を返してくれます。
コード例-3. File::Pathモジュール(フォルダ配下を再帰的に削除して、その削除した個数を求める)
use strict;
use File::Path;

my $dir = './dir'; # 削除対象のディレクトリ

my $num = rmtree($dir);

print "$num個削除しました\n";
pagetop