现在的位置: 首页 > 综合 > 正文

正则表达式之

2017年01月11日 ⁄ 综合 ⁄ 共 7332字 ⁄ 字号 评论关闭
文章目录

正则表达式的应用之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)点号、^、$、*、[…] 、[^…]

        这些在正则表达式中的意思大都一样,不过也要依据具体的实现,大部分的实现点号都是匹配除了换行符之外的所有字符,至于*的意思就很明确了,表示的是0-多个字符,无上限。也就是说 正则表达式"s*" 是一定可以匹配的,因为s可以出现也可以不出现,".*"就可以代表任意多的任意字符。
        echo "zydovech" | grep 'd*'>输出为 zydovech (其中颜色部分为匹配部分,>表示后面的是输出)
        echo "zydddddovech" | grep 'd*'>输出为zydddddovech 
        echo "zyddovech" | sed 's/d*/sss/'>zyssovech
        echo "zydovech"   | sed 's/d./zy/'   >zyzyvech
        echo "zydovech"   | grep 'd.'      >zydovech
       这里要强调的一点是在精通正则表达式书中86页(中文版,第三版)中说道在grep中^和$只有在正则表达式的开头和结尾是元字符,所以用户无法使用 end$|^start 这样的正则表达式,还说明了grep是不支持多选结构的,但是在我所使用的GNU grep中是可以的,如:
echo "start start end end" | grep 'end$\|^start'             >start start endend
sed也是支持的
        echo "start start end end" | sed 's/^start\|end$/***/'        >*** start end end 看到这个可能会问最后一个end为什么没有匹配?这和sed本身有关,sed会替换一行文本中第一次匹配的字符串,然后结束,可以加上g全部替换:

        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]'  > dsss其中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等使用相应括号内的匹配文本。叫反向引用。

        不过可以通过其他方法使用,主要有两种:

        第一种:在grep后面加-E。即 grep -E 'e?' 这种形式,这样就是等效于使用egrep。在 sed后面加 -r 等效于使用扩展正则表达式,即ERE。在grep的man手册里 还提到可以在grep后面加-P以使用Perl流派的正则表达式,这里不再多说。可以自己查看对应的手册。

第二种:通过转义\(\)

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) +、?

         grep和sed是不直接支持这两个元字符的,也就是说在grep和sed中+和?是普通字符,可以直接匹配文本,如:
        echo "zydove?ch" | grep 'e?'> 输出为zydove?ch。
        echo "zydove+ch" | grep 'e+'
 > 输出为zydov
e+ch
        echo "zydove+ch" | sed 's/e+/**/'>zydov**ch (?也同样的道理)
通过转义后可以使用:
         echo "zydove?ch" | grep 'e\?'          > 输出为zydove?ch  通过转义\?之后,\?就有元字符?的意义了(匹配0-1个字符)
         echo "zydoveeeech" | grep 'e\+'      > 输出为 zydoveeeech 现在可以看到+具有元字符的意义了。可以看出+匹配

        所有可以匹配的字符。这里的意思是有几个e我就匹配几个,事实上就是如此。有一种情况是判断如下的匹配:            echo
"zydoveeeeeech" | grep '\(e\+\)\(e\+\)'  中第一个分组和第二个分组分别匹配的是什么?这个可以通过反向引用验证:第一个分组匹配尽可能多的e。只会留下一个e给第二个分组匹配。至于为什么这样,+和*都是匹配优先的,就是指:他们会一直匹配到不能匹配为止,如果后面需要再进行回溯。上面例子的匹配过程如下:

1:e和第一个e匹配。

2:\+匹配一到多个e,匹配到最后一个e。

3:因为(e\+)至少需要匹配一个e则,经过第二步的匹配必须回溯,交换一个字符给e\+使用,交换一个字符后e\+已经可以匹配,不会再强迫之前的匹配交还字符。

4:匹配完毕。

那么是如果是下面的表达式两个括号分别匹配的是什么?

echo "zydoveeeeeech" | grep '\(e\+\)\(e*\)'        

(第一个括号匹配所有的e,最后一个括号啥都不匹配。详细的解释请参考《精通正则表达式》)

在sed中道理也是一样的,可以自己验证。

echo "zydoveeeech" | sed 's/e\?/*/'         >zydov*eeech

echo "zydoveeeech" | sed 's/e\+/**/'        >zydov**ch

在sed中很好验证前面说的那个匹配多少个e的问题,这里提前使用一下反向引用。\1和\2分别代表正则表达式中第一个括号和第二个括号所匹配的文本:

echo "zydoveeeech" | sed 's/\(e\+\)\(e\+\)/\1,\2/'         >zydoveee,ech 

其中\1就是代表第一个括号之间匹配的三个eee,\2代表了第二个括号间代表的一个e。如此和上面所说一致

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

这个正则表达式看起来比较复杂,仔细分析一下就很简单,主要是因为转义\符多了,看起来不直观。

echo "zydoveeeech" | grep 'e\{1,3\}' >zydoveeeech 

可以看到所有的e都匹配了,有人说区间量词{1,3}不是匹配一到三个e吗?的确是这样,在匹配了前三个e之后,grep 会继续匹配后面的内容。

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

其中,\w表示单词字符一般情况下表示数字和字符[0-9a-zA-Z_],\W表示的和\w相反。sed和grep是支持这两个元字符序列的。
\s表示的一般包括空格 、Tab、换行符。\S则相反。sed 支持这两个操作,但是grep不支持。为了测试\s包含空格、Tab、换行符,所以写一个测试文件data data里的内容为:

this is as

hhhhhhhhhhhhhh

其中第一个s后面是一个空格,第二个s后面是Tab。第三个s是行的结束,不知道为什么我在编辑的时候显示很好,已查看就乱了。

sed 's/s\s/**/g' data >输出为
thi**i**as
hhhhhhhhhhh
结果并不是我们所想的那样,空格和Tab是正确匹配替换了,但是换行符并没有。但是如果按照下面这样:
sed '{N;s/s\s/**/g}' data >thi**i**a**hhhhhhhhhh 
这里匹配了换行符。
其中N是sed中的命令。这里不展开说了。
\d和\D。grep和sed都不支持。不举例说了。不过我们可以用[0-9]代替。


至此一些常用的总结了一下,不过也不是很全面。还要涉及到很多其他东西,匹配模式呀,编码呀,\w
、\s、还有^、$所代表的意义不是那么简单的,这方面精通正则表达式一书说的更加详细。我也只是学习没多久,很多地方理解的还不是很好,可能有错误。欢迎指正。


 


  

抱歉!评论已关闭.