Наиболее эффективный способ чтения первого и последнего числа байтов файла в Perl

#perl

#perl

Вопрос:

Каким был бы наиболее эффективный способ чтения начала и конца огромного файла (двоичного или текстового) в заданном количестве байтов?

Пример:

 
=head2 read_file_contents(file, limit)

Given a filename, returns its partial content in bytes, with number of truncated bytes

=cut
sub read_file_contents
{
    my ($file, $limit) = @_;
    my $rv;

    # Starting and ending number of bytes to read
    $limit = $limit / 2;

    # Reading beginning of file
    my $start;

    # code goes here

    # Reading end of a file
    my $end;

    # code goes here

    $rv = $start . "nnn truncated N bytes of data nnn" . $end;

    return $rv;
}
 

Основная цель — иметь возможность быстро, без обработки всего файла, эффективно извлекать его начальные и конечные байты. Не проблема прочитать весь файл, а затем substr выполнить его нужным способом, но он не будет нормально работать с файлами размером 10 ГБ .

Любые решения будут оценены.

Комментарии:

1. Примечание: ни у StackOverflow, ни у других сообществ не было четких ответов на знакомые вопросы, поэтому речь идет скорее о создании хорошего ответа, а не просто о поиске решения для себя.

Ответ №1:

 open(my $fh, "<", $file) or die "...";
my $r = read($fh, $start, $limit) or die "...";
die "short readn" unless $r == $limit;
seek($fh, -$limit, 2) or die "...";
$r = read($fh, $end, $limit) or die "...";
 

Комментарии:

1. Это был бы отличный ответ, если бы он включал краткое объяснение и, возможно, ссылку на perldoc.

2. use Fcntl qw(SEEK_END) тогда seek($fh, -$limit, SEEK_END) было бы лучше, чем жесткое кодирование 2 там

3. Спасибо, @DaveMitchell. Ни у StackOverflow, ни у других сообществ не было четких ответов на знакомые вопросы, поэтому речь идет скорее о создании хорошего ответа, а не просто о поиске решения для себя. Как насчет подсчета количества усеченных байтов? Есть ли лучший способ, а не вычитание total-limit ? Не могли бы вы, пожалуйста, уточнить это до более удобного форматированного / объясненного ответа?

4. Downvoe из-за магических чисел. Был бы рад перейти на upvote, когда это будет исправлено.

Ответ №2:

Спасибо @DaveMitchell за понимание. Спасибо @ikegami за полезные советы. Это то, к чему я в итоге пришел.

Это может быть полезно для хвостирования журналов (возврата обратных выходных данных) или эффективного предварительного просмотра файлов любого размера.

Пример:

 use Fcntl qw(SEEK_END);

=head2 read_file_contents_limit(file, limit, [opts])

Given a filename, returns its partial content with limit in bytes,
by default collected from both beginning and end of the file 
* Options is a hash reference with
  - [head]      : Head the file only and just return beginning bytes
  - [tail]      : Tail the file only and return ending bytes
  - [reverse]   : Reverse output
  - [nomessage] : Remove truncated message

=cut
sub read_file_contents_limit
{
    my ($file, $limit, $opts) = @_;
    my $data;
    my $reverse = sub {
        return join("n", reverse split("n", $_[0]));
    };
    my $nonulls = sub {
        $_[0] =~ s/[^[:print:]nrt]/ /g;
        return $_[0];
    };

    # Is binary file
    my $binary = -B $file;

    # Open file
    open(my $fh, "<", $file) || return undef;
    binmode $fh if ($binary);

    # Get file size
    my $fsize = -s $file;

    # Return full file if requested limit fits the size
    if ($fsize <= $limit) {
        my $full;
        read($fh, $full, $fsize);
        $full = amp;$nonulls($full)
          if ($binary);
        $full = amp;$reverse($full)
          if ($opts->{'reverse'});
        return $full;
    }

    # Starting and ending number of bytes to read
    my $split = !$opts->{'head'} amp;amp; !$opts->{'tail'};
    $limit = $limit / 2 if ($split);

    # Create truncated message
    my $truncated = $fsize - $limit;
    $truncated -= $limit if ($split);
    $truncated = "nnn[--- truncated ${truncated} bytes of data ---]nnn";
    $truncated = undef if ($opts->{'nomessage'});

    # Reading beginning of file
    my $head;
    read($fh, $head, $limit);

    # Return beginning only if requested
    if ($opts->{'head'}) {
        $head = amp;$nonulls($head)
          if ($binary);
        $head = amp;$reverse($head)
          if ($opts->{'reverse'});
        return $head . $truncated;
    }

    # Reading end of file
    my $tail;
    seek($fh, -$limit, SEEK_END);
    read($fh, $tail, $limit);

    # Return ending only if requested
    if ($opts->{'tail'}) {
        $tail = amp;$nonulls($tail)
          if ($binary);
        $tail = amp;$reverse($tail)
          if ($opts->{'reverse'});
        return $truncated . $tail;
    }

    # Return combined data
    $data = $head . $truncated . $tail;

    # Remove nulls for binary
    $data = amp;$nonulls($data)
      if ($binary);

    # Reverse output if needed
    $data = amp;$reverse($data)
      if ($opts->{'reverse'});
    return $data;
}
 

