我有一个巨大的xz
压缩文本文件huge.txt.xz
有数百万行,太大而无法保持未压缩(60GB)。
我想从那个巨大的文本文件中快速过滤/选择大量行(~1000 行)到文件filtered.txt
中。例如,可以选择的行号可以在单独的文本文件中指定,select.txt
格式如下:
10
14
...
1499
15858
总的来说,我设想了一个 shell 命令,如下所示,其中"待确定"是我正在寻找的命令:
xz -dcq huge.txt.xz | "TO BE DETERMINED" select.txt >filtered.txt
我设法从一个密切相关的问题中找到了一个几乎可以完成这项工作的awk
程序 - 唯一的问题是它需要一个文件名而不是从stdin读取。不幸的是,我并不真正了解awk
脚本,并且没有足够的awk
来更改它以在这种情况下工作的方式。
这就是现在有效的缺点,即拥有 60GB 文件而不是流式传输:
xz -dcq huge.txt.xz >huge.txt
awk '!firstfile_proceed { nums[$1]; next }
(FNR in nums)' select.txt firstfile_proceed=1 >filtered.txt
灵感:https://unix.stackexchange.com/questions/612680/remove-lines-with-specific-line-number-specified-in-a-file
与OP当前的想法保持一致:
xz -dcq huge.txt.xz | awk '!firstfile_proceed { nums[$1]; next } (FNR in nums)' select.txt firstfile_proceed=1 -
其中-
(在行尾)告诉awk
从 stdin 读取(在本例中为通过管道传输到awk
调用的xz
的输出)。
执行此操作的另一种方法(替换上述所有代码):
awk '
FNR==NR { nums[$1]; next } # process first file
FNR in nums # process subsequent file(s)
' select.txt <(xz -dcq huge.txt.xz)
删除评论并缩减为"单行":
awk 'FNR==NR {nums[$1];next} FNR in nums' select.txt <(xz -dcq huge.txt.xz)
添加一些逻辑来实现 Ed Morton 的评论(一旦 FNR>select.txt
中的最大值,就退出处理):
awk '
# process first file
FNR==NR { nums[$1]
maxFNR= ($1>maxFNR ? $1 : maxFNR)
next
}
# process subsequent file(s):
FNR > maxFNR { exit }
FNR in nums
' select.txt <(xz -dcq huge.txt.xz)
笔记:
- 请记住,我们正在谈论扫描数百万行输入...
FNR > maxFNR
显然会给整个操作增加一些CPU/处理时间(尽管时间比FNR in nums
少)- 如果操作例行需要从文件的最后 25% 中提取行,那么
FNR > maxFNR
可能提供很少的好处(并且可能会减慢操作速度) - 如果操作例行地在文件的前 50% 中找到所有需要的行,那么
FNR> maxFNR
可能值得花费 CPU/处理时间来防止扫描整个输入流(再说一次,xz
操作,在整个文件上,可能是最大的时间消耗) - 最终结果:额外的
NFR > maxFNR
测试可能会加快/减慢整个过程,具体取决于在典型运行中需要处理多少输入流;OP 需要运行一些测试以查看整体运行时是否存在(明显)差异
澄清我之前的评论。我将展示一个简单的可重现示例:
linelist
内容:
10
15858
14
1499
为了模拟长输入,我将使用seq -w 100000000
。
将sed解决方案与我的建议进行比较,我们有:
#!/bin/bash
time (
sed 's/$/p/' linelist > selector
seq -w 100000000 | sed -nf selector
)
time (
sort -n linelist | sed '$!{s/$/p/};$s/$/{p;q}/' > my_selector
seq -w 100000000 | sed -nf my_selector
)
输出:
000000010
000000014
000001499
000015858
real 1m23.375s
user 1m38.004s
sys 0m1.337s
000000010
000000014
000001499
000015858
real 0m0.013s
user 0m0.014s
sys 0m0.002s
将我的解决方案与awk进行比较:
#!/bin/bash
time (
awk '
# process first file
FNR==NR { nums[$1]
maxFNR= ($1>maxFNR ? $1 : maxFNR)
next
}
# process subsequent file(s):
FNR > maxFNR { exit }
FNR in nums
' linelist <(seq -w 100000000)
)
time (
sort -n linelist | sed '$!{s/$/p/};$s/$/{p;q}/' > my_selector
sed -nf my_selector <(seq -w 100000000)
)
输出:
000000010
000000014
000001499
000015858
real 0m0.023s
user 0m0.020s
sys 0m0.001s
000000010
000000014
000001499
000015858
real 0m0.017s
user 0m0.007s
sys 0m0.001s
在我的结论中,使用q
seq
与awk
解决方案相当。对于可读性和可维护性,我更喜欢awk
解决方案。
无论如何,这个测试很简单,只对小比较有用。例如,我不知道如果我针对真正的压缩文件(带有大量磁盘 I/O)对此进行测试会是什么结果。
>编辑 by Ed Morton:任何导致所有输出值都小于一秒的速度测试都是糟糕的测试,因为:
- 一般来说,没有人关心X是在0.1秒还是0.2秒内运行,除非在大循环中被调用,否则它们都足够快,并且
- 缓存之类的事情会影响结果,并且 通常,对于执行速度无关紧要的小型输入集运行速度
- 更快的脚本对于执行速度确实很重要的大型输入集,运行速度会变慢(例如,如果对于小输入较慢的脚本花费时间设置数据结构,使其在较大的输入中运行得更快)
上面示例的问题在于它只尝试打印 4 行,而不是 OP 说他们必须选择的 1000 行,因此它不会练习 sed 和 awk 解决方案之间的差异,导致 sed 解决方案比 awk 解决方案慢得多,即 sed 解决方案必须测试每行输入的每个目标行号,而 awk解决方案只对当前行执行单个哈希查找。它是输入文件每一行上的 order(N) 与 order(1) 算法。
下面是一个更好的示例,显示从 1000000行文件中打印每 100 行(即将选择 1000 行)而不是从任何大小文件中打印 4 行:
$ cat tst_awk.sh
#!/usr/bin/env bash
n=1000000
m=100
awk -v n="$n" -v m="$m" 'BEGIN{for (i=1; i<=n; i+=m) print i}' > linelist
seq "$n" |
awk '
FNR==NR {
nums[$1]
maxFNR = $1
next
}
FNR in nums {
print
if ( FNR == maxFNR ) {
exit
}
}
' linelist -
$ cat tst_sed.sh
#!/usr/bin/env bash
n=1000000
m=100
awk -v n="$n" -v m="$m" 'BEGIN{for (i=1; i<=n; i+=m) print i}' > linelist
sed '$!{s/$/p/};$s/$/{p;q}/' linelist > my_selector
seq "$n" |
sed -nf my_selector
$ time ./tst_awk.sh > ou.awk
real 0m0.376s
user 0m0.311s
sys 0m0.061s
$ time ./tst_sed.sh > ou.sed
real 0m33.757s
user 0m33.576s
sys 0m0.045s
如您所见,awk 解决方案的运行速度比 sed 解决方案快 2 个数量级,并且它们产生了相同的输出:
$ diff ou.awk ou.sed
$
如果我使输入文件变大并通过设置从中选择 10,000 行:
n=10000000
m=1000
在每个脚本中,对于 OP 的使用来说可能变得更加现实,差异变得非常令人印象深刻:
$ time ./tst_awk.sh > ou.awk
real 0m2.474s
user 0m2.843s
sys 0m0.122s
$ time ./tst_sed.sh > ou.sed
real 5m31.539s
user 5m31.669s
sys 0m0.183s
即 awk 在 2.5 秒内运行,而 sed 需要 5.5 分钟!
如果您有行号文件,请将p
添加到每个行号的末尾,并将其作为sed
脚本运行。
如果linelist
包含
10
14
1499
15858
然后sed 's/$/p/' linelist > selector
创建
10p
14p
1499p
15858p
然后
$: for n in {1..1500}; do echo $n; done | sed -nf selector
10
14
1499
我没有发送足够的行来匹配 15858,因此没有打印。
这与从文件解压缩的工作方式相同。
$: tar xOzf x.tgz | sed -nf selector
10
14
1499