从文件中获取第n行的Bash工具

有“规范”的方法吗?我一直在使用head -n | tail -1来解决这个问题,但我一直在想是否有一个Bash工具专门从文件中提取一行(或一系列行)。

通过“规范”我的意思是一个程序,其主要功能是这样做。

646250 次浏览

headtail的管道对于一个巨大的文件来说会很慢。我建议sed这样:

sed 'NUMq;d' file

其中NUM是您要打印的行数;因此,例如,sed '10q;d' file打印file的第10行。

说明:

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

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行

等等…

如果你想添加行,你可以检查这个:

ses:在某个位置插入一行

您还可以为此使用Perl:

perl -wnl -e '$.== NUM && print && exit;' some.file

您也可以使用Sed print并退出:

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

哇,所有的可能性!

试试这个:

sed -n "${lineNum}p" $file

或者其中之一取决于您的Awk版本:

awk  -vlineNum=$lineNum 'NR == lineNum {print $0}' $fileawk -v lineNum=4 '{if (NR == lineNum) {print $0}}' $fileawk '{if (NR == lineNum) {print $0}}' lineNum=$lineNum $file

您可能需要尝试#0或#1命令)。

有没有一种工具只打印特定的行?不是标准工具之一。然而,sed可能是最接近和最简单的使用。

awk是非常快的:

awk 'NR == num_line' file

如果为true,则执行awk的默认行为:{print $0}


替代版本

如果您的文件恰好很大,您最好在阅读所需行后exit。这样您可以节省CPU时间请参阅答案末尾的时间比较

awk 'NR == num_line {print; exit}' file

如果您想从bash变量中给出行号,您可以使用:

awk 'NR == n' n=$num fileawk -v n=$num 'NR == n' file   # equivalent

查看使用exit节省了多少时间,特别是如果该行恰好在文件的第一部分:

# Let's create a 10M lines filefor ((i=0; i<100000; i++)); do echo "bla bla"; done > 100Klinesfor ((i=0; i<100; i++)); do cat 100Klines; done > 10Mlines
$ time awk 'NR == 1234567 {print}' 10Mlinesbla bla
real    0m1.303suser    0m1.246ssys 0m0.042s$ time awk 'NR == 1234567 {print; exit}' 10Mlinesbla bla
real    0m0.198suser    0m0.178ssys 0m0.013s

所以差异是0.198秒和1.303秒,大约快6倍。

这个问题被标记为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(包括),并说你不想自己做数学,并在stdout上打印它们:

mapfile -s $((42-1)) -n $((666-42+1)) ary < fileprintf '%s' "${ary[@]}"

如果您也需要处理这些行,那么存储尾随的换行符并不方便。在这种情况下,使用-t选项(trim):

mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file# do stuffprintf '%s\n' "${ary[@]}"

你可以有一个函数为你做到这一点:

