如何在Unix命令行或shell脚本中洗牌文本文件的行?

339

我想随机打乱文本文件的行顺序并创建一个新文件。该文件可能有几千行。

我可以如何使用catawkcut等进行操作?


4
使用标准工具在Redhat Linux上如何随机化文件中的行?在Redhat Linux上,可以使用shuf命令来随机化文件中的行。例如,要随机化名为file.txt的文件中的行,可以运行以下命令:shuf file.txt -o file.txt - Dennis Williamson
是的,在那个原始问题中还有一些不错的答案。 - Ruggiero Spearman
所以,你在制作WPA字典吗?(只是随意猜测) - thahgr
19个回答

418
您可以使用shuf。在至少某些系统上(似乎不属于POSIX)。
正如jleedev所指出的那样:sort -R也可能是一个选择。至少在一些系统上;好吧,你明白了。已经指出sort -R并没有真正洗牌,而是根据它们的哈希值对项目进行排序。 [编辑说明: sort -R几乎洗牌,只有重复的行/排序键总是彼此相邻。换句话说:只有具有唯一输入行/键才是真正的随机洗牌。虽然输出顺序确实由哈希值确定,但随机性来自于选择随机哈希函数-请参见手册。]

39
shufsort -R 有些微小的不同,因为 sort -R 根据元素的哈希值来随机排序,这意味着 sort -R 会把重复的元素放在一起,而 shuf 则是将所有元素随机打乱。 - semekh
151
针对 OS X 用户:运行 brew install coreutils 安装核心工具,然后使用 gshuf ... 命令。 - ELLIOTTCABLE
15
sort -Rshuf 应该被视为完全不同的命令。sort -R 是确定性的。如果你在相同的输入上在不同的时间调用它两次,你会得到相同的答案。而 shuf 则产生随机输出,因此在相同的输入上它很可能会给出不同的输出结果。 - EfForEffort
23
不正确。"sort -R" 每次调用时都使用一个不同的随机哈希键,因此每次都会产生不同的输出。 - Mark Pettit
4
随机性注释:根据GNU文档,“默认情况下,这些命令使用一个由少量熵初始化的内部伪随机生成器,但可以使用--random-source=file选项指定使用外部源。” - Royce Williams
显示剩余12条评论

93

使用 Perl 一行代码就可以简单地实现 Maxim 方案的版本

perl -MList::Util=shuffle -e 'print shuffle(<STDIN>);' < myfile

7
我已将此设置为在OS X上的洗牌别名。谢谢! - The Unfun Cat
这是此页面上唯一返回真实随机行的脚本。其他 awk 解决方案经常会打印重复输出。 - Felipe Alvarez
1
但是要小心,因为在输出中你会失去一行 :) 它只会与另一行连接 :) - JavaRunner
@JavaRunner:我猜你是在谈论没有结尾的\n输入;是的,那个\n必须存在 - 而且通常也确实存在 - 否则你会得到你所描述的结果。 - mklement0
1
非常简洁。我建议将<STDIN>替换为<>,这样解决方案也可以处理来自文件的输入。 - mklement0
其他答案提供了您可能已经拥有或尚未拥有的实用程序。不过,每个人都有perl(如果您没有,则某些需要的东西在某个时候将需要它)。 - Mars

68

这个答案在以下方面补充了许多优秀的现有答案:

  • 现有答案被打包成了灵活的shell函数

    • 这些函数不仅可以接受标准输入,还可以接受文件名参数
    • 这些函数采取额外措施以通常的方式处理SIGPIPE(静默终止,退出码为141),而不是嘈杂地中断。当将函数输出管道传输到早期关闭的管道时,例如管道传输到head时,这一点非常重要。
  • 进行了性能比较



shuf() { awk 'BEGIN {srand(); OFMT="%.17f"} {print rand(), $0}' "$@" |
               sort -k1,1n | cut -d ' ' -f2-; }
