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

时间:2011-05-16 19:33:42

标签: bash shell unix awk sed

这样做是否有“规范”方式?我一直在使用head -n | tail -1来解决问题,但我一直想知道是否有一个Bash工具专门从文件中提取一行(或一系列行)。

“canonical”是指一个程序,其主要功能是这样做。

20 个答案:

答案 0 :(得分:651)

headtail管道对于一个巨大的文件来说会很慢。我建议sed像这样:

sed 'NUMq;d' file

其中NUM是您要打印的行号;所以,例如,sed '10q;d' file打印file的第10行。

说明:

当行号为NUMq时,

NUM会立即退出。

d会删除该行而不是打印它;这在最后一行被禁止,因为q导致在退出时跳过其余的脚本。

如果变量中有NUM,则需要使用双引号而不是单引号:

sed "${NUM}q;d" file

答案 1 :(得分:258)

sed -n '2p' < file.txt

将打印第2行

sed -n '2011p' < file.txt

2011th line

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

第10行到第33行

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

第1和第3行

依旧......

要添加带有sed的行,您可以查看:

sed: insert a line in a certain position

答案 2 :(得分:79)

我有一个独特的情况,我可以对此页面上提出的解决方案进行基准测试,因此我将此答案作为所提议解决方案的合并,并为每个解决方案包含运行时间。

设置

我有一个3.261千兆字节的ASCII文本数据文件,每行有一个键值对。该文件总共包含3,339,550,320行,并且在我尝试过的任何编辑器中都无法打开,包括我的首选Vim。我需要对这个文件进行子集化,以便调查我发现的一些值,这些值只能在行~500,000,000左右开始。

因为文件有很多行:

  • 我只需要提取行的一个子集来对数据执行任何有用的操作。
  • 阅读导致我关心的价值的每一行都需要很长时间。
  • 如果解决方案读取了我关心的行并继续阅读文件的其余部分,则会浪费时间阅读近30亿条不相关的行,并且需要的时间超过必要的6倍。

我的最佳情况是一个解决方案,它只从文件中提取一行而不读取文件中的任何其他行,但我想不出我将如何在Bash中完成此任务。

为了我的理智,我不会试图阅读我自己的问题所需的全部500,000,000行。相反,我将尝试从3,339,550,320中提取50,000,000行(这意味着读取完整文件所需的时间比所需时间长60倍)。

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

<强>基线

首先让我们看一下head tail解决方案的方式:

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

real    1m15.321s

5000万行的基线是00:01:15.321,如果我直接行5亿行,那可能是~12.5分钟。

<强>切

我怀疑这个,但值得一试:

$ time cut -f50000000 -d$'\n' myfile.ascii
pgm_icnt = 0

real    5m12.156s

这个以00:05:12.156运行,这比基线慢得多!我不确定它是在整个文件中读取还是在停止之前读取了5000万行,但不管这看起来不是解决这个问题的可行办法。

<强> AWK

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

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

real    1m16.583s

此代码在00:01:16.583中运行,速度仅慢约1秒,但仍未改善基线。按此速率,如果排除了退出命令,则可能需要大约76分钟才能读取整个文件!

<强>的Perl

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

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

real    1m13.146s

此代码在00:01:13.146中运行,比基线快约2秒。如果我在完整的500,000,000上运行它可能需要大约12分钟。

<强> SED

董事会的最佳答案,这是我的结果:

$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0

real    1m12.705s

此代码在00:01:12.705中运行,比基线快3秒,比Perl快〜0.4秒。如果我在完整的500,000,000行上运行它可能需要大约12分钟。

<强>映射文件

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

<强>结论

看起来,在大多数情况下,很难改进head tail解决方案。最好sed解决方案提高效率约3%。

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

行50,000,000

  1. 00:01:12.705(-00:00:02.616 = -3.47%)sed
  2. 00:01:13.146(-00:00:02.175 = -2.89%)perl
  3. 00:01:15.321(+00:00:00.000 = + 0.00%)head|tail
  4. 00:01:16.583(+00:00:01.262 = + 1.68%)awk
  5. 00:05:12.156(+00:03:56.835 = + 314.43%)cut
  6. 排500,000,000

    1. 00:12:07.050(-00:00:26.160)sed
    2. 00:12:11.460(-00:00:21.750)perl
    3. 00:12:33.210(+00:00:00.000)head|tail
    4. 00:12:45.830(+00:00:12.620)awk
    5. 00:52:01.560(+00:40:31.650)cut
    6. 第3,338,559,320行

      1. 01:20:54.599(-00:03:05.327)sed
      2. 01:21:24.045(-00:02:25.227)perl
      3. 01:23:49.273(+00:00:00.000)head|tail
      4. 01:25:13.548(+00:02:35.735)awk
      5. 05:47:23.026(+04:24:26.246)cut

答案 3 :(得分:44)

使用awk非常快:

awk 'NR == num_line' file

如果是这样,则执行awk的默认行为:{print $0}


替代版本

如果你的文件很大,那么在阅读完所需的专栏后你最好exit。这样可以节省CPU时间。

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

如果您想从bash变量中提供行号,可以使用:

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

答案 4 :(得分:26)

哇,所有的可能性!

试试这个:

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

