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

Vla*_*tch 543 unix bash shell awk sed

这样做是否有"规范"方式?我一直在使用head -n | tail -1哪种技巧,但我一直想知道是否有一个Bash工具专门从文件中提取一行(或一系列行).

"规范"是指一个程序,其主要功能就是这样做.

anu*_*ava 733

headtail对于一个巨大的文件,和管道将是缓慢的.我建议sed像这样:

sed 'NUMq;d' file
Run Code Online (Sandbox Code Playgroud)

NUM您要打印的行号在哪里; 所以,例如,sed '10q;d' file打印第10行file.

说明:

NUMq当行号为时,将立即退出NUM.

d将删除该行而不是打印它; 这在最后一行被禁止,因为q退出时会跳过脚本的其余部分.

如果你有NUM一个变量,你会想要使用双引号而不是单引号:

sed "${NUM}q;d" file
Run Code Online (Sandbox Code Playgroud)

  • 我认为`tail -n + NUM文件| head -n1`可能同样快或更快.至少,当我在一个有50万行的文件上尝试使用NUM为250000时,我的系统(显着)更快.YMMV,但我真的不明白为什么会这样. (67认同)
  • 对于那些想知道的人来说,这个解决方案似乎比下面提出的`sed -n'NUMp'和`sed'NUM!d'`解决方案快6到9倍. (38认同)
  • `sed'NUMq`将输出第一个'NUM`个文件,`; d`将删除除最后一行之外的所有文件. (16认同)
  • 你是对的,似乎`tail | head`比这个答案中提出的`sed`命令快2到3倍 - 我应该测试它... (13认同)
  • @ mklement0:我认为它与`tail`的实现有关,也与stdio间接有关.您可以尝试将`tail -n + $ HUGE foo`与UUOC版本进行比较:`cat foo | tail -n + $ HUGE`.有时这实际上加快了速度(尽管所有关于UUOC的笑话)因为它击败了像mmap这样的非优化. (4认同)
  • @SkippyleGrandGourou:在加速上加上一个数字没有多大意义,因为加速完全取决于目标线在文件中的"多远",因为这个答案的优化在于 - 在打印目标线后_立即出现,因此无需读取文件的_rest_. (3认同)
  • @SkippyleGrandGourou:我在`/ dev/shm`上创建了一个包含1亿行的文件.`sed'NUM!d'`和`sed -n NUMp`需要**14.8秒**实时获得100万分之一的线.但是`sed'NUMq;'`需要15.1秒**.并且`tail | head`实时只花了**3.0秒**! (2认同)
  • @SkippyleGrandGourou:鉴于此optimization_的特定性质,即使你的_ranges_数字也是_pointless作为一般声明_.唯一的_general takeaway_是这样的:(a)这种优化可以安全地应用于所有输入,(b)_effects的范围从none到dra_,取决于所寻找的线的索引相对于整个线的数量. (2认同)
  • 测试此类操作的性能时,请确保将系统I / O缓存排除在外。第二次读取文件时,很可能是从RAM中读取文件,而第一次是从磁盘中读取的-这可能是第二次操作要快得多的原因。在运行基准测试之前,请确保有足够的可用内存来缓存整个文件,并使用“ cat文件> / dev / null”读取该文件以对其进行缓存,或者清除它们之间的系统I / O缓存。 (2认同)
  • 不它不是。如果没有 `q` 它将处理完整文件 (2认同)

jm6*_*666 278

sed -n '2p' < file.txt
Run Code Online (Sandbox Code Playgroud)

将打印第2行

sed -n '2011p' < file.txt
Run Code Online (Sandbox Code Playgroud)

2011年线

sed -n '10,33p' < file.txt
Run Code Online (Sandbox Code Playgroud)

第10行到第33行

sed -n '1p;3p' < file.txt
Run Code Online (Sandbox Code Playgroud)

第1和第3行

等等...

要使用sed添加行,您可以检查:

sed:在某个位置插入一条线

  • @RafaelBarbosa在这种情况下```不是必需的.简单来说,我喜欢使用重定向,因为我经常使用重定向,如`sed -n'100p'<<(some_command)` - 所以,通用语法:).它并没有那么有效,因为重定向是在分叉时用shell完成的,所以...它只是一个偏好...(是的,它是一个字符更长):) (5认同)
  • 当读取 50M 行的文件时,这比 tail/head 组合慢大约 5 倍 (3认同)
  • 为什么在这种情况下需要“&lt;”?如果没有它,我不会获得相同的输出吗? (2认同)
  • @jm666 实际上它多了 2 个字符,因为您通常会在 &lt; 后面放置 '&lt;' 以及一个额外的空格 ' ' ,而不是在没有使用 &lt; 的情况下仅放置一个空格:) (2认同)
  • @ rasen58空格也是字符吗?:) /好的,开个玩笑-你说得对/ :) (2认同)

