正则表达式的应用之grep、sed
一:前言
本文主要介绍正则表达式在grep、sed下的应用。我所用的是GNU grep 2.5.4(grep -V 查看版本信息)、GNU sed 4.2.1(sed --version)。
grep很多人很熟悉,sed这个工具可能会不熟悉,不过没关系,下面重点在正则表达式,下面的测试中我都用sed 's/···/···/' 类似这种结构,其中s代表替换,第一个/···/之间的是正则表达式,第二个/···/之间是要替换的文本。
GNU版本的都差不多,但是如果不是GNU版本的话,对于元字符的支持和解释可能是不同的,这里要大概说一下正则表达式的流派(流派不同对元字符的定义和解释不同),在POSIX规范中,涉及到BRE、ERE流派,而本文所要介绍的grep和sed 都属于 BRE。 不过现在的BRE和ERE功能上并没有多大的区别,只是差异在元字符的转义上,比如在grep中是不能直接使用+元字符的,但是经过转义后(即:\+)就和ERE中的+元字符表达同样的意思了(匹配一到多个字符)。关于流派之类的详细说明可以参考《精通正则表达式》一书。下图为关于POSIX正则表达式流派概览:(基于我自己使用的工具)
正则表达式特性 | BRE | ERE |
点号、^、$、[···] 、[^···] | 支持 | 支持 |
任意数目 量词 | * | * |
+和? 量词 | 需要转义 \+ 和 \? | 直接使用 + ? |
区间量词 | 需要转义\{min,max\} | 直接使用{min,max} |
分组 | 需要转义\(···\) | 直接使用(···) |
量词可否作用于括号 | 支持 | 支持 |
反向引用 | \1到\9 | 不支持 |
多选结构 | 需要转义 \| | 直接使用 | |
二:grep和sed 的应用实例
1)点号、^、$、*、[…] 、[^…]
echo "start start end end" | grep 'end$\|^start' >start start endend
echo "start start end end" | sed 's/^start\|end$/***/g'>*** start end end ***
[…]表达的是字符组的概念,不过我们要记得第一个规则就是[…]值匹配一个字符,无论[…]里写了多少字符。
echo "zydovech" | grep 'd[ovech]' >zydovech 这里代表d后面是o、v、e、c、h中的一个,是任何一个都可以。但是d[ovech]一共只能匹配两个字符。
第二个规则是:在[…]内元字符的定义是不一样的,我只遇到过-(连字符)^(排除字符,这个字符在字符组外面表达的意思是不一样的) 而且必须在特定的位置才是元字符,-在正则表达式的开头的话就不是元字符了,^必须在开头,表示的是除了字符组内的字符以外的字符。
echo "zydovech" | grep 'd[a-z]' >zydovech 其中[a-z]表示从a-z之间的一个字符,是按照ASCII的顺序。[0-9]表示的是数字[A-Z]表示的是大写字母,注意不要写成一些奇怪的范围,比如[1-z]等,这要的范围不仅不好理解,而且匹配的范围也很难说清楚。但是-在字符组前面就不再是元字符[-sa]则-只是普通的字符,
echo "zy-s" | grep '[-sa]' >zy-s可以看出-不再是元字符
[^…]称为排除型字符组,匹配单个未列出的字符。但是必须匹配一个字符
echo "dsssd" | grep '[^s]' >dsssd其中d是没有列出的字符
echo "saaaad" | grep '[^s]s' 无法匹配,因为s前面没有字符。
2) 分组()
grep和sed是不直接支持分组元字符的,他们可以当做普通字符去匹配。
echo "zy(dove)ch" | grep '(dove)' >zy(dove)ch
echo "zy(dove)ch" | sed 's/(dove)/**/' >zy**ch
这个分组称为捕获分组(?:)这个是非捕获分组,不过grep和sed不支持,这里就不介绍了。它会保存在括号里匹配的内容,供使用,不同的工具和语言有不同的使用方法,grep和sed通过 \1和\2等使用相应括号内的匹配文本。叫反向引用。
不过可以通过其他方法使用,主要有两种:
第二种:通过转义\(\)
echo "zy(dove)ch" | grep '\(dove\)' >zy(dove)ch
通过转义后()不再是普通字符,而是代表分组的字符。他会保存括号内的正则表达式所匹配的文本内容,供以后使用,grep 和sed通过\1使用。
括号的作用有两个,一个就是单纯的分组,而就是捕获括号内正则表达式所匹配的内容,当然这两个功能不是独立,是同时起作用的。假如希望zydodododove 中的多个do就要用到分组。
echo "zydododovech" | grep '\(do\)*' >zydododovech 这里分组把括号里的内容当做整体,通配符*作用于这个整体。
echo "zydododovech" | sed 's/\(do\)*/**/' >zy**vech
接下来看看反向引用的概念:
echo "zydododovech" | grep '\(d.\)\1' >zydododovech
这里的正则表达式,应该可以明白吧,\(d.\)这个代表分组,点号匹配的是任意字符,就是o,而\1就是反向引用,\1的值就是前面括号所匹配的文本,文本匹配到的内容就是do,所以此时的\1就是do,整个正则表达式的意思就是d后面跟着一个字符,然后紧跟着相同的序列,这在匹配HTML的标签时挺有用的,因为大多数HTML标签都是成对的出现)。
不知道会不会有人有这个疑问,为啥子匹配结果是zydododovech而不是zydododovech?我刚开始学习的时候就有这个疑问,后来知道正则表达式是从左向右扫描字符串,匹配,但匹配到之后,会从匹配结束的位置开始从新匹配。上面的例子就是,从左向右匹配,当匹配到第二do时第一次匹配结束,接下来就开始从第二个do结束的位置开始从新匹配。剩余的字符串没有可以匹配的内容了。
在sed中可以更加清楚的看到括号和反向引用的使用
echo "zydodovech" | sed 's/\(d.\)\(.o\)/\1,\2/' >zydo,dovech
单独看\(d.\)\(o.\) 我们知道他们匹配的是dodo。然后sed 用\1,\2去替换文本中的dodo,通过上面的分析知道\1就是第一个括号匹配的内容do,\2是第二个括号匹配的内容do 然后\1,\2就代表了do,do 。则输出结果就明显了。
3) +、?
所有可以匹配的字符。这里的意思是有几个e我就匹配几个,事实上就是如此。有一种情况是判断如下的匹配: echo "zydoveeeeeech" | grep '\(e\+\)\(e\+\)' 中第一个分组和第二个分组分别匹配的是什么?这个可以通过反向引用验证:第一个分组匹配尽可能多的e。只会留下一个e给第二个分组匹配。至于为什么这样,+和*都是匹配优先的,就是指:他们会一直匹配到不能匹配为止,如果后面需要再进行回溯。上面例子的匹配过程如下:
在sed中道理也是一样的,可以自己验证。1:e和第一个e匹配。
2:\+匹配一到多个e,匹配到最后一个e。
3:因为(e\+)至少需要匹配一个e则,经过第二步的匹配必须回溯,交换一个字符给e\+使用,交换一个字符后e\+已经可以匹配,不会再强迫之前的匹配交还字符。
4:匹配完毕。
那么是如果是下面的表达式两个括号分别匹配的是什么?
echo "zydoveeeeeech" | grep '\(e\+\)\(e*\)'
(第一个括号匹配所有的e,最后一个括号啥都不匹配。详细的解释请参考《精通正则表达式》)
在sed中很好验证前面说的那个匹配多少个e的问题,这里提前使用一下反向引用。\1和\2分别代表正则表达式中第一个括号和第二个括号所匹配的文本:echo "zydoveeeech" | sed 's/e\?/*/' >zydov*eeech
echo "zydoveeeech" | sed 's/e\+/**/' >zydov**ch
其中\1就是代表第一个括号之间匹配的三个eee,\2代表了第二个括号间代表的一个e。如此和上面所说一致echo "zydoveeeech" | sed 's/\(e\+\)\(e\+\)/\1,\2/' >zydoveee,ech
echo "zydoveeeech" | sed 's/\(e\+\)\(e*\)/\1,\2/' >zydoveeee,ch
可以看到第一个括号匹配了所有的e。第二个括号啥都没匹配到。
4)区间量词{}
grep和sed也不是直接支持区间量词的,也要通过转义使用,区间量词{min,max}就是表示最少出现min次,最多出现max次。echo "zydoveeech" | grep 'e\{1,3\}' >zydoveeech {}也是匹配优先的,会尽可能匹配多的字符。
echo ''zydoveeeeech" | sed 's/\(e\{1,4\}\)\(e\{1,4\}\)/\1,\2/' >zydoveeee,ech
这个正则表达式看起来比较复杂,仔细分析一下就很简单,主要是因为转义\符多了,看起来不直观。
可以看到所有的e都匹配了,有人说区间量词{1,3}不是匹配一到三个e吗?的确是这样,在匹配了前三个e之后,grep 会继续匹配后面的内容。echo "zydoveeeech" | grep 'e\{1,3\}' >zydoveeeech
可以猜测一下匹配的内容是什么,可以自己尝试一下,具体的匹配过程可能不清楚,大致解释如下:echo "zydoveeech" | grep '\(e\{1,3\}\)\1'
1:e\{1,3\}匹配尽可能多的e,即把三个e都匹配完。
2:\1代表括号里的内容,现在是eee,然后匹配,发现没有e可以匹配了,则匹配失败。
3:匹配失败后e\{1,3\} 需要进行回溯,交还一个e,此时\1 代表ee,因为e\{1,3\}已经匹配了两个e了,所以只有一个e给\1匹配所以还是匹配失败。
4:再回溯,再交还一个e,此时e\{1,3\}值匹配到一个e,此时\1也就代表一个e,在e\{1,3\}匹配到一个e后,还有两个e提供给\1进行匹配,所以此时可以匹配。
5:上一次正则表达式成功匹配了两个ee,还有一个e,接下来按照上面的步骤继续进行尝试,但是匹配不到更过的单词了。。整个匹配结束。所以结果只匹配了前两个e。
5)多选结构 |
还有在grep和sed中多选结构|也不是元字符,所以,不经过转义的|只是普通的字符。
echo "zydove|ch" | grep 'dove|' >zydove|ch
echo "zydove|ch" | sed 's/|/*/' >zydove*ch
echo "zydovech" | grep 'do\|ch' >zydovech
echo "zydovech" | sed 's/do\|ch/**/' >zy**vech
再次强调,sed在完成第一次匹配后,会结束本次匹配,所以ch没被替换。
多选结构需要注意的是|把正则表达式分成两部分,把左右两部分分别看做一个整体,可以通过分组改变。即:zydo|vech 匹配的是zydo 或者vech 。zydo和vech是整体。但是若想改变加分组限制。
zy(do|ve)ch 则表示的是zy后面跟do或者ve
echo "zydovech" | grep 'zydo\|vech' >zydovech
可以匹配,zydo 先匹配成功,继续匹配的时候vech又匹配成功,此时表达的是匹配zydo或者vech
echo "zydovech" | grep 'zy\(do\|ve\)ch'
匹配失败。因为此时表达的意思是zy后跟do或者ve 然后再跟ch。这里的括号就进行了分组。在使用多选结构的时候这点要注意。
6)单词分界符
正则表达式中有很多表示单词分界的。比如:\< \> 单词的开始和结束。\b就是单词分界符(一侧是单词字符,一侧不是,基本上[0-9a-zA-Z]都是单词字符,空白属于非单词字符,这里只是一般情况下),\B和\b表达的意思相反。在我所使用的grep和sed中是支持这两种方式的。
echo "zycats cat dscat catdf" | grep 'cat' >zycatscat dscat catdf
echo "zycats cat dscat catdf" | grep '\bcat\b' >zycats cat dscat catdf
echo "zycats cat dscat catdf" | grep '\<cat\>' >zycats cat dscat catdf
echo "zycats cat dscat catdf" | grep 'cat\b' >zycats cat dscat catdf
echo "zycats cat dscat catdf" | grep '\Bcat\B' >zycats cat dscat catdf
echo "zycats cat dscat catdf" | sed 's/\Bcat\b/**/' >zycats cat ds** catdf
echo "zycats cat dscat catdf" | sed 's/\<cat\>/**/' >zycats ** dscat catdf
7)\w \W \s \S \d \D
this is as
hhhhhhhhhhhhhh
其中第一个s后面是一个空格,第二个s后面是Tab。第三个s是行的结束,不知道为什么我在编辑的时候显示很好,已查看就乱了。