有没有一种“规范”的方法来做这件事?我一直在使用head -n | tail -1
来完成这个操作,但我一直在想是否有一款Bash工具可以从文件中提取一行(或多行)。
所谓“规范”,即指一个主要功能是执行该操作的程序。
head
和管道符与tail
在处理大文件时速度较慢。我建议像这样使用sed
:
sed 'NUMq;d' file
这里的NUM
是您想要打印的行数; 例如, sed '10q;d' file
打印file
文件中的第10行。
解释:
NUMq
将在行数为NUM
时立即退出。
d
将删除该行而不是打印它;但在最后一行,由于q
导致脚本在退出时被跳过,因此不会删除最后一行。
如果您将NUM
存储在变量中,则需要使用双引号而不是单引号:
sed "${NUM}q;d" file
sed -n 'NUMp'
和 sed 'NUM!d'
方法快6至9倍。 - Skippy le Grand Gouroutail -n+NUM file | head -n1
可能会和原来的命令一样快,甚至更快。至少在我的系统上,当我使用 NUM 为 250000 的文件,文件有五十万行时,这个命令比原来那个要(显著)快。你的情况可能不同,但我实在看不出来为什么会不同。 - riciq
它将处理整个文件。 - anubhavafoo="$(sed "4q;d" file4)"
。 - anubhavased -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 -n '100p' < <(some_command)
这样的重定向语法-所以,这是通用语法 :)。它并不会影响效果,因为当分叉自身时,重定向是由Shell完成的...这只是一种个人偏好...(而且是多了一个字符) :) - clt60$ 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行
sed
perl
head|tail
awk
cut
第500,000,000行
sed
perl
head|tail
awk
cut
第3,338,559,320行
sed
perl
head|tail
awk
cut
head
+ tail
运行两个进程的开销对于单个文件来说可以忽略不计,但是当你在许多文件上执行此操作时,就会开始显现出来。 - tripleee使用 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 'BEGIN{FS=RS}(NR == num_line) {print; exit}' file
可以减少字段拆分的开销。 - kvantourFS=RS
可以避免字段分割呢? - fedorquiFS=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
- kvantournextfile
语句后,我发现:注意:多年来,nextfile一直是一个常见的扩展。在2012年9月,它被接受并纳入了POSIX标准。_,所以你是对的。它还补充说:_BWK awk和mawk的当前版本也支持nextfile。但是,它们不允许在函数体内使用nextfile语句。 - fedorquitail -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系统中预计会被大量优化的操作。tail -n+N | head -1
: 3.7 sechead -N | tail -1
: 4.6 secsed Nq;d
: 18.8 sechead | 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
之间的性能是否有所不同?或者这取决于打印哪一行(文件开头还是文件结尾)? - wisbuckyhead -5 | tail -1
和 tail -n+5 | head -1
的区别。实际上,我找到了另一个回答,进行了测试比较,并发现 tail | head
更快。https://dev59.com/5G025IYBdhLWcg3wZ1Ny#48189289 - wisbuckyhead -7 -q input*.txt | tail -1
从多个文件input*.txt
中获取第7行?目前,这只会从input*.txt
列表中列出的第一个文件中获取第7行。 - algae节省两个按键,无需使用括号即可打印第N行:
sed -n Np <fileName>
^ ^
\ \___ 'p' for printing
\______ '-n' for not printing by default
例如,要打印第100行:sed -n 100p foo.txt
哇,所有的可能性!
试试这个:
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
你可能需要尝试使用nawk
或gawk
命令。
是否有一种工具只打印特定的行?没有标准工具可以做到这一点。然而,sed
可能是最接近并且最简单易用的。
如果这个问题标记为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]}"
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内置命令,不使用外部命令!
你也可以使用sed来打印并退出:
sed -n '10{p;q;}' file # print line 10
-n
选项禁用了默认的打印每一行的操作,这点通过快速查看man页面肯定已经得知。 - tripleeeperl -wnl -e '$.== NUM && print && exit;' some.file
awk
和sed
,我相信还有人可以想出一个Perl单行命令之类的方法 ;) - 0xC0000022L