从文件中获取第n行的Bash工具

854

有没有一种“规范”的方法来做这件事?我一直在使用head -n | tail -1来完成这个操作,但我一直在想是否有一款Bash工具可以从文件中提取一行(或多行)。

所谓“规范”,即指一个主要功能是执行该操作的程序。


11
“Unix方式”是将能够很好地完成各自工作的工具链接在一起。所以我认为你已经找到了一个非常合适的方法。其他方法包括 awksed,我相信还有人可以想出一个Perl单行命令之类的方法 ;) - 0xC0000022L
4
双重命令提示“head | tail”解决方案不是最优的。其他更接近最优的解决方案已被提出。 - Jonathan Leffler
你有没有对哪种解决方案在平均情况下最快进行过基准测试? - Marcin
8
在Unix.SE上,针对一个大文件在[cat行X到行Y的范围内]的基准测试结果(参考值)。(在两年多之后如果你还有疑问,@Marcin) - Kevin
13
如果查询不存在于输入中的行,则“head | tail”解决方案无效:它将打印最后一行。 - jarno
显示剩余3条评论
24个回答

7
作为对CaffeineConnoisseur非常有帮助的基准测试答案的跟进...我很想知道“mapfile”方法与其他方法相比有多快(因为这并没有被测试),所以我自己尝试了一次快速而肮脏的速度比较,因为我手头上确实有bash 4。当我在尝试时,还加入了一个“tail | head”方法的测试(而不是head | tail),因为人们都在赞扬它。我找到的文件大小远远没有测试文件那么大;我能够找到的最好的东西是一个14M的家谱文件(长行是由空格分隔的,仅有不到12000行)。
简短版:mapfile看起来比cut方法更快,但比其他所有方法都慢,所以我会称其为鸡肋。另一方面,“tail | head”似乎可能是最快的,虽然在这个文件大小下,与sed相比的差别并不是很大。
$ time head -11000 [filename] | tail -1
[output redacted]

real    0m0.117s

$ time cut -f11000 -d$'\n' [filename]
[output redacted]

real    0m1.081s

$ time awk 'NR == 11000 {print; exit}' [filename]
[output redacted]

real    0m0.058s

$ time perl -wnl -e '$.== 11000 && print && exit;' [filename]
[output redacted]

real    0m0.085s

$ time sed "11000q;d" [filename]
[output redacted]

real    0m0.031s

$ time (mapfile -s 11000 -n 1 ary < [filename]; echo ${ary[0]})
[output redacted]

real    0m0.309s

$ time tail -n+11000 [filename] | head -n1
[output redacted]

real    0m0.028s

希望这有所帮助!

6
以上所有答案都直接回答了问题。但是这里有一个不太直接的解决方案,但可能更重要的想法,用于引发思考。
由于行长度是任意的,在第n行之前文件的所有字节都需要被读取。如果您有一个巨大的文件或需要多次重复此任务,并且这个过程非常耗时,那么您应该认真考虑是否应该首先以不同的方式存储数据。
真正的解决方案是拥有一个索引,例如在文件开头指示行开始位置的位置。您可以使用数据库格式,或者只需在文件开头添加一个表格。或者创建一个单独的索引文件来陪伴您的大型文本文件。
例如,您可以创建一个换行符的字符位置列表:
awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx

然后使用tail读取文件,它实际上会直接seek到文件中适当的位置!

例如,要获取第1000行:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • 由于awk是“字符感知”的,而tail则不是,因此此方法可能无法与2字节/多字节字符一起使用。
  • 我还没有针对大文件进行测试。
  • 另请参见此答案
  • 或者,将您的文件拆分为较小的文件!

6

对于大文件来说,最快的解决方案始终是使用tail|head,前提是需要知道以下两个距离:

  • 从文件开头到起始行的距离。我们称之为S
  • 从最后一行到文件结尾的距离。我们称之为E

然后,我们可以使用以下方法:

mycount="$E"; (( E > S )) && mycount="+$S"
howmany="$(( endline - startline + 1 ))"
tail -n "$mycount"| head -n "$howmany"

howmany只是需要的行数计数。

https://unix.stackexchange.com/a/216614/79743中有更多细节。


1
请澄清 SE 的单位,即字节、字符或行数。 - agc
@agc 这里的单位是行。 - Lee Meador
我不认为这是一个好的解决方案,因为它需要文件结束的距离作为输入 - 当你计算到达末尾的距离时,你已经至少扫描了整个文件一次,所以这有什么意义呢? - RARE Kpop Manifesto

4
如果您有多个由“\n”(通常是新行)分隔的行,您也可以使用“cut”命令:
echo "$data" | cut -f2 -d$'\n'

您将从文件中获取第二行。使用 -f3 可以让您得到第三行。

2
可以用于显示多行:cat FILE | cut -f2,5 -d$'\n' 将显示文件中的第2和第5行。(但它不会保持顺序。) - Andriy Makukha

4

根据其他人提到的内容,我希望这个功能在我的bash shell中能够快速实现。

创建一个文件:~/.functions

将以下内容添加到文件中:

getline() { line=$1 sed $line'q;d' $2 }

然后将其添加到~/.bash_profile中:

source ~/.functions

现在当你打开一个新的bash窗口时,你可以像下面这样调用函数:

getline 441 myfile.txt


在使用$1之前,无需将其分配给另一个变量,并且您正在破坏任何其他全局line。 在Bash中,对于函数变量,请使用local;但是在这里,如已经说明的那样,可能只需执行sed“$1d;q”“$2”。(还要注意引用“$2”。) - tripleee
正确,但自注释的代码可能会更有帮助。 - Mark Shust at M.academy

3

