ti-enxame.com

Qual é o melhor caminho PHP ler as últimas linhas de um arquivo?

No meu aplicativo PHP eu preciso ler várias linhas começando no final de Muitos arquivos (principalmente logs). Às vezes preciso apenas do último, às vezes preciso de Dezenas ou centenas. Basicamente, quero algo tão flexível quanto o comando Unix tail .

Há perguntas aqui sobre como obter a última linha de um arquivo (mas Eu preciso de N linhas), e diferentes soluções foram dadas. Não tenho certeza sobre qual Um é o melhor e qual funciona melhor.

60
lorenzo-s

Visão geral dos métodos

Pesquisando na internet, me deparei com diferentes soluções. Eu posso agrupá-los em três abordagens:

  • ingênuo aqueles que usam a função file() PHP;
  • trapaça aqueles que executam o comando tail no sistema;
  • mighty aqueles que pulam alegremente em um arquivo aberto usando fseek().

Acabei escolhendo (ou escrevendo) cinco soluções, uma ingênua uma, uma enganar uma E três poderosasas.

  1. O mais conciso ingênuo solution , Usando funções de matriz incorporadas.
  2. A única solução possível baseada no comando tail , que possui Um pequeno grande problema: ela não roda se tail não estiver disponível, como em Não-Unix ( Windows) ou em ambientes restritos que não permitem funções do sistema .
  3. A solução na qual bytes únicos são lidos a partir do final da pesquisa de arquivos Para (e contando) caracteres de nova linha, encontrados aqui.
  4. A solução multi-byte buffered otimizada para arquivos grandes, encontrada here.
  5. Uma versão ligeiramente modificada da solução # 4 na qual o tamanho do buffer é Dinâmico, decidido de acordo com o número de linhas a serem recuperadas.

