有这样做的“规范”方式吗?我一直在使用 head -n | tail -1
来解决问题,但我一直想知道是否有一个 Bash 工具可以专门从文件中提取一行(或一系列行)。
“规范”是指其主要功能正在执行此操作的程序。
awk
和 sed
,我相信有人也能想出一个 Perl 单行器;)
head | tail
解决方案是次优的。已经提出了其他更接近最佳的解决方案。
head | tail
解决方案不起作用:它将打印最后一行。
对于大文件,head
和带有 tail
的管道会很慢。我会建议 sed
像这样:
sed 'NUMq;d' file
其中 NUM
是您要打印的行号;例如,sed '10q;d' file
打印 file
的第 10 行。
解释:
当行号为 NUM
时,NUMq
将立即退出。
d
将删除该行而不是打印它;这在最后一行被禁止,因为 q
导致退出时跳过脚本的其余部分。
如果变量中有 NUM
,则需要使用双引号而不是单引号:
sed "${NUM}q;d" file
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
<
在这种情况下是不必要的。简单地说,我更喜欢使用重定向,因为我经常使用像 sed -n '100p' < <(some_command)
这样的重定向 - 所以,通用语法:)。它的效果并不差,因为在分叉时使用 shell 完成重定向,所以......这只是一种偏好......(是的,它长了一个字符):)
我有一个独特的情况,我可以对这个页面上提出的解决方案进行基准测试,所以我写这个答案是对所提出的解决方案的整合,每个解决方案都包含运行时间。
设置
我有一个 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%) 削减
行 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) 头|尾 00:12:45.830 ( +00:00:12.620) awk 00:52:01.560 (+00:40:31.650) 切
第 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) 头|尾 01:25:13.548 ( +00:02:35.735) awk 05:47:23.026 (+04:24:26.246) 切
head
+ tail
运行两个进程的开销可以忽略不计,但当您对多个文件执行此操作时就会开始显示。
使用 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
awk 'FNR==n' n=10 file1 n=30 file2 n=60 file3
时,这种方法中 awk 的真正威力就显现出来了。使用 GNU awk 可以使用 awk 'FNR==n{print;nextfile}' n=10 file1 n=30 file2 n=60 file3
加速。
FS=RS
如何避免字段拆分?
FS=RS
并没有避免字段拆分,而是只解析$0的,只分配一个字段,因为$0
中没有RS
根据我的测试,在性能和可读性方面,我的建议是:
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'
很有趣,但我认为与 head/tail 解决方案相比,开箱即用的人会更少,而且它也比 tail/head 慢。
在我的测试中,两个尾部/头部版本的性能始终优于 sed 'NUMq;d'
。这与发布的其他基准一致。很难找到反面/正面非常糟糕的情况。这也不足为奇,因为这些是您期望在现代 Unix 系统中进行大量优化的操作。
为了了解性能差异,这些是我为一个大文件(9.3G)得到的数字:
尾-n+N |头 -1:3.7 秒
头-N |尾巴 -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
这是在我的机器上运行的输出(带有 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
head | tail
与 tail | head
的性能是否不同?还是取决于正在打印哪一行(文件开头与文件结尾)?
head -5 | tail -1
与 tail -n+5 | head -1
。实际上,我找到了另一个进行测试比较的答案,发现 tail | head
更快。 stackoverflow.com/a/48189289
head -7 -q input*.txt | tail -1
从多个文件 input*.txt
中获取第 7 行?目前这只会从 input*.txt
中列出的第一个文件中获取第 7 行。
哇,所有的可能性!
尝试这个:
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]}"
如果您需要一系列行,请说范围 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 内置命令!
保存两次击键,不使用括号打印第 N 行:
sed -n Np <fileName>
^ ^
\ \___ 'p' for printing
\______ '-n' for not printing by default
例如,要打印第 100 行:
sed -n 100p foo.txt
您也可以使用 sed 打印并退出:
sed -n '10{p;q;}' file # print line 10
-n
选项禁用打印每一行的默认操作,您肯定会通过快速浏览手册页发现这一点。
sed
中,所有 sed
答案的速度都差不多。因此(对于 GNU sed
)这是最好的 sed
答案,因为它可以为大文件和小 nth line 值节省时间。
您也可以为此使用 Perl:
perl -wnl -e '$.== NUM && print && exit;' some.file
作为 CaffeineConnoisseur 非常有用的基准测试答案的后续行动......我很好奇“mapfile”方法与其他方法相比有多快(因为未经测试),所以我自己尝试了一个快速而肮脏的速度比较我确实有 bash 4。在我对最佳答案的评论之一中提到的“tail | head”方法(而不是head | tail)中进行了测试,因为人们正在对其赞不绝口。我没有任何接近使用的测试文件大小的东西;我能在短时间内找到的最好的文件是一个 14M 的谱系文件(用空格分隔的长行,不到 12000 行)。
简短版本:mapfile 看起来比 cut 方法快,但比其他所有方法都慢,所以我称之为哑巴。尾巴 |头,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
希望这可以帮助!
大文件的最快解决方案总是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 中有更多详细信息
S
和E
的单位(即字节、字符或行)。
以上所有答案都直接回答了这个问题。但这是一个不太直接的解决方案,但可能是一个更重要的想法,可以引发思考。
由于行长是任意的,因此需要读取文件第 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 不是。
我还没有针对大文件对此进行测试。
另请参阅此答案。
或者 - 将您的文件拆分成更小的文件!
使用其他人提到的内容,我希望这是我的 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"
的引用。)
如果你得到多行由 \n 分隔(通常是新行)。您也可以使用“剪切”:
echo "$data" | cut -f2 -d$'\n'
您将从文件中获得第二行。 -f3
为您提供第三行。
cat FILE | cut -f2,5 -d$'\n'
将显示 FILE 的第 2 行和第 5 行。 (但它不会保留顺序。)
已经有很多好的答案了。我个人选择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
使用 sed 以变量作为行号打印第 n 行:
a=4
sed -e $a'q:d' file
这里的“-e”标志用于将脚本添加到要执行的命令中。
查看 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
我已将上述一些答案放入一个简短的 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
使用 head -n <1-indexed line number you want>
获取前 n 行,然后使用 tail -1
获取最后一行:
echo "$data" | head -n 50 | tail -1
如果您的文件少于 n 行,这将返回最后一行。
这不是 bash 解决方案,但我发现顶级选择不能满足我的需求,例如,
sed 'NUMq;d' file
速度足够快,但挂了几个小时,并没有说明任何进展。我建议编译这个 cpp 程序并使用它来找到你想要的行。您可以使用 g++ main.cpp
编译它,其中 main.cpp 是包含以下内容的文件。我得到了 a.out 并用 ./a.out
执行了它
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
int main() {
string filename;
cout << "Enter filename ";
cin >> filename;
int needed_row_number;
cout << "Enter row number ";
cin >> needed_row_number;
int progress_line_count;
cout << "Enter at which every number of rows to monitor progress ";
cin >> progress_line_count;
char ch;
int row_counter = 1;
fstream fin(filename, fstream::in);
while (fin >> noskipws >> ch) {
int ch_int = (int) ch;
if (row_counter == needed_row_number) {
cout << ch;
}
if (ch_int == 10) {
if (row_counter == needed_row_number) {
return 0;
}
row_counter++;
if (row_counter % progress_line_count == 0) {
cout << "Progress: line " << row_counter << endl;
}
}
}
return 0;
}
我想反驳 perl
比 awk
快的概念:
因此,虽然我的测试文件没有那么多行,但它的大小也是 7.58 GB
的两倍 -
我什至给了 perl
一些内置的优势——比如在行号中进行硬编码,并且排在第二位,从而从操作系统缓存机制中获得任何潜在的加速(如果有的话)
f="$( grealpath -ePq ~/master_primelist_18a.txt )"
rownum='133668997'
fg;fg; pv < "${f}" | gwc -lcm
echo; sleep 2;
echo;
( time ( pv -i 0.1 -cN in0 < "${f}" |
LC_ALL=C mawk2 '_{exit}_=NR==+__' FS='^$' __="${rownum}"
) ) | mawk 'BEGIN { print } END { print _ } NR'
sleep 2
( time ( pv -i 0.1 -cN in0 < "${f}" |
LC_ALL=C perl -wnl -e '$.== 133668997 && print && exit;'
) ) | mawk 'BEGIN { print } END { print _ } NR' ;
fg: no current job
fg: no current job
7.58GiB 0:00:28 [ 275MiB/s] [============>] 100%
148,110,134 8,134,435,629 8,134,435,629 <<<< rows, chars, and bytes
count as reported by gnu-wc
in0: 5.45GiB 0:00:07 [ 701MiB/s] [=> ] 71%
( pv -i 0.1 -cN in0 < "${f}" | LC_ALL=C mawk2 '_{exit}_=NR==+__' FS='^$' ; )
6.22s user 2.56s system 110% cpu 7.966 total
77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=
in0: 5.45GiB 0:00:17 [ 328MiB/s] [=> ] 71%
( pv -i 0.1 -cN in0 < "${f}" | LC_ALL=C perl -wnl -e ; )
14.22s user 3.31s system 103% cpu 17.014 total
77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=
如果您认为它会有所作为(也没有安装),我可以使用 perl 5.36
甚至 perl-6
重新运行测试,但差距为
7.966 secs (mawk2)
与 17.014 secs (perl 5.34)
在两者之间,后者是先前的两倍多,似乎很清楚哪一个在 ASCII
文件中获取单行深度确实更快。
This is perl 5, version 34, subversion 0 (v5.34.0) built for darwin-thread-multi-2level
Copyright 1987-2021, Larry Wall
mawk 1.9.9.6, 21 Aug 2016, Copyright Michael D. Brennan
sed -n 'NUMp'
和sed 'NUM!d'
解决方案快 6 到 9 倍。tail -n+NUM file | head -n1
可能同样快或更快。至少,当我在一个有 50 万行的文件上尝试 NUM 为 250000 时,它在我的系统上(显着)更快。 YMMV,但我真的不明白为什么会这样。q
,它将处理完整文件foo="$(sed "4q;d" file4)"