print_file_range() {# $1-$2 is the range of file $3 to be printed to stdoutlocal arymapfile -s $(($1-1)) -n $(($2-$1+1)) ary < "$3"printf '%s' "${ary[@]}"}

没有外部命令,只有Bash内置!

要打印第n行,请使用ses并将变量作为行号:

a=4sed -e $a'q:d' file

这里的'-e'标志用于将脚本添加到要执行的命令中。

对于大文件,最快的解决方案始终是ails|head,前提是两个距离:

  • 从文件的开头到起始行。让我们称之为S
  • 从最后一行到文件末尾的距离。就E

是已知的。然后,我们可以使用这个:

mycount="$E"; (( E > S )) && mycount="+$S"howmany="$(( endline - startline + 1 ))"tail -n "$mycount"| head -n "$howmany"

HOWONH只是所需的行数。

更多细节https://unix.stackexchange.com/a/216614/79743

如果您有多行由\n(通常是新行)分隔。您也可以使用“剪切”:

echo "$data" | cut -f2 -d$'\n'

您将从文件中获得第二行。-f3为您提供第三行。

我有一个独特的情况,我可以对这个页面上提出的解决方案进行基准测试,所以我写这个答案作为提议的解决方案的整合,每个解决方案都包含运行时间。

设置

我有一个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 -1pgm_icnt = 0
real    1m15.321s

行5000万的基线是00:01:15.321,如果我直接去行5亿它可能是~12.5分钟。

削减

我对此表示怀疑,但值得一试:

$ time cut -f50000000 -d$'\n' myfile.asciipgm_icnt = 0
real    5m12.156s

这个花了00:05:12.156运行,这比基线慢得多!我不确定它是在停止之前读完整个文件还是只读到5000万行,但无论如何,这似乎不是问题的可行解决方案。

AWK

我只使用exit运行解决方案,因为我不会等待完整文件运行:

$ time awk 'NR == 50000000 {print; exit}' myfile.asciipgm_icnt = 0
real    1m16.583s

这段代码运行在00:01:16.583,只慢了~1秒,但仍然没有比基线有所改善。以这种速度,如果排除退出命令,读取整个文件可能需要~76分钟!

perl

我也运行了现有的Perl解决方案:

$ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.asciipgm_icnt = 0
real    1m13.146s

此代码在00:01:13.146运行,比基线快约2秒。如果我在完整的500,000,000上运行它可能需要约12分钟。

ed

黑板上的最高答案,这是我的结果:

$ time sed "50000000q;d" myfile.asciipgm_icnt = 0
real    1m12.705s

这段代码在00:01:12.705中运行,比基线快3秒,比Perl快约0.4秒。如果我在完整的500,000,000行上运行它,可能需要约12分钟。

mapfile

我有bash 3.1,因此无法测试mapfile解决方案。

结论

看起来,在大多数情况下,很难改进headtail解决方案。充其量,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

根据我的测试,在性能和易读性方面,我的建议是:

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/bashreadonly file=tmp-input.txtreadonly size=1000000000readonly pos=500000000readonly retries=3
seq 1 $size > $fileecho "*** head -N | tail -1 ***"for i in $(seq 1 $retries) ; dotime head "-$pos" $file | tail -1doneecho "-------------------------"echoecho "*** tail -n+N | head -1 ***"echo
seq 1 $size > $filels -alhg $filefor i in $(seq 1 $retries) ; dotime tail -n+$pos $file | head -1doneecho "-------------------------"echoecho "*** sed Nq;d ***"echo
seq 1 $size > $filels -alhg $filefor i in $(seq 1 $retries) ; dotime sed $pos'q;d' $filedone/bin/rm $file

这是在我的机器上运行的输出(ThinkPadX1 Carbon,带有SSD和16G内存)。我假设在最终运行中,所有内容都来自缓存,而不是磁盘:

*** head -N | tail -1 ***500000000
real    0m9,800suser    0m7,328ssys     0m4,081s500000000
real    0m4,231suser    0m5,415ssys     0m2,789s500000000
real    0m4,636suser    0m5,935ssys     0m2,684s-------------------------
*** tail -n+N | head -1 ***
-rw-r--r-- 1 phil 9,3G Jan 19 19:49 tmp-input.txt500000000
real    0m6,452suser    0m3,367ssys     0m1,498s500000000
real    0m3,890suser    0m2,921ssys     0m0,952s500000000
real    0m3,763suser    0m3,004ssys     0m0,760s-------------------------
*** sed Nq;d ***
-rw-r--r-- 1 phil 9,3G Jan 19 19:50 tmp-input.txt500000000
real    0m23,675suser    0m21,557ssys     0m1,523s500000000
real    0m20,328suser    0m18,971ssys     0m1,308s500000000
real    0m19,835suser    0m18,830ssys     0m1,004s

上述所有答案都直接回答了这个问题。但这里有一个不那么直接的解决方案,但可能更重要的想法,引发思考。

由于行长是任意的,因此要读取文件第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是“字符感知”的,但ails不是。
  • 我没有针对大文件测试过这个。
  • 另见这个答案
  • 或者-将您的文件拆分为更小的文件!

已经有很多好的答案了。我个人使用awk。为了方便起见,如果您使用bash,只需将以下内容添加到您的~/.bash_profile。并且,下次登录时(或者如果您在此更新后获取.bash_profile),您将有一个新的漂亮的“第n个”函数可用于通过管道传输文件。

执行此操作或将其放入~/.bash_profile(如果使用bash)并重新打开bash(或执行source ~/.bach_profile

# print just the nth piped in linenth () { awk -vlnum=${1} 'NR==lnum {print; exit}'; }

然后,要使用它,只需通过它。例如,:

$ yes line | cat -n | nth 55  line

作为CaffeineConnoisseur非常有用的基准测试答案的后续行动……我很好奇'mapfile'方法与其他方法相比有多快(因为没有测试过),所以我自己尝试了一个快速和肮脏的速度比较,因为我确实有bash 4方便。当我在上面的答案上的一条评论中提到的“ail|head”方法(而不是head|ail)时,我测试了一下,因为人们都在称赞它。我没有任何与使用的测试文件大小相近的东西;我能在短时间内找到的最好的是一个14M的血统文件(用空格分隔的长行,不到12000行)。

简短的版本:mapfile看起来比剪切方法快,但比其他任何方法都慢,所以我称之为哑弹。尾部|头部,OTOH,看起来可能是最快的,尽管与se相比,这种大小的文件差异并不大。

$ 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

希望这有帮助!

使用其他人提到的,我希望这是我bash shell中的一个快速和花哨的函数。

创建一个文件:~/.functions

添加到它的内容:

getline(){//获取代码行=1美元Sed$line'q; d'2美元}

然后将其添加到您的~/.bash_profile

source ~/.functions

现在,当你打开一个新的bash窗口时,你可以这样调用函数:

getline 441 myfile.txt

我已经将上述一些答案放入一个简短的bash脚本中,您可以将其放入一个名为get.sh的文件中并链接到/usr/local/bin/get(或您喜欢的任何其他名称)。

#!/bin/bashif [ "${1}" == "" ]; thenecho "error: blank line number";exit 1fire='^[0-9]+$'if ! [[ $1 =~ $re ]] ; thenecho "error: line number arg not a number";exit 1fiif [ "${2}" == "" ]; thenecho "error: blank file name";exit 1fised "${1}q;d" $2;exit 0

确保它是可执行的

$ chmod +x get

链接它以使其在PATH上可用

$ ln -s get.sh /usr/local/bin/get

在看了最高答案基准之后,我实现了一个小助手函数:

function nth {if (( ${#} < 1 || ${#} > 2 )); thenecho -e "usage: $0 \e[4mline\e[0m [\e[4mfile\e[0m]"return 1fiif (( ${#} > 1 )); thensed "$1q;d" $2elsesed "$1q;d"fi}

基本上,您可以以两种方式使用它:

nth 42 myfile.txtdo_stuff | nth 42

保存两次击键,打印第N行,不使用括号:

sed  -n  Np  <fileName>^   ^\   \___ 'p' for printing\______ '-n' for not printing by default

例如,要打印第100行:

sed -n 100p foo.txt

这不是一个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;}

更新1:在awk中找到<强>多更快的方法

  • 只需5.353秒即可获得133.6 mn以上的行:
rownum='133668997'; ( time ( pvE0 < ~/master_primelist_18a.txt |
LC_ALL=C mawk2 -F'^$' -v \_="${rownum}" -- '!_{exit}!--_' ) )
in0: 5.45GiB 0:00:05 [1.02GiB/s] [1.02GiB/s] [======> ] 71%( pvE 0.1 in0 < ~/master_primelist_18a.txt |LC_ALL=C mawk2 -F'^$' -v  -- ; )  5.01s user

1.21s system 116% cpu#1

77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=

===============================================

我想反驳perlawk快的观点:

因此,虽然我的测试文件几乎没有那么多行,但它的大小也是两倍,在7.58 GB-

我甚至给了perl一些内置的优势-比如行号中的硬编码,并且还排在第二位,从而从OS缓存机制中获得任何潜在的加速,如果有的话

 f="$( grealpath -ePq  ~/master_primelist_18a.txt )"rownum='133668997'fg;fg; pv < "${f}" | gwc -lcmecho; 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 jobfg: no current job7.58GiB 0:00:28 [ 275MiB/s] [============>] 100%        
148,110,134 8,134,435,629 8,134,435,629   <<<< rows, chars, and bytescount 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 total77.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 total77.37219=195591955519519519559551=0x296B0FA7D668C4A64F7F=

我可以用perl 5.36甚至perl-6重新运行测试,如果你认为它会有所不同(也没有安装),但是差距为

#0 vs#1

在这两者之间,后者是之前的两倍多,似乎很清楚哪一个确实更快地获取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

得到第n行(单行)

如果您想要一些以后可以自定义而无需处理bash的东西,您可以编译这个c程序并将二进制文件放入您的自定义二进制文件目录中。这假设您知道如何编辑. bashrc文件相应地(只有当你想编辑你的路径变量时),如果你不知道,这个是一个有用的链接。

要运行此代码,请使用(假设您命名了二进制“line”)。

line [target line] [target file]

示例

line 2 somefile.txt

代码:

#include <stdio.h>#include <string.h>#include <stdlib.h>
int main(int argc, char* argv[]){
if(argc != 3){fprintf(stderr, "line needs a line number and a file name");exit(0);}
int lineNumber = atoi(argv[1]);int counter = 0;char *fileName = argv[2];
FILE *fileReader = fopen(fileName, "r");if(fileReader == NULL){fprintf(stderr, "Failed to open file");exit(0);}
size_t lineSize = 0;char* line = NULL;
while(counter < lineNumber){getline(&line, &linesize, fileReader);counter++}
getline(&line, &lineSize, fileReader);
printf("%s\n", line);
fclose(fileReader);return 0;}

编辑:删除了搜索,并用同时循环替换它