Todas as soluções work. No sentido de que eles retornam o resultado esperado de Qualquer arquivo e para qualquer número de linhas que pedirmos (exceto para a solução # 1, que pode Quebrar PHP limites de memória caso de arquivos grandes, não retornando nada). Mas qual deles É melhor?

Testes de performance

Para responder a pergunta eu corro testes. É assim que essas coisas são feitas, não é?

Eu preparei um arquivo sample 100 KB juntando arquivos diferentes encontrados em Meu diretório /var/log. Então eu escrevi um script PHP que usa cada uma das Cinco soluções para recuperar 1, 2, .., 10, 20, ... 100, 200, ..., 1000 linhas Do final do arquivo. Cada teste individual é repetido dez vezes (isto é Algo como 5 × 28 × 10 = 1400 testes), medindo média decorrido Tempo em microssegundos.

Eu corro o script na minha máquina de desenvolvimento local (Xubuntu 12.04, PHP 5.3.10, CPU dual core de 2.70 GHz, 2 GB de RAM) usando o interpretador PHP da linha de comando . Aqui estão os resultados:

Execution time on sample 100 KB log file

Solução # 1 e # 2 parecem ser os piores. A solução # 3 só é boa quando precisamos Ler algumas linhas. Soluções # 4 e # 5 parecem ser as melhores. Observe como o tamanho do buffer dinâmico pode otimizar o algoritmo: o tempo de execução é um pouco Menor para algumas linhas, devido ao buffer reduzido.

Vamos tentar um arquivo maior. E se tivermos que ler um arquivo de log de 10 MB?

Execution time on sample 10 MB log file

Agora a solução # 1 é de longe a pior: na verdade, carregar todo o arquivo de 10 MB Na memória não é uma ótima idéia. Eu corro os testes também em arquivos de 1MB e 100MB, E é praticamente a mesma situação.

E para pequenos arquivos de log? Esse é o gráfico para um arquivo de 10 KB:

Execution time on sample 10 KB log file

A solução # 1 é a melhor agora! Carregar um 10 KB na memória não é um grande problema Para o PHP. Também # 4 e # 5 apresenta bom desempenho. No entanto, este é um caso Edge: um log de 10 KB Significa algo como linhas de 150/200 ...

Você pode baixar todos os meus arquivos de teste, fontes e resultados aqui .

Pensamentos finais

A Solução # 5 é altamente recomendada para o caso de uso geral: funciona muito bem Com cada tamanho de arquivo e funciona particularmente bem ao ler algumas linhas.

Evite solução # 1 se você Ler arquivos maiores que 10 KB.

Solução # 2 E # 3 Não são os melhores para cada teste que eu corro: # 2 nunca roda em menos de 2ms e # 3 é fortemente influenciado pelo número de linhas que você pergunta (funciona muito bem apenas com 1 ou 2 linhas).

216
lorenzo-s

Esta é uma versão modificada que também pode pular as últimas linhas: 

/**
 * Modified version of http://www.geekality.net/2011/05/28/php-tail-tackling-large-files/ and of https://Gist.github.com/lorenzos/1711e81a9162320fde20
 * @author Kinga the Witch (Trans-dating.com), Torleif Berger, Lorenzo Stanco
 * @link http://stackoverflow.com/a/15025877/995958
 * @license http://creativecommons.org/licenses/by/3.0/
 */    
function tailWithSkip($filepath, $lines = 1, $skip = 0, $adaptive = true)
{
  // Open file
  $f = @fopen($filepath, "rb");
  if (@flock($f, LOCK_SH) === false) return false;
  if ($f === false) return false;

  if (!$adaptive) $buffer = 4096;
  else {
    // Sets buffer size, according to the number of lines to retrieve.
    // This gives a performance boost when reading a few lines from the file.
    $max=max($lines, $skip);
    $buffer = ($max < 2 ? 64 : ($max < 10 ? 512 : 4096));
  }

  // 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") {
    if ($skip > 0) { $skip++; $lines--; }
  } else {
    $lines--;
  }

  // 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
    $chunk = fread($f, $seek);

    // Calculate chunk parameters
    $count = substr_count($chunk, "\n");
    $strlen = mb_strlen($chunk, '8bit');

    // Move the file pointer
    fseek($f, -$strlen, SEEK_CUR);

    if ($skip > 0) { // There are some lines to skip
      if ($skip > $count) { $skip -= $count; $chunk=''; } // Chunk contains less new line symbols than
      else {
        $pos = 0;

        while ($skip > 0) {
          if ($pos > 0) $offset = $pos - $strlen - 1; // Calculate the offset - NEGATIVE position of last new line symbol
          else $offset=0; // First search (without offset)

          $pos = strrpos($chunk, "\n", $offset); // Search for last (including offset) new line symbol

          if ($pos !== false) $skip--; // Found new line symbol - skip the line
          else break; // "else break;" - Protection against infinite loop (just in case)
        }
        $chunk=substr($chunk, 0, $pos); // Truncated chunk
        $count=substr_count($chunk, "\n"); // Count new line symbols in truncated chunk
      }
    }

    if (strlen($chunk) > 0) {
      // Add chunk to the output
      $output = $chunk . $output;
      // Decrease our line counter
      $lines -= $count;
    }
  }

  // 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
  @flock($f, LOCK_UN);
  fclose($f);
  return trim($output);
}
4
Kinga the Witch

Isso também funcionaria:

$file = new SplFileObject("/path/to/file");
$file->seek(PHP_INT_MAX); // cheap trick to seek to EoF
$total_lines = $file->key(); // last line number

// output the last twenty lines
$reader = new LimitIterator($file, $total_lines - 20);
foreach ($reader as $line) {
    echo $line; // includes newlines
}

Ou sem o LimitIterator:

$file = new SplFileObject($filepath);
$file->seek(PHP_INT_MAX);
$total_lines = $file->key();
$file->seek($total_lines - 20);
while (!$file->eof()) {
    echo $file->current();
    $file->next();
}

Infelizmente, seus segfaults de teste no meu computador, por isso não posso dizer como ele funciona.