shuf() { perl -MList::Util=shuffle -e 'print shuffle(<>);' "$@"; }
shuf() { python -c '
import sys, random, fileinput; from signal import signal, SIGPIPE, SIG_DFL;    
signal(SIGPIPE, SIG_DFL); lines=[line for line in fileinput.input()];   
random.shuffle(lines); sys.stdout.write("".join(lines))
' "$@"; }

请查看底部有关此函数的Windows版本。

shuf() { ruby -e 'Signal.trap("SIGPIPE", "SYSTEM_DEFAULT");
                     puts ARGF.readlines.shuffle' "$@"; }

性能比较:

说明:这些数字是在配备 3.2 GHz Intel Core i5 处理器和 Fusion Drive 的2012年底的 iMac 上运行 OSX 10.10.3 时获取的。虽然计时会随着操作系统、机器规格和使用的 awk 实现而变化(例如,OSX 上使用的 BSD awk 版本通常比 GNU awk 和尤其是 mawk 更慢),但这应该提供了相对性能的一般感觉。

输入文件是使用 seq -f 'line %.0f' 1000000 生成的1百万行文件
时间按升序排列(最快的第一项):

  • shuf
    • 0.090s
  • Ruby 2.0.0
    • 0.289s
  • Perl 5.18.2
    • 0.589s
  • Python
    • 1.342s(使用 Python 2.7.6); 2.407s(!) (使用 Python 3.4.2)
  • awk + sort + cut
    • 3.003s(使用 BSD awk); 2.388s(使用 GNU awk (4.1.1)); 1.811s(使用 mawk (1.3.4));

进一步比较以下未打包为函数的解决方案:

  • sort -R(如果有重复的输入行,则不是真正的洗牌)
    • 10.661s - 分配更多内存似乎没有什么区别
  • Scala
    • 24.229s
  • bash 循环 + 排序
    • 32.593s

结论:

  • 如果可以的话,使用 shuf - 它是最快的。
  • Ruby 的表现不错,其次是 Perl
  • Python 比 Ruby 和 Perl 明显慢,而且在比较 Python 版本时,2.7.6 比 3.4.1 快得多。
  • 尽可能避免使用 POSIX 兼容的 awk + sort + cut 组合方式;你所使用的 awk 实现很重要(mawk 比 GNU awk 快,BSD awk 最慢)。
  • 避免使用 sort -Rbash 循环和 Scala。

Python 解决方案的 Windows 版本(Python 代码相同,只是引号和与信号相关的语句有所变化,因为这些语句在 Windows 上不受支持):

  • 对于 PowerShell(在 Windows PowerShell 中,如果您想通过管道发送非 ASCII 字符,需要调整 $OutputEncoding):
# Call as `shuf someFile.txt` or `Get-Content someFile.txt | shuf`
function shuf {
  $Input | python -c @'
import sys, random, fileinput;
lines=[line for line in fileinput.input()];
random.shuffle(lines); sys.stdout.write(''.join(lines))
'@ $args  
}

请注意,PowerShell 可以通过其 Get-Random 命令本地洗牌(尽管性能可能存在问题)。例如:
Get-Content someFile.txt | Get-Random -Count ([int]::MaxValue)

  • 对于 cmd.exe (批处理文件):

保存至文件 shuf.cmd,例如:

@echo off
python -c "import sys, random, fileinput; lines=[line for line in fileinput.input()]; random.shuffle(lines); sys.stdout.write(''.join(lines))" %*

在Windows上不存在SIGPIPE,因此我使用了这个简单的一行代码: python -c"import sys, random; lines =[x for x in sys.stdin.read().splitlines()]; random.shuffle(lines); print(\"\n\".join([line for line in lines]));" - elig
@elig:谢谢,但是从原始解决方案中省略from signal import signal, SIGPIPE, SIG_DFL; signal(SIGPIPE, SIG_DFL);就足够了,并保留了还能传递文件名_参数_的灵活性 - 除了引用之外不需要更改任何内容 - 请参见我在底部添加的新部分。 - mklement0

27

我使用一个小型的 Perl 脚本,我称之为 "unsort" :

#!/usr/bin/perl
use List::Util 'shuffle';
@list = <STDIN>;
print shuffle(@list);

我还有一个以空值为分隔符的版本,称为"unsort0"...在与find-print0等工具一起使用时非常方便。

另外,顺便点赞'shuf',我不知道现在在coreutils中已经有了它...如果你的系统没有'shuf',上面的内容仍然可能会有用。


不错,RHEL 5.6 没有 shuf。 - Maxim Egorushkin
1
干得好!我建议将<STDIN>替换为<>,以便使该解决方案能够处理来自文件的输入。 - mklement0

23

这是一种对程序员来说比较简单但对CPU压力较大的尝试,它在每行前面添加一个随机数,然后对它们进行排序,并从每行中删除随机数。实际上,这些行是随机排序的。

cat myfile | awk 'BEGIN{srand();}{print rand()"\t"$0}' | sort -k1 -n | cut -f2- > myfile.shuffled

9
UUOC是一个缩写,意为"不要进行无用的重复操作"。将文件传递给awk本身即可。 - ghostdog74
1
没错,我用 head myfile | awk ... 进行调试。然后我只是将它改成了 cat;这就是为什么它留在那里的原因。 - Ruggiero Spearman
不需要在排序时使用“-k1 -n”,因为awk的“rand()”输出是0到1之间的小数,并且重要的是以某种方式重新排序。 “-k1”可能通过忽略行的其余部分来加速它,尽管rand()的输出应该足够唯一,以便短路比较。 - bonsaiviking
@ghostdog74:大多数所谓的无用 cat 用法实际上对于保持管道命令之间的一致性非常有用。最好保留 cat filename |(或 < filename |),而不是记住每个单独程序如何接受文件输入(或不接受)。 - ShreevatsaR
2
shuf() { awk 'BEGIN{srand()}{print rand()"\t"$0}' "$@" | sort | cut -f2- ;} - Meow
@bonsaiviking: -k1 是多余的,因为它仍然将整行作为一个整体进行排序,并没有指定停止字段;如果要真正限制排序到第一个字段,则需要使用 -k1,1。然而,使用 -n 可以明显加快排序速度。 因此,您应该使用 -k1,1 -n(以明确表述)或者利用排序键是第一个字段并且 sort 在检测数字时使用最长前缀匹配这一事实,只需使用 -n - mklement0

16

这是一个awk脚本

awk 'BEGIN{srand() }
{ lines[++d]=$0 }
END{
    while (1){
    if (e==d) {break}
        RANDOM = int(1 + rand() * d)
        if ( RANDOM in lines  ){
            print lines[RANDOM]
            delete lines[RANDOM]
            ++e
        }
    }
}' file

输出

$ cat file
1
2
3
4
5
6
7
8
9
10

$ ./shell.sh
7
5
10
9
6
8
2
1
3
4

做得很好,但在实践中比OP自己的答案慢得多,后者将awksortcut结合使用。对于不超过几千行的情况,这没有太大区别,但对于更高的行数,它很重要(阈值取决于所使用的awk实现)。稍微简化一下就是用while (e<d)替换while (1){if (e==d) {break}这两行。 - mklement0

11
一个Python的一行代码:
python -c "import random, sys; lines = open(sys.argv[1]).readlines(); random.shuffle(lines); print ''.join(lines)," myFile

仅需打印一行随机文本:

python -c "import random, sys; print random.choice(open(sys.argv[1]).readlines())," myFile

但是看看这篇文章,了解 Python 的random.shuffle()存在的缺陷。如果存在许多元素(超过2080个),那么其效果将不理想。


2
“drawback” 不仅限于 Python。有限的 PRNG 周期可以通过使用来自系统的熵重新播种 PRNG 来解决,就像 /dev/urandom 所做的那样。要从 Python 中利用它:random.SystemRandom().shuffle(L) - jfs
join() 不需要在 '\n' 上吗?这样每行就可以单独打印了吧? - elig
@elig:不,因为.readLines()返回带有换行符的行。 - mklement0

9
简单的基于awk的函数可以完成这项工作:
shuffle() { 
    awk 'BEGIN{srand();} {printf "%06d %s\n", rand()*1000000, $0;}' | sort -n | cut -c8-
}

使用方法:

any_command | shuffle

这个应该可以在几乎所有的UNIX系统上运行。已在Linux、Solaris和HP-UX上测试。

更新:

请注意,在一些不支持数字排序的系统上,前导零 (%06d) 和 rand() 的乘法会使其正常工作。可以通过字典序(也称为普通字符串比较)进行排序。


将以下程序相关内容从英语翻译成中文。仅返回翻译后的文本: 将原帖的答案打包为函数是一个好主意;如果附加"$@", 它也可以处理_文件_作为输入。没有理由去乘以 rand() ,因为 sort -n 可以排序十进制小数。但是,控制 awk 的输出格式是一个好主意,因为默认格式 %.6grand() 会输出偶尔使用 指数 表示法的数字。 虽然在实践中随机排列高达100万行足以满足需求,但很容易支持更多行而不付出太多性能代价;例如%.17f - mklement0
1
@mklement0 在我写我的答案时,我没有注意到OP的答案。据我所记,rand()被乘以10e6是为了使其与Solaris或HP-UX sort配合使用。"$@"的想法很好。 - Michał Šrajer
1
明白了,谢谢。也许您可以在答案本身中添加这个乘法的理由。根据POSIX规范,sort应该能够处理十进制小数(即使有千位分隔符,就像我刚刚注意到的那样)。 - mklement0

8

一个简单直观的方法是使用shuf命令。

示例:

假设words.txt文件内容如下:

the
an
linux
ubuntu
life
good
breeze

要对行进行洗牌,请执行以下操作:

$ shuf words.txt

这将把打乱顺序的行输出到标准输出; 因此,你需要将其管道到一个输出文件中,如下所示:

$ shuf words.txt > shuffled_words.txt

一个这样的洗牌运行可能会产生以下结果:

breeze
the
linux
an
ubuntu
good
life

8

胜利属于Ruby:

ls | ruby -e 'puts STDIN.readlines.shuffle'

1
很棒的东西;如果你使用 puts ARGF.readlines.shuffle,你可以让它同时适用于标准输入和文件名参数。 - mklement0
1
更短的 ruby -e 'puts $<.sort_by{rand}' — ARGF 已经是一个可枚举对象,因此我们可以通过按随机值排序来打乱行。 - akuhn

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