从文件中获取第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个回答

1100

head和管道符与tail在处理大文件时速度较慢。我建议像这样使用sed

sed 'NUMq;d' file

这里的NUM是您想要打印的行数; 例如, sed '10q;d' file 打印file文件中的第10行。

解释:

NUMq将在行数为NUM时立即退出。

d将删除该行而不是打印它;但在最后一行,由于q导致脚本在退出时被跳过,因此不会删除最后一行。

如果您将NUM存储在变量中,则需要使用双引号而不是单引号:

sed "${NUM}q;d" file

60
对于那些想知道的人,这个解决方案似乎比下面提出的 sed -n 'NUMp'sed 'NUM!d' 方法快6至9倍。 - Skippy le Grand Gourou
86
我认为 tail -n+NUM file | head -n1 可能会和原来的命令一样快,甚至更快。至少在我的系统上,当我使用 NUM 为 250000 的文件,文件有五十万行时,这个命令比原来那个要(显著)快。你的情况可能不同,但我实在看不出来为什么会不同。 - rici
2
不,没有 q 它将处理整个文件。 - anubhava
1
@Fiddlestiques:别忘了加引号才能让它变成 foo="$(sed "4q;d" file4)" - anubhava
1
@anubhava - 谢谢 - 现在明白了 - 使用echo "$foo"而不是echo $foo - Fiddlestiques
显示剩余11条评论

412
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

第1和第3行

等等...

要在sed中添加行,您可以查看此内容:

sed:在特定位置插入行


3
为什么在这种情况下需要使用 '<' 符号?如果没有它,我不是也能得到相同的结果吗? - Rafael Barbosa
9
@RafaelBarbosa 在这种情况下,尖括号<不是必需的。只是我个人喜欢使用重定向,因为我经常使用像sed -n '100p' < <(some_command)这样的重定向语法-所以,这是通用语法 :)。它并不会影响效果,因为当分叉自身时,重定向是由Shell完成的...这只是一种个人偏好...(而且是多了一个字符) :) - clt60
1
@jm666 实际上,如果您通常会在“<”后面放置额外的空格“ ”,而不是只有一个空格,那么它会比没有使用“<”多2个字符 :) - rasen58
2
@rasen58 空格也是一个字符吗? :) /好的,只是开玩笑 - 你是对的/ :) - clt60
3
这比使用尾部/头部组合读取包含5000万行的文件要慢大约5倍。 - duhaime
显示剩余3条评论

130
我有一个独特的情况,在这一页上我可以对所提出的解决方案进行基准测试,因此我将撰写此答案,包括每个解决方案的运行时间。
设置:
我的数据文件是一个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

我也运行了现有的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分钟。

mapfile

我有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

8
我在想将整个文件直接丢到 /dev/null 中需要多长时间。 (如果这只是一个硬盘基准测试会怎样?) - sanmai
2
我感到一种扭曲的冲动,想向你致敬,因为你拥有一个3GB以上的文本文件词典。无论出于何种理由,这种做法都非常注重文本性 :) - Stabledog
使用 head + tail 运行两个进程的开销对于单个文件来说可以忽略不计,但是当你在许多文件上执行此操作时,就会开始显现出来。 - tripleee

65

使用 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

使用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.198秒与1.303秒,快了约6倍。


3
这种方法始终会比较慢,因为awk尝试进行字段拆分。通过awk 'BEGIN{FS=RS}(NR == num_line) {print; exit}' file可以减少字段拆分的开销。 - kvantour
@kvantour确实,GNU awk的nextfile非常适合这种情况。为什么FS=RS可以避免字段分割呢? - fedorqui
1
FS=RS does not avoid field splitting, but it only parses the $0 ones and only assigns one field because there is no RS in $0 - kvantour
@RAREKpopManifesto 哦,我以为是。查看GNU Awk用户指南中的nextfile语句后,我发现:注意:多年来,nextfile一直是一个常见的扩展。在2012年9月,它被接受并纳入了POSIX标准。_,所以你是对的。它还补充说:_BWK awk和mawk的当前版本也支持nextfile。但是,它们不允许在函数体内使用nextfile语句。 - fedorqui
1
@fedorqui 我为你感到高兴。 - RARE Kpop Manifesto
显示剩余5条评论

