如何从文件中读取前 n 行和后 n 行?

How can I read first n and last n lines from a file?

如何读取文件的前 n 行和后 n 行?

对于 n=2,我读到 online (head -n2 && tail -n2) 会起作用,但它不起作用。

$ cat x
1
2
3
4
5
$ cat x | (head -n2 && tail -n2)
1
2

n=2 的预期输出为:

1
2
4
5

您可能想要这样的东西:

... | awk -v OFS='\n' '{a[NR]=[=10=]} END{print a[1], a[2], a[NR-1], a[NR]}'

或者如果您需要指定一个数字并考虑到@Wintermute 的敏锐观察,即您不需要缓冲整个文件,那么您真正想要的是:

... | awk -v n=2 'NR<=n{print;next} {buf[((NR-1)%n)+1]=[=11=]}
         END{for (i=1;i<=n;i++) print buf[((NR+i-1)%n)+1]}'

我认为这方面的数学是正确的——希望你能想到使用一个由 NR 索引的旋转缓冲区,该缓冲区由缓冲区的大小修改,并调整为使用 1-n 范围内的索引而不是 0- (n-1).

为了帮助理解上面索引中使用的模数运算符,这里有一个示例,其中包含中间打印语句以显示其执行时的逻辑:

$ cat file   
1
2
3
4
5
6
7
8

.

$ cat tst.awk                
BEGIN {
    print "Populating array by index ((NR-1)%n)+1:"
}
{
    buf[((NR-1)%n)+1] = [=13=]

    printf "NR=%d, n=%d: ((NR-1 = %d) %%n = %d) +1 = %d -> buf[%d] = %s\n",
        NR, n, NR-1, (NR-1)%n, ((NR-1)%n)+1, ((NR-1)%n)+1, buf[((NR-1)%n)+1]

}
END { 
    print "\nAccessing array by index ((NR+i-1)%n)+1:"
    for (i=1;i<=n;i++) {
        printf "NR=%d, i=%d, n=%d: (((NR+i = %d) - 1 = %d) %%n = %d) +1 = %d -> buf[%d] = %s\n",
            NR, i, n, NR+i, NR+i-1, (NR+i-1)%n, ((NR+i-1)%n)+1, ((NR+i-1)%n)+1, buf[((NR+i-1)%n)+1]
    }
}
$ 
$ awk -v n=3 -f tst.awk file
Populating array by index ((NR-1)%n)+1:
NR=1, n=3: ((NR-1 = 0) %n = 0) +1 = 1 -> buf[1] = 1
NR=2, n=3: ((NR-1 = 1) %n = 1) +1 = 2 -> buf[2] = 2
NR=3, n=3: ((NR-1 = 2) %n = 2) +1 = 3 -> buf[3] = 3
NR=4, n=3: ((NR-1 = 3) %n = 0) +1 = 1 -> buf[1] = 4
NR=5, n=3: ((NR-1 = 4) %n = 1) +1 = 2 -> buf[2] = 5
NR=6, n=3: ((NR-1 = 5) %n = 2) +1 = 3 -> buf[3] = 6
NR=7, n=3: ((NR-1 = 6) %n = 0) +1 = 1 -> buf[1] = 7
NR=8, n=3: ((NR-1 = 7) %n = 1) +1 = 2 -> buf[2] = 8

Accessing array by index ((NR+i-1)%n)+1:
NR=8, i=1, n=3: (((NR+i = 9) - 1 = 8) %n = 2) +1 = 3 -> buf[3] = 6
NR=8, i=2, n=3: (((NR+i = 10) - 1 = 9) %n = 0) +1 = 1 -> buf[1] = 7
NR=8, i=3, n=3: (((NR+i = 11) - 1 = 10) %n = 1) +1 = 2 -> buf[2] = 8
head -n2 file && tail -n2 file

awk -v n=4 'NR<=n; {b = b "\n" [=10=]} NR>=n {sub(/[^\n]*\n/,"",b)} END {print b}'

前n行被NR<=n;覆盖。对于最后 n 行,我们只跟踪保存 latest n 行的缓冲区,重复在末尾添加一个并从前面删除一个(在第一个 n 之后)。

