【问题标题】:Get random lines from large files in bash从bash中的大文件中获取随机行
【发布时间】:2015-05-20 02:06:52
【问题描述】:

如何从无法放入内存的超大文件中获取n 随机行。

如果我可以在随机化之前或之后添加过滤器,那就太好了。


更新 1

在我的情况下,规格是:

  • >一亿行
  • > 10GB 文件
  • 通常随机批量大小 10000-30000
  • 512RAM 托管 ubuntu 服务器 14.10

所以从文件中丢失几行不会是一个大问题,因为它们有万分之一的机会,但是性能和资源消耗将是一个问题

【问题讨论】:

  • 只是为了澄清一下:从你自己的回答来看,当你说“n 行批次”时,你的意思是n 随机选择的行单独不是从随机起点开始的n连续行块
  • 是的,“批次”可能不是最好的表达方式,抱歉 :)
  • 嗯。感谢您的澄清。不幸的是,这意味着我的回答对你不起作用。我会更新我的答案,但你能告诉我你期望的n 的值吗?您想显示 >1 亿行中的多少行?另外,你想在随机化之前/之后添加什么样的“过滤器”?
  • 通常随机批次 10-30k
  • 请参阅我的回答中的更新#2。

标签: bash command-line random-sample line-processing


【解决方案1】:

在这样的限制因素下,下面的方法会更好。

  • 寻找文件中的随机位置(例如,您将在某行中处于“内部”)
  • 从这个位置往回走,找到给定行的开始
  • 继续打印整行

为此,您需要一个可以在文件中查找的工具,例如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

脚本执行非常低级的 IO 操作,即它非常快。 (在我的笔记本上,从 10M 中选择 30k 行需要半秒)。

【讨论】:

  • 伟大的解决方案确实比任何需要先计算/拆分成行的方法都快得多。只是要注意约束(对于 OP 来说不是问题):最大。行长度假定为 257 字节(易于更改),并且行可能多次被选中。只能用于您可以在其中查找的文件,例如排除标准输入和 FIFO。
  • @mklement0 - 是的,这里有限制 - 速度的价格。顺便说一句,2*256 使用$seekdiff&lt;&lt;1 - 例如256&lt;&lt;1 = 512 :)
  • Re 256&lt;&lt;1 = 512:您的 缓冲区 长度为 512 字节,但您不知道它相对于行首的位置。因此,在最坏的情况下,缓冲区中的第一个(也可能是唯一的)\n 位于字节位置 256,这意味着您可以完全返回的最长行是 256 个字节长(我最初错误地说是 257) .关于 OP 要求的最后一个想法:由于他想要前置和后置过滤器,因此您必须将前置过滤器的结果发送到临时服务器。文件并将其名称传递给您的 Perl 脚本。
  • @jm666 感谢给了我学习 Perl 的灵感 ?️ ?️ 就我而言,我的线路很大,所以我增加了seekdiff
【解决方案2】:

这里有一个小 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 行。

此解决方案的优点是管道更少、资源密集型管道更少(即没有| sort ... |)、更少的平台依赖性(即没有sort -R,这是GNU-sort-specific)。

请注意,这依赖于 Bash 的 $RANDOM 变量,它实际上可能是随机的,也可能不是随机的。此外,如果您的源文件包含超过 32768^2 行,它将错过行,并且如果您指定的行数 (N) 大于 1 并且随机开始,则会出现失败边缘情况point 距离开始小于 N 行。解决这个问题留给读者作为练习。 :)


更新 #1:

mklement0 在 cmets 中就head ... | tail ... 方法的潜在性能问题提出了一个很好的问题。老实说,我不知道答案,但我希望headtail 都经过充分优化,不会在显示输出之前缓冲所有输入。

如果我的希望没有实现,这里有一个替代方案。这是一个基于 awk 的“滑动窗口”尾巴。我会将它嵌入到我之前编写的函数中,以便您可以根据需要对其进行测试。

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 脚本替换了函数之前版本中的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 个随机数应该只占用大约 170KB 的内存,但是数组在 RAM 中的表示方式取决于您使用的 awk 的实现。 (虽然通常,awk 实现(包括 Ubuntu 中的 Gawk)非常擅长将内存浪费降至最低。)

这对你有用吗?

