Perl入門ゼミ

テキスト処理、Linuxサーバー管理、Web開発
  1. Perl
  2. ファイル入出力
  3. here

安全にファイルを読み書きする

ファイルを読み込んで同じファイルに書き込むときは、読み書きできるオープンモード "+<" を指定して、書き戻すことは非常に危険です。もし書きこんでいる最中にシステムがクラッシュすると、ファイルが破損して復元することができなくなるからです。

安全に書き込みを行うためには、いったん新しいファイルに書き込んでから、ファイル名を元のファイル名に変更することで上書きするようにします。

以下は、安全な読み書きをするためのサンプルです。エラーチェックをしっかり行う必要があります。

use strict;
use warnings;

# ファイル名を変更するために使用する。
use File::Copy; 

# 編集して書き戻すので指定するファイルは注意してください。
my $infile = shift;

# 一時ファイル。他のプロセスとなるべく
# 重ならないように、プロセスID($$)を一時ファイル名に
# 使用する。
my $tmpfile = "tmp$$";

open(my $infh, "<", $infile)
  or die "Cannot open $infile: $!";

# 書き出し用に一時ファイルを開く。
open(my $tmpfh, ">", $tmpfile)
  or die "Cannot open $tmpfile: $!";
    
while (1) {
    my $line;

    # 読み込みエラーを確実に回避
    undef $!;
    # エラーかEOFの場合は、undefが返る
    unless (defined($line = <$infh>)) { 
      if ($!) {
        # エラーの場合
        die "Error read $infh: $!";
      }
      else {
        # EOFの場合
        last;
      }
    }
    
    if ($. == 1) {
      # 1行目だけファイルの先頭にupdate:という文字を結合
      $line = "update:" . $line ; 
    }
    
    # 書き込みエラーを確実に回避
    print $tmpfh $line
      or die "Error Writing $tmpfile: $!";
}                                            

close $infh;

# closeのエラーを確実に回避
close $tmpfh
  or die "Error closing $tmpfile";

# 一時ファイル名を元のファイル名に変更。
File::Copy::move($tmpfile, $infile)
  or die "Cannot move $tmpfile to $infile";

安全にファイルを読み書きを行う手法

open関数のドキュメントを読めば、ファイルを読み書きするには、open関数に、"+<" というオープンモードを指定すればよいと思うかもしれません。確かにこの方法でも読み書きできますが、非常に危険です。

書き込みしている最中にシステムがクラッシュするとファイルが破損して復元できなくなります。

そこで安全に読み書きするために以下の手法が用いられます。

  1. 入力ファイルを読み込む。
  2. 編集した内容を一時的作成したファイルに書き込む。
  3. 一時的に作成したファイル名を元のファイル名に変更する。

こうすれば、3番のファイル名の変更が行われるまでは、元のファイルの内容が保たれます。たとえファイル名変更中にシステムがクラッシュしたとしても、ファイルの内容が失われるという事態は回避することができます。

コード解説

(1) 書き込み用一時ファイル名の作成

my $tmpfile = "tmp$$";

一時ファイル名には、プロセスID( $$ ) を含ませるようにしておきます。 一時ファイル名にプロセスID( $$ )を含むということを、規約にしておけば一時ファイルが他のプロセスと競合するということを避けることができます( 規約が破られればこの限りではないです。 )

(2)読み込みファイルと書き込み用一時ファイルの両方をオープンする

open(my $infh, "<", $infile)
  or die "Cannot open $infile: $!";

open(my $tmpfh, ">", $tmpfile)
  or die "Cannot open $tmpfile: $!";

入力ファイルは読み込みモード( "<" )、書き込み用一時ファイルは書き込みモード( ">" ) でオープンします。

(3) ファイル読み込みのエラーチェック

    unless (defined($line = <$infh>)) {
      if ($!) {
        die "Error read $infh: $!";
      }
      else{
         last;
      }
    }

み込みの最終にエラーが起こった場合は、whileループを抜けてしまします。すると、途中まで処理されたもので、元のファイルが上書きされるということが起こります。このようなことが起こらないように、読み込みエラーを確実に回避するようにコーディングをする必要があります。

(4) ファイル書き込み時のエラーチェック

    print $tmpfh $line
      or die "Error Writing $tmpfile: $!";

ァイル書き込みでエラーが起こった場合も、誤った内容で元のファイルが上書きされるということが起こります。このようなことが起こらないように、確実に書き込みエラーを回避するようにコーディングする必要があります。

(5) ファイルクローズ時のエラーチェック

close $infh;

# closeのエラーを確実に回避
close $tmpfh
  or die "Error closing $tmpfile";

読み込みファイルのクローズのエラーチェックは省略しても深刻な問題になることはなですが、書き込んだファイルに対するエラーチェックは行う必要があります。

Perlのprint関数は、書き込み内容をバッファリング(メモリに蓄積すること)します。バッファリングするということは、print関数を呼び出した時点では、実際にはファイルに書き込まないということです。ファイルへの出力というのは、非常に速度が遅いので、書き込み内容をバッファに貯めておいて、一度に書き出すということが暗黙的に行われています。

書き込み内容がある程度までたまれば、ファイルに出力されますが、それまでは出力されません。その場合は、close関数が呼ばれた時点で、ファイルに出力されます(バッファ内容を強制的にファイルに書き出すことをフラッシュと呼びます)。

だから、closeにもし失敗するとバッファがフラッシュされないままになってしまい、正しい内容を書き込むことができません。このようなことが起こらないように、確実にクローズエラーを回避するようにコーディングする必要があります。

(6) 書き込み用一時ファイル名を元のファイル名にリネームする

# 一時ファイル名を元のファイル名に変更。
File::Copy::move($tmpfile, $infile)
  or die "Cannot move $tmpfile to $infile";

(参考)die関数

File::Copyモジュールを使って、書き込み用一時ファイル名を元のファイル名に変更します。これで、完了です。