使用行数组而不是单个缓冲区可以更有效地做到这一点,但即使有千兆字节的输入,你可能会浪费更多的脑力时间来写出来,而不是在计算机上保存时间按运行吧。

预计到达时间: 因为上面的时间估计在(现已删除的)评论中引起了一些讨论,我将添加一些尝试过的轶事。

对于一个巨大的文件(100M 行,3.9 GiB,n=5),它花费了 454 秒,而@EdMorton 的行缓冲解决方案只执行了 30 秒。对于更适度的输入(“仅”数百万行),比率相似:4.7 秒对 0.53 秒。

这个解决方案中几乎所有的额外时间似乎都花在了 sub() 函数上;一小部分也确实来自字符串连接比仅替换数组成员慢。

这可能对你有用 (GNU sed):

sed -n ':a;N;s/[^\n]*/&/2;Ta;2p;$p;D' file

这会保留 2 行的 window(将 2 替换为 n)行,然后打印前 2 行,并在文件末尾打印 window,即最后 2 行。

这是打印前 10 行和后 10 行的 GNU sed 单行代码:

gsed -ne'1,10{p;b};:a;$p;N;21,$D;ba'

如果你想在它们之间打印一个'--'分隔符:

gsed -ne'1,9{p;b};10{x;s/$/--/;x;G;p;b};:a;$p;N;21,$D;ba'

如果您使用的是 Mac 并且没有 GNU sed,则无法压缩那么多:

sed -ne'1,9{' -e'p;b' -e'}' -e'10{' -e'x;s/$/--/;x;G;p;b' -e'}' -e':a' -e'$p;N;21,$D;ba'

说明

gsed -ne' 在没有自动打印模式的情况下调用 sed space

-e'1,9{p;b}' 打印前 9 行

-e'10{x;s/$/--/;x;G;p;b}' 打印第 10 行并附加 '--' 分隔符

-e':a;$p;N;21,$D;ba' 打印最后 10 行

使用GNU parallel。打印前三行和后三行:

parallel {} -n 3 file ::: head tail

基于 dcaswell's answer,以下 sed 脚本打印文件的前 10 行和后 10 行:

# Make a test file first
testit=$(mktemp -u)
seq 1 100 > $testit
# This sed script:
sed -n ':a;1,10h;N;${x;p;i\
-----
;x;p};11,$D;ba' $testit
rm $testit

产生这个:

1
2
3
4
5
6
7
8
9
10
-----
90
91
92
93
94
95
96
97
98
99
100

如果您正在使用支持进程替换的 shell,另一种实现此目的的方法是写入多个进程,一个用于 head,一个用于 tail。假设在此示例中,您的输入来自管道,为您提供未知长度的内容。您只想使用前 5 行和后 10 行并将它们传递到另一个管道:

cat | { tee >(head -5) >(tail -10) 1>/dev/null} | cat

使用{}从组内收集输出(在进程shells内会有两个不同的程序写入stdout)。 1>/dev/null 是为了摆脱额外的副本 tee 将尝试写入它自己的标准输出。

这演示了概念和所有移动部分,但在实践中可以通过使用 tee 的 STDOUT 流而不是丢弃它来稍微简化它。请注意,这里仍然需要命令分组,以便通过下一个管道传递输出!

cat | { tee >(head -5) | tail -15 } | cat

显然用您实际执行的操作替换管道中的 cat。如果您的输入可以处理相同的内容以写入多个文件,您可以完全消除 tee 的使用以及使用 STDOUT 进行修改。假设您有一个接受多个 -o 输出文件名标志的命令:

{ mycommand -o >(head -5) -o >(tail -10)} | cat

这是另一个 AWK 脚本。假设头部和尾部可能有重叠。

文件script.awk

BEGIN {range = 3} # Define the head and tail range
NR <= range {print} # Output the head; for the first lines in range
{ arr[NR % range] = [=10=]} # Store the current line in a rotating array
END { # Last line reached
    for (row = NR - range + 1; row <= NR; row++) { # Reread the last range lines from array
        print arr[row % range];
    }
}

运行 脚本

seq 1 7 | awk -f script.awk

输出

1
2
3
5
6
7

头尾重叠:

seq 1 5 |awk -f script.awk


1
2
3
3
4
5