需求:现有一个 1G 左右的日志文件,大约有 500 多万行,用 php 返回最后几行的内容。
在 php 中,对于文件的读取时,最快捷的方式莫过于使用一些诸如 file、file_get_contents 之类 的函数,简简单单的几行代码就能很漂亮的完成我们所需要的功能。但当所操作的文件是一个比较大的文件时,这些函数可能就显的力不从心, 下面将从一个需求入手来说明对于读取大文件时,常用的操作方法。
1. 直接采用 file 函数来操作
由于 file 函数是一次性将所有内容读入内存,而 php 为了防止一些写的比较糟糕的程序占用太多的内存而导致系统内存不足,使服务器出现宕机,所以默认情况下限制只能最大使用内存 16M, 这是通过 php.ini 里的 memory_limit = 16M 来进行设置,这个值如果设置 -1,则内存使用量不受限制。
下面是一段用 file 来取出这具文件最后一行的代码。代码执行大概 2 分钟左右。
01 $fp = fopen($file, "r");
02 $num = 10;
03 $chunk = 4096;
04 $fs = sprintf("%u", filesize($file));
05 $max = (intval($fs) == PHP_INT_MAX) ? PHP_INT_MAX : filesize($file);
06 for ($len = 0; $len < $max; $len += $chunk) {
07 $seekSize = ($max – $len > $chunk) ? $chunk : $max – $len;
08 fseek($fp, ($len + $seekSize) * -1, SEEK_END);
09 $readData = fread($fp, $seekSize) . $readData;
10
11 if (substr_count($readData, "\n") >= $num + 1) {
12 preg_match("!(.*?\n){".($num)."}$!", $readData, $match);
13 $data = $match[0];
14 break;
15 }
16 }
17 fclose($fp);
18 echo $data;
我机器是 2 个 G 的内存,当按下 F5 运行时,系统直接变灰,差不多 20 分钟后才恢复过来,可见将这么大的文件全部直接读入内存,后果是多少严重,所以不在万不得以,memory_limit 这东西不能调得太高,否则只有打电话给机房,让 reset 机器了。
2. 直接调用 linux 的 tail 命令来显示最后几行
在 linux 命令行下, 可以直接使用 tail -n 10 access.log 很轻易的显示日志文件最后几行, 可以直接用 php 来调用 tail 命令, 执行 php 代码如下. 整个代码执行完成耗时 0.0034 (s)
1 file = 'access.log';
2 $file = escapeshellarg($file); // 对命令行参数进行安全转义
3 $line = `tail -n 1 $file`;
4 echo $line;
3. 直接使用 php 的 fseek 来进行文件操作
这种方式是最为普遍的方式, 它不需要将文件的内容全部读入内存, 而是直接通过指针来操作, 所以效率是相当高效的. 在使用 fseek 来对文件进行操作时, 也有多种不同的方法, 效率可能也是略有差别的, 下面是常用的两种方法.
方法一:
首先通过 fseek 找到文件的最后一位 EOF,然后找最后一行的起始位置,取这一行的数据,再找次一行的起始位置,再取这一行的位置,依次类推,直到找到了 $num 行。
view sourceprint?
01 function tail($fp,$n,$base=5)
02 {
03 assert($n>0);
04 $pos = $n+1;
05 $lines = array();
06 while(count($lines)< =$n){
07 try{
08 fseek($fp,-$pos,SEEK_END);
09 } catch (Exception $e){
10 fseek(0);
11 break;
12 }
13 $pos *= $base;
14 while(!feof($fp)){
15 array_unshift($lines,fgets($fp));
16 }
17 }
18 return array_slice($lines,0,$n);
19 }
20 var_dump(tail(fopen("access.log","r+"),10));
方法二:
还是采用 fseek 的方式从文件最后开始读, 但这时不是一位一位的读, 而是一块一块的读, 每读一块数据时, 就将读取后的数据放在一个 buf 里, 然后通过换行符 (\n) 的个数来判断是否已经读完最后 $num 行数据.
01 $fp = fopen($file, "r");
02 $line = 10;
03 $pos = -2;
04 $t = " ";
05 $data = "";
06 while ($line > 0) {
07 while ($t != "\n") {
08 fseek($fp, $pos, SEEK_END);
09 $t = fgetc($fp);
10 $pos –;
11 }
12 $t = " ";
13 $data .= fgets($fp);
14 $line –;
15 }
16 fclose ($fp);
17 echo $data
方法三:
1 ini_set('memory_limit','-1');
2 $file = 'access.log';
3 $data = file($file);
4 $line = $data[count($data)-1];
5 echo $line;