有没有一种“规范”的方法?我一直在使用head-n|tail-1来完成这个任务,但我一直在想是否有一个Bash工具专门从文件中提取一行(或一系列行)。

我所说的“规范”是指一个主要功能就是这样做的程序。


当前回答

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

仅需5.353秒即可获得133.6 mn以上的行:

行号='133668997';(时间(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.21s系统116%cpu 5.353总计

77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=

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

我想质疑perl比awk更快的观点:

因此,虽然我的测试文件的行数没有那么多,但它的大小也是原来的两倍,为7.58GB-

我甚至给了perl一些内置的优势——比如行号中的硬编码,并且还排在第二位,从而从OS缓存机制中获得了任何潜在的加速(如果有的话)

 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=

我可以用perl5.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

其他回答

根据我的测试,就性能和可读性而言,我的建议是:

尾部-n+n|头部-1

N是您想要的行号。例如,tail-n+7 input.txt | head-1将打印文件的第7行。

tail-n+n将打印从第n行开始的所有内容,head-1将使其在一行之后停止。


可选的head-N|tail-1可能更可读。例如,这将打印第7行:

head-7 input.txt | tail-1

当谈到性能时,较小的文件大小没有太大的差异,但当文件变大时,尾部|头部(从上方)的性能会优于尾部|头部。

排名靠前的是“NUMq;d’很有意思,但我认为,与头/尾解决方案相比,开箱即用的人更少,而且它也比尾/头慢。

在我的测试中,两个尾部/头部版本都优于sed的NUMq;d’一致。这与发布的其他基准一致。很难找到尾巴/脑袋真的很坏的案例。这也不奇怪,因为这些操作在现代Unix系统中会被大量优化。

为了了解性能差异,以下是我从一个巨大文件(9.3G)中得到的数字:

tail-n+n | head-1:3.7秒头-N|尾-1:4.6秒sed Nq;d: 18.8秒

结果可能有所不同,但总体而言,性能头部|尾部和尾部|头部对于较小的输入来说是可比的,sed总是慢了一个重要因素(大约5倍左右)。

要复制我的基准测试,您可以尝试以下操作,但请注意,它将在当前工作目录中创建一个9.3G文件:

#!/bin/bash
readonly file=tmp-input.txt
readonly size=1000000000
readonly pos=500000000
readonly retries=3

seq 1 $size > $file
echo "*** head -N | tail -1 ***"
for i in $(seq 1 $retries) ; do
    time head "-$pos" $file | tail -1
done
echo "-------------------------"
echo
echo "*** tail -n+N | head -1 ***"
echo

seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
    time tail -n+$pos $file | head -1
done
echo "-------------------------"
echo
echo "*** sed Nq;d ***"
echo

seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
    time sed $pos'q;d' $file
done
/bin/rm $file

这是在我的机器上运行的输出(ThinkPad X1 Carbon,带有SSD和16G内存)。我假设在最后一次运行中,所有内容都将来自缓存,而不是磁盘:

*** head -N | tail -1 ***
500000000

real    0m9,800s
user    0m7,328s
sys     0m4,081s
500000000

real    0m4,231s
user    0m5,415s
sys     0m2,789s
500000000

real    0m4,636s
user    0m5,935s
sys     0m2,684s
-------------------------

*** tail -n+N | head -1 ***

-rw-r--r-- 1 phil 9,3G Jan 19 19:49 tmp-input.txt
500000000

real    0m6,452s
user    0m3,367s
sys     0m1,498s
500000000

real    0m3,890s
user    0m2,921s
sys     0m0,952s
500000000

real    0m3,763s
user    0m3,004s
sys     0m0,760s
-------------------------

*** sed Nq;d ***

-rw-r--r-- 1 phil 9,3G Jan 19 19:50 tmp-input.txt
500000000

real    0m23,675s
user    0m21,557s
sys     0m1,523s
500000000

real    0m20,328s
user    0m18,971s
sys     0m1,308s
500000000

real    0m19,835s
user    0m18,830s
sys     0m1,004s

以上所有答案都直接回答了这个问题。但这是一个不那么直接的解决方案,但可能是一个更重要的想法,可以引起人们的思考。

由于行长度是任意的,因此需要读取文件第n行之前的所有字节。如果您有一个巨大的文件或需要多次重复此任务,并且此过程非常耗时,那么您应该认真考虑是否应该首先以不同的方式存储数据。

真正的解决方案是有一个索引,例如在文件的开头,指示行开始的位置。您可以使用数据库格式,或者在文件开头添加一个表。或者,创建一个单独的索引文件,与大型文本文件一起使用。

例如,您可以为换行符创建一个字符位置列表:

awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx

然后用tail读取,它实际上直接查找文件中的适当点!

例如获得线1000:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1

这可能不适用于2字节/多字节字符,因为awk是“字符识别”的,但tail不是。我还没有对一个大文件进行测试。另请参阅此答案。或者,将文件拆分为更小的文件!

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

sed 'NUMq;d' file

速度足够快,但挂了几个小时,没有告诉任何进展。我建议编译这个cpp程序并使用它来查找所需的行。您可以使用g++main.cpp编译它,其中main.cpp是包含以下内容的文件。我得到了一个,并执行了它/a.输出

#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;
}

您也可以使用Perl实现这一点:

perl -wnl -e '$.== NUM && print && exit;' some.file

使用其他人提到的内容,我希望这是我的bash shell中一个快速而出色的函数。

创建文件:~/.functions

添加内容:

getline(){行=$1sed$line'q;2美元}

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

source~/.函数

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

获取行441 myfile.txt