或其中一个取决于您的Awk版本:

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可能是最接近和最简单的用途。

答案 5 :(得分:20)

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

Useful one-line scripts for sed

答案 6 :(得分:20)

这个问题被标记为Bash,这是Bash(≥4)的做法:将mapfile-s(跳过)和-n(计数)选项一起使用。

如果您需要获取文件的第42行file

mapfile -s 41 -n 1 ary < file

此时,您将拥有一个数组ary,其中的字段包含file行(包括尾随换行符),我们跳过前41行({{1 }}),并在读取一行(-s 41)后停止。所以这真的是第42行。打印出来:

-n 1

如果您需要一系列线条,比如42-666(含)范围,并说您不想自己做数学,并在标准输出上打印:

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

如果您还需要处理这些行,那么存储尾随换行符并不是很方便。在这种情况下,使用mapfile -s $((42-1)) -n $((666-42+1)) ary < file printf '%s' "${ary[@]}" 选项(修剪):

-t

你可以有一个功能为你做这个:

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

没有外部命令,只有Bash builtins!

答案 7 :(得分:12)

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

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'很有意思,但我认为开箱即用的人比头/尾解决方案更少,并且它也比尾/头慢。< / p>

在我的测试中,尾部/头部版本的表现始终优于sed 'NUMq;d'。这符合发布的其他基准。很难找到尾巴/头部非常糟糕的情况。这也就不足为奇了,因为这些操作在现代Unix系统中会被大量优化。

要了解性能差异,这些是我获得的大型文件的数量(9.3G):

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

结果可能会有所不同,但性能head | tailtail | head通常与较小的输入相当,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

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

*** 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

答案 8 :(得分:11)

您也可以使用sed打印并退出:

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

答案 9 :(得分:7)

您也可以使用Perl:

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

答案 10 :(得分:6)

对于大文件,最快的解决方案始终是尾部,只要两个距离:

  • 从文件的开头到起始行。让我们称之为$.cookie('menu-data', "Hello"); alert($.cookie("menu-data"));
  • 从最后一行到文件末尾的距离。是S

是众所周知的。然后,我们可以使用它:

E

howmany只是所需行数。

https://unix.stackexchange.com/a/216614/79743

中的更多细节

答案 11 :(得分:4)

作为CaffeineConnoisseur的一个非常有用的基准测试答案的后续内容......我很好奇地看到了#map;方法与其他方法进行了比较(因为没有经过测试),所以我尝试了一个快速和肮脏的速度比较,因为我有bash 4方便。投入了对尾巴的测试头&#34;方法(而不是头部)在我对它的最佳答案的评论之一中提到,因为人们正在赞美它。我没有任何与所用测试文件大小差不多的东西;我能在短时间内找到的最好的是一个14M的谱系文件(长行以空格分隔,不到12000行)。

简短版本:mapfile看起来比cut方法快,但比其他所有方法都慢,所以我称之为dud。尾巴|头,OTOH,看起来它可能是最快的,虽然这个尺寸的文件与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

希望这有帮助!

答案 12 :(得分:4)

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

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

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

e.g。您可以为换行创建一个字符位置列表:

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

然后使用tail阅读,其中seek直接到文件中的相应位置!

e.g。得到第1000行:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • 这可能不适用于2字节/多字节字符,因为awk是&#34;字符感知&#34;但尾巴不是。
  • 我还没有针对一个大文件对此进行测试。
  • 另见this answer
  • 或者 - 将文件拆分为较小的文件!

答案 13 :(得分:3)

如果你有多个由\ n分隔的行(通常是新行)。你也可以使用'cut':

echo "$data" | cut -f2 -d$'\n'

您将从文件中获取第二行。 -f3为您提供第3行。

答案 14 :(得分:2)

已经有很多好的答案。我个人跟awk一起去。为方便起见,如果您使用bash,只需将以下内容添加到~/.bash_profile即可。并且,下次登录时(或者如果您在此更新后获取.bash_profile),您将有一个新的漂亮的“第n”函数可用于管理文件。

执行此操作或将其放入〜/ .bash_profile(如果使用bash)并重新打开bash(或执行source ~/.bach_profile

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

然后,要使用它,只需通过它。 。E.g,:

$ yes line | cat -n | nth 5 5 line

答案 15 :(得分:1)

使用带有变量作为行号的sed打印第n行:

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

这里'-e'标志用于将脚本添加到要执行的命令。

答案 16 :(得分:1)

使用别人提到的内容,我希望这是一个快速的&amp;我的bash shell中的花花公子功能。

创建文件:~/.functions

添加内容:

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

然后将其添加到您的~/.bash_profile

source ~/.functions

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

getline 441 myfile.txt

答案 17 :(得分:1)

在查看了the top answerthe benchmark之后,我实现了一个很小的帮助函数:

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

答案 18 :(得分:0)

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

#!/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

确保它可以通过

执行
$ chmod +x get

通过

链接使其在PATH上可用

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

负责任地享受!

P

答案 19 :(得分:0)

保存两次击键,不使用括号打印第N行:

sed  -n  Np  <fileName>
      ^   ^
       \   \___ 'p' for printing
        \______ '-n' for not printing by default 

例如打印第 100 行:

sed -n 100p foo.txt