Bash工具从文件获取第n行


603

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

“规范”是指主要功能是执行此操作的程序。


10
“ Unix方式”是将功能良好的工具链接在一起。因此,我认为您已经找到了一种非常合适的方法。其他方法还包括awksed,我相信有人也可以提出一种Perl方案;)
0xC0000022L

3
双重命令建议head | tail解决方案不是最优的。已经提出了其他更接近最优的解决方案。
乔纳森·勒夫勒

您是否针对哪个案例在一般情况下最快进行了基准测试?
Marcin

5
Unix和Linux 上,在一个巨大文件上,第X行至第Y行的基准(范围)。(抄送@Marcin,如果您在两年后仍想知道的话)
凯文(Kevin

6
head | tail如果您查询输入中不存在的行,该解决方案将不起作用:它将打印最后一行。
jarno

Answers:


800

head和管道tail对于大型文件来说会很慢。我建议sed这样:

sed 'NUMq;d' file

NUM您要打印的行号在哪里;因此,例如,sed '10q;d' file打印的第十行file

说明:

NUMq行号为时将立即退出NUM

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

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

sed "${NUM}q;d" file

44
对于那些想知道的人,此解决方案似乎比下面提出的sed -n 'NUMp'sed 'NUM!d'解决方案快6到9倍。
Skippy le Grand Gourou 2014年

75
我认为tail -n+NUM file | head -n1可能会一样快或更快。至少,当我尝试使用NUM是250000且文件行数为100万行时,它在系统上的速度(明显)更快。YMMV,但我真的不明白为什么会这样。
rici 2014年

2
@rici(先前评论的修订)在Linux(Ubuntu 12.04,Fedora 20)上,使用cat确实确实更快(几乎快一倍),但前提是尚未缓存文件缓存文件后,直接使用filename参数的速度更快(大约快1/3),而cat性能保持不变。奇怪的是,在OS X 10.9.3上,这似乎没有任何区别:cat/ no cat,是否缓存文件。@anubhava:我很高兴。
mklement0 2014年

2
@SkippyleGrandGourou:鉴于此优化特殊性质,一般而言,即使您的数字范围没有意义。唯一的概括是:(a)此优化可以安全地应用于所有输入,(b)效果范围从无到显着,具体取决于所寻求的行相对于总行数的索引。
mklement0 2014年

17
sed 'NUMq将输出第一个NUM文件,;d并删除除最后一行以外的所有文件。
anubhava

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

将打印第二行

sed -n '2011p' < file.txt

2011年

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

第10行到第33行

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

第一和第三行

等等...

要使用sed添加行,您可以检查以下内容:

sed:在特定位置插入一行


6
@RafaelBarbosa <在这种情况下是不必要的。简而言之,我偏爱使用重定向,因为我经常使用重定向,例如sed -n '100p' < <(some_command)-通用语法:)。它并不是很有效,因为重定向是在派生自身时使用shell完成的,所以...它只是一个首选项...(是的,它是一个字符长):)
jm666

1
@ jm666其实是2个字符,因为你通常把“<”以及之后<多余的空格“”作为oppposed只是一个空间,如果你没有使用<:)
rasen58

2
@ rasen58空格也是字符吗?:) /好吧,开个玩笑
-youre

1
@duhaime当然,如果有人需要进行优化。但是恕我直言,“常见”问题还可以,而且区别并不明显。此外,head/ tail不能解决问题sed -n '1p;3p'-也可以打印更多不相邻的行...
jm666

1
@duhaime当然是-注释正确无误。:)
jm666

93

我有一个独特的情况,可以在此页面上对提出的解决方案进行基准测试,因此我将这个答案写成对提出的解决方案的合并,其中包括每个解决方案的运行时间。

设定

我有一个3.261 GB的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解决方案:

$ 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

第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

第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

4
我不知道将整个文件放入/ dev / null需要多长时间。(如果这只是硬盘基准测试该怎么办?)
sanmai

我对您拥有3+ gig文本文件字典的所有权感到不高兴。无论理由如何,它都包含文本性:)
Stabledog

51

有了awk它很快:

awk 'NR == num_line' file

如果为true,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

查看使用可以节省多少时间exit,特别是如果该行恰好位于文件的第一部分中时:

# Let's create a 10M lines file
for ((i=0; i<100000; i++)); do echo "bla bla"; done > 100Klines
for ((i=0; i<100; i++)); do cat 100Klines; done > 10Mlines

$ time awk 'NR == 1234567 {print}' 10Mlines
bla bla

real    0m1.303s
user    0m1.246s
sys 0m0.042s
$ time awk 'NR == 1234567 {print; exit}' 10Mlines
bla bla

real    0m0.198s
user    0m0.178s
sys 0m0.013s

因此相差0.198s和1.303s,快了6倍。


由于awk尝试进行字段拆分,因此此方法总是比较慢。场分割的开销可以减少awk 'BEGIN{FS=RS}(NR == num_line) {print; exit}' file
kvantour

当您要串联file1的n1,file2的n2,n3或file3 ...时,awk的真正作用就显现出来了awk 'FNR==n' n=10 file1 n=30 file2 n=60 file3。使用GNU awk可以使用加快速度awk 'FNR==n{print;nextfile}' n=10 file1 n=30 file2 n=60 file3
kvantour

