流过滤大量由标准输入中的行号指定的行

Stream filter large number of lines that are specified by line number from stdin

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

我想快速 filter/select 大量行(~1000s)从那个巨大的文本文件到一个文件filtered.txt。 select 的行号可以在单独的文本文件 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[]; 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[]; next } (FNR in nums)' select.txt firstfile_proceed=1 -

-(在行尾)告诉 awk 从 stdin 读取(在这种情况下,来自 xz 的输出被管道传输到 awk打电话)。

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

awk '
FNR==NR { nums[]; next }             # process first file
FNR in nums                            # process subsequent file(s)
' select.txt <(xz -dcq huge.txt.xz)

删除评论并缩减为 'one-liner':

awk 'FNR==NR {nums[];next} FNR in nums' select.txt <(xz -dcq huge.txt.xz)

添加一些逻辑来实现 Ed Morton 的评论(一旦 FNR > 来自 select.txt 的最大值就退出处理):

awk '
# process first file

FNR==NR      { nums[]
               maxFNR= (>maxFNR ?  : maxFNR)
               next
             }

# process subsequent file(s):

FNR > maxFNR { exit }
FNR in nums
' select.txt <(xz -dcq huge.txt.xz)

备注:

  • 请记住,我们正在谈论扫描数百万行输入...
  • FNR > maxFNR 显然会为整体操作增加一些 cpu/processing 时间(虽然比 FNR in nums 少)
  • 如果操作通常需要从文件的最后 25% 中提取行,那么 FNR > maxFNR 可能没有什么好处(并且可能减慢操作速度)
  • 如果操作通常在文件的前 50% 中找到所有需要的行,那么 FNR> maxFNR 可能值得 cpu/processing 时间来避免扫描整个输入流(然后同样,对整个文件的 xz 操作可能是最大的时间消耗者)
  • 最终结果:额外的 NFR > maxFNR 测试可能 speed-up/slow-down 整个过程取决于在典型的 运行 中需要处理多少输入流; OP 需要 运行 一些测试来查看整体 运行time
  • 是否存在(明显的)差异

如果您有行号文件,请将 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

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

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[]
               maxFNR= (>maxFNR ?  : 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.

对真实的压缩文件进行测试,结果会是什么

Ed Morton 编辑:

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

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

上面例子的问题是它只是试图打印 4 行而不是 OP 所说的他们必须 select 的 1000 行,所以它没有行使 sed 之间的区别导致 sed 解决方案比 awk 解决方案慢得多的 awk 解决方案是 sed 解决方案必须测试每一行输入的每个目标行号,而 awk 解决方案只对当前行进行一次散列查找。这是输入文件每一行的 order(N) 与 order(1) 算法。

这里有一个更好的例子,显示了从 1000000 行文件中每第 100 行打印一次(即 select 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[]
            maxFNR = 
            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
$

如果我通过设置使输入文件变大 select 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 运行s 在 2.5 秒内,而 sed 需要 5.5 分钟!