关于shell:从文件中获取第n行的Bash工具

Bash tool to get nth line from a file

有没有一种"规范"的方法来做到这一点?我一直在使用head -n | tail -1来实现这个技巧,但我一直在想是否有一个bash工具专门从文件中提取一行(或一系列行)。

"规范"是指一个程序,它的主要功能就是这样做。


管与tailhead和巨大的文件将是缓慢的。我会像这样:suggest sed

1
sed 'NUMq;d' file

NUM是一些线你想打印;操作系统,例如,在10 sed '10q;d' filefile到打印线。

解释:

NUMq将退出立即当一NUM线数。

d将删除它,而不是印刷线,这是因为抑制的最后一q线的其余部分被跳过,当quitting的剧本。

如果你有一个在NUM变量,你将要使用双引号而不是单:

1
sed"${NUM}q;d" file


1
sed -n '2p' < file.txt

第二,将打印线

1
sed -n '2011p' < file.txt

2011th线

1
sed -n '10,33p' < file.txt

线到线10 33

1
sed -n '1p;3p' < file.txt

线1和3

在线等…………………

如果你可以将线与检查:对话,这

对话:插入线在一定的位置


我有一个独特的情况,我可以在这个页面上对建议的解决方案进行基准测试,所以我写这个答案是将建议的解决方案与每个解决方案包含的运行时间合并在一起。

设置

我有一个3.261千兆字节的ASCII文本数据文件,每行一个键值对。该文件总共包含3339550320行,并且拒绝在我尝试过的任何编辑器中打开,包括我的go to vim。我需要对这个文件进行子集,以便调查我发现的一些值,这些值只从大约500000000行开始。

因为文件有这么多行:

  • 我只需要提取行的一个子集,就可以对数据进行任何有用的操作。
  • 阅读每一行我关心的价值观都需要很长时间。
  • 如果解决方案读取的内容超过了我关心的行,并且继续读取文件的其余部分,那么它将浪费时间读取近30亿个无关行,并比需要的时间长6倍。

我的最佳方案是在不读取文件中任何其他行的情况下,只从文件中提取一行,但我想不出如何在bash中完成这一点。

为了我的理智,我不会去阅读我自己的问题所需要的全部5亿行。相反,我将尝试从3339550320中提取第50000000行(这意味着读取完整文件将比需要的时间长60倍)。

我将使用time内置的工具对每个命令进行基准测试。

基线

首先,让我们看看headtail解决方案是如何:

1
2
3
4
$ time head -50000000 myfile.ascii | tail -1
pgm_icnt = 0

real    1m15.321s

第5000万行的基线是00:01:15.321,如果我直走到第5亿行,大概是12.5分钟。

我对这件事很怀疑,但值得一试:

1
2
3
4
5
$ time cut -f50000000 -d$'
'
myfile.ascii
pgm_icnt = 0

real    5m12.156s

这一次用了00:05:12.156跑,比基线慢得多!我不确定它是读取整个文件,还是在停止前读取多达5000万行,但无论如何,这似乎不是解决问题的可行方案。

AWK

我只使用exit运行解决方案,因为我不会等待完整文件运行:

1
2
3
4
$ time awk 'NR == 50000000 {print; exit}' myfile.ascii
pgm_icnt = 0

real    1m16.583s

这段代码以00:01:16.583的速度运行,只慢了约1秒,但仍然没有改善基线。按照这个速度,如果exit命令被排除在外,读取整个文件可能需要大约76分钟!

珀尔

我也运行了现有的Perl解决方案:

1
2
3
4
$ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii
pgm_icnt = 0

real    1m13.146s

此代码以00:01:13.146运行,比基线快约2秒。如果我能以5亿美元的速度运行,大概需要12分钟。

塞德

最重要的答案是:

1
2
3
4
$ time sed"50000000q;d" myfile.ascii
pgm_icnt = 0

real    1m12.705s

此代码在00:01:12.705内运行,比基线快3秒,比Perl快约0.4秒。如果我把它放在5亿行,大概需要12分钟。

映射文件

我有bash 3.1,因此无法测试mapfile解决方案。

结论

在大多数情况下,似乎难以改进headtail解决方案。sed解决方案最多可提高3%的效率。

