从Unix中随机选择文件中的行而不将其全部读入

53

我有一个包含1000万行的文件,我想从中随机选择1/100行。这是我编写的AWK代码,但它会先将整个文件内容读入内存中。我的电脑内存无法处理这么大的文件。有其他方法可以实现吗?

awk 'BEGIN{srand()}
!/^$/{ a[c++]=$0}
END {  
  for ( i=1;i<=c ;i++ )  { 
    num=int(rand() * c)
    if ( a[num] ) {
        print a[num]
        delete a[num]
        d++
    }
    if ( d == c/100 ) break
  }
 }' file

FYI,1000个样本就可以获得+/-3%的误差范围,而1万个样本则可以获得+/-1%的误差范围。1000万的样本量太大了。 - Steven Huwig
@Steven:非常感谢您的帮助。您是如何得出上面的MOE数字的?有什么参考资料吗?我的统计背景比较薄弱,我真的想学习更多知识。顺便说一句,您的观点似乎与下面的cadrian不同(即1%不够)。 - neversaint
1
http://www.americanresearchgroup.com/moe.html - Steven Huwig
@StevenHuwig 链接似乎已经离线。缓存:https://web.archive.org/web/20131104235055/http://www.americanresearchgroup.com/moe.html 维基百科:https://en.wikipedia.org/wiki/Margin_of_error - exic
11个回答

91

如果你有那么多行的话,你确定你需要恰好1%吗?还是统计估计就足够了呢?

在第二种情况下,只需在每一行随机到1%即可...

awk 'BEGIN {srand()} !/^$/ { if (rand() <= .01) print $0}'

如果您想要标题行以及随机抽样的若干行,请使用以下代码:

awk 'BEGIN {srand()} !/^$/ { if (rand() <= .01 || FNR==1) print $0}'

10
从理论上讲是可能的,但实际上不太可能。对于发帖者所说的一千万行文件(由泊松分布给出),没有任何一行被打印出来的概率约为10^-43430。相当于在第一次尝试中猜解一个144 千字节 的加密密钥。 - David Z
3
对于原帖的情况(10^7行),选择的元素数量有99.8%的概率在10,000的1%范围内。 - David Z
4
好的观点,但是一个正确的“算法”需要是正确的,而不是“极有可能是正确的”。在我看来是这样。 - Steven Huwig
2
请注意,该算法会对较长的行产生偏差。如果您想纠正这一点,您必须加权采样(例如,对长度为2倍的行进行1/2的采样)。如何准确地执行此操作留给读者作为练习(提示:构建行长度的经验分布)。 - medriscoll
9
@Steven:不是需要算法完全正确,而是需要足够正确。在这种情况下,它已经足够正确了。 - Reid
显示剩余5条评论

59

你使用了awk,但我不知道是否必需。如果不是必需的话,这里有一种使用Perl的简单方法(而无需将整个文件加载到内存中):

cat your_file.txt | perl -n -e 'print if (rand() < .01)'

(从评论中提取的更简单形式):

perl -ne 'print if (rand() < .01)' your_file.txt 

理论上讲,它可能不会打印任何值。 - Steven Huwig
请查看我对cadrian答案的评论。 - David Z
18
@Steven,请阅读原帖。他的文件有10^7行,他不输出的概率是0.99^100000000。认为这样说是不可接受的是荒谬的。如果你担心这种级别的错误,那么你就不应该使用计算机。由于宇宙射线的影响,你更有可能得到错误的输出。 - Bill
好的,现在 那就是 合法的了。 :-) - Bill
3
这个版本(第二个例子)速度飞快。我刚刚在SSD上运行了一个15GB的文件,很快就完成了。最终得到了一个150MB的文件。不错。 - markwatson
相比之下,上面的awk版本非常慢。对于从谷歌搜索到这个答案的下一个随机人:首先尝试这个答案! - yshavit

21

我在Gawk中编写了这段完全相同的代码,你很幸运。它的长度部分是因为它保留了输入顺序。可能可以进行性能优化。

这个算法在不知道输入大小的情况下也是正确的。我在这里发布了一篇罗塞塔石头关于它的文章。(我没有发布这个版本,因为它进行了不必要的比较。)

原始线程:提交供您审查 - 在awk中进行随机抽样。

# Waterman's Algorithm R for random sampling
# by way of Knuth's The Art of Computer Programming, volume 2

BEGIN {
    if (!n) {
        print "Usage: sample.awk -v n=[size]"
        exit
    }
    t = n
    srand()

}

NR <= n {
    pool[NR] = $0
    places[NR] = NR
    next

}

NR > n {
    t++
    M = int(rand()*t) + 1
    if (M <= n) {
        READ_NEXT_RECORD(M)
    }

}

END {
    if (NR < n) {
        print "sample.awk: Not enough records for sample" \
            > "/dev/stderr"
        exit
    }
    # gawk needs a numeric sort function
    # since it doesn't have one, zero-pad and sort alphabetically
    pad = length(NR)
    for (i in pool) {
        new_index = sprintf("%0" pad "d", i)
        newpool[new_index] = pool[i]
    }
    x = asorti(newpool, ordered)
    for (i = 1; i <= x; i++)
        print newpool[ordered[i]]

}