Caf*_*eur 84

我有一个独特的情况,我可以对本页提出的解决方案进行基准测试,因此我将这个答案写成所提议解决方案的合并,并包含每个解决方案的运行时间.

建立

我有一个3.261千兆字节的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
Run Code Online (Sandbox Code Playgroud)

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

我怀疑这个,但它值得一试:

$ time cut -f50000000 -d$'\n' myfile.ascii
pgm_icnt = 0

real    5m12.156s
Run Code Online (Sandbox Code Playgroud)

这个运行时间为00:05:12.156,这比基线慢得多!我不确定它是在整个文件中读取还是在停止之前读取了5000万行,但无论如何这似乎都不是解决问题的可行办法.

AWK

我只运行了解决方案,exit因为我不打算等待运行完整文件:

$ time awk 'NR == 50000000 {print; exit}' myfile.ascii
pgm_icnt = 0

real    1m16.583s
Run Code Online (Sandbox Code Playgroud)

此代码运行在00:01:16.583,这只慢了约1秒,但仍然没有基线的改进.如果排除退出命令,这个速率可能需要大约76分钟来读取整个文件!

Perl的

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

$ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii
pgm_icnt = 0

real    1m13.146s
Run Code Online (Sandbox Code Playgroud)

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

SED

董事会的最佳答案,这是我的结果:

$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0

real    1m12.705s
Run Code Online (Sandbox Code Playgroud)

此代码在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

  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

  • 我不知道将整个文件放入/ dev / null需要多长时间。(如果这仅仅是硬盘基准测试该怎么办?) (2认同)
  • 我有一种反常的冲动,想要向你拥有 3 多台演出文本文件字典致敬。无论理由是什么,这都包含了文本性:) (2认同)

fed*_*qui 48

随着awk这是相当快:

awk 'NR == num_line' file
Run Code Online (Sandbox Code Playgroud)

如果是这样,awk则执行默认行为:{print $0}.


替代版本

如果你的文件很大,那么exit在阅读完所需的行后你会更好.这样可以节省CPU时间.

awk 'NR == num_line {print; exit}' file
Run Code Online (Sandbox Code Playgroud)

如果要从bash变量中提供行号,可以使用:

awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file   # equivalent
Run Code Online (Sandbox Code Playgroud)

  • 我希望在这里阅读[tag:awk]回复.关于出口的好注意,不会想到它.也许还包括等效的`awk -vn = $ num'NR == n'`? (3认同)
  • 这种方法总是会比较慢,因为 awk 尝试进行字段分割。字段分割的开销可以通过 `awk 'BEGIN{FS=RS}(NR == num_line) {print; 退出}'文件` (3认同)

Sau*_*ahu 33

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

sed  -n  Np  <fileName>
      ^   ^
       \   \___ 'p' for printing
        \______ '-n' for not printing by default 
Run Code Online (Sandbox Code Playgroud)

例如,要打印第 100 行:

sed -n 100p foo.txt      
Run Code Online (Sandbox Code Playgroud)


Dav*_* W. 26

哇,所有的可能性!

试试这个:

sed -n "${lineNum}p" $file
Run Code Online (Sandbox Code Playgroud)