Пример того, как это можно использовать для хвостового файла журнала и отображения последних строк журнала вверху.

Использование:

 say read_file_contents_limit('/var/webmin/miniserv.log', 2000, {'tail', 1, 'reverse', 1});
 

Вывод:

 [--- truncated 1092091 bytes of data ---]


10.211.55.2 - root [14/Dec/2020:16:47:37  0000] "GET /favicon.ico HTTP/1.1" 200 15086
10.211.55.2 - root [14/Dec/2020:16:47:37  0000] "GET /debug.cgi HTTP/1.1" 200 3662
10.211.55.2 - root [14/Dec/2020:16:47:36  0000] "GET /favicon.ico HTTP/1.1" 200 15086
10.211.55.2 - root [14/Dec/2020:16:47:36  0000] "GET /debug.cgi HTTP/1.1" 200 3662
10.211.55.2 - root [14/Dec/2020:16:47:35  0000] "GET /favicon.ico HTTP/1.1" 200 15086
10.211.55.2 - root [14/Dec/2020:16:47:35  0000] "GET /debug.cgi HTTP/1.1" 200 3662
10.211.55.2 - root [14/Dec/2020:16:47:34  0000] "GET /favicon.ico HTTP/1.1" 200 15086
10.211.55.2 - root [14/Dec/2020:16:47:34  0000] "GET /debug.cgi HTTP/1.1" 200 3662
10.211.55.2 - root [14/Dec/2020:16:47:30  0000] "GET /favicon.ico HTTP/1.1" 200 15086
10.211.55.2 - root [14/Dec/2020:16:47:30  0000] "GET /debug.cgi HTTP/1.1" 200 3662
10.211.55.2 - root [14/Dec/2020:16:47:24  0000] "GET /favicon.ico HTTP/1.1" 200 15086
 

Комментарии:

1. Совет: следует использовать open(my $fh, «<:raw», $file)` для двоичных файлов

2. Совет: используйте use Fcntl qw(SEEK_END); и seek($fh, -$limit, SEEK_END) вместо использования магических чисел.

3. Спасибо, @ikegami! Однако :raw требуется только для Windows и ничего не делает в Linux? Кроме того, почему бы не использовать magic number? Предполагается ли, что оно изменено?

4. Re raw: Это может иметь значение и в unix, и в каждой из моих программ. С таким же успехом можно привыкнуть делать это правильно. По крайней мере, это сигнализирует читателю о ваших намерениях. /// Re магические числа, читаемый код. Самое главное при программировании. Ваш код будет изменен, поддержан, отлажен, предоставлен общий доступ и т. Д. И для этого нужно уметь его читать. В общем, они часто могут меняться и могут отличаться в разных системах. (Я не думаю, что это относится конкретно к ним, но …?)

5. @ikegami — я собираюсь отредактировать свой ответ. Быстрый вопрос, хотя, говоря о use Fcntl qw(SEEK_END); — его следует использовать вне подраздела, в верхней части файла? Было бы нормально использовать его внутри вложенного файла, или это не имело бы смысла, поскольку use в любом случае вызывалось во время компиляции, или оно было бы ограничено выполнением вложенного файла, если оно помещено внутрь? Если он вызывается во время компиляции и не может быть использован внутри подпрограммы, можно ли использовать eval "use Fcntl qw(SEEK_END);" его и поместить его внутри подпрограммы? Что говорит об этом теория и лучшая практика?

Ответ №3:

Проверьте размер файла, затем выполните поиск ближе к концу…

https://perldoc.perl.org/functions/seek

Комментарии:

1. Спасибо — это скорее комментарий, а не ответ.

2. Нет, это ответ. Плохой, но он отвечает на вопрос.