ブログトップ 記事一覧 ログイン 無料ブログ開設

サンプルコードによるPerl入門 〜 伝統と信頼のPerlを学ぼう 〜

2011-02-12

ファイル入出力の基礎をマスターする

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

ファイルオープン

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

 ファイルをオープンするには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など)。

例外処理

 ファイルオープンがもし失敗したとすれば、それ移行では正しい処理をすることができないということを意味します。ですので、一般的にはファイルオープンが失敗したことを通知してプログラムを終了させる必要があります。このような場合には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 $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 { }」という書き方ですが、このように記述すると戻り値を返却するブロックを作成することができます。ブロックの中で最後に評価された値が戻り値になります。

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

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」です。これは定数であり変更することはできません。

UnixでWindowsのファイルを編集する

 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": $!/;


目次へ

nobodynobody 2011/04/25 13:27 > とてもわかりにくいですね。ファイルの内容を一度に読み込むための標準関数がないので
read()があります

b-windb-wind 2011/04/25 13:50 open と同じように close にも or die つけとくべき。

# while (my $line = <$fh>)
この書き方だと空行や、"0" のみの列が拾えないね。
実際には改行コード有るから、問題になるのは最終行だけかもだけど。
バイナリファイルの場合も含めて defined で判定するのがセオリー(バッドノウハウ?)

名無し名無し 2011/04/25 14:35 >b-windさん
while (my $line = <$fh>)という文はPerlが気を利かせて、while ( defined(my $line = <$fh>) )と、自動で(暗黙に)definedを挿入してくれます。

\nはUnixでもWindowsでも「0A」で、OS Xより古いMacは「0D」だと思います。PerlIOの:crlfレイヤを通ると、入力では「0D 0A」が「0A」に、出力では「0A」が「0D 0A」に変換されます。

perlcodesampleperlcodesample 2011/04/25 17:54 >b-windさん
書き込みのときは、close時点でフラッシュが行われるので、closeにもdieをつけておいたほうが完璧ですね。普通のスクリプトの場合はケースバイケースだと思います。そもそもcloseが失敗したということは、システム全体がおかしいということです。ディスク容量がいっぱいだったりして、他のプログラムもきっとまともに動かないと思います。厳密なことが求められるプログラムの場合は、closeも必ずdieすべきだと思います。

perlcodesampleperlcodesample 2011/04/25 18:06 > nobodyさん
「ファイルの内容を一度に読み込むための標準関数がない」というのは私の勘違いでした。テキストを修正してあります。

b-windb-wind 2011/04/26 14:09 >そもそもcloseが失敗したということは、システム全体がおかしいということです。
システム全体がおかしい状態ならそのままコードを動かしても良いの?
ってのは言い過ぎかも知れないけど、ビミョーな状態で書き込みエラーになる事は有るよ。

「Perlの初学者の方にもぜひ教えてあげてください」って趣旨だよね?
初学者向けだからこそ「厳密なこと」かどうかにかかわらず、「やった方が良い事」を書くべきだと思うんだけど。
バージョン限定すれば「use autodie;」で良いんだけどね…

perlcodesampleperlcodesample 2011/04/26 19:29 >b-windさん
「そもそもcloseが失敗したということは、システム全体がおかしいということです」というのはいいすぎでした。オープンモードが書き込みと追加書き込みの場合の部分について記事を修正しました。

shiroteshirote 2011/08/30 11:30 そもそもdie関数はなんで要るんですか?
書き込み中にクラッシュしたとしても、元の内容は残るのでしたら、closeの時点で必要ないと思うんですが
書き込みに反映されていないかをすぐに確かめないのが一般的ですか?

perlcodesampleperlcodesample 2011/09/02 16:22 >shiroteさん
 Perlは書き込み内容をバッファリングしますので、printの時点では実際にファイルに書き込まれているかどうかはわかりません。

 closeを行うとフラッシュも同時に行われるので、バッファリングされたデータは確実にファイルに書き込まれます。ですから、closeが失敗した場合は、書き込みが完了していない可能性が極めて高いと思ってよいでしょう。