1
Gordon

Eu gosto do método a seguir, mas não funciona em arquivos de até 2 GB.

<?php
    function lastLines($file, $lines) {
        $size = filesize($file);
        $fd=fopen($file, 'r+');
        $pos = $size;
        $n=0;
        while ( $n < $lines+1 && $pos > 0) {
            fseek($fd, $pos);
            $a = fread($fd, 1);
            if ($a === "\n") {
                ++$n;
            };
            $pos--;
        }
        $ret = array();
        for ($i=0; $i<$lines; $i++) {
            array_Push($ret, fgets($fd));
        }
        return $ret;
    }
    print_r(lastLines('hola.php', 4));
?>
0
sergiotarxz

Ainda outra função, você pode usar regexes para separar itens. Uso

$last_rows_array = file_get_tail('logfile.log', 100, array(
  'regex'     => true,          // use regex
  'separator' => '#\n{2,}#',   //  separator: at least two newlines
  'typical_item_size' => 200, //   line length
));

A função:

// public domain
function file_get_tail( $file, $requested_num = 100, $args = array() ){
  // default arg values
  $regex         = true;
  $separator     = null;
  $typical_item_size = 100; // estimated size
  $more_size_mul = 1.01; // +1%
  $max_more_size = 4000;
  extract( $args );
  if( $separator === null )  $separator = $regex ? '#\n+#' : "\n";

  if( is_string( $file ))  $f = fopen( $file, 'rb');
  else if( is_resource( $file ) && in_array( get_resource_type( $file ), array('file', 'stream'), true ))
    $f = $file;
  else throw new \Exception( __METHOD__.': file must be either filename or a file or stream resource');

  // get file size
  fseek( $f, 0, SEEK_END );
  $fsize = ftell( $f );
  $fpos = $fsize;
  $bytes_read = 0;

  $all_items = array(); // array of array
  $all_item_num = 0;
  $remaining_num = $requested_num;
  $last_junk = '';

  while( true ){
    // calc size and position of next chunk to read
    $size = $remaining_num * $typical_item_size - strlen( $last_junk );
    // reading a bit more can't hurt
    $size += (int)min( $size * $more_size_mul, $max_more_size );
    if( $size < 1 )  $size = 1;

    // set and fix read position
    $fpos = $fpos - $size;
    if( $fpos < 0 ){
      $size -= -$fpos;
      $fpos = 0;
    }

    // read chunk + add junk from prev iteration
    fseek( $f, $fpos, SEEK_SET );
    $chunk = fread( $f, $size );
    if( strlen( $chunk ) !== $size )  throw new \Exception( __METHOD__.": read error?");
    $bytes_read += strlen( $chunk );
    $chunk .= $last_junk;

    // chunk -> items, with at least one element
    $items = $regex ? preg_split( $separator, $chunk ) : explode( $separator, $chunk );

    // first item is probably cut in half, use it in next iteration ("junk") instead
    // also skip very first '' item
    if( $fpos > 0 || $items[0] === ''){
      $last_junk = $items[0];
      unset( $items[0] );
    } // … else noop, because this is the last iteration

    // ignore last empty item. end( empty [] ) === false
    if( end( $items ) === '')  array_pop( $items );

    // if we got items, Push them
    $num = count( $items );
    if( $num > 0 ){
      $remaining_num -= $num;
      // if we read too much, use only needed items
      if( $remaining_num < 0 )  $items = array_slice( $items, - $remaining_num );
      // don't fix $remaining_num, we will exit anyway

      $all_items[] = array_reverse( $items );
      $all_item_num += $num;
    }

    // are we ready?
    if( $fpos === 0 || $remaining_num <= 0 )  break;

    // calculate a better estimate
    if( $all_item_num > 0 )  $typical_item_size = (int)max( 1, round( $bytes_read / $all_item_num ));
  }

  fclose( $f ); 

  //tr( $all_items );
  return call_user_func_array('array_merge', $all_items );
}
0
biziclop