@kvantour确实,GNU awk的nextfile对于此类事情非常有用。如何FS=RS避免字段分裂?
fedorqui'SO停止伤害'

1
FS=RS不会避免字段拆分,但只会解析$ 0,并且仅分配一个字段,因为没有RSin$0
kvantour

@kvantour我一直在进行一些测试,FS=RS但没有发现时间上的差异。那我问一个问题以便您可以扩展呢?谢谢!
fedorqui'SO停止伤害'

29

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

tail -n+N | head -1

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

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


替代方案head -N | tail -1可能更具可读性。例如,这将打印第七行:

head -7 input.txt | tail -1

在性能方面,较小的文件并没有太大的区别,但是tail | head当文件变大时,它的性能将优于(从上方)。

投票最多的人sed 'NUMq;d'很有趣,但是我想说的是,相比头/尾解决方案,开箱即用的人会更少地理解它,而且比尾/头解决方案还慢。

在我的测试中,两个尾巴/头部版本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

1
head | tailvs 之间的性能是否不同tail | head?还是取决于打印的是哪行(文件开头还是文件结尾)?
wisbucky

1
@wisbucky我没有确切的数字,但是先使用尾巴后跟“ head -1”的一个缺点是您需要提前知道总长度。如果您不知道,则必须先将其计数,这将是性能损失。另一个缺点是使用起来不太直观。例如,如果您有1到10的数字,并且想获得第三行,则必须使用“ tail -8 | head -1”。比“ head -3 | tail -1”更容易出错。
PhilippClaßen18年

抱歉,我应该提供一个示例来说明。head -5 | tail -1VS tail -n+5 | head -1。实际上,我找到了另一个答案进行了测试比较,发现tail | head更快。stackoverflow.com/a/48189289
wisbucky

1
@wisbucky感谢您提及它!我进行了一些测试,必须同意,它总是稍微快一些,与我看到的线的位置无关。鉴于此,我更改了答案,并包括基准,以防有人想要重现它。
PhilippClaßen18年

27

哇,所有的可能性!

尝试这个:

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

您可能必须尝试使用nawkor gawk命令)。

有没有只打印特定行的工具?不是标准工具之一。但是,sed可能是最接近和最简单的使用方法。



21

这个问题被标记为Bash,这是Bash(≥4)的处理方式:mapfile-s(skip)和-n(count)选项一起使用。

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

mapfile -s 41 -n 1 ary < file

至此,您将获得一个数组ary,该数组的字段包含的行file(包括尾随的换行符),我们已跳过了前41行(-s 41),并在读取了一行(-n 1)之后停止了。这就是第42行。要打印出来:

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

如果您需要一定范围的行,请说范围为42–666(含),并说您不想自己做数学,并在stdout上打印它们:

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

如果您也需要处理这些行,则存储尾随的换行符并不是很方便。在这种情况下,请使用-t选项(trim):

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

您可以使用一个函数为您执行此操作:

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内置函数!


11

您还可以使用sed print并退出:

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

6
-n选项将禁用默认操作以打印每行,就像您通过快速浏览手册页肯定会发现的那样。
13年

GNU中, sed所有sed答案的速度都差不多。因此(对于GNU来说 sed),这是最好的sed答案,因为这样可以节省大文件和小nth行值的时间。
agc


6

对于大文件,最快的解决方案始终是tail | head,前提是两个距离:

  • 从文件的开头到起始行。让我们称之为S
  • 从最后一行到文件末尾的距离。就是这样E

众所周知。然后,我们可以使用以下代码:

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

多少只是所需的行数。

https://unix.stackexchange.com/a/216614/79743中的更多详细信息


1
请澄清的单元SE,(即字节,字符,或线)。
agc

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
  • 这可能不适用于2字节/多字节字符,因为awk是“可识别字符”的,而tail则不是。
  • 我尚未针对大型文件进行过测试。
  • 另请参阅此答案
  • 或者-将文件拆分为较小的文件!

5

作为CaffeineConnoisseur很有帮助的基准测试答案的后续措施……我很好奇“ mapfile”方法与其他方法相比有多快(因为未经测试),因此我自己尝试了快速和较慢的速度比较我确实有方便的bash 4。当我在上面回答时,对其中一个注释中提到的“尾|头”方法(而不是头|尾)进行了测试,因为人们正在赞美它。我没有所用测试文件大小的任何东西;在短时间内,我能找到的最好的文件是一个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

希望这可以帮助!


4

使用其他人提到的内容,我希望它成为bash shell中的快速功能。

创建一个文件: ~/.functions

添加内容:

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

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

source ~/.functions

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

getline 441 myfile.txt


3

如果您用\ n分隔多行(通常是新行)。您也可以使用“剪切”:

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

您将从文件中获得第二行。-f3给你第三行。


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

2

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

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

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


2
冒号是语法错误,应为分号。
Tripleee '16

2

已经有很多好的答案。我个人与awk一起去。为了方便起见,如果您使用bash,请将以下内容添加到中~/.bash_profile。而且,下次登录时(或者如果您在此更新后获取.bash_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

考虑看看后最多的回答 基准,我已经实现了一个很小的辅助函数:

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.