【问题标题】:How can I read first n and last n lines from a file?如何从文件中读取前 n 行和后 n 行?
【发布时间】:2015-04-21 08:43:29
【问题描述】:

如何读取文件的前 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

【问题讨论】:

  • 另外,您发送的链接没有帮助,因为我真的不知道范围。我正在为此寻找一个简单的解决方案
  • 有趣的是,cat x | (head -n2 && tail -n2) 不起作用,但 (head -n2 && tail -n2) < x 起作用。我得思考一下为什么会这样。
  • 如果输入文件是 3 行长,预期的输出是什么?会是1 2 31 2 2 3 还是别的什么?如果它只有 2 行长怎么办 - 输出会是 1 2 1 21 1 2 21 2 还是其他什么?
  • 我不认为head && tail 技巧是可靠的。来自 GNU coreutils 的head 对于管道和常规文件(来源:源代码)的行为不同,在一种情况下按块读取,但在另一种情况下则不然。依赖于这样的实现细节似乎是个坏主意——不能保证head 会留下它不打印的所有内容供tail 使用。

标签: bash awk sed head tail


【解决方案1】:

你可能会想要这样的东西:

... | awk -v OFS='\n' '{a[NR]=$0} 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]=$0}
         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] = $0

    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

【讨论】:

  • +1 因为这在管道中有效。您可能会添加一个更详细的版本,该版本将文件(流)考虑在内,少于 4 行(头+尾)..
  • @EdMorton 但是它仍然需要在内存中缓冲整个流。。(但是,如果它应该在管道中工作,我看不到没有缓冲的方法,除了将流保存到临时文件)
  • 是的,现在它对于大文件是不可扩展的。它仍然对我有用。
  • 我想知道为什么猫 x | (head -n2 && tail -n2) 不起作用...因为这将是完美的解决方案
  • 我明白,但错误只是我设置了ORS='\n',而我应该设置OFS='\n'。现在已经解决了,不需要在字段之间显式地硬编码"\n"s。
【解决方案2】:
head -n2 file && tail -n2 file

【讨论】:

  • UUOC。 head -n2 x &amp;&amp; tail -n2 x
  • @rici:这很容易解决:D
  • 如果文件长度不超过 3 行,则不会产生正确的输出。
  • 解释一下。
  • 如果单个 head 缓冲区足够长以至于文件中没有足够的行用于 @987654325 @上班。
【解决方案3】:

awk -v n=4 'NR&lt;=n; {b = b "\n" $0} NR&gt;=n {sub(/[^\n]*\n/,"",b)} END {print b}'

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

使用一组行而不是单个缓冲区可以更有效地执行此操作,但即使有千兆字节的输入,您可能会浪费更多的大脑时间来编写它而不是节省计算机时间运行它。

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

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

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

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

      这是一个打印前 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

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

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

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

          基于dcaswell's answer,以下sed 脚本打印文件的第一行和最后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
          

          【讨论】:

          • 虽然它适用于短于 20 行的文件,但它似乎吞噬了短于 10 行的文件的最后一行。呃。
          【解决方案8】:

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

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

          {} 的使用从组内部收集输出(将有两个不同的程序写入进程外壳内的标准输出)。 1&gt;/dev/null 是为了摆脱多余的副本,tee 将尝试写入它自己的标准输出。

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

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

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

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

          【讨论】:

            【解决方案9】:

            这是另一个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] = $0} # 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
            

            【讨论】:

              猜你喜欢
              • 2010-12-18
              • 1970-01-01
              • 2019-02-28
              • 1970-01-01
              • 1970-01-01
              • 2014-07-14
              • 1970-01-01
              • 2023-02-19
              相关资源
              最近更新 更多