PHPにはファイル末尾から行単位で文字列を取得する関数がありません。
PHP: ファイルシステム 関数 – Manual
残念ながらLaravelを始めとしたいくつかのフレームワーク中にもそのような関数は備わっていません。
次のコードはこのファイル末尾からn行取得するコードの例です。()内のコメント訳はGoogle翻訳の出力そのものです。
引用元:PHP: Tail tackling large files | Geekality
/** * @see https://www.geekality.net/2011/05/28/php-tail-tackling-large-files/ */ function tail($filename, $lines = 10, $buffer = 4096) { // Open the file(ファイルを開く) $f = fopen($filename, "rb"); // Jump to last character(最後の文字にジャンプ) fseek($f, -1, SEEK_END); // Read it and adjust line number if necessary(それを読み、必要ならば行番号を調整する) // (Otherwise the result would be wrong if file doesn't end with a blank line)((それ以外の場合、ファイルが空白行で終わっていなければ結果は間違っているでしょう)) if(fread($f, 1) != "\n") $lines -= 1; // Start reading(読み始めます) $output = ''; $chunk = ''; // While we would like more(もっと欲しいのですが) while(ftell($f) > 0 && $lines >= 0) { // Figure out how far back we should jump(ジャンプする距離を計算する) $seek = min(ftell($f), $buffer); // Do the jump (backwards, relative to where we are)((私たちがいる場所に対して相対的に後方へ)ジャンプします) fseek($f, -$seek, SEEK_CUR); // Read a chunk and prepend it to our output(チャンクを読み、それを私たちの出力の前に追加します) $output = ($chunk = fread($f, $seek)).$output; // Jump back to where we started reading(読み始めたところに戻る) fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); // Decrease our line counter(ラインカウンターを減らす) $lines -= substr_count($chunk, "\n"); } // While we have too many lines(行数が多すぎる) // (Because of buffer size we might have read too many)((バッファサイズのため、読み過ぎた可能性があります)) while($lines++ < 0) { // Find first newline and remove all text before that(最初の改行を見つけて、それより前のすべてのテキストを削除します) $output = substr($output, strpos($output, "\n") + 1); } // Close file and return(ファイルを閉じて戻る) fclose($f); return $output; }
ちょっとした低級言語気分です。このコードを読んでみます。
要約
- ファイル末尾からbufferバイトずつx回取り出す。
- 取り出した文字列中の行数がn以上になった場合、取り出しを終了。
- 取り出した文字列中の取り出しすぎた余分な行を削る。
- ファイル末尾からn行分のデータを得られた。
次のコードは冒頭部である6~10行目のコードです。
// Open the file(ファイルを開く) $f = fopen($filename, "rb"); // Jump to last character(最後の文字にジャンプ) fseek($f, -1, SEEK_END);
ファイルを開き、最後の文字にファイルポインタを合わせます。PHPの様な高級言語では基本意識することなるファイルの読み書きができますが、内部ではしっかりファイルポインタを用いています。
ファイルポインタはここからファイルを読み書きするという位置を表す値です。値が延々と書き込まれたファイルというテープをどこから読み書きするかというヘッダ(要はチューリングマシン)が近いです。
fseek($f, -1, SEEK_END);はファイルの終端記号の1つ前の値、つまりデータ部の末尾である最後の文字にファイルポインタを合わせているわけです。
次の部分は12~14行目のコードです。
// Read it and adjust line number if necessary(それを読み、必要ならば行番号を調整する) // (Otherwise the result would be wrong if file doesn't end with a blank line)((それ以外の場合、ファイルが空白行で終わっていなければ結果は間違っているでしょう)) if(fread($f, 1) != "\n") $lines -= 1;
上述した一連のコードは改行コードの数で行数を数えています。しかしファイルの終わりには改行コードで終わらない行が存在することが出来ます。この部分ではその調整として最終行が改行コードで終わっていない場合、1行あらかじめカウントしておきます。余談ですが、このコードは外から参照する人のために第二引数の名前を$linesとしています。内部を読む場合、$linesを$line_counterと読み替えた方がわかりやすいです。この第二引数はかなり書き換えられます。
次の部分は16~37行目のコードです。
// Start reading(読み始めます) $output = ''; $chunk = ''; // While we would like more(もっと欲しいのですが) while(ftell($f) > 0 && $lines >= 0) { // Figure out how far back we should jump(ジャンプする距離を計算する) $seek = min(ftell($f), $buffer); // このループにおける読み取り長の決定 // Do the jump (backwards, relative to where we are)((私たちがいる場所に対して相対的に後方へ)ジャンプします) fseek($f, -$seek, SEEK_CUR); // この行の実行直後、ファイルポインタは読み取り予定先頭部を指している // Read a chunk and prepend it to our output(チャンクを読み、それを私たちの出力の前に追加します) $output = ($chunk = fread($f, $seek)).$output; // この行の実行直後、ファイルポインタは読み取り終了部の一つ次の値を指している // Jump back to where we started reading(読み始めたところに戻る) fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); // この行の実行直後、ファイルポインタは読み取り先頭部を指している // Decrease our line counter(ラインカウンターを減らす) $lines -= substr_count($chunk, "\n"); // 読み取ったデータに含まれる行数分$linesの値を減らす } // ループ終了時点で$linesの値は、引数として渡された値-読み取ったデータに含まれる行数、となっている
ftell()は現在のファイルポインタをtellしてくれる関数です。末尾から読み始めた時、これが0以上ならばまだファイルを読み切っていないわけです。ファイルを読み切っておらず、出力に必要な行数分のデータも得ていないならば、ファイルを読み続けます。
次の部分はループ内前半である23~27行目のコードです。始めのこの二行でファイルポインタを次に読み取るまとまりの先頭まで動かします。
// Figure out how far back we should jump(ジャンプする距離を計算する) $seek = min(ftell($f), $buffer); // このループにおける読み取り長の決定 // Do the jump (backwards, relative to where we are)((私たちがいる場所に対して相対的に後方へ)ジャンプします) fseek($f, -$seek, SEEK_CUR); // この行の実行直後、ファイルポインタは読み取り予定先頭部を指している
buffer分ファイルポインタを先頭に向かって動かします。buffer分も読み取っていないデータが残っていないならば、ファイルポインタを先頭まで動かし、読み取っていないデータ全てを読み取ろうとします。
次の部分はループ内後半である29~36行目のコードです。ファイル中のデータを$seek分読み取り、後始末をします。
// Read a chunk and prepend it to our output(チャンクを読み、それを私たちの出力の前に追加します) $output = ($chunk = fread($f, $seek)).$output; // この行の実行直後、ファイルポインタは読み取り終了部の一つ次の値を指している // Jump back to where we started reading(読み始めたところに戻る) fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); // この行の実行直後、ファイルポインタは読み取り先頭部を指している // Decrease our line counter(ラインカウンターを減らす) $lines -= substr_count($chunk, "\n"); // 読み取ったデータに含まれる行数分$linesの値を減らす
$output = ($chunk = fread($f, $seek)).$output;
で$fを現在のファイルポインタから$seek分ファイル末尾に向かって読み取ります。
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
でfreadして動いた分のファイルポインタを戻します。
$lines -= substr_count($chunk, "\n");
で読み取ったデータ中にある改行コード数分ラインカウンターを減らします。
次の部分は39~45行目のコードです。このコードでは読み取りすぎた行を削ります。
// While we have too many lines(行数が多すぎる) // (Because of buffer size we might have read too many)((バッファサイズのため、読み過ぎた可能性があります)) while($lines++ < 0) { // Find first newline and remove all text before that(最初の改行を見つけて、それより前のすべてのテキストを削除します) $output = substr($output, strpos($output, "\n") + 1);// +1は改行コードをsubstrによる削除対象に含めるため }
これまでbufferバイトずつファイル中のデータを読み取ってきました。このため行を読みすぎている場合が考えられます。これを解決するために、strposで改行コード\nまでの文字数を読み取り、その文字数分文字列を削る作業を要求された行数になるまで繰り返します。このループに入った時点の$linesの値は、要求された行数-ファイルから読み取った行数、なので読み取りすぎたときにのみこのループが走ります。
最後にファイルを閉じて結果を返して終わりです。
// Close file and return(ファイルを閉じて戻る) fclose($f); return $output;
これで先に示した様に
要約
- ファイル末尾からbufferバイトずつx回取り出す。
- 取り出した文字列中の行数がn以上になった場合、取り出しを終了。
- 取り出した文字列中の取り出しすぎた余分な行を削る。
- ファイル末尾からn行分のデータを得られた。
と動作することでファイル末尾からn行を取得できました。ログファイルの最新部を表示する際などに便利です。