【问题标题】:Read last line from file从文件中读取最后一行
【发布时间】:2009-10-02 15:09:55
【问题描述】:

我遇到了一个问题。我在一个 Linux 机器上有一个日志,其中写入了几个正在运行的进程的输出。这个文件有时会变得非常大,我需要读取该文件的最后一行。

问题是这个动作会经常通过 AJAX 请求调用,当该日志的文件大小超过 5-6MB 时,这对服务器不利。所以我想我必须阅读最后一行而不是阅读整个文件并通过它或将其加载到 RAM 中,因为那只会加载到我的盒子。

此操作是否有任何优化,使其运行流畅且不损害服务器或杀死 Apache?

我的其他选择是exec('tail -n 1 /path/to/log'),但听起来不太好。

稍后编辑:我不想将文件放在 RAM 中,因为它可能会变得很大。 fopen() 不是一个选项。

【问题讨论】:

标签: php file file-io


【解决方案1】:

这应该可行:

$line = '';

$f = fopen('data.txt', 'r');
$cursor = -1;

fseek($f, $cursor, SEEK_END);
$char = fgetc($f);

/**
 * Trim trailing newline chars of the file
 */
while ($char === "\n" || $char === "\r") {
    fseek($f, $cursor--, SEEK_END);
    $char = fgetc($f);
}

/**
 * Read until the start of file or first newline char
 */
while ($char !== false && $char !== "\n" && $char !== "\r") {
    /**
     * Prepend the new char
     */
    $line = $char . $line;
    fseek($f, $cursor--, SEEK_END);
    $char = fgetc($f);
}

fclose($f);

echo $line;

请注意,此解决方案将重复该行的最后一个字符,除非您的文件以换行符结尾。如果您的文件没有以换行符结尾,您可以将$cursor-- 的两个实例都更改为--$cursor

【讨论】:

  • 您不想阅读该文件是什么意思?我没有读取内存中的整个文件。我只是打开一种指向它的指针,然后逐个字符地查找它。这是处理大文件的最有效方式。
  • fopen() 不像file_get_contents()
  • fopen 不会将文件加载到内存中,它只是创建一个文件描述符(一个指针)。
  • 最后一个字符重复了。当我的最后一行是</rss> 时,此代码将</rss>> 放入$line。为了解决这个问题,我将$cursor-- 的两个实例都更改为--$cursor
  • 我确认 rg89 的观察。如果文件不以换行符结尾,则代码将输出最后一个字符两次。示例 1:123\n456 - 输出:4566(不正确)。示例 2:123\n456\n - 输出:456(按预期工作)。顺便说一句,优秀的解决方案..
【解决方案2】:

使用fseek。您寻找到最后一个位置并向后寻找(使用ftell 告诉当前位置),直到找到“\n”。


$fp = fopen(".....");
fseek($fp, -1, SEEK_END); 
$pos = ftell($fp);
$LastLine = "";
// Loop backword util "\n" is found.
while((($C = fgetc($fp)) != "\n") && ($pos > 0)) {
    $LastLine = $C.$LastLine;
    fseek($fp, $pos--);
}
fclose($fp);

注意:我没有测试过。您可能需要进行一些调整。

更新:感谢Syntax Error 指出空文件。

:-D

UPDATE2:修复了另一个语法错误,$LastLine = "" 处缺少分号