function READ_NEXT_RECORD(idx) {
    rec = places[idx]
    delete pool[rec]
    pool[NR] = $0
    places[idx] = NR  
} 

这是“随机选择n行”而不是“随机选择1/100行”,因此需要进行一些预计算。尽管如此,仍然很酷。 - ephemient
是的,虽然我在评论中提到“随机选择n行”更好--样本大小与人口大小不成正比。 - Steven Huwig
1
这里是Python中的水塘抽样算法实现(未保留顺序)。注意:这里的try/except不会为你带来任何好处:当i>>k(注意:第i个项目被选择的概率为k/n,其中n是总项目数)时,替换发生在第i个项目上的概率为k/i - jfs
这是一个很棒的脚本,我只需要在第一行添加 #!gawk -f 就可以让它在我的系统上运行(并且使用 brew install gawk 安装 gawk 因为我使用的是 Mac)。我只有一个问题:这真的是蓄水池抽样吗? - Jeremy Iglehart
1
@JeremyIglehart 是的,这里的水库被称为“pool”。由于awk中的动态关联数组与其他语言中更静态的数组不同,因此它与原始材料中所设想的并不完全相同。但是,由于水库抽样的重要特征是“在线”而不是具有特定的算法复杂度,我认为它仍然可以算作水库抽样。我的附加要求是同时在同一集合上运行两个算法,以保留顺序,这并不会使水库抽样部分不再成为水库抽样部分。 - Steven Huwig

17

这应该在大多数GNU / Linux机器上都可以工作。

$ shuf -n $(( $(wc -l < $file) / 100)) $file

如果GNU shuf命令的内存管理被不当地执行,我会感到惊讶。


8
如果您查看源代码,您会发现shuf确实首先将整个文件读入内存中,但很不幸。 - Louis Marascio
2
wc -l < $file 不会输出文件名,因此您可以省略 cut 命令。 - Dennis Williamson
这很好,但非常低效,因为我们只需要一个随机子集,而不是一个随机顺序的子集。 - petrelharp

5
如何从一个未知大小的大群体中均匀地抽取N个元素的问题被称为水塘抽样(Reservoir Sampling)。(如果你喜欢算法问题,可以花几分钟时间尝试在不阅读维基百科上的算法的情况下解决它。)
在网上搜索“水塘抽样”会找到很多实现方法。这里是实现你想要的Perl和Python代码,这里还有另一个Stack Overflow讨论帖子。

5

我不懂awk,但是有一种很好的技术可以解决你描述的问题的更一般版本,在一般情况下,它比使用for line in file return line if rand < 0.01方法要快得多,因此如果您打算执行上述任务多次(数千次,数百万次),它可能会非常有用。这被称为蓄水池抽样此页面对其适用于您情况的一个版本进行了相当好的解释。



5
在这种情况下,使用水库抽样来准确获得k个值,使用awk非常容易,我很惊讶还没有解决方案建议这一点。我不得不解决相同的问题,并编写了以下awk程序进行抽样:
#!/usr/bin/env awk -f
BEGIN{
    srand();
    if(k=="") k=10
}

NR <= k {
    reservoir[NR-1] = $0;
    next;
}

{ i = int(NR * rand()) }

i < k { reservoir[i] = $0 }

END {
    for (i in reservoir) {
        print reservoir[i];
    }
}

如果将其保存为sample_lines并使其可执行,则可以像这样运行:./sample_lines -v k=5 input_file。如果没有给出k,则默认使用10。
然后需要单独确定k的值,例如通过设置-v "k=$(dc -e "$(cat input_file | wc -l) 100 / n")"来实现。

3
你可以分两步完成它:
  • 第一遍遍历文件,只是为了计算有多少行
  • 随机选择要打印的行号,将它们存储在一个排序列表(或集合)中
  • 再次遍历文件并挑选出所选位置的行

Python示例:

fn = '/usr/share/dict/words'

from random import randint
from sys import stdout

count = 0
with open(fn) as f:
   for line in f:
      count += 1

selected = set()
while len(selected) < count//100:
   selected.add(randint(0, count-1))

index = 0
with open(fn) as f:
   for line in f:
      if index in selected:
          stdout.write(line)
      index += 1

1
如果目的只是为了避免内存耗尽,并且文件是常规文件,则无需实现蓄水池抽样。如果您对文件进行两次传递,一次获取行数(例如使用wc -l),一次选择样本,则可以知道文件中的行数:
file=/some/file
awk -v percent=0.01 -v n="$(wc -l < "$file")" '
  BEGIN {srand(); p = int(n * percent)}
  rand() * n-- < p {p--; print}' < "$file"

1

不要等到最后再随机选择你的1%行,而是在“/^ $ /”中的每100行执行一次。这样,你每次只需要保留100行。


这会导致随机行的不同分布。例如,您永远不会从相同的100行集合中获得两个。 - derobert
这也会影响顺序。你永远不会在第二组的一行之前看到第三组的一行。这是非常重要的考虑因素。 - Travis Jensen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接