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 方式”是把能做好各自工作的工具链接起来。所以我想你已经找到了一个非常合适的方法。其他方法包括awksed,我相信有人也可以想出一个 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 &lt; 在这种情况下是不必要的。简单地说,我更喜欢使用重定向,因为我经常使用像sed -n '100p' &lt; &lt;(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

您可能需要尝试nawkgawk 命令)。

是否有只打印特定行的工具?不是标准工具之一。但是,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

【讨论】:

请说明SE 的单位(即字节、字符或行)。【参考方案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 对每个命令进行基准测试。

基线

首先让我们看看headtail是如何解决的:

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

【讨论】:

head | tailtail | head 之间的性能是否不同?还是取决于正在打印哪一行(文件开头与文件结尾)? @wisbucky 我没有确切的数字,但首先使用 tail 后跟“head -1”的一个缺点是您需要提前知道总长度。如果您不知道,则必须先计算它,这将在性能方面有所损失。另一个缺点是使用起来不太直观。例如,如果您有数字 1 到 10 并且想要获得第 3 行,则必须使用“tail -8 | head -1”。这比“head -3 | tail -1”更容易出错。 对不起,我应该包括一个例子来清楚。 head -5 | tail -1tail -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 读取,实际上seeks 直接指向文件中的适当点!

例如获取第 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按字母顺序排序的目录中获取文件

在 bash 中使用 ec2 命令行工具获取 ec2 实例的公共 dns 名称

bash 文本处理以删除 ascii 并从结果中获取唯一行

从 alias 命令中获取第一个参数并将其插入到文件扩展名中

在一段时间读取循环bash中从第4行获取变量

从 MySQL 数据库获取数据后,从第 n 行获取列数据并分配给变量