或其中一个取决于您的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
Run Code Online (Sandbox Code Playgroud)

(您可能必须尝试nawkgawk命令).

是否有工具只能打印特定的线?不是标准工具之一.但是,sed可能是最接近和最简单的使用方法.


Phi*_*ßen 22

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

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
Run Code Online (Sandbox Code Playgroud)

这是我的机器上运行的输出(带有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
Run Code Online (Sandbox Code Playgroud)

  • `head | 之间的性能不同吗?尾` vs `尾| 头`?还是取决于正在打印哪一行(文件开头与文件结尾)? (2认同)
  • @wisbucky 谢谢你提到它!我做了一些测试,并且不得不承认它总是稍微快一些,与我所看到的线的位置无关。鉴于此,我改变了我的答案,并且还包括了基准,以防有人想要重现它。 (2认同)

gni*_*urf 20

这个问题被标记猛砸,这里是干什么的巴什(≥4)的方式:用mapfile-s(跳过)和-n(计数)选项.

如果你需要获取文件的第42行file:

mapfile -s 41 -n 1 ary < file
Run Code Online (Sandbox Code Playgroud)

此时,您将拥有一个数组ary,其中的字段包含file(包括尾部换行符)的行,我们跳过前41行(-s 41),并在读取一行(-n 1)后停止.所以这真的是第42行.打印出来:

printf '%s' "${ary[0]}"
Run Code Online (Sandbox Code Playgroud)

如果您需要一系列线条,比如42-666(含)范围,并说您不想自己做数学,并在标准输出上打印:

mapfile -s $((42-1)) -n $((666-42+1)) ary < file
printf '%s' "${ary[@]}"
Run Code Online (Sandbox Code Playgroud)

如果你也需要处理这些行,那么存储尾随换行并不是很方便.在这种情况下使用-t选项(修剪):

mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file
# do stuff
printf '%s\n' "${ary[@]}"
Run Code Online (Sandbox Code Playgroud)

你可以有一个功能为你做这个:

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[@]}"
}
Run Code Online (Sandbox Code Playgroud)

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


小智 11

您也可以使用sed打印并退出:

sed -n '10{p;q;}' file   # print line 10
Run Code Online (Sandbox Code Playgroud)

  • `-n`选项禁用默认操作来打印每一行,因为您可以通过快速浏览手册页找到答案. (5认同)
  • 什么是`-n`在做什么? (2认同)

Tim*_*bov 7

您也可以使用Perl:

perl -wnl -e '$.== NUM && print && exit;' some.file
Run Code Online (Sandbox Code Playgroud)


Jo *_*per 7

作为 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
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助!


小智 6

对于大文件最快的解决方案总是尾部,只要两个距离:

  • 从文件的开头到起始行.让我们来称呼它S
  • 从最后一行到文件末尾的距离.是的E

众所周知.然后,我们可以使用这个:

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

howmany只是所需行数.

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


San*_*har 5

以上所有答案直接回答了问题。但是,这不是一个直接的解决方案,而是一个可能更重要的想法,可以激发思想。

由于行长是任意的,因此需要读取文件第n行之前的所有字节。如果您有一个巨大的文件,或者需要多次重复执行此任务,并且此过程非常耗时,那么您应该首先认真考虑是否应该以其他方式存储数据。

真正的解决方案是在文件的开头具有一个索引,以指示行开始的位置。您可以使用数据库格式,也可以只在文件的开头添加一个表。或者,创建一个单独的索引文件来伴随您的大文本文件。

例如,您可以为换行符创建一个字符位置列表:

awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx
Run Code Online (Sandbox Code Playgroud)

然后使用读取tail,实际上seek直接指向文件中的相应点!

例如获得第1000行:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
Run Code Online (Sandbox Code Playgroud)
  • 这可能不适用于2字节/多字节字符,因为awk是“可识别字符”的,而tail则不是。
  • 我尚未针对大型文件进行过测试。
  • 另请参阅此答案
  • 或者-将文件拆分为较小的文件!