Получите случайные строки из больших файлов в bash

Как я могу получить n случайные строки из очень больших файлов, которые не могут вписаться в память.

Также было бы здорово, если бы я мог добавлять фильтры до или после рандомизации.


update 1

в моем случае спецификации:

  • > 100 миллионов строк
  • > 10GB файлы
  • обычный случайный размер партии 10000-30000
  • 512RAM размещен на сервере ubuntu 14.10

поэтому потеря нескольких строк из файла не будет такой большой проблемой, поскольку в любом случае у них есть шанс 1 на 10000, но проблема с производительностью и ресурсами будет проблемой

Ответ 1

Здесь функция wee bash для вас. Он захватывает, как вы говорите, "пакет" строк со случайной начальной точкой внутри файла.

randline() {
  local lines c r _

  # cache the number of lines in this file in a symlink in the temp dir
  lines="/tmp/${1//\//-}.lines"
  if [ -h "$lines" ] && [ "$lines" -nt "${1}" ]; then
    c=$(ls -l "$lines" | sed 's/.* //')
  else
    read c _ < <(wc -l $1)
    ln -sfn "$c" "$lines"
  fi

  # Pick a random number...
  r=$[ $c * ($RANDOM * 32768 + $RANDOM) / (32768 * 32768) ]
  echo "start=$r" >&2

  # And start displaying $2 lines before that number.
  head -n $r "$1" | tail -n ${2:-1}
}

При необходимости отредактируйте строки echo.

Это решение имеет то преимущество, что меньше труб, менее ресурсоемких каналов (т.е. no | sort ... |), меньше зависимости от платформы (т.е. no sort -R, которая является специфичной для GNU).

Обратите внимание, что это зависит от переменной bash $RANDOM, которая может или не может быть случайной. Кроме того, он будет пропускать строки, если ваш исходный файл содержит более 32768 ^ 2 строки, и есть случай края сбоя, если количество строк, которые вы определили (N), равно > 1, а случайная начальная точка меньше, чем N строк из начало. Решение, которое остается для упражнения читателем.:)


ОБНОВЛЕНИЕ # 1:

mklement0 задает отличный вопрос в комментариях о потенциальных проблемах производительности с подходом head ... | tail .... Я честно не знаю ответа, но я надеюсь, что оба head и tail будут оптимизированы настолько, что они не будут буферизировать ВСЕ входные данные до отображения их вывода.

В случае, если моя надежда не будет выполнена, вот альтернатива. Это awk-based "скользящее окно". Я вложу его в более раннюю функцию, которую я написал, чтобы вы могли проверить ее, если хотите.

randline() {
  local lines c r _

  # Line count cache, per the first version of this function...
  lines="/tmp/${1//\//-}.lines"
  if [ -h "$lines" ] && [ "$lines" -nt "${1}" ]; then
    c=$(ls -l "$lines" | sed 's/.* //')
  else
    read c _ < <(wc -l $1)
    ln -sfn "$c" "$lines"
  fi

  r=$[ $c * ($RANDOM * 32768 + $RANDOM) / (32768 * 32768) ]

  echo "start=$r" >&2

  # This simply pipes the functionality of the `head | tail` combo above
  # through a single invocation of awk.
  # It should handle any size of input file with the same load/impact.
  awk -v lines=${2:-1} -v count=0 -v start=$r '
    NR < start { next; }
    { out[NR]=$0; count++; }
    count > lines { delete out[start++]; count--; }
    END {
      for(i=start;i<start+lines;i++) {
        print out[i];
      }
    }
  ' "$1"
}

Вложенный awk script заменяет конвейер head ... | tail ... в предыдущей версии функции. Он работает следующим образом:

  • Он пропускает строки до "начала", как определено ранней рандомизацией.
  • Он записывает текущую строку в массив.
  • Если массив больше числа строк, которые мы хотим сохранить, он исключает первую запись.
  • В конце файла он печатает записанные данные.

В результате процесс awk не должен увеличивать объем памяти, потому что выходной массив обрезается так быстро, как только он был создан.

ПРИМЕЧАНИЕ. Я не проверял это с вашими данными.


ОБНОВЛЕНИЕ # 2:

Hrm, с обновлением вашего вопроса, что вам нужны N случайных строк, а не блок строк, начинающихся с произвольной точки, нам нужна другая стратегия. Системные ограничения, которые вы наложили, довольно серьезны. Следующим может быть опция, также использующая awk со случайными номерами, все еще из Bash:

randlines() {
  local lines c r _

  # Line count cache...
  lines="/tmp/${1//\//-}.lines"
  if [ -h "$lines" ] && [ "$lines" -nt "${1}" ]; then
    c=$(ls -l "$lines" | sed 's/.* //')
  else
    read c _ < <(wc -l $1)
    ln -sfn "$c" "$lines"
  fi

  # Create a LIST of random numbers, from 1 to the size of the file ($c)
  for (( i=0; i<$2; i++ )); do
    echo $[ $c * ($RANDOM * 32768 + $RANDOM) / (32768 * 32768) + 1 ]
  done | awk '
    # And here inside awk, build an array of those random numbers, and
    NR==FNR { lines[$1]; next; }
    # display lines from the input file that match the numbers.
    FNR in lines
  ' - "$1"
}

Это работает путем подачи списка случайных номеров строк в awk как "первый" файл, а затем с awk печатайте строки из "второго" файла, номера строк которого были включены в "первый" файл. Он использует wc для определения верхнего предела генерируемых случайных чисел. Это означает, что вы будете читать этот файл дважды. Если у вас есть другой источник для количества строк в файле (например, базы данных), включите его здесь.:)

