Perlのファイル入出力の基礎



  1. Perl



  2. here

Perlのファイル入出力の基礎をマスターしましょう。 テキストファイルの読み込みと書き込みができるようになることを目標にします。

ファイルオープン

ファイルを読み込むためには最初にファイルをオープンする必要があります。ファイルはOSによって管理されているので、最初にプログラムでファイルを扱いたいということをOSに伝える必要があります。OSは対象のファイルの識別子をプログラムに返却します。

ファイルをオープンするにはopen関数:title=open関数]を使用します。

my $file = 'data.txt';
open my $fh, '<', $file
  or die qq/Can't open file "$file" : $!/;

ファイルのオープンの処理についてひとつづつ解説します。

open関数

open関数は3つの引数を受け取ります。第一引数にはレキシカル変数、第二引数にはオープンモード、第三引数にはファイル名を指定します。ファイルがオープンされると第一引数で指定したレキシカル変数にファイルハンドルが格納されます。ファイルからの入力や、ファイルへの出力はこのファイルハンドルを通して行います。第二引数はオープンモードで、ファイルから内容を読み取りたい場合は「<」を指定します。

 open関数はファイルが存在しないなどの理由で失敗するとundefを返却します。失敗した場合はdie関数を使って例外を投げるようにします。or演算子を利用するとorの左辺の実行文が偽の場合は、右辺の実行文を実行するという処理を記述できます。

オープンモード

オープンモードはファイルをどのようなモードでオープンするかを指定するものです。よく利用されるオープンモードには次のようなものがあります。

オープンモード 意味
読み込み
> 書き込み
>> 追加書き込み

読み込みモード「<」はファイルの内容を読み込みたい場合に使用します。書き込みモード「>」はファイルに書き込みたい場合に利用します。ファイルが存在しない場合は、自動的に作成されます。ファイルが存在する場合はオープンしたときにファイルの内容がすべて消去されます。追加書き込みモード「>>」はファイルの末尾に新しい行を追加したい場合に利用します。ファイルが存在しない場合は、自動的に作成されます。

読み書き両用のモードは存在するのですが、利用する機会はほとんどないでしょう。後ほど解説しますが、ファイルを読み込んでから、同一のファイルに書き込みたい場合は、別のアプローチをとるほうがよいからです。

ファイル名

ファイル名は、絶対パスあるいは相対パスで指定することができます。絶対パスというのはファイルの完全な名前のことで「c:\foo\bar\data.txt」(Windows)や「/foo/ba/data.txt」(Unix)のようなファイル名のことです。

相対パスというのはカレントディレクトリからのパスで、カレントディレクトリが「c:\foo」だった場合は、相対パスは「bar\data.txt」(Windows)のようになります。相対パスの始まりは「\」や「/」などがつきません。

ファイル名は絶対パスで指定するのがよいでしょう。プログラムの中でカレントディレクトリを移動してしまった場合に、相対パスでした場合は異なるファイルを意図せずに指してしまうということが考えられるからです。またコマンドラインから起動しないプログラムの場合は、カレントディレクトリがどこであるかを把握できないこともあるでしょう。

またファイル名の区切り文字についてですが、Windowsの場合であっても「/」という区切り文字を使うことができます。「c:/foo/bar/data.txt」と記述してもopen関数は正しく動きます。またopen関数以外でもファイル名を受け取る関数は区切り文字が「/」であっても正しく動きます(たとえばunlinkやglobなど)。

日本語のファイル名を利用する方法

Perlで日本語のファイル名を利用することができますが、ファイル名は必ずそれぞれのOSの文字コードで指定する必要があります。

ソースコードは、UTF-8で保存して、先頭に「use utf8」をつけましょう。encode関数を使って、それぞれのOSの文字コードに変換します。

use utf8;
use Encode 'encode';

my $file = "あいうえお";

# Windowsの場合
open my $fh, '<', encode('cp932', $file);

# Linuxの場合
open my $fh, '<', encode('UTF-8', $file);

# Mac OS Xの場合
use Encode::UTF8Mac;
open my $fh, '<', encode(''utf-8-mac'', $file);

Mac OS Xは単純なUTF-8ではなくって、特殊なUTF-8で保存されているので注意しましょう。Encode::UTF8Macモジュールを使います。

日本語の扱いについては以下の解説が詳しいです。

例外処理

ファイルオープンがもし失敗したとすれば、それ移行では正しい処理をすることができないということを意味します。ですので、一般的にはファイルオープンが失敗したことを通知してプログラムを終了させる必要があります。このような場合にはdie関数を使って例外を発生させます。die関数の第一引数には通知したいメッセージを指定します。

