Bash工具从文件中获取第n行
Posted
技术标签:
【中文标题】Bash工具从文件中获取第n行【英文标题】:Bash tool to get nth line from a file 【发布时间】:2011-08-26 17:09:26 【问题描述】:有这样做的“规范”方式吗?我一直在使用 head -n | tail -1
来解决问题,但我一直想知道是否有一个 Bash 工具可以专门从文件中提取一行(或一系列行)。
我所说的“规范”是指其主要功能正在执行此操作的程序。
【问题讨论】:
“Unix 方式”是把能做好各自工作的工具链接起来。所以我想你已经找到了一个非常合适的方法。其他方法包括awk
和sed
,我相信有人也可以想出一个 Perl 单行代码;)
双命令表明head | tail
解决方案不是最佳的。已经提出了其他更接近最佳的解决方案。
您是否针对平均情况下最快的解决方案进行了基准测试?
在cat line X to line Y on a huge file Unix & Linux 上的基准(范围)。 (cc @Marcin,以防两年多后你仍然想知道)
head | tail
解决方案不起作用,如果您查询输入中不存在的行:它将打印最后一行。
【参考方案1】:
head
和带有tail
的管道对于大文件来说会很慢。我建议像这样sed
:
sed 'NUMq;d' file
其中NUM
是您要打印的行号;例如,sed '10q;d' file
打印file
的第 10 行。
解释:
当行号为NUM
时,NUMq
会立即退出。
d
将删除该行而不是打印它;这在最后一行被禁止,因为q
导致退出时跳过脚本的其余部分。
如果变量中有NUM
,则需要使用双引号而不是单引号:
sed "$NUMq;d" file
【讨论】:
对于那些想知道的人来说,这个解决方案似乎比下面提出的sed -n 'NUMp'
和 sed 'NUM!d'
解决方案快 6 到 9 倍。
我认为tail -n+NUM file | head -n1
可能会一样快或更快。至少,当我在一个有 50 万行的文件上尝试 NUM 为 250000 时,它在我的系统上(显着)更快。 YMMV,但我真的不明白为什么会这样。
不,不是。如果没有q
,它将处理完整文件
@Fiddlestiques:别忘了引用 foo="$(sed "4q;d" file4)"
@anubhava - 谢谢 - 现在知道了 - echo "$foo" 而不是 echo $foo【参考方案2】:
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: insert a line in a certain position
【讨论】:
为什么在这种情况下需要' @RafaelBarbosa<
在这种情况下是不必要的。简单地说,我更喜欢使用重定向,因为我经常使用像sed -n '100p' < <(some_command)
这样的重定向——所以,通用语法:)。它的效果并不差,因为在分叉时使用 shell 完成重定向,所以......这只是一种偏好......(是的,它长了一个字符):)
@jm666 实际上它长了 2 个字符,因为您通常会在
@rasen58 空格也是字符吗? :) /好吧,开个玩笑-你说得对/ :)
在读取 50M 行的文件时,这比尾部/头部组合慢约 5 倍【参考方案3】:
您也可以为此使用 Perl:
perl -wnl -e '$.== NUM && print && exit;' some.file
【讨论】:
在对包含 6,000,000 行的文件进行测试并检索任意行 #2,000,000 时,此命令几乎是即时的,并且比 sed 答案快得多。【参考方案4】:您也可以使用 sed 打印并退出:
sed -n '10p;q;' file # print line 10
【讨论】:
-n
选项禁用打印每一行的默认操作,您肯定会通过快速浏览手册页发现。
在 GNU sed
中,所有 sed
答案的速度都差不多。因此(对于 GNU sed
)这是最好的 sed
答案,因为它可以节省大文件和小 nth line 值的时间。【参考方案5】:
哇,所有的可能性!
试试这个:
sed -n "$lineNump" $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
(您可能需要尝试nawk
或gawk
命令)。
是否有只打印特定行的工具?不是标准工具之一。但是,sed
可能是最接近且最容易使用的。
【讨论】:
【参考方案6】: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 倍。
【讨论】:
这种方法总是会比较慢,因为 awk 会尝试进行字段拆分。字段拆分的开销可以减少awk 'BEGINFS=RS(NR == num_line) print; exit' file
当您想要连接 file1 的 n1 行、file2 的 n2 行、n3 或 file3 ...awk 'FNR==n' n=10 file1 n=30 file2 n=60 file3
时,这种方法中 awk 的真正威力就显现出来了。使用 GNU awk 可以使用 awk 'FNR==nprint;nextfile' n=10 file1 n=30 file2 n=60 file3
加速。
@kvantour 确实,GNU awk 的 nextfile 非常适合此类事情。 FS=RS
如何避免字段拆分?
FS=RS
并没有避免字段拆分,而是只解析$0的,只分配一个字段,因为$0
中没有RS
@kvantour 我一直在用FS=RS
进行一些测试,并没有看到时间上的差异。我问一个关于它的问题怎么样,这样你就可以扩展?谢谢!【参考方案7】:
这个问题被标记为 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
选项(修剪):
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 内置命令!
【讨论】:
【参考方案8】:使用 sed 打印第 n 行,并将变量作为行号:
a=4
sed -e $a'q:d' file
这里的“-e”标志用于将脚本添加到要执行的命令中。
【讨论】:
冒号是语法错误,应该是分号。【参考方案9】:大文件最快的解决方案总是tail|head,前提是两个距离:
从文件的开头到起始行。让我们称之为S
从最后一行到文件末尾的距离。就这样E
众所周知。然后,我们可以使用这个:
mycount="$E"; (( E > S )) && mycount="+$S"
howmany="$(( endline - startline + 1 ))"
tail -n "$mycount"| head -n "$howmany"
howman 只是所需的行数。
更多细节在https://unix.stackexchange.com/a/216614/79743
【讨论】:
请说明S
和E
的单位(即字节、字符或行)。【参考方案10】:
如果您有多个由 \n 分隔的行(通常是新行)。你也可以使用'cut':
echo "$data" | cut -f2 -d$'\n'
您将从文件中获得第二行。 -f3
给你第三行。
【讨论】:
也可用于显示多行:cat FILE | cut -f2,5 -d$'\n'
将显示文件的第 2 行和第 5 行。 (但它不会保留顺序。)【参考方案11】:
我有一个独特的情况,我可以对本页上提出的解决方案进行基准测试,所以我写这个答案是为了整合所提出的解决方案,并包含每个解决方案的运行时间。
设置
我有一个 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,如果我直接进入第 50000 万行,可能需要约 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 行
-
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%) awk
00:05:12.156 (+00:03:56.835 = +314.43%) cut
第 500,000,000 行
-
00:12:07.050 (-00:00:26.160)
sed
00:12:11.460 (-00:00:21.750) perl
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
第 3,338,559,320 行
-
01:20:54.599 (-00:03:05.327)
sed
01:21:24.045 (-00:02:25.227) perl
01:23:49.273 (+00:00:00.000) head|tail
01:25:13.548 (+00:02:35.735) awk
05:47:23.026 (+04:24:26.246) cut
【讨论】:
我想知道将整个文件放入 /dev/null 需要多长时间。 (如果这只是一个硬盘基准测试呢?) 我有一种反常的冲动,要对您拥有 3+ gig 文本文件字典的所有权低头。无论是什么原理,这都包含文本性:) 使用head
+ tail
运行两个进程的开销对于单个文件来说可以忽略不计,但是当您对多个文件执行此操作时就会开始显示。【参考方案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'
很有趣,但我认为与头/尾解决方案相比,开箱即用的人会更少,而且它也比尾/头慢。
在我的测试中,两个尾部/头部版本的性能始终优于 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 | tail
和tail | 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
【讨论】:
head | tail
和 tail | head
之间的性能是否不同?还是取决于正在打印哪一行(文件开头与文件结尾)?
@wisbucky 我没有确切的数字,但首先使用 tail 后跟“head -1”的一个缺点是您需要提前知道总长度。如果您不知道,则必须先计算它,这将在性能方面有所损失。另一个缺点是使用起来不太直观。例如,如果您有数字 1 到 10 并且想要获得第 3 行,则必须使用“tail -8 | head -1”。这比“head -3 | tail -1”更容易出错。
对不起,我应该包括一个例子来清楚。 head -5 | tail -1
与 tail -n+5 | head -1
。实际上,我找到了另一个进行测试比较的答案,发现tail | head
更快。 ***.com/a/48189289
@wisbucky 感谢您提及!我做了一些测试,不得不同意它总是稍微快一点,与我所看到的线的位置无关。鉴于此,我改变了我的答案,并且还包括了基准,以防有人想要复制它。【参考方案13】:
以上所有答案都直接回答了问题。但这是一个不太直接的解决方案,但可能是一个更重要的想法,可以引发思考。
由于行长是任意的,所以文件第 n 行之前的所有字节都需要被读取。如果您有一个巨大的文件或需要多次重复此任务,并且此过程很耗时,那么您应该首先认真考虑是否应该以不同的方式存储数据。
真正的解决方案是有一个索引,例如在文件的开头,指示行开始的位置。您可以使用数据库格式,或者只是在文件开头添加一个表。或者,创建一个单独的索引文件来伴随您的大文本文件。
例如您可以为换行创建一个字符位置列表:
awk 'BEGINc=0;print(c)c+=length()+1;print(c+1)' file.txt > file.idx
然后用tail
读取,实际上seek
s 直接指向文件中的适当点!
例如获取第 1000 行:
tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
这可能不适用于 2 字节/多字节字符,因为 awk 可以“识别字符”,但 tail 不能。
我还没有针对大文件对此进行过测试。
另见this answer。
或者 - 将您的文件拆分为更小的文件!
【讨论】:
【参考方案14】:已经有很多好的答案了。我个人选择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
【讨论】:
【参考方案15】:作为 CaffeineConnoisseur 非常有用的基准测试答案的后续行动...我很好奇“mapfile”方法与其他方法相比有多快(因为未经测试),所以我尝试了快速而肮脏的速度比较自己,因为我确实有 bash 4。当我在上面的时候,对顶部答案中的一个 cmets 中提到的“tail | head”方法(而不是 head | tail)进行了测试,因为人们正在歌颂它。我没有任何接近使用的测试文件大小的东西;我能在短时间内找到的最好的文件是一个 14M 的谱系文件(用空格分隔的长行,不到 12000 行)。
短版:mapfile 看起来比 cut 方法快,但比其他所有方法都慢,所以我称之为哑巴。尾巴 | head, 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
希望这会有所帮助!
【讨论】:
【参考方案16】:使用其他人提到的内容,我希望这是我的 bash shell 中的一个快速且花哨的功能。
创建文件:~/.functions
添加内容:
getline()
line=$1
sed $line'q;d' $2
然后将此添加到您的~/.bash_profile
:
source ~/.functions
现在当你打开一个新的 bash 窗口时,你可以像这样调用函数:
getline 441 myfile.txt
【讨论】:
在使用之前无需将$1
分配给另一个变量,并且您正在破坏任何其他全局line
。在 Bash 中,对函数变量使用local
;但在这里,如前所述,可能只是做sed "$1d;q" "$2"
。 (另请注意"$2"
的引用。)
正确,但拥有自我记录的代码可能会有所帮助。【参考方案17】:
我已将上述一些答案放入一个简短的 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 "$1q;d" $2;
exit 0
确保它是可执行的
$ chmod +x get
链接它以使其在PATH
上可用
$ ln -s get.sh /usr/local/bin/get
【讨论】:
【参考方案18】:在查看了the top answer 和the 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
【讨论】:
【参考方案19】:保存两次击键,不使用括号打印第N行:
sed -n Np <fileName>
^ ^
\ \___ 'p' for printing
\______ '-n' for not printing by default
例如,打印第 100 行:
sed -n 100p foo.txt
【讨论】:
以上是关于Bash工具从文件中获取第n行的主要内容,如果未能解决你的问题,请参考以下文章
在 bash 中使用 ec2 命令行工具获取 ec2 实例的公共 dns 名称