Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2359167
  • 博文数量: 527
  • 博客积分: 10343
  • 博客等级: 上将
  • 技术积分: 5565
  • 用 户 组: 普通用户
  • 注册时间: 2005-07-26 23:05
文章分类

全部博文(527)

文章存档

2014年(4)

2012年(13)

2011年(19)

2010年(91)

2009年(136)

2008年(142)

2007年(80)

2006年(29)

2005年(13)

我的朋友

分类: LINUX

2007-03-28 14:35:24

grep
====

要在一个给定的文件中搜索某个字串, 这是最普通不过的任务:
grep pattern filename

如果匹配的项很多, 而你只想关注前10项呢?

grep pattern filename | head -10

对于filename比较小的情况, 一切OK. 但是, 在真实的工作环境中, 文件大至几百M呢? 你可能(这里留下话头, 有些版本的grep实现会在head 读取10行后退出时也马上退出)会明显感觉到延迟, 你会怀疑你的pattern是否正确, 怀疑你的grep命令有没有被alias偷偷改写, 怀疑程序会不会在等待一个输入, 怀疑telnet是不是断线了, 你会尝试敲几个字符, 看看终端有没有反应, 或者你会按CTRL-Z

如果碰巧你想搜索的pattern 在文件的前面部分出现, 你已经看到了所希望的TOP 10, 但它grep就是挂在那里不退出, 你一定会有一种过河拆桥的冲动想用CTRL-C, CTRL-\, CTRL-Q 灭掉grep

如何在得到想到的东西之后让grep及时停手? 如果只是想知道某文件中是否包含一个pattern, 可以用
grep -l pattern filename
grep会在找到第一个匹配之后退出文件后续部分的读取, 速度很快. 但是, 缺点是明显的, 你无法知道其第一个匹配的行是什么样子.

sed 的q 命令
===========

q代表quit, 这个quit不仅是逻辑上的quit, 实际实现上也不再读取文件的后续部分了.
sed -n '/pattern/{p;q;}' filename
{p;q;} 是一个命令组, 表示如果匹配到/pattern/ 依次执行这个命令组中的命令.

sed的这个命令做到了 1) 显示第一个匹配的行 2) 找到第一个匹配行后不再读取其后的内容, 退出处理

但, 如果想显示前10个而不仅仅是第一个匹配行呢? sed无能为力了, 它没有内置的变量可供记录10这样的状态.

awk 的nextfile
==============

awk 支持变量, 这使得很容易知道"前10个匹配"这样的概念, 关键只在中止对文件的读取这一步了. exit语句. 注意即使有exit语句 END块还是会被执行, 这里用不到END块

awk 'BEGIN{i=0}(i>9){exit;}/pattern/{i++;print;}' filename
三个部分:
a) BEGIN{i=0} 在处理之初给变量i赋0值, 由于awk中的变量会在第一次被使用时获取一个0初值, 所以这一个部分其实可以省略
b) (i>9){ exit; }  如果该变量大于9就退出处理
c) /pattern/{ i++; print; }  如果匹配模式, i自增1, 同时显示该行

测试表明, sed和awk中的方案的确能有效地减少处理时间, 在处理大文件时顾及效率. 这也说明unix下的这些小工具各自具有自己的特殊个性, 具体应用时要关于取材.

管道的问题
=========

现在我捡起前面的话头, grep pattern big_file | head -10 可能是最明显也最快的解决方案. 这一切取决于grep 程序的实现是如何处理 SIGPIPE信号的, 对于管道, 当读进程已经退出而写进程仍尝试写入时, 写进程会收到一个SIGPIPE 信号, 如果grep 在收到该信号时会退出, 它自然不再去读文件后面的内容, 如果它忽略了该信号, 就会出现本文前面提及的情况.

下面是我对管道这一特点的验证:

#!/bin/bash

# test pipe

#trap '' 13

trap 'echo catch SIGPIPE i = $i > result.txt; exit 2' 13

echo abc declare -i i=0

while [ $i -lt 200000 ]; do echo $i let i=i+1 done

echo xyz


此时进行了管道的处理, 脚本退出, 脚本中的while循环是个人为的耗时的操作, 大概耗时10秒. 为什么不用sleep 10是有原因的, 正如CPU的异常只发生在一条机器指令执行完成的边界一样, UNIX的信号只发生在一个系统调用完成的边界上, 而sleep 10是一个系统调用, 所以pipe.sh 有机会处理SIGPIPE 信号是在sleep 10之后. 可能是同样的道理, 用 > /dev/null 同样会延迟pipe.sh 处理信号的机会.

time { ./pipe.sh | head -1; } | tail -1
结果是
abc

real    0m0.062s
user    0m0.039s
sys     0m0.023s
查看 result.txt 文件, 两者都说明pipe.sh 正确地收到了信号并及时退出了.

然后, 只是把上面的脚本中的trap部分修改为默认的忽略该信号:

#!/bin/bash

# test pipe

trap '' 13

#trap 'echo catch SIGPIPE i = $i > result.txt; exit 2' 13

echo abc declare -i i=0

while [ $i -lt 200000 ]; do echo $i let i=i+1 done

echo xyz


再次运行time 查看处理时间:
real    0m10.623s
user    0m9.422s
sys     0m1.201s

大相径庭!

我不知道POSIX的规范有没有对grep的实现做这样的规定, 如果你的系统上grep处理了SIGPIPE并退出, 那么grep pattern big_file | head -10 是最好的方案, 因为grep 的正则表达式引擎被认为是世界上最快的.

启示: 一个好的脚本, 如果希望它的输出很多, 并且它的输出可能会被 | head -1 这样的命令搭配, 应该考虑在管道被中断时优雅地退出.

cat result.txt 的内容也可以看出脚本因SIGPIPE而退出时, while循环正被执行到半途, 其中的i = $i 甚至能告诉你执行到了第几次, 当然, 这个值每次都可能不同.

巧妙构造的脚本可以揭示系统内部的很多原理, 当然, 向外看, 能帮助我们写出更好的命令/脚本.
阅读(888) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~