【问题标题】:Randomly Pick Lines From a File Without Slurping It With Unix从文件中随机选择行而不用 Unix 啜饮它
【发布时间】:2010-10-16 02:13:19
【问题描述】:

我有一个 10^7 行的文件,我想在其中随机选择 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

【问题讨论】:

标签: linux unix awk random-sample file-processing


【解决方案1】:

在这种情况下,使用 awk 获取准确 k 值的水库采样是微不足道的,我很惊讶没有任何解决方案建议这样做。我不得不解决同样的问题,我编写了以下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")"

【讨论】:

    【解决方案2】:

    如果目的只是避免内存耗尽,并且文件是常规文件,则无需执行存储库采样。如果您在文件中执行两次传递,则可以知道文件中的行数,一次获取行数(如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"
    

    【讨论】:

      【解决方案3】:

      如何从大量(未知大小)中均匀采样 N 个元素的问题被称为Reservoir Sampling。 (如果您喜欢算法问题,请不要阅读 Wikipedia 上的算法,花几分钟尝试解决它。​​)

      在网络上搜索“Reservoir Sampling”会发现很多实现。 Here 是实现您想要的 Perl 和 Python 代码,here 是另一个讨论它的 Stack Overflow 线程。

      【讨论】:

        【解决方案4】:

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

        $ shuf -n $(( $(wc -l < $file) / 100)) $file
        
        如果通过GNU SHUF命令不恰当地完成内存管理,我会感到惊讶。

        【讨论】:

        • 如果您查看源您将看到shuf确实将整个文件读入内存,不幸的是。 span>
        • wc -l &lt; $file not输出文件名,以便省略cut命令。 span>
        • 这很好,但非常效率低,因为我们只需要一个随机子集,而不是随机顺序的子集。 span>
        【解决方案5】:

        如果您有那么多行,您确定要正好 1% 还是统计估计就足够了?

        在第二种情况下,只需在每行随机化 1%...

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

        如果您想要标题行加上后面的随机行样本,请使用:

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

        【讨论】:

        • @Steven:理论上是的,实际上不是。对于 OP 的 1000 万行文件(由泊松分布给出),不打印行的概率约为 10^-43430。相当于在第一次尝试时通过猜测来破解 144 千字节的加密密钥。
        • 另外,对于 OP 的情况(10^7 行),所选择的元素数量有 99.8% 的可能性在 10,000 的 1% 以内。
        • 好点,但是正确的算法需要是正确的,而不是“极有可能是正确的”。无论如何,恕我直言。
        • 请注意,此算法会偏向较长的行。如果您想对此进行纠正,则必须对采样进行加权(例如,采样线的长度通常是 1/2 的 2 倍)。具体如何做到这一点留给读者练习(提示:建立行长的经验分布)。
        • @Steven:不。算法需要正确足够。在这种情况下,它是正确的。
        【解决方案6】:

        我不知道 awk,但是有一种很棒的技术可以解决您所描述的问题的更一般版本,并且在一般情况下它比for line in file return line if rand 方法,因此如果您打算多次(数千、数百万)次执行上述任务,它可能会很有用。它被称为reservoir samplingthis page 对它的适用于您的情况的版本有很好的解释。

        【解决方案7】:

        您使用了 awk,但我不知道它是否需要。如果不是,这是使用 perl 的一种简单方法(并且无需将整个文件加载到内存中):

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

        (更简单的形式,来自 cmets):

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

        【讨论】:

        • 理论上可以不打印任何值。
        • 在 cadrian 的回答中查看我的 cmets
        • @Steven,阅读原文。他的文件有 10^7 行。他没有得到输出的几率是 0.99^100000000。说不能接受是荒谬的。如果您担心这种级别的错误,您不应该使用计算机。由于 comsic 射线,您更有可能得到不正确的输出。
        • 好的,现在这是合法的。 :-)
        • 这个版本(第二个例子)非常快。我很快就在 SSD 上运行了一个 15GB 的文件。得到一个 150MB 的文件。不错。
        【解决方案8】:

        我在 Gawk 中编写了这个确切的代码——你很幸运。它很长,部分原因是它保留了输入顺序。可能会有一些性能增强。

        在事先不知道输入大小的情况下,这个算法是正确的。我在这里发布了rosetta stone。 (我没有发布这个版本,因为它做了不必要的比较。)

        原帖:Submitted for your review -- random sampling in 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行”,所以需要一点预计算。不过还是很酷。
        • 是的,虽然我评论了“随机选择 n 行”更好的问题——样本大小与总体大小不成正比。
        • 这里是Reservoir Sampling algorithm implementation in Python (without preserving order)。注意:try/except here 不会给你买任何东西:i-th 项目发生 替换 的概率是 k / i 其中i >> k(注意:概率i-th item is chosen is k / n where n is the total number of items.
        • 这是一个很棒的脚本,我所做的只是将#!gawk -f 添加为第一行以使其在我的系统上运行(并且gawk brew install gawk 因为我有一个mac)我只有一个问题:Is this truly reservoir sampling?
        • @JeremyIglehart 是的,这里的水库称为pool。它与原始材料中的设想并不完全相同,因为 awk 中的动态关联数组与其他语言中的更多静态数组相比,但由于储层采样的重要特征是“在线”而不是具有特定的算法复杂性,我认为它仍然很重要。我对保留顺序的附加要求只是在同一个集合上同时运行两种算法,这不会将水库采样部分更改为不是水库采样部分。
        【解决方案9】:

        你可以分两次完成:

        • 遍历文件一次,只是为了计算有多少行
        • 随机选择要打印的行的行号,将它们存储在排序列表(或集合)中
        • 再次遍历文件并挑选出选定位置的行

        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
        

        【讨论】:

          【解决方案10】:

          与其等到最后随机选择 1% 的行,不如在“/^$/”中每 100 行执行一次。这样,您一次只能保存 100 行。

          【讨论】:

          • 这会导致随机行的不同分布。例如,你永远不会有两个来自同一个 100 行集合。
          • 也会影响订单。你永远不会在第二组的一行之前有第三组的一行。当然,重要的考虑因素。
          猜你喜欢
          • 2012-03-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多