die メッセージ;

ファイルオープンに失敗すると次のようなメッセージが表示されます。例外が発生した行番号を含めたメッセージが表示されます。

Can't open file "data.txt" : No such file or directory at a.pl line 6.

例外のメッセージに含める内容

 例外のメッセージには以下の内容を含めるのがよいでしょう。

  1. ユーザーに伝えたいこと
  2. システムコールが失敗した場合にOSから通知されたメッセージの内容($!に設定される)
die qq/Can't open file "$file" : $!/;

「Can't open file "$file"」で、ユーザーにファイルのオープンが失敗したことを伝えています。また$!という特殊変数には、システムコールを実行したときに、それが失敗した場合にOSから通知されたメッセージの内容が設定されるので例外のメッセージ含めるようにします。

例外処理についての詳しい解説は以下の記事をご覧ください。

ファイル読み込み

ファイルを読み込みモードでオープンすれば、ファイルから内容を読み込むことができます。

# 読み込みモードでオープン
my $file = 'data.txt';
open my $fh, '<', $file
  or die qq/Can't open file "$file" : $!/;

ファイルを1行づつ読み込むには行入力演算子を使用します。

# 行入力演算子
<ファイルハンドル>

while文を利用して行を1行づつ読み込んでいきます。以下はすべての行を標準出力(画面)に出力する例です。

while (my $line = <$fh>) {
  print $line;
}

行入力演算子で行を1行読みこんで$lineという変数に代入しています。これをwhile文を使ってファイルの末尾まで行っています。

行入力演算子はファイルの末尾に達するとundefを返却するので、ファイルの末尾でwhile文を脱出します。

ファイルクローズ

ファイルへの読み込みや書き込みが終わったらファイルをクローズしましょう。

ファイルをクローズするにはclose関数を使用します。

close $fh;

特にファイルへの書き込みを行っている場合はクローズの処理のときに、ファイルへ書き込み内容が完全に反映される(フラッシュ)ので、closeは大切な処理といえます。

ファイルへの書き込み

次はファイルへの書き込みを行ってみましょう。ファイルに書き込むには最初に書き込みモード「>」でファイルをオープンします。

# 追加書き込みモードでオープン
my $file = 'data.txt';
open my $fh, '>', $file
  or die qq/Can't open file "$file" : $!/;

ファイルへ文字列を書き込むにはprint関数を使用します。

print ファイルハンドル 文字列

ファイルハンドルと文字列の間にカンマがないことに注意してください(Perlのややこしい間違いやすい部分のひとつです)。

my $str = "Hello\n";
print $fh $str;

書き込みが終わったらファイルクローズを行います。書き込みの場合はcloseを行った時点でファイルへの書き込みが完了するので、例外処理を加えておくと安心です。

close $fh or die qw/Can't close file "$file": $!/;

data.txtに「Hello」という文字列が書き込まれているのが確認できます。

ファイルへの追加書き込み

ファイルへの追加書き込みを行ってみましょう。ファイルに書き込むには最初に追加書き込みモード「>>」でファイルをオープンします。

# 書き込みモードでオープン
my $file = 'data.txt';
open my $fh, '>>', $file
  or die qq/Can't open file "$file" : $!/;

ファイルへ文字列を書き込むにはprint関数を使用します。

my $str = "Hello\n";
print $fh $str;

書き込みが終わったらファイルクローズを行います。書き込みの場合はcloseを行った時点でファイルへの書き込みが完了するので、例外処理を加えておくと安心です。

close $fh or die qw/Can't close file "$file": $!/;

繰り返しプログラムを実行するとdata.txtに「Hello」という文字列が追加されていくのが確認できます。

Hello
Hello
Hello

ファイルの内容を一度に読み込む

ファイルの内容を一度にすべて読み込むには次のようにします。

my $content = do { local $/; <$fh> };

これはわかりにくいですが、一般的にはこのように記述されることが多いです。

 「$/」という特殊変数には、入力レコードセパレータが設定されています。デフォルトではOSの標準の改行コードが入力レコードセパレータとして設定されています。入力レコードセパレータをundefに設定することで、行入力演算子を使ってファイルの内容を一度に読み込むことができます。つまり$/にundefを設定すると、<$fh>を実行したときにファイルのすべての内容を取得することができます。ただし、$/はグローバルなものなので、変更したら必ず元の値に戻しておく必要があります。

localは変数の内容を一時的に変更するためのものです。localはレキシカル変数以外の変数に対して利用することができます。

local 変数;

と記述すると、スコープの終わりまで変数の内容を一時的にundefにすることができます。スコープが終了すると、元の値が復元されます。