【讨论】:

  • 有趣的方法,但我不清楚 OP 是否真的在随机起始位置寻找 连续线块;其他答案假设每个n 行都应该单独随机选择-我已经要求OP进行澄清。除此之外:使用head ... | tail ... 方法,如果起始行接近一个非常大的文件的末尾,性能是否值得关注?
  • 好点,@mklement0。我不知道头部+尾部组合在极端情况下是否会表现出色。为了好玩,我已经用另一种方法更新了我的答案。我很想知道你的想法。
  • 重新更新 #1:对于我的测试,我必须进行两个修复:start=$c 应该是 start=$r,而 { delete out[start++]; count--; } 缺少 exit{ delete out[start++]; count--; exit } 。另外,为了使这个函数和原始函数的行为相同,我将原始函数中的 head -n $r "$1" 更改为 head -n $(( r + ${2:-1} )) "$1"
  • 感谢您的跟进。重做正面或反面(更新 #1):在我的 OSX 10.10.2 机器上,基于awk 的解决方案使用gawkmawkhead ... | tail .. 快得多。解决方案,而奇怪的是,预装的 BSD (BWK) awk 速度较慢 - 不知道为什么 BSD awk 在这里表现如此糟糕。以下是从 1 亿行文件的第 9000 万行开始按升序获取 50 行的时间(对它们持保留态度,这不是一个控制良好的实验):mawk:0m8。 069s;呆呆:0m12.353s;头+尾:1m1.910s; BSD awk:1m28.559s
  • 重新更新 #2:问题是您仍在提取 input order 中的行;例如,如果您的输入是按字母顺序列出的单词列表,那么您的输出也将按字母顺序排序。要解决此问题,您必须缓存输出行并在 END 块中以“文件​​ 1”顺序对其进行迭代。此外,一旦达到提取计数,退出主循环是值得的。在 awk 内部的 BEGIN 块中(使用 rand())而不是在 bash 中构建随机索引数组也会加快速度。
【解决方案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 脚本中

#!/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//'

【讨论】:

  • 值得一提的是,-R 是 POSIX 标准的扩展。 GNU 和 BSD 排序的最新版本都实现了它,但遗憾的是,OSX 停留在一个不支持它的古老 GNU 排序版本(OSX 10.10 的 v5.93)上。
  • @mklement0 - 是的,你是对的!我也在优胜美地,使用 GNU utils,由macports 安装。
  • 它工作正常;试试printf ' 1\t\ttwo\t\tthree' | cut -f2- | cat -t
  • 当没有足够的 RAM(比较哈希值)时,sort -R 如何工作?它进入交换吗?同样在我回答之前,我尝试了shuf,但它失败并出现“内存不足”通知
  • 按块排序并在需要时使用文件临时文件 - 不需要将整个数据集加载到内存中。它会非常慢。在这种情况下,(512RAM)也许这不是最好的方法。 (取决于:1.)您需要多少次随机行 2.)您每次需要多少行 3.)文件内容多久更改一次)。
【解决方案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]"

没有麦克和简

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

【讨论】:

  • 在 [罕见] 情况下,我相信这可以返回少于请求的行数。
  • 是的,我正要指定 :) 它也可能与开头和结尾的行不匹配。无论如何都不错
  • | grep -o '^[0-9]\+' 不是必需的,并且会在类似 BSD 的平台上中断,例如,wc -l 输出前导空格。
  • 通过过滤器运行文件两次可能会使这个过程变得很慢。这通常返回的行数少于请求的行数(正如您所说,您可以接受)。您必须通过 -v 选项将 shell 变量 $chances 的值传递给 awk - 您不能直接在单引号 awk 脚本中引用 shell 变量(并且使用双引号字符串不是一个好主意)。跨度>
【解决方案5】:

我使用rl进行线随机化,发现它的表现相当不错。不确定它如何适应您的情况(您只需这样做,例如rl FILE | head -n NUM)。你可以在这里得到它:http://arthurdejong.org/rl/

【讨论】:

  • 有趣,但请注意 rl 页面本身现在建议使用 shuf 代替,并且 OP 在评论中表示他使用 shuf 内存不足。
猜你喜欢
  • 1970-01-01
  • 2015-11-26
  • 2015-01-06
  • 2012-06-04
  • 1970-01-01
  • 2010-10-28
  • 2017-08-03
  • 1970-01-01
相关资源
最近更新 更多