【讨论】:

  • 如果文件是空的,你会得到一个无限循环。修复它: while(($C = fgetc($fp)) != "\n" && $pos > 0 ) {
【解决方案3】:

您正在寻找fseek 函数。那里的 cmets 部分提供了有关如何读取文件最后一行的工作示例。

【讨论】:

    【解决方案4】:

    这是Ionuț G. Stan

    的代码

    我稍微修改了您的代码,使其成为可重用的函数

    function read_last_line ($file_path){
    
    
    
    $line = '';
    
    $f = fopen($file_path, 'r');
    $cursor = -1;
    
    fseek($f, $cursor, SEEK_END);
    $char = fgetc($f);
    
    /**
    * Trim trailing newline chars of the file
    */
    while ($char === "\n" || $char === "\r") {
        fseek($f, $cursor--, SEEK_END);
        $char = fgetc($f);
    }
    
    /**
    * Read until the start of file or first newline char
    */
    while ($char !== false && $char !== "\n" && $char !== "\r") {
        /**
         * Prepend the new char
         */
        $line = $char . $line;
        fseek($f, $cursor--, SEEK_END);
        $char = fgetc($f);
    }
    
    return $line;
    }
    

    echo read_last_line('log.txt');

    你会得到最后一行

    【讨论】:

      【解决方案5】:

      如果你知道行长的上限,你可以这样做。

      $maxLength = 1024;
      $fp = fopen('somefile.txt', 'r');
      fseek($fp, -$maxLength , SEEK_END); 
      $fewLines = explode("\n", fgets($fp, $maxLength));
      $lastLine = $fewLines[count($fewLines) - 1];
      

      响应编辑:fopen 只是获取文件的句柄(即确保它存在,进程具有权限,让操作系统知道进程正在使用该文件,等等...) .在这个例子中,只有文件中的 1024 个字符会被读入内存。

      【讨论】:

      • 如果最后一行是 1025 个字符长怎么办?
      • 对于您知道的短行文件来说,这似乎是一个简短的解决方案。不适用于大行大文件
      【解决方案6】:
      function readlastline() 
      { 
             $fp = @fopen("/dosmnt/LOGFILE.DAT", "r"); 
             $pos = -1; 
             $t = " "; 
             while ($t != "\n") { 
                   fseek($fp, $pos, SEEK_END); 
                   $t = fgetc($fp); 
                   $pos = $pos - 1; 
             } 
             $t = fgets($fp); 
             fclose($fp); 
             return $t; 
      } 
      

      来源:http://forums.devshed.com/php-development-5/php-quick-way-to-read-last-line-156010.html

      【讨论】:

      • 如果文件被不断写入,您可能希望在开始时存储文件的当前长度,然后再从该位置向后搜索。这样,即使在查找时添加了更多行,也可以保证找到完整的行。如果考虑性能,您可以抓取块而不是逐字节搜索。
      • 如何做...while 这样你就不需要初始化 $t = " ";这有点草率。 ;)
      【解决方案7】:

      您的问题类似于this one

      避免将整个文件加载到内存中的最佳方法似乎是:

      $file = escapeshellarg($file); // for the security concious (should be everyone!)
      $line = `tail -n 1 $file`;
      

      【讨论】:

      • 不,没那么简单。每次文件时我都无法加载到 RAM 中。它可以轻松达到 10-20MB,每个客户端每秒都会发出请求。不是解决方案,导致 Apache 崩溃。
      • tail -n 1 /path/to/log 选项写在问题正文中......虽然不是很有趣
      • 如果您愿意,可以选择稍短一些的选项:sed -n /path/to/log
      【解决方案8】:

      是否有可能从另一侧进行优化? 如果是这样,只需让日志记录应用程序在截断它时始终将行记录到文件中(即 > 而不是 >>)

      虽然可以通过“猜测”来实现一些优化,但只需打开文件并使用平均日志行宽,您就可以猜测最后一行的位置。使用 fseek 跳转到该位置并找到最后一行。

      【讨论】:

      • 很遗憾,我无法控制应用程序。第二个问题是每个操作都必须存在日志文件,因此只记录一行将是一个混乱的问题。
      【解决方案9】:

      来自http://php.net/manual/en/function.fseek.phpcmets 的未经测试的代码

      jim at lfchosting dot com 05-Nov-2003 02:03
      Here is a function that returns the last line of a file.  This should be quicker than reading the whole file till you get to the last line.  If you want to speed it up a bit, you can set the $pos = some number that is just greater than the line length.  The files I was dealing with were various lengths, so this worked for me. 
      
      <?php 
      function readlastline($file) 
      { 
              $fp = @fopen($file, "r"); 
              $pos = -1; 
              $t = " "; 
              while ($t != "\n") { 
                    fseek($fp, $pos, SEEK_END); 
                    $t = fgetc($fp); 
                    $pos = $pos - 1; 
              } 
              $t = fgets($fp); 
              fclose($fp); 
              return $t; 
      } 
      ?>

      【讨论】:

        【解决方案10】:

        这是我的解决方案,只有一个循环

                $line = '';
                $f = fopen($file_path, 'r');
                $cursor = 0 ;
                do  {
                    fseek($f, $cursor--, SEEK_END);
                    $char = fgetc($f);
                    $line = $char.$line;
                } while (
                        $cursor > -1 || (
                         ord($char) !== 10 &&
                         ord($char) !== 13
                        )
                );
        

        【讨论】:

          【解决方案11】:

          这里是答案的汇编,包含在一个函数中,该函数可以指定应该返回多少行。

          function getLastLines($path, $totalLines) {
            $lines = array();
          
            $fp = fopen($path, 'r');
            fseek($fp, -1, SEEK_END);
            $pos = ftell($fp);
            $lastLine = "";
          
            // Loop backword until we have our lines or we reach the start
            while($pos > 0 && count($lines) < $totalLines) {
          
              $C = fgetc($fp);
              if($C == "\n") {
                // skip empty lines
                if(trim($lastLine) != "") {
                  $lines[] = $lastLine;
                }
                $lastLine = '';
              } else {
                $lastLine = $C.$lastLine;
              }
              fseek($fp, $pos--);
            }
          
            $lines = array_reverse($lines);
          
            return $lines;
          }
          

          【讨论】:

            【解决方案12】:

            此函数将允许您从末尾逐行读取最后一行或(可选)整个文件,通过传递 $initial_pos 将告诉从哪里开始从末尾读取文件(负整数)。

            function file_read_last_line($file, $initial_pos = -1) {
              $fp = is_string($file) ? fopen($file, 'r') : $file;
              $pos = $initial_pos;
              $line = '';
              do {
                fseek($fp, $pos, SEEK_END);
                $char = fgetc($fp);
                if ($char === false) {
                  if ($pos === $initial_pos) return false;
                  break;
                }
                $pos = $pos - 1;
                if ($char === "\r" || $char === "\n") continue;
                $line = $char . $line;
              } while ($char !== "\n");
              if (is_string($file)) fclose($file);
              return $line;
            }
            

            然后,读取最后一行:

            $last_line = file_read_last_line('/path/to/file');
            

            从末尾逐行读取整个文件:

            $fp = fopen('/path/to/file', 'r');
            $pos = -1;
            while (($line = file_read_last_line($fp, $pos)) !== false) {
              $pos += -(strlen($line) + 1);
              echo 'LINE: ' . $line . "\n";
            }
            fclose($fp);
            

            【讨论】:

            • 看起来很像早先发布的对同一问题的答案stackoverflow.com/a/37748401/2943403,只是答案更清晰,因为它将所有循环条件写入while(),而不是您的具有@987654327 的sn-p @ 和 continue 乱扔循环体。我实际上并没有测试任何一个答案的正确性——只是一个观察。
            【解决方案13】:

            逐块向后遍历文件,找到新行后停止,返回最后一个“\n”之后的所有内容。

            function get_last_line($file) {
                if (!is_file($file)) return false;
                $fileSize   = filesize($file);  
                $bufferSize = 1024; // <------------------ Change buffer size here.
                $bufferSize = ($fileSize > $bufferSize) ? $bufferSize : $fileSize;
            
                $fp = fopen($file, 'r');
                
                $position = $fileSize - $bufferSize;
                $data = "";
                while (true) {
                    fseek($fp, $position);
                    $chunk  = fread($fp, $bufferSize);
                    $data   = $chunk.$data;
                    $pos    = strrchr($data, "\n");
                    
                    if ($pos !== false) return substr($pos, 1);
                    if ($position <= 0) break;
                    $position -= $bufferSize;
                    if ($position <=0) $position = 0;
                }
                
                // whole file is 'the last line' :)
                return $data;
            }
            

            你可以自己定义块的长度。

            更小的块 = 更少的内存使用,更多的迭代。

            更大的块 = 更大的内存使用,更少的迭代。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-11-02
              • 2012-07-22
              • 1970-01-01
              • 1970-01-01
              • 2021-08-29
              相关资源
              最近更新 更多