次にdoブロックですが、「do { ... }」のように記述すると戻り値を返却するブロックを作成することができます。ブロックの中で最後に評価された値が戻り値になります。

全体を通して以下のような処理を行っていることになります。

my $content = do { # ブロックの作成
  # 入力レコードセパレーターを一時的にundefに
  local $/;

  # すべての内容を読み込む
  <$fh>
};

# $contentには「do { }」の最後の評価された値が代入され
# 入力レコードセパレータの値は復元される

改行コードについて

改行コードというのは行末をあらわす特別な文字列のことです。改行コードはOSによって異なり、Windowsにおいては16進数で「0D 0A」という文字の並びであり、Unixにおいては「0A」です。

Perlの文字列として記述すると以下のようになります。「\x」という表記で16進数で文字を記述できます。

# Windowsの改行コード
my $ln_win  = "\x0D\x0A";

# Unixの改行コード
my $ln_unix = "\x0A";
行入力演算子の挙動

行入力演算子「<ファイルハンドル>」は、入力レコードセパレータを発見するとその直後の位置までを1行として取得します。入力レコードセパレータは「$/」という変数に設定されており、Windowsにおいては「0D 0A」であり、Unixにおいては「0A」です。

# Windowsでの入力レコードセパレータ
$/ = "\x0D\x0A";

# Unixでの入力レコードセパレータ
$/ = "\x0A";
chomp関数の挙動

chomp関数を使用すると、末尾の改行を取り除くことができますが、実際は末尾にある入力レコードセパレータを削除しています。

chomp 文字列;

つまり、入力レコードセパレータを変更することで取り除く改行を変更することができます。

\nで出力される文字

\nは改行をコードを表しますが、Windowsにおいては16進数で「0D 0A」という文字の並びであり、Unixにおいては「0A」です。これは定数であり変更することはできません。

UnixWindowsのファイルを編集する

UnixでWindwosのファイルを編集することを考えましょう。Unix上ですので、デフォルトの入力レコードセパレータ「$/」には「0D 0A」が代入されており、\nというエスケープシーケンスは「0D 0A」が出力されます。Windowsで編集されたファイルの改行コードは「0D 0A」になっており、これに対応させる必要があります。

# ファイルオープン
my $file = 'data.txt';
open my $fh, '<', $file
  or die qq/Can't open file "$file" : $!/;

# ファイルの読み込み
{
  # 改行コード
  my $ln = "\x0D0A"

  # 入力レコードセパレータを一時的に変更
  local $/ = $ln;

  # 行が正しく読み込まれる
  while (my $line = <$fh>) {

    # 改行が正しく取り除かれる
    chomp $line;
    
    # 何かの処理
    
    print "$line$ln"; # リダイレクトなどで他のファイルに出力することを想定
  }
}

出力される改行コードを変更してもかまわない場合であれば\nを出力するのがよいでしょう。

ファイルの読み書きのアプローチ

ファイルの読み書きを行いたい場合は、オープンモードで読み書きモードで開けばよいのではないかと思うかもしれませんが、実際に読み書きを行うときは一般的には読み書きモードでは開きません。なぜなら書き込み途中でシステムがクラッシュしてしまうと、元の内容まで破壊されてしまうからです。書き込み完了まで元の内容を残しておけば、書き込み中にクラッシュしたとしても、元の内容は残ります。

ですから、ファイルへの読み書きを行うには次のようなアプローチを取ります。

  1. ファイル内容を読み込む
  2. 内容の更新
  3. 一時ファイルへの書き出し
  4. 一時ファイル名を元のファイル名に変更

一時的なファイルに書き出してから、書き込みが完了してから、一時ファイルを元のファイル名に変更するというようにします。一時ファイル名は他のファイルと重ならないような名前にしたほうがよいでしょう。

実際のスクリプトは次のようになります。

use strict;
use warnings;

use File::Copy 'move';

# (1) ファイルの内容を読み込む
my $file = 'date.txt';
open my $fh, '<', $file
  or die qq/Can't open file "$file": $!/;
my $content = do {local $/; <$fh>};
close $fh;

# (2) 内容の更新
$content = 'Hello!';

# (3) 一時ファイルへの書き出し
my $temp_file = "$file.$$." . int(rand 10000);
open my $temp_fh, '>', $temp_file
  or die qq/Can't open file "$file": $!/;
print $temp_fh $content;
close $temp_fh or die qq/Can't open file "$file": $!/;

# (4) 一時ファイル名を元のファイル名に変更
move $temp_file, $file
  or die qq/Can't move "$temp_file" to "$file": $!/;

(参考)File::Copy