正则表达式(Regular Expression, RE),就是用某种模式去匹配一类字符串的一个公式,据说最初来源于人工智能理论。它现在被各种文本编辑软件、类库、脚本工具(awk/grep/sed)等广泛支持,而且Microsoft的工具也开始支持。前段时间使用这些脚本工具完成过一个数据库文本文件的格式处理工作,感觉到正则表达式实在是太强大的,这个思想其实完全可以应用于C语言的开发,已经有可用的RE Library了。
在Linux下首先要理解一下元字符的概念。元字符是一类表达的是不同于字面本身含义的字符。有两类元字符:shell的元字符和正则表达式元字符,它们是各司其职的。shell元字符由Unix/Linux的shell来解析,正则表达式的元字符则是由各种执行模式匹配操作的程序来解析,如vi、grep、sed、awk(bash提供了一套模式匹配元字符,类似于grep、sed、awk所使用的正则表达式的元字符,但是还是有所不同的。)
举个简单的例子。比如“*”星号。shell中的星号是通配符,表示匹配0个或者多个任意字符,而RE中的星号则是表示匹配0个或多个相同的前导字符。所以,在RE中,常常用“.*”来表示0个或者多个任意字符。
也就是说,你首先要对比分清shell下的元字符和RE下的元字符并非相同,不可混用。使用工具来应用RE时,要注意使用单引号把正则表达式括起来,防止被shell当作其元字符解析。如果能够区分了,那么还要理解RE也并非是完全一样。现在的正则表达式有两套字符集,一套是基本元字符集,另一套是扩展元字符集。POSIX标准为此提供了一套RE标准。也就是各个工具首先是支持基本元字符集,但是并不一定支持所有的扩展元字符集,这就是实现相关的问题了。需要针对使用RE的工具(比如vim、grep、sed、awk、perl、python等)的帮助文档,看看它所支持的RE元字符集有那些,然后才能设计出合理高效的正则表达式,完成自己的工作。
到这里,基本上把正则表达式的概念,来源,与shell的区别,本身的不同实现都搞清楚了。这样,也就不会询问这个正则表达式在sed下好用,但是使用vim就不行,为什么?很简单,元字符的支持问题。
有一本《正则表达式之道》(A Tao of Regular Expressions),实际上就是针对Unix Regular Expression作出的分析。它不仅介绍了正则表达式的一般概念,还对Unix下的工具支持的正则表达式做了举例说明,最后还有一个对元字符的支持比较图(虽然版本都有点老了,不过对概念的理解还是非常好的)。另外,在CU shell版精华贴中,对此有很多优秀的总结。可以再加深一下认识。
下面针对正则表达式的贪婪规则做了两个练习。
正则表达式有最长匹配的特性,也就是贪婪规则。
举例一:
|
[armlinux@lqm bash]$ cat test1 uid=500(guest) gid=500(others) groups=500(users),11(floppy)
|
下面把小括号内的值单独取出,如果利用贪婪规则,解决方案如下:
|
1 sed -e 's/[^(]*(\([^)]*\).*/\1/' test1 2 sed -e 's/[^(]*(\([^)]*\)[^(]*(\([^)]*\).*/\2/' test1 3 sed -e 's/[^(]*(\([^)]*\)[^(]*(\([^)]*\)[^(]*(\([^)]*\).*/\3/' test1 4 sed -e 's/.*(\(.*\))/\1/' test1
|
上面四条命令分别可以获得括号内的数值。如果是不利用贪婪规则,还有其他的解决方案:
|
//利用sed和awk
[armlinux@lqm bash]$ sed -e 's/[()]/:/g' test1 | awk -F: '{ print $2 }' guest [armlinux@lqm bash]$ sed -e 's/[()]/:/g' test1 | awk -F: '{ print $4 }' others [armlinux@lqm bash]$ sed -e 's/[()]/:/g' test1 | awk -F: '{ print $6 }' users [armlinux@lqm bash]$ sed -e 's/[()]/:/g' test1 | awk -F: '{ print $8 }' floppy
//利用tr和cut [armlinux@lqm bash]$ tr "()" ':' < test1 | cut -d: -f2 guest [armlinux@lqm bash]$ tr "()" ':' < test1 | cut -d: -f4 others [armlinux@lqm bash]$ tr "()" ':' < test1 | cut -d: -f6 users [armlinux@lqm bash]$ tr "()" ':' < test1 | cut -d: -f8 floppy
|
上面的这两种方式的思路都是首先利用合适的分隔符来分割域,然后获取指定域。整体来看,第二种利用tr和cut倒是好一些,因为tr和cut相对于sed和awk来说毕竟要小一些。如下:
|
[armlinux@lqm bash]$ ls -hl $(which sed) -rwxr-xr-x 1 root root 46K May 3 2007 /bin/sed [armlinux@lqm bash]$ ls -hl $(which awk) lrwxrwxrwx 1 root root 4 Oct 25 08:09 /bin/awk -> gawk [armlinux@lqm bash]$ ls -hl $(which gawk) -rwxr-xr-x 1 root root 247K Feb 22 2005 /bin/gawk [armlinux@lqm bash]$ ls -hl $(which tr) -rwxr-xr-x 1 root root 34K May 5 16:14 /usr/bin/tr [armlinux@lqm bash]$ ls -hl $(which cut) -rwxr-xr-x 1 root root 29K May 5 16:14 /bin/cut
|
举例二:
|
[armlinux@lqm bash]$ cat test2 aaa:2008<hr><br>aaa:2009<hr><br>
|
这是CU shell版讨论的一个例子。目标是分别取出2008和2009两部分,利用贪婪规则如下:
|
2009 sed -e 's/.*:\([^<]*\).*/\1/' test2 2008 sed -e 's/[^:]*:\([^<]*\).*/\1/' test2
|
还有其他的解决方案。
|
[armlinux@lqm bash]$ tr "<>" ':' < test2 | cut -d: -f2 2008 [armlinux@lqm bash]$ tr "<>" ':' < test2 | cut -d: -f7 2009
|