有效地合并/排序/唯一的大量文本文件


8

我正在尝试天真:

$ cat * | sort -u > /tmp/bla.txt

失败与:

-bash: /bin/cat: Argument list too long

因此,为了避免一个愚蠢的解决方案(创建一个巨大的临时文件):

$ find . -type f -exec cat {} >> /tmp/unsorted.txt \;
$ cat /tmp/unsorted.txt | sort -u > /tmp/bla.txt

我虽然可以使用文件一个一个地处理文件(这样可以减少内存消耗,并且更接近流传输机制):

$ cat proc.sh
#!/bin/sh
old=/tmp/old.txt
tmp=/tmp/tmp.txt
cat $old "$1" | sort -u > $tmp
mv $tmp $old

接着是:

$ touch /tmp/old.txt
$ find . -type f -exec /tmp/proc.sh {} \;

有没有更简单的unix样式替代品:cat * | sort -u当文件数量达到时MAX_ARG?为这样的常见任务编写一个小的Shell脚本感觉很尴尬。


2
根本不需要串联吗?sort会自动为多个文件输入..但随后sort -u *也会失败,Argument list too long我想
Sundeep

Answers:


8

使用GNU sortprintf内置的shell (如今,除了的某些变体外,所有类似POSIX 的外壳pdksh):

printf '%s\0' * | sort -u --files0-from=- > output

现在,存在的问题是,由于该管道的两个组件同时并独立运行,因此当左边的那个组件扩展*glob时,右边的那个组件可能已经创建了output文件,这可能会引起问题(也许-u此处不存在)就像output输入和输出文件一样,因此您可能希望将输出转到另一个目录(> ../output例如),或者确保glob与输出文件不匹配。

在这种情况下解决它的另一种方法是编写它:

printf '%s\0' * | sort -u --files0-from=- -o output

这样一来,它的sort开放output写入和(在我的测试),它已收到文件前的完整列表,它不会做(这么长时间后,水珠已扩大)。output如果所有输入文件都不可读,这也将避免破坏。

zsh或编写的另一种方式bash

sort -u --files0-from=<(printf '%s\0' *) -o output

这是使用进程替换(其中的<(...)位置由指向管道读取端的文件路径替换printf)。该功能来自ksh,但ksh坚持在<(...)命令中扩展单独的参数,因此您不能在--option=<(...)语法中使用它。它将使用以下语法运行:

sort -u --files0-from <(printf '%s\0' *) -o output

请注意,cat在某些文件不以换行符结尾的情况下,您会看到与提供文件输出的方法有所不同:

$ printf a > a
$ printf b > b
$ printf '%s\0' a b | sort -u --files0-from=-
a
b
$ printf '%s\0' a b | xargs -r0 cat | sort -u
ab

另请注意,该代码sort使用语言环境(strcollate())中的归类算法进行排序,并sort -u报告按该算法对同一行进行排序的每一行,而不是字节级别的唯一行。如果您只关心行在字节级别上是唯一的,而不是太在乎它们的排序顺序,则您可能需要将语言环境固定为C,其中基于字节值进行排序(memcmp();可能会加快速度)事情显着上升):

printf '%s\0' * | LC_ALL=C sort -u --files0-from=- -o output

感觉更自然,这也为sort优化内存消耗提供了机会。不过,我仍然觉得printf '%s\0' *打字有点复杂。
malat

您可以使用find . -type f -maxdepth 1 -print0代替printf '%s\0' *,但是我不能说它更容易输入。当然,后者更容易定义为别名!
Toby Speight

@TobySpeight echo确实有个-n,我希望这样的东西printf -0 %s似乎比'%s\0'
malat

@Toby,-maxdepth并且-print0是GNU扩展(尽管广泛支持的这些天)。使用其他finds(尽管如果您使用GNU排序,那么您也可能会找到GNU),您可以这样做LC_ALL=C find . ! -name . -prune -type f ! -name '.*' -exec printf '%s\0' {} +LC_ALL=C即使使用GNU仍排除包含无效字符的隐藏文件find),但是通常情况下,这有点过头了有printf内置的。
斯特凡Chazelas

2
@malat,您始终可以将print0函数定义为print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@";}print0 * | sort...
StéphaneChazelas

11

因为printf是内置的,所以一个简单的修复程序至少在Bash中有效,并且命令行参数限制不适用于它:

printf "%s\0" * | xargs -0 cat | sort -u > /tmp/bla.txt

echo * | xargs也可以使用,除了处理带有空格的文件名等)。


与不需要接受的答案相比,这似乎是一个更好的答案,因为它不需要cat为每个文件生成单独的进程。
LarsH '17

4
@LarsH,find -exec {} +每执行一次将多个文件打包在一起。有了find -exec \;它将每个文件一只猫。
ilkkachu

啊,很高兴知道。(填充)
LarsH

9
find . -maxdepth 1 -type f ! -name ".*" -exec cat {} + | sort -u -o /path/to/sorted.txt

这将连接当前目录中的所有非隐藏常规文件,并将它们的合并内容排序(同时删除重复的行)到该文件中/path/to/sorted.txt


我试图一次只使用两个文件,以避免浪费大量内存(我的文件数量很大)。您是否相信|将适当地链接操作以限制内存使用?
2015年

2
sort如果内存要求,则@malat 将执行核外排序。相比之下,管道的左侧将消耗很少的内存。
库萨兰达

1

效率是一个相对术语,因此您实际上必须指定要最小化的因素。cpu,内存,磁盘,时间等。为便于讨论,我将假定您要最大程度地减少内存使用,并愿意花费更多的cpu周期来实现这一目标。如StéphaneChazelas提供的解决方案效果很好

sort -u --files0-from <(printf '%s\0' *) > ../output

但他们假设各个文本文件一开始就具有高度的唯一性。如果他们不这样做,即如果之后

sort -u < sample.txt > sample.srt

sample.srt比sample.txt小10%以上,然后通过合并之前删除文件中的重复项,可以节省大量内存。通过不链接命令,您还将节省更多的内存,这意味着不同进程的结果不需要同时存储在内存中。

find /somedir -maxdepth 1 type f -exec sort -u -o {} {} \;
sort -u --files0-from <(printf '%s\0' *) > ../output

1
内存使用情况是很少关注sortsort度假村使用临时文件时内存使用量超过阈值(通常比较小)。base64 /dev/urandom | sort -u会填满您的磁盘,但不会占用大量内存。
斯特凡Chazelas

嗯,至少大多数sort实现都是这种情况,包括1972年Unix v3中的原始实现,但显然不是busybox sort。大概是因为该程序旨在在没有永久存储的小型系统上运行。
斯特凡Chazelas

请注意,yes | sort -u(所有重复的数据)不必使用超过几个字节的内存,更不用说disk了。但是sort至少使用GNU和Solaris ,我们看到它在其中写入了2个字节的大文件/tmpy\n每输入几兆字节),因此最终将填满磁盘。
斯特凡Chazelas

0

就像@ilkkachu一样,但是cat(1)是不必要的:

printf "%s\0" * | xargs -0 sort -u

另外,如果数据太长,也许您想使用sort(1)选项 --parallel = N

N是您的计算机具有的CPU数量时

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.