48
根据我的测试,在性能和可读性方面,我建议使用: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'很有趣,但是我认为它能够直接被理解的人会比头尾解决方案少,而且它也比头尾慢。
在我的测试中,两个头尾版本始终表现优于sed 'NUMq;d'。这与发布的其他基准测试结果相一致。很难找到头尾版本真正糟糕的情况。这也并不奇怪,因为这些是在现代Unix系统中预计会被大量优化的操作。
要了解性能差异,这些是我在一个非常大的文件(9.3G)中得到的结果:
  • tail -n+N | head -1: 3.7 sec
  • head -N | tail -1: 4.6 sec
  • sed Nq;d: 18.8 sec
结果可能有所不同,但一般来说,对于较小的输入,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

这是我在我的机器上运行的输出结果(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

2
head | tailtail | head之间的性能是否有所不同?或者这取决于打印哪一行(文件开头还是文件结尾)? - wisbucky
1
@wisbucky 我没有确切的数字,但是使用tail后跟“head -1”的一个缺点是你需要事先知道总长度。如果你不知道它,你就必须先计数,这将影响性能。另一个缺点是它不太直观。例如,如果你有1到10的数字,并且想要获取第3行,你需要使用“tail -8 | head -1”。这比“head -3 | tail -1”更容易出错。 - Philipp Claßen
1
抱歉,我应该提供一个例子来更清楚地表达。head -5 | tail -1tail -n+5 | head -1 的区别。实际上,我找到了另一个回答,进行了测试比较,并发现 tail | head 更快。https://dev59.com/5G025IYBdhLWcg3wZ1Ny#48189289 - wisbucky
2
@wisbucky 谢谢你提到这个问题!我进行了一些测试,不管是哪一行,它似乎总是稍微快一些。鉴于此,我改变了我的答案,并且也包括了基准测试,以便有人想要重现它。 - Philipp Claßen
有没有一种简单的方法可以将此解决方案扩展到同时处理多个文件?例如,使用head -7 -q input*.txt | tail -1从多个文件input*.txt中获取第7行?目前,这只会从input*.txt列表中列出的第一个文件中获取第7行。 - algae
如果没有N行会怎么样? - Lee Meador

32

节省两个按键,无需使用括号即可打印第N行:

sed  -n  Np  <fileName>
      ^   ^
       \   \___ 'p' for printing
        \______ '-n' for not printing by default 
例如,要打印第100行:
sed -n 100p foo.txt      

1
请注意,第一行的N = 1而不是零。 - Yeti

28

哇,所有的可能性!

试试这个:

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可能是最接近并且最简单易用的。


21

如果这个问题标记为Bash,则以下是使用Bash(≥4)的方法:使用mapfile命令,并使用-s(跳过)和-n(计数)选项。

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

mapfile -s 41 -n 1 ary < file

现在,您将拥有一个数组ary,其字段包含file的行(包括尾随换行符),我们已经跳过了第41行(-s 41),并在读取一行后停止(-n 1)。因此,这实际上是第42行。要打印它:

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

如果您需要一系列的行,比如从42到666(包括这两个行号),但又不想自己算,可以将它们打印到标准输出中。
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内置命令,不使用外部命令!


13

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

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

6
-n选项禁用了默认的打印每一行的操作,这点通过快速查看man页面肯定已经得知。 - tripleee
1
在GNU的sed中,所有的sed答案速度都差不多。因此(对于GNU的sed),这是最好的sed答案,因为它可以节省大文件和小的nth行值的时间。 - agc

9
你也可以使用Perl来完成这个任务:
perl -wnl -e '$.== NUM && print && exit;' some.file

在对包含6,000,000行的文件进行测试并检索任意第2,000,000行时,该命令几乎是瞬间完成的,并且比sed答案快得多。 - NoCake

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