码迷,mamicode.com
首页 > 其他好文 > 详细

《精通正则表达式》

时间:2015-05-27 06:21:42      阅读:297      评论:0      收藏:0      [点我收藏+]

标签:

许多种工具都支持正则表达式(文本编辑器、文字处理软件、系统工具、数据库引擎等),
不过,要想充分挖掘正则表达式的能力,还是应当将它作为编程语言的一部分。例如Java,
JScript,Visual Basic,VBScript,JavaScript,ECMAScript,C,C++,C#,elisp,Perl,Python,
Tcl,Ruby,PHP,sed和awk。


正则表达式regular expression是强大,便捷,高效的文本处理工具。正则表达式本身,
加上如同一门袖珍编程语言的通用模式表示法general pattern notation,赋予使用者描述
和分析文本的能力。配合上特定工具提供的额外支持,正则表达式能够添加、删除、分离、
叠加、插入和修整各种类型的文本和数据。


正则表达式的使用难度只相当于文本编辑器的搜索命令,但功能却与完整的文本处理语言一
样强大。


解决重复单词问题的完整程序可能仅仅只需要几行代码。使用一个正则表达式的搜索和替换
命令,读者就可以查找文档中的重复单词,并把它们标记为高亮。加上另一个,你可以删除
所有不包含重复单词的行(只留下需要在结果中出现的行)。最后,利用第三个正则表达式
你可以确保结果中的所有行都以它所在文件的名字开头。


宿主语言(例如Perl,Java以及VB.NET)提供了外围的处理支持,但是真正的能力来自正则表
达式。


我们都知道,report.txt是一文件名,但是,如果你用过Unix或者DOS/Windows的话,你就
知道"*.txt"能够用来选择多个文件。在此类文件名(称为“文件群组”file globs或者
“通配符”wildcards)中,有些字符具有特殊的意义。星号表示“任意文本”,问号表示
“任意单个字符”。所以,文件群组“*.txt”以能够匹配字符的*符号开头,以普通文字
.txt结尾。


完整的正则表达式由两种字符构成。特殊字符special characters,例如文件名例子中的*,
称为“元字符",其他为”文字“,或者是普通文本字符。正则表达式与文件名模式的区别就
在于,正则表达式的元字符提供了更强大的描述能力。文件名模式只为有限的需求提供了有
限的元字符,但是正则表达式“语言”为高级应用提供了丰富而且描述力极强的元字符。


我们可以把正则表达式想象为普通的语言,普通字符对应普通语言中的单词,而元字符对应
语法。我们用正则表达式^(From|Subject):来寻找以‘From:‘或者‘Subject:‘开头的行。其
中^(|)都是特殊字符,元字符。




行的起始和结束
或许最容易理解的元字符就是脱字符号^和美元符号$了,在检查一行文本时,^代表一行
的开始,$代表结束。正则表达式cat寻找的是一行文本中任意位置的cat,但是^cat只寻找
行首的cat。^用来把匹配文本(这个表达式的其他部分匹配的字符)“锚定”anchor在这一
行的开头。同样cat$只寻找位于行末的cat,例如以scat结尾的行。
读者最好能养成按照字符来理解正则表达式的习惯。


脱字符号和美元符号的特别之处就在于,它们匹配的是一个位置,而不是具体的文本。
在正则表达式中,除了使用cat之类的普通字符,还可以使用下面几节介绍的元字符。




字符组character classes


匹配若干字符之一。如果我们需要搜索的是单词"grey",同时又不确定它是否写作"gray",
就可以使用正则表达式结构体[...]。它容许使用者列出在某处期望匹配的字符,通常被称
作字符组character class。如[ea]能匹配a或者e。


字符组元字符


在字符组内部,字符组元字符character class metacharacter ‘-‘(连字符)表示一个范围
H[1-6]与H[123456]是完全一样的。[0-9]和[a-z]是常用的匹配数字和小写字母的简便方式。
多重范围也是容许的,例如[0123456789abcdefABCDEF]可以写作[0-9a-fA-F]当然也可以写
成[A-Fa-f0-9]顺序无所谓。这3个正则表达式非常适用于处理十六进制数字。我们还可以
随心所欲地把字符范围与普通文本结合起来:
[0-9A-Z_!.?]能够匹配一个数字、大写字母下画线、惊叹号、点号,或者是问号。


请注意,只有在字符组内部,连字符才是元字符,否则它就只能匹配普通的连字符号。其
实,即使在字符组内部,它也不一定就是元字符,如果连字符出现在字符组开头,它表示
的就只是一个普通字符,而不是一个范围。同样的道理,问号和点号通常被当作元字符处
理,但在字符组中则不是如此,在[0-9A-Z_!.?]里面,真正的特殊字符就只有两个连字符。




排除型字符组


用[^...]取代[...],这个字符组就会匹配任何未列出的字符。例如,[^1-6]匹配除了1到6
以外的任何字符。这个字符组中开头的^表示“排除”。


这里的^的前面的表示行首的脱字符是一样的。字符确实相同,但意义截然不同。英语里
的"wind",根据情境的不同,可能表示一阵强烈的气流(风),也可能表示给钟表上发条;
元字符也是如此。我们已经看过用来表示范围的连字符的例子。只有在字符组内部(而且
不是第一个字符的情况下),连字符才能表示范围。在字符组外部,^表示一个行锚点line
anchor,但是在字符组内部(而且必须是紧接在字符组的第一个方括号之后),它就是一
个元字符。




用点号匹配任意字符
元字符.(通常称为点号dot或者小点point)是用来匹配任意字符的字符组的简便写法。如果
我们需要在表达式中使用一个“匹配任何字符”的占位符placeholder,用点号就很方便。
例如,如果我们需要搜索03/19/76,03-19-76或者03.19.76,不怕麻烦的话用一个明确容许
‘/‘,‘-‘,‘.‘的字符组来构建正则表达式,例如03[-./]19[-./]76。也可以简单地尝试
03.19.76.
在前者中,点号并不是元字符,因为它们在字符组内部(记住,在字符组里面和外面,元字
符的定义和意义是不一样的)。这里的连字符同样也不是元字符,因为它们都紧接在[或[^
之后。如果连字符不在字符组的开头,例如[.-/],就是用来表示范围的,但在这里如果这
样写就是错误的用法。


一个字符组,即使是排除型字符组,也需要匹配一个字符。


在03.19.76中点号是元字符,它能够匹配任意字符(包括我们期望的连字符、句号和斜线)
所以这个正则表达式也能够匹配下面的字符串19203319 7639。


使用正则表达式,清楚地了解目标文本是非常重要的。




多选结构


匹配任意子表达式
|是一个非常简捷的元字符,它的意思是“或”or。依靠它,我们能够把不同的子表达式组
合成一个总的表达式,而这个总的表达式又能够匹配任意的子表达式。在这样的组合中,子
表达式称为“多选分支”。


回头来看gr[ea]y的例子,有意思的是,它还可以写作grey|gray,或者是gr(e|a)y。
后者用括号来划定多选结构的范围(正常情况下,括号也是元字符)。请注意,gr[e|a]y
不符合我们的要求---在这里,‘|‘只是一个和a与e一样的普通字符。
对表达式gr(e|a)y来说,括号是必须的,因为如果没有括号,gre|ay的意思就成了“gre或
者ay”,而这不符合我们的要求。多选结构可以包括很多字符,但不能超越括号的界限。
另一个例子是(first|1st).[Ss]treet。事实上,因为first和1st都以st结尾,我们可以把
这个结合体缩略表示为[fir|1]st.[Ss]treet。


gr[ea]y与gr(e|a)y的例子可能会让人觉得多选结构与字符组没有太大的区别,但是请注意:
一个字符组只能匹配目标文本中的单个字符,而每个多选结构自身都可能是完整的正则表
达式,都可以匹配任意长度的文本。




同样,在一个包含多选结构的表达式中使用脱字符和美元符号的时候也要小心,我们希望
在每个多选分支之前都有脱字符,之后都有:,所以应该使用括号来限制这些多选分支:
^(from|subject|date):
现在3个多选分支都受括号的限制,所以,这个正则表达式的意思是:匹配一行的起始位
置,然后匹配from,subject或date中的任意一个,然后匹配:。




单词分界符


某些版本的egrep对单词识别提供了有限的支持:也就是单词分界符(单词开头和结束的位置)
的匹配。
如果你的egrep支持“元字符序列”\<和\>就可以使用它们来匹配单词分界的位置。可以把
它们想象为单词版本的^和$。分别用来匹配单词的开头和结束位置。就像作为行锚点的脱字
符和美元符号一样,它们锚定了正则表达式的其他部分,但在匹配过程中并不对应到任何字
符。
表达式\<cat\>的意思是“匹配单词的开头位置,然后是cat这3个字母,然后是单词的结束
位置”。更直接点说就是匹配cat这个单词。你也可以用\<cat和cat\>来匹配以cat开头和结
束的单词。


请注意,<和>本身并不是元字符----只有当它们与斜线结合起来的时候,整个序列才具有特
殊意义。这就是我称其为“元字符序列”的原因。并不是所有版本的egrep都支持单词分界
符。


在字符组内部,元字符的定义规则(及它们的意义)是不一样的。例如,在字符组外部,点
号是元字符,但在内部则不是如此。相反,连字符只有在字符组内部(这是普通情况)才是
元字符,否则就不是。脱字符在字符组外部表示一个意思,在字符组内部紧接着[时表示另
一个意思,其他情况下又表示别的意思。


不要混淆多选项和字符组。记住,字符组只能匹配一个字符。相反,多选项可以匹配任意
长度的文本,每个多选项可能匹配的文本都是独立的。不过,多选项没有像字符组那样的
排除功能。




可选项元素optional items
现在来看color和colour的匹配。它们的区别在于,后面的单词比前面的多一个u,我们可
用colou?r来解决这个问题。元字符?也就是问号代表可选项。把它加在一个字符的后面,
就表示此处容许出现这个字符,不过它的出现并非匹配成功的必要条件。


?这个元字符与我们之前看到的元字符都不同,它只作用于之前紧邻的元素。




其他量词:重复出现other quantifiers:repetition
+加号和*星号的作用与问号类似。元字符+表示之前紧邻的元素出现一次或多次,而*表示之
前紧邻的元素出现任意多次,或者不出现。换种说法就是,...*表示匹配尽可能多的次数,
如果实在无法匹配,也不要紧。...+的意思与之类似,也是匹配尽可能多的次数,但如果
连一次匹配都无法完成,就报告失败。问号、加号和星号这3个元字符,统称为量词quantifiers
因为它们限定了所作用元素的匹配次数。


与...?一样,正则表达式中...*也是永远也不会匹配失败的,区别只在于它们的匹配结果。
而...+在无法进行任何一次匹配时,会报告匹配失败。


接下来看类似<HR.SIZE>这样的HTML tag,它表示一条高度为14像素的穿越屏幕的水平线。在
最后的尖括号之前可以出现任意多个空格。此外,在等号两边也容许出现任意多个空格。最
后,在HR和SIZE之间必须有至少一个空格。为了处理更多的空格,我们可以在.后添加*,不
过最好还是写为.+。加号确保至少有一个空格出现。所以我们得到HR.+SIZE.*=.*14.*>。
如果我们要找的不仅仅是高度为14的tag,而是所有这些tag。所以我们必须用能匹配普通
数值general number的表达式来替换14.在这里,“数值”是由一位或多位数字digits构成
的。[0-9]可以匹配一个数字,因为至少出现一次,所以我们使用加号量词,结果就是用
[0-9]+替换14。一个字符组是一个“元素”unit,所以它可以直接加加号、星号等,而不
需要括号。
这样我们就得到了<HR.+SIZE.*=.*[0-9]+.*>
当然,你写成<HR +SIZE *= *[0-9]+ *>也正确,但这个表达式就看起来有些诡异了,是因
为星号和加号作用的对象大都是空格,而人眼习惯于把空格和普通字符区分开来。在阅读
正则表达式时,我们必须改变这种习惯,因为空格符也是普通字符之一。


规定重现次数的范围:区间
某些版本的egrep能够使用元字符序列来自定义重现次数的区间:...{min,max}。这称为
区间量词,例如...{3, 12}能够容许的重现次数在3到12之间。问号对应的区间量词是
{0, 1}。支持区间表示法的egrep版本并不多,但有许多另外的工具支持它。




括号及反向引用parentheses and backreferences
到目前为止,我们已经见过括号的两种用途:限制多选项的范围;将若干字符组合为一个单
元,受问号或星号之类量词的作用。括号还有另一种用途,虽然它在egrep中并不常见(不
过流行的GNU版本确实支持这一功能),但在其他工具软件中很常见。


在许多流派flavor的正则表达式中,括号能够“记住”它们包含的子表达式匹配的文本。


我们先把\<the.+the\>中的第一个the替换为能够匹配任意单词的正则表达式[A-Za-z]+
然后在两端加上括号;最后把后一个the替换为特殊的元字符序列\1。就得到了
\<([A-Za-z]+).+\1\>
在支持反向引用的工具软件中,括号能够“记忆”其中的子表达式匹配的文本,不论这些
文本是什么,元字符序列\1都能记住它们。


当然,在一个表达式中我们可以使用多个括号。再用\1,\2,\3等来表示第一,第二,第三组
括号匹配的文本。括号是按照开括号‘(‘从左至右的出现顺序进行的,所以([a-z])([0-9])\1\2
中的\1代表[a-z]匹配的内容,而\2代表[0-9]匹配的内容。


在the.the的例子中,[A-Za-z]+匹配第一个the。因为这个子表达式在括号中,所以\1代表
的文本就是the,如果.+能够匹配,后面的\1要匹配的文本就是the。如果\1也能成功匹配,
最后的\>对应单词的结尾(如果文本是the.theft,这一条就不满足。如果整个表达式能匹
配成功,我们就得到一个重复单词。




神奇的转义the grent escape
如果需要匹配的某个字符本身就是元字符呢?例如,如果我想要检索互联网的主机名
ega.att.com使用ega.att.com可能得到megawatt.com puting的结果。因为.本身就是元字
符,它可以匹配任何字符,包括空格。
真正匹配文本中点号的元序列应该是反斜线加上点号的组合:ega\.att\.com。\.称为“转
义的点号”或者“转义的句号”。这样的办法适用于所有的元字符,不过在字符组内部
无效。


这样使用的反斜线称为“转义符escape”----它作用的元字符会失去特殊含义,成了普通
字符。


例如,我们还可以用\([a-zA-Z]\)来匹配一个括号内的单词,例如(very)。在开闭括号之
前的反斜线消除了开闭括号的特殊意义,于是他们能够匹配文本中的开闭括号。


如果反斜线后紧跟的不是元字符,反斜线的意义就依程序的版本而定。例如,我们已经知道
某些版本的程序把\<,\>,\1当作元字符序列对待。


大多数程序设计语言和工具都支持字符组内部的转义,但是大多数版本的egrep不支持,它们
会把反斜线当作字符组内部列出的普通字符。




更多的例子 a few more example
编写正则表达式时,按照预期获得成功的匹配要花去一半的工夫,另一半的工夫用来考虑如
何忽略那些不符合要求的文本。


变量名
许多程序设计语言都有标识符(identifier,例如变量名)的概念,标识符只包含字母、数
字以及下画线,但不能以数字开头。我们可以用[a-zA-Z][a-zA-Z0-9_]*来匹配标识符。第
一个字符组匹配可能出现的第一个字符,第二个(包括对应的*)匹配余下的字符。如果标
识符的长度有限制,例如最长只能是32个字符,我们可以使用前面介绍的区间量词{min,
max},我们可以用{0,31}来替代最后的*;




引号内的字符串
匹配引号内的字符串最简单的办法是使用这个表达式:"[^"]*"。两端的引号用来匹配字符串
开头和结尾的引号。


关于引号字符串,更有用(也更复杂)的定义是,两端的双引号之间可以出现由反斜线转
义的双引号。




美元金额(可能包含小数)
\$[0-9]+(\.[0-9][0-9])?




HTTP/HTML URL
Web URL的形式可能有很多种,所以构造一个能够匹配所有形式的URL的正则表达式颇有难度
不过,稍微降低一点要求的话,我们能够用一个相当简单的正则表达式来匹配大多数常见
的URL。


常见的HTTP/HTML URL是下面这样的:
http://hotname/path.html
当然.htm的结尾也是很常见。


hostname(主机名,例如www.yahoo.com)的规则比较复杂。


要想在复杂性和完整性之间求得平衡,一个重要的因素是了解待搜索的文本。


表示时刻的文字,例如“9:17 am”或者“12:30 pm”
匹配表示时刻的文字可能有不同的严格程度。
[0-9]?[0-9]:[0-9][0-9].(am|pm)
能够匹配9:17.am或者12:30.pm,但也能匹配无意义的时刻,如99:99.pm。
首先看小时数,我们知道,如果小时数是一个两位数,第一位只能是1。但是1?[0-9]仍然
能够匹配19(也能够匹配0),所以更好的办法应该是把小时部分分为两种情况来处理,
1[012]匹配两位数,[1-9]匹配一位数,结果就是(1[012]|[1-9])
分钟数就简单些。第一位数字应该是[0-5],此时第二位数字应该是[0-9]。综合起来就是
(1[012]|[1-9]):[0-5][0-9].(am|pm)。
([01]?[0-9]|2[0-3]):[0-5][0-9].(am|pm)




子表达式subexpression
“子表达式”指的是整个正则表达式中的一部分,通常是括号内的表达式,或者是由|分隔
的多选分支。例如在 ^(subject|date):*中,subject|date通常被视为一个子表达式。其中
的subject和date也算得上子表达式。而且,严格说起来,s,u,b,j这些字符,都算子表达式


1-6这样的字符序列并不能算H[1-6].*的子表达式,因为1-6所属的字符组是不可分割的单元
但是H,[1-6],。*都是H[1-6].*的子表达式。


与多选分支不同的是,量词(星号、加号和问号)作用的对象是它们之前紧邻的子表达式。
所以mis+pell中的+作用的是s而不是mis或者is。当然,如果量词之前紧邻的是一个括号
包围的子表达式,整个子表达式(无论多复杂)都被视为一个单元。


汉语Chinese、日语Japanese、韩语Korean和越南语Vietnamese,这4种语言都必须使用多字
节编码。


使用括号的3个理由是:限制多选结构、分组的捕获文本。


转义有3种情况:
1.\加上元字符,表示匹配元字符所使用的普通字符,如\*
2.\加上非元字符,组成一种由具体实现方式规定其意义的元字符序列。如\<
3.\加上任意其他字符,默认情况就是匹配此字符,也就是说,反斜线被忽略了。


由星号和问号限定的对象在“匹配成功”时可能并没有匹配任何字符。




第2章 入门示例拓展


Perl简单入门
Perl是一门功能强大的脚本语言。Perl关于文本处理和正则表达式的许多概念来自两种专
业化的语言awk和sed,它们都非常不同于“传统”的语言,如C和Pascal。


现在来看一个简单的例子:
$celsius = 30;
$fahrenheit = ($celsius * 9 、 5) + 32; # 计算华氏温度
print "$celsius C is $fahrenheit F.\n"; # 返回摄氏和华氏温度


$fahrenheit和$celsius之类的普通变量一般以$开头,可以保存一个数值或者任意长度的文本。
从#到行尾都是注释。


Perl也提供了跟其他流行的语言类似的控制结构:
$celsius = 20;
while ($celsius <= 45)
{
$fahrenheit = ($celsius * 9 / 5) + 32; # 计算华氏温度
print "$celsius C is $fahrenheit F.\n";
$celsius = $selsius + 5;
}
在Perl中,变量不需要事先声明就能使用。


使用正则表达式匹配文本matching text with regular expression
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。


把=~读作“匹配matches”可能比较省事,所以
if ($reply =~ m/^[0-9]+$/)
读作:
如果变量$reply所含的文本能够匹配正则表达式^[0-9]+$。
如果^[0-9]+$能够匹配$reply的内容,$reply =~ m/^[0-9]+$/的返回值为true,否则为false
请注意,如果$reply中包含任意的数字字符,$reply =~ m/[0-9]+/(相比之前的表达式,去掉
了开头的脱字符和结尾的美元符)的返回值就是true。两端的^...$保证整个$reply只包含
数字。


现在把上面两个例子结合起来。首先提示用户输入一个值,接收这个输入,用一个正则表达式
来验证,确保输入的是一个数值。如果是,我们就计算相应的华氏温度,否则,我们输出一
条报警信息:
print "Enter a temperature in Celsius:\n";
$celsius  = <STDIN>; #从用户处接受一个输入
chomp($celsius); #去掉$celsius后面的换行符


if ($celsius =~ m/^[0-9]+$/){
$fatrenheit = ($celsius * 9  / 5) + 32; #计算华氏温度
print "$celsius C is $fahrenhei F\n";
}else  {
print "Expecting a number, so I don‘t understand\"$celsius\".\n";
}
VB.NET是个明显的例外,在那里转义双引号用‘""‘而不是‘\"。


Perl中同样也存在printf来进行格式化输出:
printf "%.2f C is %.2f F\n", $celsius, $fahrenheit;
这里的printf类似c语言中的printf,或者Pascal,Tcl,elisp和Python中的format。它不会更改
变量的值,而只是改变显示的方式。




向更实用的程序前进
让我们扩展这个例子,容许输入负数的可能出现的小数部分。这个问题的计算部分没问题---
Perl通常情况下不区分整数和浮点数。但我们这里需要修改我们的正则表达式。
我们添加一个-?来容许最前面的负数符号,实际上,我们可以用[-+]?来处理开头的正负号。


要容许可能出现的小数部分,我们添加(\.[0-9]*)?。
这样,我们就可以得到这样的条件判断语句:
if ($celsius =~ m/^[-+]?[0-9]+(\.[0-9]*)?$/){}




成功匹配的副作用
在前面,我们已经看到过某些版本的egrep支持作为元字符的\1,\2,\3,用来保存前面的括号
内的子表达式实际匹配的文本。Perl和其他许多支持正则表达式的语言都支持这些功能。而且
匹配成功之后,在正则表达式之外的代码仍然能够引用这些匹配的文本。


Perl的办法是通过变量$1,$2,$3等等,它们都是变量,而变量名则是数字。正则表达式匹配成
功一次,Perl就会设置一次。


现在我们先不考虑小数部分,之后再来看它。请比较:
$celsius =~ m/^[-+]?[0-9]+[CF]$/
$celsius =~ m/^([-+]?[0-9])([CF])$/
虽然添加括号并没有改变表达式的意义。但他们确实围住了我们期望匹配字符串中“有价值”
的文本的子表达式。$1保存那些数字,而$2保存C或者F。


温度转换程序:
print "Enter a temperature (e.g 32F, 100C):\n";
$input = <STDIN>; # 接收用户输入的一行文本
chomp($input);  # 去掉文本末尾的换行符


if ($input =~ m/^([-+]?[0-9]+)([CF])$/)
{
# 如果程序运行到此,则已经匹配。$1保存数字,$2保存C或F
$InputNum = $1; # 把数据保存到已命名的变量中
$type = $2; # 保存字符


if ($type eq "C"){ # eq测试两个字符串是否相等
# 输入为摄氏温度,则计算华氏温度
$celsius = $InputNum;
$fahrenheit = ($celsius * 9 / 5) + 32;
} else {
# 如果不是“C”,则必然是"F",计算摄氏温度
$fahrenheit = $InputNum;
$celsius = ($fahrenheit - 32) * 5 / 9;
}
#现在得到了两个温度,显示结果:
printf "%.2f C is %.2F\n", $celsius, $fahrenheit;
} else{
#如果最开始的正则表达式无法匹配,报警
print "Expecting a number followed by \"C\" of \"F\",\n";
print "so I don‘t understand \"$input\".\n";
}




错综复杂的正则表达式
现在我们对这个程序做三点改进:像之前一样能够接收浮点数,容许f或者c是小写,容许数
字和字母之间存在空格。
我们已经知道,添加(\.[0-9]*)?就能够处理浮点数:
if ($input =~ m/^([-+]?[0-9]+(\.[0-9]*)?)([CF])$/)
上面说明了括号的嵌套关系。在[CF]之前添加一组括号并不会直接影响整个正则表达式的
意义,但是会产生间接的影响,因为现在[CF]所在的括号排在第3位。


接下来,我们要处理数字和字母之间可能出现的空格。我们知道,正则表达式中的空格字
符正好对应匹配文本中的空格字符,所以.*能够匹配任意数目的空格(但并不是必须出现
空格)。如然写成 *也不够灵活。因为我们可能容许其他的空白字符whitespace。例如常
见的制表符tabs。


为了方便使用,Perl提供了\t这个元字符,它能够匹配制表符----相比真正的制表符,它
的好处就在于看得更清楚。其他的还有\n,\f,\b等,\b在某些情况下是退格符,有些情况下
又表示单词分界符。




数量丰富的元字符
像多数语言一样,Perl的字符串也有自己的元字符,它们完全不同于正则表达式元字符。
字符串的元字符中有一些跟正则表达式中对应的元字符一模一样。




非捕获型括号(?:...)
我们用括号包围(\.[0-9]*)?来正确分组,所以我们能够用一个问号正确地作用于整个
\.[0-9]*,把它们作为可选项部分。这样的副作用就是这个括号的子表达式捕获的文本
保存到$2中了。如果有这样一种括号,它只能用于分组,而不会影响文本的捕获和变量
的保存。
Perl以及近期出现的其他正则表达式流派提供了这个功能。
(...)用来分组和捕获,而(?:...)表示只分组不捕获,
所以,整个表达式变成:


if ($input =~ m/^([-+]?[0-9]+(?:\.[0-9]*)?)([CF])$/)
现在,即使[CF]两端的括号的确是排在第三位,它匹配的文本也会保存到$2中,因为(?:...)
不会影响捕获计数。
这样做的好处有两点:第一是避免了不必要的捕获操作,提高了匹配效率。另一个好处是,
总的来说,根据情况选择合适的括号能够让程序更清晰。但它也增加了整个表达式的阅读
难度。如果匹配只需要进行一次,而不需要在循环中多次匹配,效率并不重要。


例如,对shell来说,空格符就是一个元字符,它用来分隔命令和参数,或者参数与参数。
在许多shell中,单引号是元字符,单引号内的字符串中的字符不需要被当作元字符处理
(DOS)使用双引号。


在shell中使用引号容许我们在正则表达式中使用空格。否则,shell会把空格认作参数之
间的分隔符,而不是把整个表达式传递给egrep。许多shell能够识别的元字符包括$,*,?之
类,我们在正则表达式中也会用到这些元字符。


那么\b的情况呢?这是一个正则表达式的问题:在Perl的正则表达式中,\b通常是匹配一个
单词分界符的,但是在字符组中,它匹配一个退格符。




用\s匹配所有“空白”
许多流派的正则表达式提供了一种方便的办法:\s来表示所有“空白字符”的字符组,其中
包括空格符、制表符、换行符和回车符。
现在我们的程序变成:
$input =~ m/^([-+]?[0-9]+(\.[0-9]*)?)\s*([CF])$/


在Perl中/i修饰符表示忽略大小写,/g表示全局匹配,/x表示宽松排列的表达式。


许多工具都有自己的正则表达式流派。Perl和egrep可能属于同一个流派,但是Perl的正
则表达式中的元字符更多。许多其他的语言,类似java,Python,.NET和TCL,它们的流派
类似Perl。


Perl用$variable =` m/regex/来判断一个正则表达式是否能匹配某个字符串。m表示匹配
而斜线用来标注正则表达式的边界(它们本身不属于正则表达式)。整个测试语句作为一个
单元,返回true或者false值。


元字符----具有特殊意义的字符----的定义在正则表达式中并不是统一的。元字符的含义取
决于具体的情况(shell,正则表达式,字符串)。在正则表达式内部,字符组有自己的“子
语言”,其中的元字符是不同的。


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]


(?:...)这个麻烦的写法可以用来分组文本,但并不捕获。


匹配成功之后,Perl可以用$1,$2,$3之类的变量来保存相对应的(...)括号内的子表达式匹配
的文本。使用这些变量,我们能够用正则表达式从字符串中提取信息(其他语言所使用的
方式有所不同)。子表达式的编号按照开括号的出现先后排序,从1开始。




使用正则表达式修改文本
到现在,我们遇到的例子都只是从字符串中“提取”信息。现在我们来看Perl和其他许多语言
提供的一个正则表达式特性:替换substitution,也可以叫“查找和替换search and replace"
我们已经看到,$var =~ m/regex/尝试用正则表达式来匹配保存在变量中的文本,并返回表
示能否匹配的布尔值。与之类似的结构$var =~ s/regex/replacement/则更进一步:如果正
则表达式能够匹配$var中的某段文本,则将这段匹配的文本替换为replacement。其中regex
与之前m/.../的用法一样,而replacement则是作为双引号内的字符串。这就是说,在其中
可以使用变量$1,$2来引用之前匹配的具体文本。


所以,使用$var =~ s/.../.../可以改变$var中的文本,如果没有找到匹配的文本,也就不
会有替换发生。例如,如果$var包括Jeff*Friedl,运行:
$var =~ s/Jeff/Jeffrey/;
$var的值就变成Jeffery*Friedl。如果再运行一次,就得到Jeffreyery*Friedl。要避免
这种情况,也许我们需要添加单词分界的元字符。
前面提到过,某些版本的egrep支持\<和\>作为单词起始和单词结束的元字符。Perl提供了
统一的元字符\b来代表这两者:
$var =~ m/\bJeff\b/Jeffery/;


修饰符/g表示“全局替换”。会替换所有与正则表达式匹配的文本。




下面来看另一个例子:
对一个浮点数,保留小数点后面两位数字,如果第三位不为零,也需要保留,去掉其他的数
字。结果就是12.37500000055633或者12.375会被修正为12.375,而37.500被修正为37.50。
我们用这个表达式:$price变量包含了需要修正字符串
$price =~ s/(\.\d\d[1-9]?)\d*/$1/


请不要混淆操作符<>与shell的重定向符号">filename"或者是Perl的大于/小于号。Perl中
的<>相当于其他语言中的getline()函数。


在字符组内部,括号不再具有特殊含义,因此也不需要转义。




用环视功能为数值添加逗号
大的数值,如果在其间加入逗号,会更容易看懂。下面的程序:
print "The US population is $pop\n";
可能输出"The US population is 298444215,但对大多数说英语的人来说,298,444,215
看起来更加自然。
你可能会想到,我们应该从这个数的右边开始,每次数3位数字,如果左边还有数字的话,
就加入一个逗号。但可惜正则表达式一般是从左向右工作的。不过换个思路就会发现,逗
号应该加在“左边有数字,右边数字的个数正好是3的倍数的位置”。我们使用一组相对较
新的正则表达式特性“环视”来轻松地解决这个问题。


环视结构不匹配任何字符,只匹配文本中的特定位置,这一点与单词分界符\b,锚点^和$相
似。但是,环视比它们更加通用。


一种类型的环视叫“顺序环视lookahead”,作为表达式的一部分,顺序环视顺序从左到右
查看文本,尝试匹配子表达式,如果能够匹配,就返回匹配成功信息。肯定型顺序环视用
特殊的序列(?=...)来表示,例如(?=\d),它表示如果当前位置右边的字符中数字则匹配
成功。另一种环视称为逆序环视,它逆序从右向左查看文本。它用特殊的序列(?<=...)
表示,例如,(?<=\d),如果当前位置的左边还有一位数字,则匹配成功。


环视不会占用字符
环视在检查子表达式能否匹配的过程中,它们本身不会占用任何文本。


你或许会发现(?=Jeffrey)Jeff和Jeff(?=rey)是等价的,它们都能够匹配Jeffrey这个单词
中的Jeff。


环视结构使用特殊的表示法,就像之前我们介绍的非捕获型括号(?:...)一样,它们使用
特殊的字符序列作为自己的“开括号”。这样的“开括号”序列还有许多种,但它们都以
两个字符"(?"开头。问号之后的字符用来标志特殊的功能。
分组但不捕获(?:...)
顺序环视(?=...)
逆序环视(?<=...)


再看另外一个例子,首先我们要把所有Jeffs替换为Jeff‘s,最简单的方法是
s/\bJeffs\b/Jeff‘s/g
如果使用环视可以这样写这个正则表达式:
s/\bJeff(?=s\b)/Jeff‘/g


因为s\b只是顺序环视表达式的一部分,所以它匹配的s不属于最终的匹配文本。为什么不在
最终匹配的结果中包含顺序环视匹配过的文本呢?通常,这是因为我们希望在表达式的后面
部分,或者在稍后应用正则表达式时,再次检测这段文本。
如果我们也使用逆顺环视,结果就是
s/(?<=\bJeff)(?=s\b)/‘/g
s/(?=s\b)(?<=\bJeff)/‘/g
上面的两个使用顺序和逆顺的环视的表达式完全相同,只是颠倒了两个环视结构。环视并
不会占用任何文本。


回到逗号的例子
在这个例子中,我们需要通过正则表达式寻找到某个位置,然后插入文本。
$pop =` s/(?<=\d)(?=(\d\d\d)+$)/./g;
这里确实输出了我们期望的值298.444.215。不过,\d\d\d两边的括号是捕获型括号。但是
在这里,我们只用它来分组,把加号作用于3位数字,所以不需要把它们捕获的文本保存到$1
中。
因此我们可以写成(?<=\d)+(?=(?:\d\d\d)+$)/./g


以上的顺序环视和逆序环视应该被称作肯定顺序环视和肯定逆序环视。因为它们成功的条件
是子表达式在这些位置能够匹配。正则表达式还提供了相对应的否定顺序环视和否定逆序
环视。它们成功的条件是子表达式无法匹配。


四种类型的环视
类型 正则表达式 匹配成功的条件。。。
肯定顺序环视 (?=...) 子表达式能够匹配右侧文本
否定顺序环视 (?!...) 子表达式不能匹配右侧文本


肯定逆序环视 (?<=...) 子表达式能够匹配左侧文本
否定逆序环视 (?<!...) 子表达式不能匹配左侧文本


所以,如果单词分界符的意思是:一侧是\w而另一侧不是\w,我们就能用(?<!\w)(?=\w)
来表示单词起始分界符,用(?<=\w)(?!\w)表示单词结束分界符。把两者结合起来,
(?<!\w)(?=\w)|(?<=\w)(?!\w)就等价于\b。


不通过逆序环视添加逗号
逆序环视和顺序环视一样,所获的支持十分有限,使用也不广泛。顺序环视比逆序环视早
出现几年,尽管Perl现在两者都支持,许多其他语言却不是这样。所以,下面我们不用逆
序环视来解决添加逗号的问题:
$text =~ s/(\d)(?=(\d\d\d)+(?!\d))/$1./g;
它与之前有表达式的差别在于,开头的\d所处的肯定逆序环视变成一捕获型括号,replacement
字符串则在逗号之前加入了相应的$1。


Text to HTML转换
undef $/; # 进入“file-slurp”(文件读取)模式
$text =  <>; # 读入命令行中指定的第一个文件


大多数系统采用换行符作为一行的终结符,而某些系统(主要是windows)使用回车/换行的
结合体。


^和$通常匹配的不是逻辑行的开头和结尾,而是整个的字符串的开头和结尾位置。所以,既
然目标字符串中有多个逻辑行,就需要采取不同的办法。
幸好大多数支持正则表达式的语言提供了一个简单的办法,即“增强的行锚点”匹配模式,
在这种模式下,^和$会从字符串模式切换到逻辑行模式。在Perl中,使用m/修饰符来选择此
模式:
$text =~ s/^$/<p>/mg;
请注意,这里同时使用了/m和/g,你可以以任何顺序排列需要使用的多个修饰符。


不过,如果在“空行”中包含空格符或者其他空白字符,这么做就行不通。为了处理空白
字符,我们使用^.*$或者是^[.\t\r]$来区配某些系统在换行符之前的空格符、制表符或
者回车符。


我们可以使用\s,这样^\s*$,如果用\s取代[.\t\r],因为\s能够匹配换行符,所以整个表
达式的意思就不再是“寻找空行及只包括空白字符的行”,而是“寻找连续、空行、和只包括
空白字符的行的结合。


将E-mail地址转换为超链接形式
例如:jfriedl@oreillly.com会被转换为<a.href="mailto:jfriedl@oreilly.com">
jfriedl@oreilly.com</a>。
用正则表达式来匹配或者验证E-mail地址是常见的情况。E-mail地址的标准规范异常繁杂,
所以很难做到百分之百的准确。E-mail地址的基本形式是username@hostname,
HTTP URL的基本形式是http://hostname/path,其中/path部分是可选的。


运算符、函数和对象
诜多现代语言提供专用的函数和对象来处理正则表达式。
例如,可能有一个函数接收表示正则表达式的字符串,以及用于搜索的文本,然后根据正则
表达式能否匹配该文本,返回真值或假值。更常见的情况是,这两个功能(首先对一个正则
表达式的字符串进行解释,然后把它应用到文本当本)被分割为两个或更多分离的函数。
Java版本的正则表达式包含了更多的反斜线,原因是Java要求正则表达式必须字符串方式
提供。所以正则表达式中的反斜线必须转义。




在学习任何一门支持正则表达式的语言时,我们需要注意两点:正则表达式的流派,以及
该语言运用正则表达式的方式。


用Java解决重复单词问题
import java.io.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;


public class TwoWord
{
public static void main(String [] args)
{
Pattern regex1 = Pattern.compile(
"\\b([a-z]+)((?:\\s|\\ <[^>]+\\>)+)(\\1\\b)",
Pattern.CASE_INSENSITIVE);
String replace1 = "\033[7m$1\033[m$2\033[7m$3\033[m";
Pattern regex2 = Pattern.compile("^(?:[^\\e]*\\n)+", Pattern.MULTILINE);
Pattern regex3 = Pattern.compile("^([^\\n]+)", Pattern.MULTILINE);


//对于命令行的每个参数进行如下处理...
for (int i = 0; i < args.length; i++)
{
try {
BufferedReader in = new BufferedReader(new FileReader(args[i]));
String text;

//For each paragraph of each file...
while ((text = getPara(in)) != null)
{
//应用3条替换规则
text = regex1.matcher(text).replaceAll(replace1);
text = regex2.matcher(text).replaceAll("");
text = regex3.matcher(text).replaceAll(args[i] + ":$1");

//显示结果
System.out.print(text);
}
}catch(IOException e){
System.err.println("can‘t read["+args[i]+"]: "+e.getMessage());
}
}
}


//用于读入一段文本的子程序
static String getPara(BufferedReader in) throws java.io.IOException
{
StringBuffer buf = new StringBuffer();
String line;

while ((line = in.readLine()) != null &&
(buf.length() == 0 || line.length() != 0))
{
buf.append(line + "\n");
}
return buf.length() == 0? null: buf.toString();
}
}




第3章 正则表达式的特性和流派概览
工具不同,正则表达式的写法和用法都有很大的不同。
在某种特定的宿主语言或工具软件中使用正则表达式时,主要有3个问题值得注意:


支持的元字符,以及这些元字符意义。这通常称为正则表达式 的“流派”flavor。


正则表达式与语言或工具的交互方法。譬如,如何进行正则表达式操作,容许进行哪些操作
以及这些操作的目标文本类型。


正则表达式引擎如何将表达式应用到文本。语言或工具的设计者实现正则表达式的方法,
对正则表达式能够取得的结果有重要的影响。


qed编辑器有条命令,显示正在编辑的文件中能够匹配特定正则表达式的行。该信念
"g/RegularExpression/p",读作‘Global Regular Expression Print"(应用正则表达式的
全局输出)。这个功能非常实用,最终成为独立的工具grep,之后又产生了egrep,扩展的grep。




POSIX---标准化的尝试
诞生于1986年的POSIX是Portable Operating System Interface(可移植操作系统接口)
的缩写,它是一系统标准,确保操作系统之间的移植性。


POSIX把各种常见的正则表达式的流派分为两大类:Basic Regular Expressions(BREs)和
Extended Regular Expressions(EREs)。POSIX程序必须支持其中的任意一种。


POSIX标准的主要特性之一是local,它是一组关于语言和文化传统----例如日期和时间的格
式、货币币值、字符编码对应的意义等---的设定。locals的目的在于让程序变得国际化。它
们不是正则表达式相关的概念。


另一个例子是\w,通常用于表示“构成单词的字符”,在很多流派中,它等价于[a-zA-Z0-9_]
这个特性并不是POSIX中必须的,但容许出现,如果支持的话,\w就能对应local中的所有字母
和数字,而不仅仅限于ASCII编码的字符和数字。




流派的部分整合
Perl的初衷是文本处理,而Web页的生成其实正是文本处理,所以Perl迅速成为了开发Web
程序的语言。Perl中强大的正则流派也是如此。
其他语言的开发人员当然不会视而不见,最终在某种程序上“兼容Perl”的正则表达式包出
现了。Tcl,Python,NET,Ruby,PHP,C/C++都有各自的正则表达式包,Java语言中还有多个正
则表达式包。
另一种形式的整合始于1997年,当时Philip Hazel开发了PCRE,这是一套兼容Perl正则表达
式的库。其他的开发人员可以把PCRE整合到自己的工具和语言中,为用户提供丰富而且极具
表现力的各种正则功能。许多流行的软件都使用了PCRE,例如PHP,Apache2,Exim,Postfix和
Nmap。


Perl,NET和Java的正则表达式似乎是一样的,而实际情况却远不是这样,你可能会提出以下
问题:


星号之类的量词能否作用于括号之内的子表达式?


点号能否匹配换行符?排除型字符组能否匹配换行符?以上两都能否匹配NUL字符?


行锚点line anchor是名符其实的吗?例如他们能否识别目标字符串内部的换行符?它们算
正则表达式中的基础级别的元字符吗?还是只能应用在某些结构中?


字符组内部能出现转义字符吗?字符组内部还容许或不容许出现哪些字符?


括号能够嵌套吗?如果是,嵌套的深度是否有限制呢,还有个问题是,一共容许出现多少
括号呢?


如果容许反向引用,在进行不区分大小写的匹配时,反向引用能顺利进行吗?在极端的情况
下,反向引用的行为有意义吗?


是否可以出现八进制的转义字符\123?如果是,怎么区分它和反向引用呢?十六进制的转义
字符呢?这种支持是正则引擎提供的,还是由其他工具提供的?


\w只支持数字和字符,还是包括其他字符?不同的单词分界符元字符对构成“单词分界符”
的字符的定义不一样,\w是否与它们保持一致?它们是按照locale的定义呢,还是支持
Unicode?


许多问题只是语法的差异,但也有许多并非如此。比方说,了解到egrep的(Jul|July)在GNU
Emacs中必须写成\(Jul|July\)。


98  124/544

《精通正则表达式》

标签:

原文地址:http://blog.csdn.net/lp310018931/article/details/46040419

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!