mackeymackey 2012/10/30 04:12 汎用機の現場で30年弱プログラミング経験者です
Perlの現場に配属されたばかりで、知識が足りません
現場に、Perl精通者が皆無な為、困っています

上記の事項を参考に以下を
コーディングしましたが、上手く行きません
御知恵を拝借出来ませんか?

CHECK {
open(IN,"in.txt"); #業務では、ディレクトリ配下のファイルを全て読みます
my @data = do { local $/; <IN> };
close(IN);

$/ = "\n";

open(OUT,">out.txt");

$match = "\BKBK"; #業務では、パラメータファイルを読みます
#パラメータファイルの内容は、メタ文字を含みます
$match = quotemeta($match);
@data = grep(/$match/,@data);

foreach (@data){
print OUT qq/"in.txt" , "$_"/;
}
close(OUT);
}

nackeynackey 2012/11/03 00:51 Perl大量データ高速検索

検索処理を行なっています
処理時間の短縮手法についてアドバイスが
あれば...御願いしたいのですが...

検索対象ファイル:1〜256バイト可変長テキストデータ(メタ文字を含む)
検索対象ファイル件数:1千万〜1億件
検索対象ファイル本数:10本以上

検索キーワード:2バイト〜26バイト程(メタ文字を含む)
検索キーワード件数:1千件以上

検索対象ファイルから、検索ワードが1件※でも、見つかった場合、
検索対象ファイル名、検索ワードとその行を出力する
※2件以上は、考慮しない

実行環境
言語:ActivePerl 5.8.8
OS:Microsoft Windows XP SP3
CPU:Core 2 Duoクラス
メモリ:2GB

テスト環境
OS:Microsoft Windows7
CPU:Core 2 Quadクラス
メモリ:4GB

(1)優先事項は、処理時間短縮

(2)キーワード検索が目的の為、
 トータルの処理時間が、短縮できれば、
 検索対象ファイルの分割は可能

(3)使用言語は、ActivePerl 5.8.8
 WSHが速い場合は、検討
 その他の言語は、不可

ソースコードは関係する箇所を、
抜粋しています

以下のようにコーディングした場合、
ファイルの読み込みは、劇的に速くなるのですが、
後続の処理で、入力セパレータが元に戻せません

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

CHECKブロックをコーディングした、
場合処理時間が速くなりました

処理時間の測定は、以下のコーディングとし、
n回実行し、検証しました

use Time::HiRes qw(gettimeofday tv_interval);
my $start=[gettimeofday];
# 処理
print tv_interval $start, [gettimeofday];

参考サイト
http://www.ibm.com/developerworks/jp/linux/library/l-optperl/

nackeynackey 2012/11/03 01:43 match,grep,index
メタ文字が含まれていても
該当する行を検索する場合は、
indexが一番速い?でしょうか?

perlcodesampleperlcodesample 2012/11/05 21:52 nackeyさん

>検索対象ファイル:1〜256バイト可変長テキストデータ(メタ文字を含む)
>検索対象ファイル件数:1千万〜1億件
>検索対象ファイル本数:10本以上

 これだけデータが存在すると、Perlではどれだけがんばってもだめだと思います。感覚としては、1ファイル1千万件くらいまでが限界ではないでしょうか。(これでも1時間くらいはかかるかも)

 おすすめする解決作は、いったんデータベースに格納することです。格納するまでは時間がかかりますが、いったん格納してしまえば、インデックスという機能を使うと、劇的に検索速度を上げることができます。

 MySQLかSQLiteというデータベースを使うのはどうでしょうか。DBD::mysqlやDBD::SQLiteで検索すると、Perlからの使い方もわかるかなと思います。

nackeynackey 2012/11/06 13:03 回答、有り難う御座います。実験してみます

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/perlcodesample/20110212/1303702930
リンク元