已经有很多不错的答案了,我个人更倾向于使用 awk。如果你使用的是 bash,只需将以下内容添加到你的 ~/.bash_profile 文件中。然后,下次登录时(或者在更新后执行 source ~/.bach_profile),你就可以通过一个新的神奇函数“nth”来处理你的文件了。

执行此命令或将其放入你的 ~/.bash_profile 文件中(如果使用 bash),并重新打开 bash(或执行 source ~/.bach_profile)。

# print just the nth piped in line
nth () { awk -vlnum=${1} 'NR==lnum {print; exit}'; } 

然后,要使用它,只需在管道中使用它。例如:
$ yes line | cat -n | nth 5
     5  line

1

这不是一个bash解决方案,但我发现前面的选择都不能满足我的需求,例如:

sed 'NUMq;d' file

这个程序运行速度已经足够快,但是会挂起数小时且没有任何进展提示。我建议编译这个cpp程序并使用它来找到你想要的行。你可以用g++ main.cpp来编译,其中main.cpp是下面内容的文件。我得到了a.out并通过./a.out执行它。

#include <iostream>
#include <string>
#include <fstream>

using namespace std;

int main() {
    string filename;
    cout << "Enter filename ";
    cin >> filename;

    int needed_row_number;
    cout << "Enter row number ";
    cin >> needed_row_number;

    int progress_line_count;
    cout << "Enter at which every number of rows to monitor progress ";
    cin >> progress_line_count;

    char ch;
    int row_counter = 1;
    fstream fin(filename, fstream::in);
    while (fin >> noskipws >> ch) {
        int ch_int = (int) ch;
        if (row_counter == needed_row_number) {
            cout << ch;
        }
        if (ch_int == 10) {
            if (row_counter == needed_row_number) {
                return 0;
            }
            row_counter++;
            if (row_counter % progress_line_count == 0) {
                cout << "Progress: line " << row_counter << endl;
            }
        }

    }
    return 0;
}

1

在查看最佳答案基准测试之后,我实现了一个小型辅助函数:

function nth {
    if (( ${#} < 1 || ${#} > 2 )); then
        echo -e "usage: $0 \e[4mline\e[0m [\e[4mfile\e[0m]"
        return 1
    fi
    if (( ${#} > 1 )); then
        sed "$1q;d" $2
    else
        sed "$1q;d"
    fi
}

基本上您可以以两种方式使用它:
nth 42 myfile.txt
do_stuff | nth 42

1
使用sed打印第n行,其中n是一个变量:

使用sed命令打印指定行数的内容,其中行数可以是一个变量:

a=4
sed -e $a'q:d' file

在这里,“-e”标志用于将脚本添加到要执行的命令中。

3
冒号是语法错误,应该是分号。 - tripleee

0

更新 1:在 awk 中找到了更快的方法

  • 只需5.353秒即可获取133.6百万以上的一行:
rownum='133668997'; ( time ( pvE0 < ~/master_primelist_18a.txt |

LC_ALL=C mawk2 -F'^$' -v \_="${rownum}" -- '!_{exit}!--_' ) )
in0: 5.45GiB 0:00:05 [1.02GiB/s] [1.02GiB/s] [======> ] 71%            
     ( pvE 0.1 in0 < ~/master_primelist_18a.txt | 
     LC_ALL=C mawk2 -F'^$' -v  -- ; )  5.01s user 

1.21秒系统 116% CPU 5.353 总计

77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=

===============================================

我想质疑“perl”比“awk”更快的观点:
虽然我的测试文件的行数不是很多,但它的大小是两倍,为7.58 GB。
我甚至给了“perl”一些内置优势——像硬编码行号,并且选择第二个,从而获得任何可能的操作系统缓存机制加速。
 f="$( grealpath -ePq  ~/master_primelist_18a.txt )"
 rownum='133668997'
 fg;fg; pv < "${f}" | gwc -lcm 
 echo; sleep 2; 
 echo; 
 ( time ( pv -i 0.1 -cN in0 < "${f}" | 
        
    LC_ALL=C mawk2 '_{exit}_=NR==+__' FS='^$' __="${rownum}" 

 ) ) | mawk 'BEGIN { print } END { print _ } NR' 
 sleep 2
 ( time ( pv -i 0.1 -cN in0 < "${f}" | 

    LC_ALL=C perl -wnl -e '$.== 133668997 && print && exit;' 

 ) ) | mawk 'BEGIN { print }  END { print _ } NR' ;

fg: no current job
fg: no current job
7.58GiB 0:00:28 [ 275MiB/s] [============>] 100%
        
148,110,134 8,134,435,629 8,134,435,629   <<<< rows, chars, and bytes 
                                               count as reported by gnu-wc



      in0: 5.45GiB 0:00:07 [ 701MiB/s] [=> ] 71%            
( pv -i 0.1 -cN in0 < "${f}" | LC_ALL=C mawk2 '_{exit}_=NR==+__' FS='^$' ; )  
   6.22s user 2.56s system 110% cpu 7.966 total
   77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=


      in0: 5.45GiB 0:00:17 [ 328MiB/s] [=> ] 71%            
( pv -i 0.1 -cN in0 < "${f}" | LC_ALL=C perl -wnl -e ; )  
   14.22s user 3.31s system 103% cpu 17.014 total
   77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=

如果您认为这会有所不同(我还没有安装任何一个),我可以使用perl 5.36甚至perl-6重新运行测试,但是两者之间存在

7.966秒(mawk2)17.014秒(perl 5.34)

两者之间的差距,后者超过前者的两倍以上,似乎很清楚哪个确实更快地获取深层ASCII文件中的单个行。

This is perl 5, version 34, subversion 0 (v5.34.0) built for darwin-thread-multi-2level

Copyright 1987-2021, Larry Wall


mawk 1.9.9.6, 21 Aug 2016, Copyright Michael D. Brennan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接