大家还记得第一篇里提到过的查找重复单词的例子吗?完整解决这个问题只需要perl之类的语言写几行代码。
比如我们想确定每个文件中“ResetSize”出现的次数与“SetSize”出现的次数是否一样多。使用perl命令如下:
% perl -0ne ‘print “$ARGV\n” if s/RestSize//ig != s/SetSize//ig’ *
现在可以不明白这条命令,但要注意到这条命令有多简洁。
perl关于文本处理和正则表达式的许多概念来自两种专业化的语言 awk和sed,非常不同于传统的语言,如C。
2.1 使用正则表达式匹配文本
perl可以以多种方式使用正则表达式,最简单的就是检查变量中的文本能否由某个正则表达式匹配。下面的代码检查$reply中所含的字符串,报告这个字符串是否全部由数字组成:
if ($reply = ~ m/^[0-9]+$/) { print “only digits\n”; } else { print “not only digits\n”; }
第一行代码也许有些奇怪:正则表达式是“^[0-9]+$”,两边的m/…/告诉perl该对正则表达式进行什么操作。m代表尝试进行“正则表达式匹配 (regular expression match)”。斜线用来标记接线。之前的”=~”用来连接m/…/和欲搜索的字符串,即本例中的$reply。可以把”=~”读作匹配。
上面这个正则不能很好的 匹配浮点数。扩展这个表达式,允许有 负数和 可能的小数部分。 (1)容许负数我们就可以添加“ [+-]?”来处理开头的符号。 (2)容许可能出现的小数我们可以添加”(\.[0-9]*)?”。综合起来就是”^[+-]?[0-9]+(\.[0-9]*)?$”。它能够匹配32,-3.23,+98.3这样的文字。
2.2 成功匹配的副作用
第一篇我们看到某些版本的egrep支持作为元字符的”\1”,”\2”用来保存前面的括号内的子表达式实际匹配的文字。perl和其他许多支持正则表达式的语言都支持这些功能,而且匹配成功之后,在正则表达式之外的代码仍然能引用这些匹配的文本。
但是perl是通过变量$1,$2等来指向第一组,第二组括号内的子表达式实际匹配的文本。
现在我们比较”^[+-]?[0-9]+[CF]$”和”^([+-]?[0-9]+)([CF])$”。添加的括号改变了正则表达式的意义了吗?我们前面知道括号的使用场景,这里既没有改变星号或其他量词的作用对象,也没改变多选分支的范围,也就是这个表达式仍然能够匹配相同的文本。不过,他们确实围住了我们期望匹配字符串中有价值的子表达式。在perl里,第一个括号里的内容就会自动保存到$1里,第二个括号匹配的内容会自动保存到$2里,我们可以在程序里通过$1和$2两个变量来引用匹配到的内容。这个括号也称为 捕获型括号,灰常有用。也就是第一篇说到的括号的三个用途的第三种。
2.3 错综复杂的正则表达式
在perl之类的高级语言中,正则表达式的使用与其程序的逻辑是混合在一起的。针对上面的例子我们再增加一个匹配小数的功能,添加一个”(\.[0-9]*)?”就可以,最终为:
“([+-]?[0-9]+(\.[0-9]*)?)([CF])$”。请注意,增加的匹配小数部分是放在原来的第一个括号内部的,这样$2保存的就是新增的小数部分。 这些都是捕获型括号,会按照从左到右的顺序保存到变量里。但是上例中新增的小数部分可能永远不会单独被引用,这样的话能不被保存到变量里就好了。也就是说需要一个 只能用于分组,而不会影响文本的捕获和变量的保存的非捕获型括号。
perl以及近期出现的其他正则表达式流派提供了这个功能。 “(…)”用来分组和捕获,而“(?:...)”只分组不捕获。(联想下[…]和[^…])。这样“^([+-]?[0-9]+(?:\.[0-9]*)?)([CF])$”中的小数部分就不会被捕获,也不会被保存到变量里了。
我们已经见过不同的字符组之间的冲突。在egrep中,我们把正则表达式包含在单引号中。整个egrep命令行写在command-shell提示符后,shell能认出它自己的元字符。例如,对shell来说,空格符就是一个元字符,它用来分隔命令和参数,或者参数与参数。在许多shell中,单引号是元字符,单引号内的字符串中的字符不需要被当做元字符处理(DOS使用双引号)。在shell中使用引号容许我们在正则表达式中使用空格,否则shell会把空格认作参数之间的分隔符,而不是把整个正则表达式传递给egrep。
在perl的正则表达式中, “\b”通常是匹配一个单词分界符的, 但在字符组中,它匹配一个退格符。前一篇我们提过,字符组可以看做一个独立的“子语言”,它里面的规范不同于正则表达式主体,这条规则也适用于perl(包括任何其他流派的正则表达式)。
补充:
\b:表示匹配一个位置,并不占用任何字符,这个位置的一侧是单词字符,一侧为非单词字符,或字符串的开始或结束位置。
还可以参考:
http://wenku.baidu.com/link?url=iWQ-tay23mSRdlOOLR9u-FLxGnFd0xEIoLNFBKL9gBGj2E5ek4HaEAgGyVibLO5n5av9IhwVtoSqBXhWonpVkZkonl7RssMTw2qC2UF5ERK
2.3.1 使用\s匹配所有空白
讨论空白的问题时,我们可以使用“[ \t]*”,这样做没问题,但许多流派的正则表达式提供了一种方便的办法:”\s”。”\s”看起来类似”\t”, “\t”代表制表符,而”\s”则能表示所有表示”空白字符(whitespace character)”的字符组,其中包括 空格符,制表符,换行符和回车符。
2.3.2 小结
尽管上面大部分例子是关于perl的,但其思想也适用其他语言。
1. 许多工具都有自己的正则表达式流派。perl和egrep可能属于同一个流派,但perl的正则表达式中的元字符更多。许多其他的语言,如java,python,.net等,他们的流派都类似于perl。
2. perl用$variable =~ m/regex/ 来判断一个正则表达式是否能匹配某个字符串。m表示“匹配”(match),而斜线用来标注正则表达式的边界(他们本身不属于正则表达式)。整个测试语句作为一个单元,返回true或者false。
3. 元字符(具有特殊意义的字符)的定义在正则表达式中并不是统一的。
4. perl和其他流派的正则表达式提供了许多用用的简记法(shorthands):
\t 制表符
\n 换行符
\r 回车符
\s 任何“空白”字符(例如空格符,制表符等)
\S 任何除“\s”之外的字符
\w 表示[a-zA-Z0-9],(\w+很有用,可以用来匹配一个单词)
\W 除\w之外的任何字符,也就是[^a-zA-Z0-9]。
\d [0-9],即数字
\D 除\d之外的任何字符,即[^0-9]。
5. /i修饰符表示此测试不区分大小写。
6. (?:…)可以用来分组文本,但并不捕获。
7. 匹配成功之后,perl可以用$1,$2,$3之类的变量来保存相对应捕获型(…)括号内的子表达式匹配的文本。
2.4 使用正则表达式修改文本
到现在,我们遇到的例子都只是从字符串中提取信息。现在我们来看perl和其他许多语言提供的一个正则表达式特性: 替换(也可以叫做‘查找和替换’)。
我们已经看到,”$var =~ m/regex/”尝试用正则表达式来匹配保存在变量中的文本,并返回表示能否匹配的布尔值.与之类似的结构” $var =~ s/regex/replacement/”则更进一步:如果正则表达式能够匹配$var中的某段文本,则将这段匹配的文本替换为replacement。其中regex与之前m/…/的用法一样,而replacement(位于第二个和第三个斜线之间,不包括斜线)则作为双引号内的字符串。也就是说,在其中可以使用变量,例如$1和$2来引用之前匹配的具体文本。
如$var的值为”Jeff Friedl”, 那么运行: $var =~ s/Jeff/Jeffrey/;
$var的值就变成”Jeffery Friedl”。如果再运行一次,就得到”Jeffreyrey Friedl”。要避免这种情况,我们就可以添加单词分界符的元字符。perl中的单词分界符是”\b”,:
$var =~ s/\bJeff\b/Jeffery/;
2.4.1 举例:修正股票价格
有时候我们得到的股票价格类似”9.05000037272”,我们希望展示的是”9.05”。抽象一下要求就是: 通常保留小数点后两位数,如果第三位不是零,也需要保留,去掉其他的数字。也就是”12.37500392”或”12.375”会被修正为12.375,而37.500被修正为37.50。
假设$price包含了需要修正的字符串,我么可以用:
$price =~ s/(\.\d\d[1-9]?)\d*/$1/;
解释下:最开始的”\.”匹配小数点。接下来的”\d\d”匹配开头的两位小数.”[1-9]?”匹配可能跟在后面的非零数字。到这里,任何匹配的文本都是我们希望保留的,所以我们用括号把它们保存到$1中,然后将$1放入replacement字符串中。正则表达式的末尾”\d*”用来匹配其他数字,但并不是我们需要保留的,所以在括号外,但必须要有。
补充:这里介绍一个类似匹配时的”/i”的选项”/g”,在替换的时候很有用,/g 全局替换符,如果需要检查的字符串包含多行需要替换的文本,每条替换规则都对所有的行生效,我们就必须使用/g。默认不使用时只替换第一个匹配的文本。这里的例子虽然不涉及这个选项,后面会遇到的。类似 “$var =~ s/regex/replacement/g”
2.5 用环视功能为数值添加逗号
大的数值,如果在其间加入逗号,会更容易理解,如”The US population is 298444215”后面的数值可能写成”298,444,215”更自然。如果用正则表达式来修正句子里的数值该如何做呢?
我们应该从这个数的右边开始,每次数3个数字,如果左边还有数字的话就加入一个逗号。但是正则表达式一般都是从左到右工作的。不过梳理一下思路会发现,逗号应该加在“ 左边有数字,右边数字的个数正好事3的倍数的位置”。这样,使用一组相对较新的正则表达式特性:“ 环视(look around)”能轻松解决这个问题。
环视结构不匹配任何字符,只匹配文本中的特定位置。这一点与单词分界符”\b”,锚点”^”和”$”相似,但是环视更加通用。
2.5.1 顺序环视 (Look ahead)和逆序环视
一种类型的环视叫“顺序环视”,作为表达式的一部分,顺序环视从左到右查看文本,尝试匹配子表达式,如果能够匹配,就返回匹配成功信息。顺序环视又分肯定型和否定性顺序环视两种,比如肯定性顺序环视(positive look ahead)用特殊的序列”(?=…)”来表示。例如”(?=\d)”表示如果当前位置右边的字符是数字则匹配成功。
另一种环视被称为逆序环视,它从右向左查看文本。他用特殊的序列(?<=…)表示,例如”(?<=\d)”表示如果当前位置的左边是一位数字则匹配成功(也就是说,紧跟在数字后面的位置)。
环视不会占用字符。
在理解顺序环视和其他环视功能时需要特别注意一点,即在检查子表达式能否匹配的过程中,他们本身不会占用任何文本。举例说明,普通正则表达式”Jeffrey”匹配文本”… by Jeffrey Friedl.”则匹配到的是 ,
![](http://dl2.iteye.com/upload/attachment/0094/9311/ec4e52ad-cf9f-3e04-9c17-a37424387b6e.bmp)
如果用肯定型顺序环视“(?=Jeffery)”匹配到的位置如下:
![](http://dl2.iteye.com/upload/attachment/0094/9313/aefdb7d7-f0d4-3ba0-aca7-3ba41568f2c1.bmp)
顺序环视会检查子表达式能否匹配,但它只寻找能够匹配的位置,而不会占用这些字符,通过与普通正则表达式结合可以得到更精确的匹配,如“(?=Jeffery)Jeff”表示只能匹配”Jeffery”这个单词中的”Jeff”。我们还会发现”Jeff(?=rey)”与它是等价的。
下面我们看下”m/(?<=\bJeff)(?=s\b)/’/g”什么意思?
这个表达式实际上并没有匹配任何字符,只是匹配了一个希望插入撇号的位置。
回到2.5节开始提到的在数字中插入逗号的问题。我们希望给一个很长的数字插入逗号,插入逗号的位置必须满足“左边是数字,右边数字的个数正好是3的倍数”。第二个要求用逆序环视很容易解决,”(?=(\d\d\d)+$)”左边只要有一位数字就能满足“左边有数字”的要求,可以用”(?<=\d)”。结合在一起就是” (?<=\d) (?=(\d\d\d)+$)”。
我们还可以使用非捕获型括号”(?:…)”来限制,得到” (?<=\d) (?=(?:\d\d\d)+$)”,这样做的好处一是不会担心与捕获型括号关联的$1是否会被用到,二是效率更高,因为引擎不需要记忆捕获的文本。
2.5.2 单词分界符和否定环视
现在再来讨论给数字插入逗号的问题,如果一个数字在一个句子中间,例如:
$text = “the population of 298444215 is growing.”;
如果用$text =~ s/(?<=\d)(?=(\d\d\d)++$)/,/g;来插入逗号是不会有效的,因为最后的$要求最后是3的倍数位数字结尾。但是我们也不能直接去掉$,因为去掉后就会把数字变成 … of 2,9,8,4,4,4,215 is growing.(仔细想想)
可能初看起来有些棘手,但我们可以用单词分界符”\b”来替换”$”.尽管我们处理的只是数字,perl的单词概念也能解决这个问题。就像”\w”一样,perl和其他语言都把数字,字母和下划线当做单词的一部分。结果,单词分界符的意思就是,在此位置的一侧是单词(例如数字),另一侧不是(比如行的末尾或空格)。
这个“ 一侧如此这般,另一侧如此那般”好像很眼熟,就像前面的顺序环视或逆序环视,这里的区别之一在于,有一侧必须使用否定的匹配。这样看来,迄今为止我们用到的顺序环视和逆序环视都应该被称为 肯定顺序环视和肯定逆序环视。因为他们成功的条件是子表达式在这些位置能够匹配。正则表达式还提供了相对应的否定顺序环视和否定逆序环视。
![](http://dl2.iteye.com/upload/attachment/0094/9315/f9065bf5-f7d5-3771-9509-d78f0fdec43a.bmp)
所以单词分界符的意思是,一侧是”\w”,而另一侧不是”\w”,就可以”(?<!\w)(?=\w)”来表示单词起始分界符,用”(?=\w)(?<!\w)”表示单词结束分界符。两者用或结合起来” (?<!\w)(?=\w)| (?=\w)(?<!\w)”就等价于”\b”(从这里我们也看到,如果不使用括号来限制范围,或’|’的范围无限延伸。比如aa|bb匹配“aa或bb”,(aa)(bb)|(cc)(dd)匹配“(aa)(bb)或(cc)(dd),但是(aa)(bb|cc)(dd)中或的范围就只是”bb或cc””)。如果语言本身不支持”\b”就可以用前者代替,但效率不如直接支持的”\b”。
对于前面的插入逗号的为题,我们真正需要的是”(?!\d)”来标记3位数字的起始计数位置,用它来取代”\b”或”$”得到:
$text =~ s/(?<=\d)(?=(\d\d\d)+(?!\d))/,/g;
2.5.3 不通过逆序环视添加逗号
逆序环视和顺序环视一样,所获的支持十分有限,使用也不广泛。尽管perl现在两者都支持,但许多其他语言不是这样。想一想如果不用逆序环视来解决加逗号的问题可能更有意义:
$text =~ s/(\d)(?=(\d\d\d)+(?!\d))/$1,/g;
与之前的区别在于,开头的”\d”所处的肯定逆序环视编程了捕获型括号,replacement字符串则在逗号之前加入了相应的$1。
如果连顺序环视也不用呢?写成:
$text =~ s/(\d)((\d\d\d)+\b)/$1,$2/g;
可以吗?
结果并非期望,会得到类似”281,421906”的字符串,因为”(\d\d\d)+”匹配的数字属于最终匹配文本,是占位的,不能作为”未匹配”部分供下次匹配。但可以借助语言的循环迭代来处理,一次匹配一个位置。