`
足至迹留
  • 浏览: 485966 次
  • 性别: Icon_minigender_1
  • 来自: OnePiece
社区版块
存档分类
最新评论

<基础-2>入门实例, 顺序环视,逆序环视

阅读更多
二、入门实例扩展
大家还记得第一篇里提到过的查找重复单词的例子吗?完整解决这个问题只需要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.”则匹配到的是 ,


如果用肯定型顺序环视“(?=Jeffery)”匹配到的位置如下:


顺序环视会检查子表达式能否匹配,但它只寻找能够匹配的位置,而不会占用这些字符,通过与普通正则表达式结合可以得到更精确的匹配,如“(?=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和其他语言都把数字,字母和下划线当做单词的一部分。结果,单词分界符的意思就是,在此位置的一侧是单词(例如数字),另一侧不是(比如行的末尾或空格)。
这个“一侧如此这般,另一侧如此那般”好像很眼熟,就像前面的顺序环视或逆序环视,这里的区别之一在于,有一侧必须使用否定的匹配。这样看来,迄今为止我们用到的顺序环视和逆序环视都应该被称为肯定顺序环视和肯定逆序环视。因为他们成功的条件是子表达式在这些位置能够匹配。正则表达式还提供了相对应的否定顺序环视和否定逆序环视。


所以单词分界符的意思是,一侧是”\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)+”匹配的数字属于最终匹配文本,是占位的,不能作为”未匹配”部分供下次匹配。但可以借助语言的循环迭代来处理,一次匹配一个位置。
  • 大小: 8.1 KB
  • 大小: 12.4 KB
  • 大小: 56.6 KB
1
4
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics