从文件中随机选择行

366
在Bash脚本中,我想要从输入文件中随机选择N行并输出到另一个文件。如何实现?

将文件随机排序并选择前N行。 - Piotr Praszmo
另请参见http://stackoverflow.com/questions/12354659/how-to-select-random-lines-from-a-file。 - Asclepius
35
这不是重复内容——他想要N行而不是1行。 - OneSolitaryNoob
1
我不同意使用 sort -R,因为它会对长文件进行大量的冗余操作。你可以使用 $RANDOM% wc -ljotsed -n(参考 https://dev59.com/5G025IYBdhLWcg3wZ1Ny#6022431)以及 bash 功能(数组、命令重定向等)来定义自己的 peek 函数,该函数可以在 500 万行的文件上运行。 - isomorphismes
8个回答

831
使用如下命令shuf-n选项,获取N行随机行:
shuf -n N input > output

8
如果您只需要一个非随机顺序的随机行集合,则shuf非常低效(对于大文件):更好的方法是执行蓄水池抽样,就像这个答案中所述。 - petrelharp
6
我在一个含有五亿行数据的文件中提取了一千行,用时13分钟。这个文件在数月内没有被访问过,保存在亚马逊EC2 SSD硬盘上。 - T. Brian Jones
1
这个本质上比 sort -R 更随机吗? - Mona Jalal
1
@MonaJalal 不是不行,只是更快,因为它根本不需要比较行。 - rogerdpack
2
最终会多次产生同一行吗? - Frederick Nord
显示剩余5条评论

196

随机排序文件并选择前100行:

lines=100
input_file=/usr/share/dict/words

# This is the basic selection method
<$input_file sort -R | head -n $lines

# If the file has duplicates that must never cause duplicate results
<$input_file sort | uniq        | sort -R | head -n $lines

# If the file has blank lines that must be filtered, use sed
<$input_file sed $'/^[ \t]*$/d' | sort -R | head -n $lines

当然,<$input_file 可以替换为任何管道标准输入。这种方法(使用 sort -R$'...\t...' 使 sed 匹配制表符)适用于 GNU/Linux 和 BSD/macOS。

49
sort 命令实际上将相同的行放在一起排序,因此如果您可能有重复的行并且已安装了 shuf(一个 GNU 工具),最好使用它来进行排序。 - Kevin
26
此外,如果您有一个相当巨大的文件 - 80000行 - 这肯定会让您等待很长时间,而“shuf -n”则相当快速。请注意不要改变原意。 - Rubens
28
在Mac OS X(10.9)中不可用sort -R。 - Mirko Ebert
3
"sort -R" 很可能是GNU选项,需要安装GNU coreutils。顺便提一句,“shuf”也是coreutils的一部分。 - jfs
1
@J.F.Sebastian 代码:sort -R input | head -n <num_lines>。输入文件大小为279GB,有20亿+行。虽然不能分享它,但无论如何,重点是您可以使用shuffle将一些行保留在内存中,以进行随机选择要输出的内容。无论您的需求是什么,排序都将对整个文件进行排序。 - Rubens
显示剩余10条评论

28

根据一条评论,Shuf回答在不到一分钟的时间内洗牌了780亿行。

接受挑战...

编辑:我打破了自己的记录

PowerShuf 在 0.047 秒内完成了此操作

$ time ./powershuf.py -n 10 --file lines_78000000000.txt > /dev/null 
./powershuf.py -n 10 --file lines_78000000000.txt > /dev/null  0.02s user 0.01s system 80% cpu 0.047 total

它如此快的原因是,我不会读取整个文件,而只是将文件指针移动10次并打印指针后的行。

Gitlab Repo

旧尝试

首先,我需要一个拥有78000000000行的文件:

seq 1 78 | xargs -n 1 -P 16 -I% seq 1 1000 | xargs -n 1 -P 16 -I% echo "" > lines_78000.txt
seq 1 1000 | xargs -n 1 -P 16 -I% cat lines_78000.txt > lines_78000000.txt
seq 1 1000 | xargs -n 1 -P 16 -I% cat lines_78000000.txt > lines_78000000000.txt

这给了我一个有780亿个换行符的文件 ;-)

现在进行乱序部分:

$ time shuf -n 10 lines_78000000000.txt










shuf -n 10 lines_78000000000.txt  2171.20s user 22.17s system 99% cpu 36:35.80 total

瓶颈在于CPU上,没有使用多线程,它将1个核心占满了100%,其他15个核心没有被使用。

Python是我经常使用的语言,所以我将用它来加速:

#!/bin/python3
import random
f = open("lines_78000000000.txt", "rt")
count = 0
while 1:
  buffer = f.read(65536)
  if not buffer: break
  count += buffer.count('\n')

for i in range(10):
  f.readline(random.randint(1, count))

我花了不到一分钟就做完了:

$ time ./shuf.py         










./shuf.py  42.57s user 16.19s system 98% cpu 59.752 total

我使用装有i9处理器和三星NVMe硬盘的Lenovo X1 Extreme二代来完成这项工作,读写速度非常快。

我知道速度可以更快,但我会留出一些空间让其他人试试。

行数计数器 来源:Luther Blissett


9
根据你对powershuf内部运作的描述,看起来它只是随机的。使用一个只有两行的文件,其中一行只有1个字符,另一行有20个字符,我期望这两行被选择的概率相等。但是根据你的程序,似乎并非如此。 - xhienne
文件长度小于4KB的问题和其他一些数学错误使得处理小文件的结果非常糟糕。我已经尽力修复了我找到的问题,请再试一次。 - Stein van Broekhoven
2
嗨,斯坦。它似乎不起作用。你按照我在上面的评论中建议的方式测试过吗?在比shuf更快之前,我认为你应该专注于制作与shuf一样准确的东西。我真的怀疑有人能用Python程序打败shuf。顺便说一下,除非你使用“-r”选项,否则shuf不会输出相同的行,当然这需要额外的处理时间。 - xhienne
1
为什么powershuf会丢弃第一行?它能否选择第一行吗?它似乎也以奇怪的方式筛选搜索结果:如果你有10行太长,然后1行有效长度,然后5行和另外一行有效长度,那么迭代会更经常地找到10行,并将大约三分之二的时间导向第一个有效行。程序并不保证这一点,但如果行实际上是按长度过滤的,然后从该集合中选择随机行,那么这对我来说是有意义的。 - Lupilum
2
问题是如何在Bash脚本中从文本文件中获取随机行,而不是如何编写Python脚本。 - dannyman

9

我的首选选项速度非常快,我对一个包含13列、23.1M行、未压缩大小为2.0GB的制表符分隔数据文件进行了采样。

# randomly sample select 5% of lines in file
# including header row, exclude blank lines, new seed

time \
awk 'BEGIN  {srand()} 
     !/^$/  { if (rand() <= .05 || FNR==1) print > "data-sample.txt"}' data.txt

# awk  tsv004  3.76s user 1.46s system 91% cpu 5.716 total

2
这太棒了 - 而且速度超快。 - abalter
随机抽样选择文件中的大约5%行。大数定律会使其接近,但由于每行都是独立决定的,因此无法保证实际上是5%的行。 - Amadan

1
seq 1 100 | python3 -c 'print(__import__("random").choice(__import__("sys").stdin.readlines()))'

0

仅为完整起见,并且因为它可以从Arch的社区存储库中获取:还有一个名为shuffle的工具,但它没有任何命令行开关来限制行数,并在其手册中发出警告:“由于shuffle将输入读入内存,因此可能无法处理非常大的文件。”


-1
# Function to sample N lines randomly from a file
# Parameter $1: Name of the original file
# Parameter $2: N lines to be sampled 
rand_line_sampler() {
    N_t=$(awk '{print $1}' $1 | wc -l) # Number of total lines

    N_t_m_d=$(( $N_t - $2 - 1 )) # Number oftotal lines minus desired number of lines

    N_d_m_1=$(( $2 - 1)) # Number of desired lines minus 1

    # vector to have the 0 (fail) with size of N_t_m_d 
    echo '0' > vector_0.temp
    for i in $(seq 1 1 $N_t_m_d); do
            echo "0" >> vector_0.temp
    done

    # vector to have the 1 (success) with size of desired number of lines
    echo '1' > vector_1.temp
    for i in $(seq 1 1 $N_d_m_1); do
            echo "1" >> vector_1.temp
    done

    cat vector_1.temp vector_0.temp | shuf > rand_vector.temp

    paste -d" " rand_vector.temp $1 |
    awk '$1 != 0 {$1=""; print}' |
    sed 's/^ *//' > sampled_file.txt # file with the sampled lines

    rm vector_0.temp vector_1.temp rand_vector.temp
}

rand_line_sampler "parameter_1" "parameter_2"

-1

在下面的代码中,'c' 是要从输入中选择的行数。根据需要进行修改:

#!/bin/sh

gawk '
BEGIN   { srand(); c = 5 }
c/NR >= rand() { lines[x++ % c] = $0 }
END { for (i in lines)  print lines[i] }

' "$@"

1
这并不保证确切地选择了 c 行。最好的情况是可以说平均选择的行数是 c - user1934428
这是不正确的:对于前c行,c/NR将会>=1(大于rand()的任何可能值),从而填充lines[]。x++ % c强制lines[]具有c个条目,假设输入中至少有c行。 - user19322235
正确的,对于前 c 行来说,c/NR保证rand 产生的任何值都要大。然后,在这之后,c/NR 可能也可能不会比 rand 大。因此,我们可以说最终 lines 包含至少 c 个条目,并且一般情况下更多,即不正好 c 条目。此外,文件的前 c 行总是被选择的,因此整个选择并不能称为随机选择。 - user1934428
1
uh,x++%c将lines[]限制为0到c-1的索引。当满足随机条件时,首先c个输入最初填充lines [],然后以循环方式替换。读者可以进行小修改(留作练习),使条目在lines[]中随机替换,而不是以循环方式替换。 - user19322235

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