在大型文件中将猫的第X行移到第Y行


132

说我有一个巨大的文本文件(> 2GB),我只是想catXY(如57890000至57890010)。

据我了解,我可以通过管道做headtail或反之亦然,即

head -A /path/to/file | tail -B

或者

tail -C /path/to/file | head -D

其中ABCD可以从文件中的行数来计算,XY

但是这种方法有两个问题:

  1. 你必须计算ABCD
  2. 该命令会pipe彼此有更多比我有兴趣阅读(例如,如果我在一个巨大的文件中间读短短的几行)线

有没有一种方法可以让Shell正常工作并输出所需的行?(同时仅提供XY)?


1
仅供参考,我的答案中添加了6种方法的实际速度测试比较。
凯文

Answers:


119

我建议sed解决方案,但是为了完整起见,

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

要在最后一行之后切出:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

速度测试:

  • 产生的100,000,000行文件 seq 100000000 > test.in
  • 阅读线50,000,000-50,000,010
  • 测试顺序不限
  • realbash的内置时间报告的时间time
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

这些绝不是精确的基准,但是差异明显且可重复,*可以很好地理解每个命令的相对速度。

*:除了前两个sed -n p;q和之间head|tail,它们似乎基本相同。


11
出于好奇:您如何在两次测试之间刷新磁盘缓存?
帕维尔Rumian

2
怎么样呢tail -n +50000000 test.in | head -n10,哪一个tail -n-50000000 test.in | head -n10会给出正确的结果呢?
Gilles 2012年

4
好的,我去做了一些基准测试。尾巴比sed快得多,区别比我预期的要大得多。
Gilles 2012年

3
@吉尔斯,你是对的,我不好。tail+|head比sed快10-15%,我添加了基准。
凯文(Kevin)

1
我意识到这个问题要求行,但是如果您使用-c来跳过字符,那tail+|head是瞬时的。当然,您不能说“ 50000000”,可能必须手动搜索要查找的部分的开头。
Danny Kirchmeier 2014年

51

如果要包含X到Y行(从1开始编号),请使用

tail -n +$X /path/to/file | head -n $((Y-X+1))

tail将读取并丢弃前X-1行(没有办法解决),然后读取并打印以下行。head将读取并打印请求的行数,然后退出。当head退出时,tail接收SIGPIPE信号和死亡,因此它不会对已阅读比从输入文件中的行缓冲器的大小的值(通常是几千字节)以上。

或者,如gorkypl建议的那样,使用sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

但是sed解决方案要慢得多(至少对于GNU实用程序和Busybox实用程序而言;如果您在管道传输缓慢且sed快速的OS上提取文件的很大一部分,则sed可能更具竞争力)。这是Linux下的快速基准测试;数据是由生成的seq 100000000 >/tmp/a,环境是Linux / amd64,/tmptmpfs,并且计算机处于空闲状态且未交换。

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

如果知道要使用的字节范围,则可以通过直接跳到起始位置来更快地提取它。但是对于行,您必须从头开始阅读并计算换行符。要从0开始(从x包含到y互斥)提取块,块大小为b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file

1
您确定它们之间没有缓存吗?头和sed之间的差异对我来说似乎太大了。
帕维尔Rumian

@gorkypl我做了几项措施,时间可比。如我所写,这都是在RAM中发生的(一切都在高速缓存中)。
Gilles 2012年

1
tail will read and discard the first X-1 line当从末尾开始给出行数时,似乎避免使用@Gilles ,在这种情况下,根据执行时间,尾部似乎从末尾开始向后读取。请阅读:http://unix.stackexchange.com/a/216614/79743

1
@BinaryZebra是的,如果输入是常规文件,则某些实现tail(包括GNU tail)具有从末尾读取的试探法。tail | head与其他方法相比,这改善了解决方案。
吉尔斯

22

head | tail方法是执行此操作的最佳方式和最佳方式之一:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

正如Gilles在评论中指出的,更快的方法是

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

之所以更快,是因为与该方法相比,前X-1条线不需要穿过管道head | tail

您所说的问题有点误导人,并且可能解释了您对这种方法的一些毫无根据的疑虑。

  • 你说你算算ABCD但你可以看到,不需要文件的行数和最多1计算是必要的,其外壳能为你闲着。

  • 您担心管道将读取比所需更多的行。实际上,这是不正确的:tail | head就文件I / O而言,效率差不多。首先,考虑所需的最少工作量:在文件中找到第X行,唯一的一般方法是读取每个字节并在计数X个换行符时停止,因为无法识别文件第X行的偏移量。一旦到达第* X *行,就必须阅读所有行以打印它们,并在第Y '行处停止。因此,任何方法都无法摆脱读取少于Y行的问题。现在,head -n $Y读取的内容不超过Y行(四舍五入到最接近的缓冲单元,但是如果正确使用缓冲会提高性能,所以不必担心开销)。此外,tail不会读取超过的内容head,因此,我们显示了head | tail读取的行数最少(再次加上一些我们忽略的可忽略的缓冲)。不使用管道的单个工具方法的唯一效率优势是更少的过程(因此开销也更少)。


1
从来没有见过重定向先行。凉爽,它使管道通畅。
clacke

14

最正统的方式(但不是最快的方式,如上面的Gilles所述)是使用sed

在您的情况下:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

-n选项意味着仅将相关行打印到stdout。

结束行号末尾的p表示在给定范围内打印行。该q在脚本的第二部分通过跳过文件的剩余部分可以节省一些时间。


1
我期望sed并且tail | head与之差不多,但是事实证明它tail | head要快得多(请参阅我的回答)。
Gilles 2012年

1
从我所读的内容中,我不知道tail/ head被认为更“正统”,因为修剪文件的两端恰恰是它们的目的。在这些材料中,sed似乎只有在需要替换时才进入画面,而在发生任何更复杂的动作时迅速将其推出画面,这是因为其用于复杂任务的语法比AWK差得多,后者接管了。
underscore_d

7

如果我们知道要选择的范围,则从第一行:lStart到最后一行:lEnd我们可以计算:

lCount="$((lEnd-lStart+1))"

如果我们知道总行数:lAll我们还可以计算到文件末尾的距离:

toEnd="$((lAll-lStart+1))"

然后,我们将两者都知道:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

从以下任何一项中选择最小的一项tailnumber

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

允许我们使用执行速度最快的命令:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

请注意,当$linestart选择了附加的加号(“ +”)。

唯一的警告是我们需要总行数,这可能需要一些额外的时间才能找到。
与往常一样:

linesall="$(wc -l < "$thefile" )"

测量的一些时间是:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

请注意,如果选定的行位于起点或终点附近,则时间会急剧变化。在文件的一侧看起来运行良好的命令在文件的另一侧可能非常慢。


评论不作进一步讨论;此对话已转移至聊天
terdon

@BinaryZebra - 的方式更好。
mikeserv

0

我经常这样做,所以写了这个脚本。我不需要找到行号,脚本可以完成所有操作。

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4

2
您正在回答未曾提出的问题。您的答案是10%tail|head,该问题已在问题和其他答案中进行了广泛讨论,而90%的问题是确定出现指定字符串/模式的行号,这不是问题的一部分。PS,您应该始终引用您的shell参数和变量;例如“ $ 3”和“ $ 4”。
G-Man
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.