Ограничивающим фактором может быть размер этого первого файла, который должен быть загружен в память. Я считаю, что случайные числа 30000 должны принимать только около 170 Кбайт памяти, но как массив представлен в RAM, зависит от реализации awk, который вы используете. (Хотя обычно реализация awk (включая Gawk в Ubuntu) довольно хороша в том, чтобы свести потери памяти к минимуму.)

Это работает для вас?

Ответ 2

В таких ограничивающих факторах будет лучше использовать следующий подход.

  • искать случайную позицию в файле (например, вы будете "внутри" в некоторой строке)
  • перейдите от этой позиции и найдите начало данной строки
  • перейти вперед и распечатать полную строку

Для этого вам нужен инструмент, который можно искать в файлах, например perl.

use strict;
use warnings;
use Symbol;
use Fcntl qw( :seek O_RDONLY ) ;
my $seekdiff = 256; #e.g. from "rand_position-256" up to rand_positon+256

my($want, $filename) = @ARGV;

my $fd = gensym ;
sysopen($fd, $filename, O_RDONLY ) || die("Can't open $filename: $!");
binmode $fd;
my $endpos = sysseek( $fd, 0, SEEK_END ) or die("Can't seek: $!");

my $buffer;
my $cnt;
while($want > $cnt++) {
    my $randpos = int(rand($endpos));   #random file position
    my $seekpos = $randpos - $seekdiff; #start read here ($seekdiff chars before)
    $seekpos = 0 if( $seekpos < 0 );

    sysseek($fd, $seekpos, SEEK_SET);   #seek to position
    my $in_count = sysread($fd, $buffer, $seekdiff<<1); #read 2*seekdiff characters

    my $rand_in_buff = ($randpos - $seekpos)-1; #the random positon in the buffer

    my $linestart = rindex($buffer, "\n", $rand_in_buff) + 1; #find the begining of the line in the buffer
    my $lineend = index $buffer, "\n", $linestart;            #find the end of line in the buffer
    my $the_line = substr $buffer, $linestart, $lineend < 0 ? 0 : $lineend-$linestart;

    print "$the_line\n";
}

Сохраните приведенное выше в каком-либо файле такое "randlines.pl" и используйте его как:

perl randlines.pl wanted_count_of_lines file_name

например.

perl randlines.pl 10000 ./BIGFILE

script выполняет очень низкоуровневые операции ввода-вывода, то есть ОЧЕНЬ БЫСТРЫЙ. (на моем ноутбуке, выбрав 30k строк из 10M заняла полсекунды).

Ответ 3

Простое (но медленное) решение

n=15 #number of random lines
filter_before | sort -R | head -$n | filter_after

#or, if you could have duplicate lines
filter_before | nl | sort -R | cut -f2- | head -$n | filter_after
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

или, если хотите, сохраните следующее в randlines script

#!/bin/bash
nl | sort -R | cut -f2 | head -"${1:-10}"

и используйте его как:

filter_before | randlines 55 | filter_after   #for 55 lines

Как это работает:

sort -R сортирует файл по вычисленным случайным хэшам для каждой строки, поэтому вы получите рандомизированный порядок строк, поэтому первые N строк являются случайными строками.

Поскольку хеширование создает один и тот же хэш для одной и той же строки, повторяющиеся строки не рассматриваются как разные. Возможно исключить дублирующиеся строки, добавляющие номер строки (с помощью nl), поэтому сортировка никогда не получит точный дубликат. После sort удаления добавленных номеров строк.

Пример:

seq -f 'some line %g' 500 | nl | sort -R | cut -f2- | head -3

печатает в последующих прогонах:

some line 65
some line 420
some line 290

some line 470
some line 226
some line 132

some line 433
some line 424
some line 196

демо с повторяющимися строками:

yes 'one
two' | head -10 | nl | sort -R | cut -f2- | head -3

в последующих тиражах печатает:

one
two
two

one
two
one

one
one
two

Наконец, если вы хотите, можете использовать вместо cut sed тоже:

sed -r 's/^\s*[0-9][0-9]*\t//'

Ответ 4

#!/bin/bash
#contents of bashScript.sh

file="$1";
lineCnt=$2;
filter="$3";
nfilter="$4";
echo "getting $lineCnt lines from $file matching '$filter' and not matching '$nfilter'" 1>&2;

totalLineCnt=$(cat "$file" | grep "$filter" | grep -v "$nfilter" | wc -l | grep -o '^[0-9]\+');
echo "filtered count : $totalLineCnt" 1>&2;

chances=$( echo "$lineCnt/$totalLineCnt" | bc -l );
echo "chances : $chances" 1>&2;

cat "$file" | awk 'BEGIN { srand() } rand() <= $chances { print; }' | grep "$filter" | grep -v "$nfilter" | head -"$lineCnt";

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

получить 1000 случайных образцов

bashScript.sh /path/to/largefile.txt 1000  

строка имеет номера

bashScript.sh /path/to/largefile.txt 1000 "[0-9]"

no mike и jane

bashScript.sh /path/to/largefile.txt 1000 "[0-9]" "mike|jane"

Ответ 5

Я использовал rl для случайной случайности и нашел, что он работает достаточно хорошо. Не знаете, как он масштабируется в вашем случае (вы просто делаете, например, rl FILE | head -n NUM). Вы можете получить его здесь: http://arthurdejong.org/rl/