(用公式% = (runtime/baseline - 1) * 100计算的百分比)

第50000000行

  • 00:01:12.705(-00:00:02.616=-3.47%)sed
  • 00:01:13.146(-00:00:02.175=-2.89%)perl
  • 00:01:15.321(+00:00:00.000=+0.00%)head|tail
  • 00:01:16.583(+00:00:01.262=+1.68%)东1〔11〕。
  • 00:05:12.156(+00:03:56.835=+314.43%)cut
  • 第500000000行

  • 00:12:07.050(-00:00:26.160)东1〔6〕。
  • 00:12:11.460(-00:00:21.750)以东十一〔9〕。
  • 00:12:33.210(+00:00:00.000)head|tail
  • 00:12:45.830(+00:00:12.620)awk
  • 00:52:01.560(+00:40:31.650)cut
  • 世界其他地区3338559320

  • 01:20:54.599(-00:03:05.327)以东十一〔6〕。
  • 01:21:24.045(-00:02:25.227)东1〔9〕。
  • 01:23:49.273(+00:00:00.000)东1〔10〕。
  • 01:25:13.548(+00:02:35.735)awk
  • 05:47:23.026(+04:24:26.246)cut

  • 它是快速和awk漂亮:

    1
    awk 'NR == num_line' file

    当这是真的,默认的行为是:{print $0}awk学院完成。

    替代版本

    如果你的文件发生的是巨大的,你最好exit里丁线后的要求。这种方式你节省CPU时间。

    1
    awk 'NR == num_line {print; exit}' file

    如果你想给线数从shell变量,你可以使用:

    1
    2
    awk 'NR == n' n=$num file
    awk -v n=$num 'NR == n' file   # equivalent


    哇,所有的可能性!

    试试这个:

    1
    sed -n"${lineNum}p" $file

    在一个或这些取决于你的awk版本:

    1
    2
    3
    awk  -vlineNum=$lineNum 'NR == lineNum {print $0}' $file
    awk -v lineNum=4 '{if (NR == lineNum) {print $0}}' $file
    awk '{if (NR == lineNum) {print $0}}' lineNum=$lineNum $file

    (你可能要尝试的nawkgawk命令)。

    有工具,特别是只做打印线?没有一个标准的工具。然而,最近的sed和简可能是使用。


    1
    2
    # print line number 52
    sed '52!d' file

    有用的sed脚本是一个地图


    这个问题被标记在bash Bash的方式做(4):使用与-smapfile(跳过)和-n(计数)的选项。

    如果你需要把这个文件file42线:

    1
    mapfile -s 41 -n 1 ary < file

    在这一点上,你有一个阵列的场ary含其中的条(包括file尾随换行符),在我们的第一个跳过的41株(-s 41)和停止后,一读线(-n 1)。这真的是在第42行。到打印出来:

    1
    printf '%s'"${ary[0]}"

    如果你需要的范围线,42–666说的范围(包括)和说,你不想你的数学,他们的在线和打印输出:

    1
    2
    mapfile -s $((42-1)) -n $((666-42+1)) ary < file
    printf '%s'"${ary[@]}"

    如果你需要这些线的过程也不是很方便,到店后换行符。在这个案例的使用-t期权(饰):

    1
    2
    3
    4
    mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file
    # do stuff
    printf '%s
    '
    "${ary[@]}"

    你可以有你的函数是:

    1
    2
    3
    4
    5
    6
    print_file_range() {
        # $1-$2 is the range of file $3 to be printed to stdout
        local ary
        mapfile -s $(($1-1)) -n $(($2-$1+1)) ary <"$3"
        printf '%s'"${ary[@]}"
    }

    在外部bash命令,仅内置!


    你也可以使用sed打印和退出:

    1
    sed -n '10{p;q;}' file   # print line 10


    根据我的测试,在性能和可读性方面,我的建议是:

    tail -n+N | head -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

    在性能方面,较小的文件大小没有太大的差别,但当文件变得巨大时,它会比tail | head更好。

    最有投票权的sed 'NUMq;d'很有意思,但我认为,与头尾解决方案相比,开箱即用的人更少,而且比头尾解决方案慢。

    在我的测试中,两个tail/heads版本始终优于sed 'NUMq;d'。这与公布的其他基准一致。很难找到一个尾巴/头非常糟糕的情况。这也不足为奇,因为在现代的UNIX系统中,这些操作是您期望得到高度优化的。

    为了了解性能差异,这些是我从一个大文件(9.3g)中得到的数字:

    • tail -n+N | head -13.7秒
    • head -N | tail -1:4.6秒
    • sed Nq;d18.8秒

    结果可能会有所不同,但一般来说,head | tailtail | head的性能与较小的输入相当,并且sed的速度总是较慢的,这是一个重要因素(大约5倍左右)。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #!/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内存)。我假设在最后一次运行中,所有内容都将来自缓存,而不是磁盘:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    *** 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


    你也可以使用Perl是这样的:

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

    该文件是最快的解决方案总是头大,尾|,提供两个距离:

    • 从文件的开始的起点。我们呼叫它S
    • 该线的距离到最后结束的文件。它是E

    是已知的。然后,我们可以使用这个。

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

    你是一只howmany线的需要。

    更多细节将在unix.stackexchange.com https://///79743 216614


    以上答案都直接回答问题。但这里有一个不那么直接的解决方案,但有一个潜在的更重要的想法,来激发人们的想法。

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

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

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

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

    然后用tail读取,实际seek直接到文件中的适当点!

    例如,要获取第1000行:

    1
    tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
    • 这可能不适用于2字节/多字节字符,因为awk是"字符感知的",但tail不是。
    • 我还没有用一个大文件测试过这个。
    • 也可以看到这个答案。
    • 或者-将文件拆分为较小的文件!

    作为对咖啡馆非常有帮助的基准答案的跟进…我很好奇"mapfile"方法与其他方法相比有多快(因为它没有经过测试),所以我自己尝试了一个快速而肮脏的速度比较,因为我有bash 4。当我在上面的答案上发表评论的时候,在人们唱赞歌的时候,对上面提到的"尾头"方法(而不是头尾)进行了测试。我没有使用的测试文件大小的任何东西;我能在短时间内发现的最好的是一个14M的系谱文件(长的行是空格分隔的,小于12000行)。

    简短的版本:mapfile看起来比cut方法快,但比其他方法慢,所以我称它为dud。尾头,奥托,看起来它可能是最快的,尽管有了这个大小的文件,与SED相比差异并不大。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    $ time head -11000 [filename] | tail -1
    [output redacted]

    real    0m0.117s

    $ time cut -f11000 -d$'
    '
    [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

    希望这有帮助!


    如果你有多个分隔线(通常由N(新线)。你可以使用"切"的目的。

    1
    2
    echo"$data" | cut -f2 -d$'
    '

    你会得到从第二行的文件。你-f3给第三线。


    已经有很多好的答案了。我个人和awk一起去。为了方便起见,如果您使用bash,只需将下面的内容添加到您的~/.bash_profile中即可。而且,下次登录时(或者如果在这个更新之后源于.bash_配置文件),您将有一个新的漂亮的"nth"函数来传输文件。

    执行此操作或将其放入~/.bash_配置文件(如果使用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


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

    创建文件:~/.functions

    添加内容:


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

    然后将此添加到您的~/.bash_profile中:

    source ~/.functions

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

    getline 441 myfile.txt


    n采用SED与印刷线的线号:可变

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

    这里的"-"标志是添加到脚本的命令被执行。


    我把上面的一些答案放到一个简短的bash脚本中,你可以把它放到一个名为get.sh的文件中,并链接到/usr/local/bin/get(或者你喜欢的其他名称)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #!/bin/bash
    if ["${1}" =="" ]; then
        echo"error: blank line number";
        exit 1
    fi
    re='^[0-9]+$'
    if ! [[ $1 =~ $re ]] ; then
        echo"error: line number arg not a number";
        exit 1
    fi
    if ["${2}" =="" ]; then
        echo"error: blank file name";
        exit 1
    fi
    sed"${1}q;d" $2;
    exit 0

    确保它是可执行的

    1
    $ chmod +x get

    链接它,使其在PATH上可用

    1
    $ ln -s get.sh /usr/local/bin/get

    负责任地享受吧!