2

我有一个巨大的xz压缩文本文件huge.txt.xz,其中包含数百万行,太大而无法保持未压缩(60GB)。

我想从那个巨大的文本文件中快速过滤/选择大量行(~1000s)到一个文件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

4

3 回答 3

7

保持OP当前的想法:

xz -dcq huge.txt.xz | awk '!firstfile_proceed { nums[$1]; next } (FNR in nums)' select.txt firstfile_proceed=1 -

-(在行尾)告诉awk从 stdin 读取的位置(在这种情况下,输出将xz通过管道传输到awk调用)。

另一种方法(替换所有上述代码):

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 需要运行一些测试以查看整体运行时是否存在(显着)差异
于 2021-09-03T19:15:38.520 回答
1

澄清我之前的评论。我将展示一个简单的可重现样本:

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

在我的结论中,seq使用qawk解决方案相当。为了可读性和可维护性,我更喜欢awk解决方案。

无论如何,这个测试很简单,只对小的比较有用。例如,我不知道如果我用大量磁盘 I/O 对真正的压缩文件进行测试,结果会怎样。


埃德莫顿编辑:

任何导致所有输出值小于一秒的速度测试都是不好的测试,因为:

  1. 一般来说,没有人关心 X 是在 0.1 秒还是 0.2 秒内运行,除非在大循环中调用它们,否则它们都足够快,并且
  2. 缓存之类的事情会影响结果,并且
  3. 通常,对于执行速度无关紧要的小输入集运行较快的脚本对于执行速度确实重要的大输入集运行较慢(例如,如果对于小输入较慢的脚本花费时间设置将让它运行得更快)

上面示例的问题是它只尝试打印 4 行而不是 OP 说他们必须选择的 1000 行,因此它不会影响 sed 和导致 sed 解决方案的 awk 解决方案之间的差异比 awk 慢得多,因为 sed 解决方案必须测试每一行输入的每个目标行号,而 awk 解决方案只对当前行进行一次哈希查找。这是输入文件每一行的 order(N) vs 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 分钟!

于 2021-09-05T17:04:00.530 回答
0

如果您有一个行号文件,请添加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
于 2021-09-03T19:24:18.727 回答