perl语言编程 第五章 模式匹配 上
上一篇 / 下一篇 2008-04-18 11:49:40 / 个人分类:关于脚本语言
Perl 内置的模式匹配让你能够简便高效地搜索大量的数据。不管你是在一个巨型的商业门户站点上用于扫描每日感兴趣的珍闻报道,还是在一个政府组织里用于精确地描述人口统计(或者人类基因组图),或是在一个教育组织里用于在你的 web 站点上生成一些动态信息,Perl 都是你可选的工具。这里的一部分原因是 Perl 的数据库联接能力,但是更重要的原因是 Perl 的模式匹配能力。如果你把“文本”的含义尽可能地扩展,那么可能你做的工作中有 90% 是在处理文本。这个领域实在就是 Perl 的最初专业,而且一直是 Perl 的目的——实际上,它甚至是 Perl 的名字:Practical Extraction and Report Language (实用抽取和报表语言)。Perl 的模式提供了在堆积成山的数据中扫描数据和抽取有用信息的强大工具。
你可以通过创建一个正则表达式(或者叫 regex)来声明一个模式,然后 Perl 的正则表达式引擎(我们在本章余下的部分称之为“引擎”)把这个正则表达式拿过并判断模式是否(以及如何)和你的数据相匹配。因为你的数据可能大部分由文本字串组成,所以你没有理由不用正则表达式搜索和替换任意字节序列,甚至有些你认为是“二进制”的数据也可以用正则处理。对于 Perl 而言,字节只不过碰巧是那些数值小于 256 的自然数而已。(更多相关内容见第十五章,Unicode。)
如果你通过别的途径已经知道正则表达式了,那么我们必须先警告你在 Perl 里的正则表达式是有些不同的。首先,理论上来讲,它们并不是完全“正则”的,这意味着 Perl 里的正则可以处理比计算机科学课程里教的正则表达式更多的事情。第二,因为它们在 Perl里用得实在是太广泛了,所以在这门语言里,它们有属于自己的特殊的变量,操作符,和引用习惯;这些东西都和语言本身紧密地结合在一起。而不象其他语言那样通过库松散地组合在一起。Perl 的程序员新手常常徒劳地寻找地这样的函数:
match( $string, $pattern ); subst( $string, $pattern, $replacement );
要知道在 Perl 里,匹配和子串都是非常基本的任务,所以它们只是单字符操作符:m/PATTERN/ 和 s/PATTERN/REPLACEMENT/(缩写为 m// 和 s///)。它们不仅语法简单,而且还象双引号字串那样分析,而不只是象普通操作符那样处理;当然它们的操作还是象操作符的,所以我们才叫它们操作符。你在本章通篇都会看到这些操作符用于匹配字串。如果字串的一部分匹配了模式,我们就说是一次成功的模式匹配。特别是在你用 s/// 的时候,成功匹配的部分将被你在 REPLACEMENT 里声明的内容代替。
本章所有的内容都是有关如何制作和使用模式的。Perl 的正则表达式非常有效,把许多含义包含到了一个很小的表达式里。所以如果你想直接理解一个很长的模式,那很有可能被吓着。不过如果你能把长句分解成短句,并且还知道引擎如何解释这些短句,那你就能理解任何正则表达式。一行正则表达式相当于好几百行 C 或者 JAVA 程序并不罕见。正则表达式可能比一个长程序的单一一行要难理解,但是如果从整体来看,正则表达式通常要比很长的程序要好理解。你只需要提前知道这些事情就可以了。
5.1 正则表达式箴言
在我们开始讲述正则表达式之前,让我们先看看一些模式的样子是什么。正则表达式里的大多数字符只是代表自身。如果你把几个字符排在一行,它们必须按顺序匹配,就象你希望的那样。因此如果你写出一个模式匹配:
/Frodo/ (译注:记得电影“魔戒”吗?;))
你可以确信除非该字串在什么地方包含子字串“Frodo”,否则该模式不会匹配上。(一个子字串只是字串的一部分。)这样的匹配可以发生在字串里的任何位置,只要这五个字符以上面的顺序在什么地方出现就行。
其他字符并不匹配自身,而是从某种角度来说表现得有些“怪异”。我们把这些字符称做元字符。(大多数元字符都是自己淘气,但是有一些坏得把旁边的字符也带“坏”了。)
下面的就是这些字符:
\ | ( ) [ { ^ $ * + ? .实际上元字符非常有用,而且在模式里有特殊的含义;我们会一边讲述,一边告诉你所有的那些含义。不过我们还是要告诉你,你仍然可以在任意时刻使用前置反斜杠的方法来匹配这十二个字符本身。比如,反斜杠本身是一个元字符,因此如果你要匹配一个文本的反斜杠,你要在反斜杠前面放一个反斜杠:\\。
要知道,反斜杠就是那种让其他字符“怪异”的字符。事实是如果你让一个怪异元字符再次怪异,它就会正常——双重否定等于肯定。因此反斜杠一个字符能够让它正确反映文本值,但是这条只对标点符号字符有用;反斜杠(平时正常)的字母数字字符作用相反:它把该文本字符变成某些特殊的东西。不论什么时候你看到下面的双字符序列:
\b \D \t \3 \s
你就应该知道这些序列是一个元符号,它们匹配某些特殊的东西。比如,\b 匹配一个字边界,而 \t 匹配一个普通水平制表字符。请注意一个水平制表符是一个字符宽,而一个字边界是零字符宽,因为它是两个字符之间的位置。所以我们管 \b 叫一个零宽度断言。当然,\t 和 \b 还是相似的,因为他们都断言某些和字串里的某个特殊位置相关的东西。当你在正则表达式里断言某些东西的时候,你的意思只是说,如果要匹配模式,那些东西必须是真的。
一个正则表达式里的大多数部分都是某种断言,包括那些普通字符,只不过它们是断言它们必须匹配自身。准确来说,它们还断言下一个东西将匹配字串里下一个字符,这也是为什么我们说水平制表符是“单字符宽”。有些断言(比如 \t)当匹配的时候就吞掉字串的一部分,而其他的(比如 \b)不会这样。但是通常我们把“断言”这个词保留给零宽度断言用。为避免混淆,我们把这些东西称做宽度为一个原子的东西。(如果你是一个物理学家,你可以把非零宽的原子当作物质,相比之下零宽断言类似无质量的光子。)
你还会看到有些元字符不是断言;而是结构元素(就好象花括弧和分号定义普通 Perl 代码的结构,但是实际上什么也不干)。这些结构元字符在某种程度上来说是最重要的元字符,因为学习阅读正则表达式关键的第一步就是让你的眼睛学会挑出结构元字符。一旦你学会了挑出结构元字符,阅读正则表达式就是如坐春风(注:当然,有时候风力强劲,但绝对不会把你刮跑。)
有一个结构元字符是竖直条,表示侯选项:
/Frodo|Pippin|Merry|Sam/ (译注:电影“魔戒”里Shire的四个小矮人)
这意味着这些字串的任何一个都会触发匹配;这个内容在本章稍后的“侯选项”节描述。并且我们还会在那节后面的“捕获和集群”节里告诉你如何使用圆括弧,把你的模式各个部分括起来分组:
/(Frodo|Drogo|Bilbo) Baggins/ (译注:Bilbo Baggins 是 Frodo 的叔叔,老一辈魔戒承载者。)
或者甚至:
/(Frod|Drog|Bilb)o Baggins/
你看到的其他的东西是我们称之为量词的东西,它表示在一行里面前面匹配的东西应该匹配几个。量词是这些东西:
* + ? *? {3} {2,5}不过你永远不会看到它们这样独立地存在。量词只有附着在原子后面才有意义 —— 也就是说,断言那些有宽度的(注:量词有点象第四章,语句和声明,里的语句修饰词,也是只能附着在单个语句后面。给一个零宽度的断言附着量词就象试图给一个声明语句附着一个 while 修饰词一样 —— 两种做法都和你跟药剂师要一斤光子一样无聊。药剂师只卖原子什么的。)量词只附着在前一个原子上,从我们人类的眼光来看,这通常量化为只有一个字符。如果你想匹配一行里的三个“bar”的副本,你得用圆括弧把“bar”的三个独立的字符组合成一个“分子”,象这样:
/(bar){3}/这样将和“barbarbar”匹配。如果你用的是 /bar{3}/,那么匹配的是“barrr“ —— 这东西表明你是苏格兰人(译注:爱尔兰人说英文的时候尾音比较长),而不是 barbarbar 人。(话又说回来,也可能不是。我们有些很喜欢的元字符就是爱尔兰人。)有关量词的更多东西,参阅后面的“量词”。
你已经看到了一些继承了正则表达式的野兽,现在一定迫不及待地想驯服它们。不过,在我们开始认真地讨论正则表达式之前,我们需要先向回追溯一些然后再谈谈使用正则表达式的模式匹配操作符。(并且如果你在学习过程中碰巧多遇到几只“野兽”,那么不妨给我们的学习向导留一条不错的技巧。)
5.2 模式匹配操作符
从动物学角度来说,Perl 的模式匹配操作符函数是某种用来关正则表达式的笼子。我们是有意这么设计的;如果我们任由正则怪兽在语言里四处乱逛,Perl 就完全是一个原始丛林了。当然,世界需要丛林——它们是生物种类多样性的引擎,但是,丛林毕竟应该放在它们应该在的位置。一样,尽管也是组合多样化的引擎,正则表达式也应该放在它们应该在的模式匹配操作符里面。那里是另外一个丛林。
因为正则表达式还不够强大,m// 和 s/// 操作符还提供了(同样也是限制)双引号代换的能力。因为模式是按照类似双引号字串那样分析的,所以所有的双引号代换都有效,包括变量代换(除非你用单引号做分隔符)和用反斜杠逃逸标识的特殊字符。(参阅本章后面的"特殊字符"。)在字串被解释成正则表达式之前首先应用这些代换。(也是 Perl 语言里极少数的几个地方之一,在这些地方一个字串要经过多于一次处理。)第一次处理是不那么正常的双引号代换,不正常是因为它知道它应该转换什么和它应该给正则表达式分析器传递什么。因此,任何后面紧跟竖直条,闭圆括弧或者字符串结尾的 $ 都不会被当作变量代换,而是当作典型的正则表达式的行尾断言。所以,如果你说:
$foo = "bar"; /$foo$/;
双引号代换过程是知道那两个 $ 符作用是不同的。它先做 $foo 的变量代换,然后把剩下的交给正则表达式分析器:
/bar$/;
这种两回合分析的另一个结果是普通的 Perl 记号分析器首先查找正则表达式的结尾,就好象它在查找一个普通字串的结尾分隔符一样。只有在它找到字串的结尾后(并且完成任意变量代换),该模式才被当作正则表达式对待。这意味着你无法在一个正则构造里面“隐藏”模式的结尾分隔符(比如一个字符表或者一个正则注释,我们还没有提到这些东西)。Perl 总是会在任何地方识别该分隔符并且在该处结束该模式。
你还应该知道在模式里面代换变量会降低模式匹配的速度,因为它会觉得需要检查变量是否曾经变化过,如果变化过,那么它必须重新编译模式(这样更会降低速度)。参阅本章后面的“变量代换”。
tr/// 转换操作符不做变量代换;它甚至连正则表达式都不用!(实际上,它可能并不属于本章,但我们实在想不出更好的地方放它。)不过,它在一个方面还是和 m// 和 s/// 一样的:它用 =~ 和 !~ 操作符与变量绑定。
第三章,单目和双目操作符,里描述的 =~ 和 !~ 操作符把它们左边的标量表达式和在右边的三个引起类操作符之一绑定在一起: m// 用于匹配一个模式,s/// 用于将某个符合模式的子字串代换为某个字串,而 tr/// (或者其同义词,y///)用于将一套字符转换成另一套。(如果把斜杠用做分隔符,你可以把 m// 写成 //,不用写 m。)如果 =~ 或 !~ 的右手边不是上面三个,它仍然当作是 m// 匹配操作,不过此时你已经没有地方放跟在后面的修饰词了(参阅后面的“模式修饰词”),并且你必须操作自己的引号包围:
print "matches" if $somestring =~ $somepattern;
不过,我们实在没道理不明确地说出来:
print "matches" if $somestring =~ m/$somepattern/;
当用于匹配操作时,有时候 =~ 和 !~ 分别读做“匹配”和“不匹配”(因为“包含”和“不包含”会让人觉得有点模糊)。
除了在 m// 和 s/// 操作符里使用外,在 Perl 的另外两个地方也使用正则表达式。split 函数的第一个参数是一个特殊的匹配操作符,它声明的是当把字串分解成多个子字串后不返回什么东西。参阅第二十九章,函数,里的关于 split 的描述和例子。qr// (“引起正则表达”)操作符同样也通过正则表达式声明一个模式,但是它不是为了匹配匹配任何东西(和 m// 做的不一样)。相反,编译好的正则表达的形式返回后用于将来的处理。参阅“变量代换”获取更多信息。
你用 m//,s/// 或者 tr/// 操作符和 =~ (从语言学上来说,它不是真正的操作符,只是某种形式的标题符)绑定操作符把某一字串和这些操作符之一绑定起来。下面是一些例子:
$haystack =~ m/meedle/ # 匹配一个简单模式 $haystack =~ /needle/ # 一样的东西 $italiano =~ s/butter/olive oil/ # 一个健康的替换 $rotate13 =~ tr/a-zA-Z/n-za-mN-ZA-M/ # 简单的加密
如果没有绑定操作符,隐含地用 $_ 做“标题”:
/new life/ and # 搜索 $_ 和 (如果找到) /new civilizations/ # 再次更宽范围地搜索 $_ s/sugar/aspartame/ # 把一个替换物替换到 $_ 里 tr/ATCG/TAGC # 修改在 $_ 里表示的DNA
因为 s/// 和 tr/// 修改它们所处理的标量,因此你只能把它们用于有效的左值:
"onshore" =~ s/on/off/; # 错;编译时错误
不过,m// 可以应用于任何标量表达式的结果:
if (( lc $magic_hat->fetch_contents->as_string) =! /rabbit/) {
print "Nyaa, what's up doc?\n";
}
else {
print "That trick never works!\n";
}但是,在这里你得更小心一些,因为 =~ 和 !~ 的优先级相当高——在前一个例子里,左边的项的圆括弧是必须的(注:如果没有圆括弧,低优先级的 lc 将会应用于整个模式匹配而不只是对 magic hat 对象的方法调用。)。!~ 绑定操作符作用和 =~ 类似,只是把逻辑结果取反:
if ($song !~ /words/) {
print qq/"$song" appears to be a song without words. \n/;
}因为 m//,s///,和 tr/// 都是引号包围操作符,所以你可以选择自己的分隔符。这时其运行方式和引起操作符 q//,qq//,qr//,和 qw//一样(参阅第二章,集腋成裘,中的“选择自己的引号”)。
$path =~ s#/tmp#/var/tmp/scratch#;
if ($dir =~ m[/bin]) {
print "No binary directories please.\n";
}当你把成对的分隔符和 s/// 或者 tr/// 用在一起的时候,如果第一部分是四种客户化的括弧对之一(尖括弧,圆括弧,方括弧或者花括弧),那么你可以为第二部分选用不同于第一部分的分隔符:
s(egg)<larva>;
s{larva}{pupa};
s[pupa]/imago/;也可以在实际使用的分隔符前面加空白字符:
s (egg) <larva>;
s {larva} {pupa};
s [pupa] /imago/;每次成功匹配了一个模式(包括替换中的模式),操作符都会把变量 $`,$&,和 $' 分别设置为匹配内容左边内容,匹配的内容和匹配内容的右边的文本。这个功能对于把字串分解为组件很有用:
"hot cross buns" =~ /cross/; print "Matched: <$`> $& <$'>\n"; # Matched: <hot > cross < buns> print "Left: <$`>\n"; # Left: <hot > print "Match: <$&>\n"; # Match: <cross> print "Right: <$'>\n"; # Right: < buns>
为了有更好的颗粒度和提高效率,你可以用圆括弧捕捉你特别想分离出来的部分。每对圆括弧捕捉与圆括弧内的模式相匹配的子模式。圆括弧由左圆括弧的位置从左到右依次排序;对应那些子模式的子字串在匹配之后可以通过顺序的变量 $1,$2,$3 等等获得:
$_ = "Bilbo Baggins's birthday is September 22"; /(.*)'s birthday is (.*)/; print "Person: $1\n"; print "Date: $2\n";
$`, $&, $' 和排序的变量都是全局变量,它们隐含地局部化为属于此闭合的动态范围。它们的存在直到下一次成功的匹配或者当前范围的结尾,以先到者为准。我们稍后在其它课题里有关于这方面内容里更多介绍。
一旦 Perl 认为你的程序的任意部分需要 $`, $&, 或 $',它就会为每次模式匹配提供这些东西。这样做会微微减慢你的程序的速度。Perl 同样还利用类似的机制生成 $1,$2 等等,因此你也会为每个包含捕捉圆括弧的模式付出一些代价。(参阅“集群”获取在保留分组的特征的同时避免捕获的开销的方法。)但如果你从不使用 $`,$& 或者 $',那么不带捕获圆括弧的模式不会有性能损失。因此,如果可能地话,通常你应该避免使用 $`,$& 和 $',尤其是在库模块里。但是如果你必须至少使用它们一次(而且有些算法的确因此获益非浅),那么你就随便用它们吧,因为你已经为之付出代价了。在最近的 Perl 版本里,$& 比另外两个开销少。
5.2.1 模式修饰词
我们稍后将逐个讨论模式匹配操作符,但首先我们先谈谈另一个这些模式操作符都有的共性:修饰词。
你可以在一个 m//,s///,qr//,或者 tr/// 操作符的最后一个分隔符后面,以任意顺序放一个或多个单字母修饰词。为了保持清晰,修饰词通常写成“/o 修饰词”并且读做“斜杠 o 修饰词”),即使最后的分隔符可能不是一个斜杠也这么叫。(有时候人们把“修饰词”叫做“标志”或者“选项”也可以。)
有些修饰词改变单个操作符的特性,因此我们将在后面仔细讨论它们。其他的修改正则表达式的解释方式,所以我们在这里讨论它们。m//, s/// 和 qr// 操作符(tr/// 操作符并不接受正则表达式,所以这些修饰词并不适用。)的最后一个分隔符后面都接受下列修饰词:
| 修饰词 | 含义 |
|---|---|
| /i | 忽略字母的大小写(大小写无关) |
| /s | 令 . 匹配换行符并且忽略不建议使用的 $* 变量 |
| /m | 令 ^ 和 $ 匹配下一个嵌入的 \n。 |
| /x | 忽略(大多数)空白并且允许模式中的注释 |
| /o | 只编译模式一次 |
/i 修饰词是说同时匹配大写或者小写(以及在 Unicode 里的标题)。也是为什么/perl/i 将匹配字串 "PROPERLY" 或 "perlaceous"(几乎是完全不同的东西)。use locale 用法可能也会对被当作相同的东西有影响。(这可能对包含 Unicode 的字串有负面影响。)
/s 和 /m 修饰词并不涉及任何古怪的东西。它们只是影响 Perl 对待那些包含换行符的匹配的态度。不过它们和你的字串是否包含换行符无关;它们关心的是 Perl 是否应该假设你的字串包含单个行(/s)还是多个行(/m),因为有些元字符根据你是否需要让它们工作于面向行的模式而有不同的行为。
通常,元字符 "." 匹配除了换行符以外的任何单个字符,因为它的传统含义是匹配一行内的某个字符。不过,带有 /s 时,"." 元字符也可以匹配一个换行符,因为你已经告诉 Perl 忽略该字串可能包含多个换行符的情况。 (/s 修饰词同样还令 Perl 忽略我们已经不鼓励使用的 $* 变量,我们也希望你也忽略。)另一方面,/m 修饰词还修改元字符 ^ 和 $ 的解释 —— 通过令它们匹配字串里的换行符后面的东西,而不仅仅是字串的结尾。参阅本章的”位置“节的例子。
/o 操作符控制模式的重新编译。除非你选用的分隔符是单引号(m'PATTERN',s'PATTERN'REPLACEMENT',或者 qr'PATTERN'),否则每次计算模式操作符的时候,任何模式里的变量都会被代换(并且可能会导致模式的重新编译)。如果你希望这样的模式被且只被编译一次;那么就该使用 /o 修饰词。这么做可以避免开销巨大的运行时重新编译;这么做非常有用,尤其是你在转换的值在执行中不会改变的情况下。不过,/o 实际上是让你做出了不会改变模式中的变量的承诺。如果你改变了这些变量,Perl 设置都不会注意到。为了更好地控制重编译,你可以使用 qr// 正则表达式引起操作符。详情请参阅本章后面的“变量代换”节。
/x 是表达修饰词:它允许你利用空白和解释性注释扩展你的模式的易读性,你甚至还可以把模式扩展得超过一行的范围。
也就是说,/x 修改空白字符(还有 # 字符)的含义:它们不再是普通字符那样的自匹配字符,而是转换成元字符,这些元字符的特征类似空白(和注释字符)。因此,/x 允许(在模式里面)将空白,水平制表符和换行符用于格式化,就象普通 Perl 代码一样。它还允许用通常在模式里没有特殊含义的 # 字符引入延伸到当前模式行行尾的注释。(注:请注意不要在注释里包含模式分隔符——因为“先找结尾”的规则,Perl 没办法知道你在该点上并不想结束。)如果你想匹配一个真正的空白字符(或者 # 字符),那你就要把它们放到字符表里,或者用反斜杠逃逸,或者用八进制或者十六进制逃逸的编码。(但是空白通常用一个 \s* 或 \s+ 序列匹配,因此实际中这种情况出现得并不多。)
总结而言,这些特性朝着把传统的正则表达式变成更可读的语言迈进了一大步。从"回字有四种写法"精神出发,现在写一个正则表达式的方法是不止一种了。实际上,我们有不止两种的方法:(译注:TMTOWTDI:"There's More Then One Way To Do It", "做事的方法不止一种". Perl 文化口号,见本书尾部的词汇表.)
m/\w+:(\s+\w+)\s*\d+/; # 一个词,冒号,空白,词,空白,数字。
m/\w+: (\s+ \w+) \s* \d+/x; # 一个词,冒号,空白,词,空白,数字。
m{
\w+: # 匹配一个词和一个冒号。
( # 分组开始。
\s+ # 匹配一个或多个空白。
\w+ # 匹配另外一个词。
) # 分组结束。
\s* # 匹配零或更多空白。
\d+ # 匹配一些数字
}x;我们会在本章稍后描述这些元符号。(本节本来是讲模式修饰词的,但是我们却因为对 /x 过于兴奋而超出了我们的控制。)下面是一个正则表达式,它找出一个段落里面的重复的词,我们从 Perl Cookbook 里直接把这个例子偷了出来。它使用 /x 和 /i 修饰词,以及后面描述的 /g 修饰词。
# 找出段落里面的重复的单词,可能会跨越行界限。
# 将 /x 用于空白和注释,/i 以匹配在"Is is this ok?"里的两个`is'
# 用 /g 找出所有重复。
$/ = ""; # "paragrep" 模式
while( <> ) {
while ( m{
\b # 从字边界开始
(\w\S+) # 找出一个字块
(
\s+ # 由一些空白分隔
\l # 然后再次分块
)+ # 重复动作
\b # 直到另外一个字边界
}xig
)
{
print "dup word '$1' at paragraph $.\n";
}
}当对本章运行这个程序时,它的输出象下面这样:
dup word 'that' at paragraph 100 (译注:只对英文原版有效 :))
看到这些,我们就知道这个重复是我们有意做的。
5.2.2 m// 操作符(匹配)
EXPR =~ m/PATTERN/cgimosx EXPR =~ /PATTERN/cgimosx EXPR =~ ?PATERN?cgimosx m/PATTERN/cgimosx /PATTERN/cgimosx ?PATTERN?cgimosx
m// 操作符搜索标量 EXPR 里面的字串,查找 PATTERN。如果使用 / 或 ? 做分隔符,那么开头的 m 是可选的。? 和 ' 做分隔符时都有特殊含义:前者表示只匹配一次;后者禁止进行变量代换和六种转换逃逸(\U 等,后面描述)。
如果 PATTERN 计算出的结果是空字串,则要么是你用 // 把它声明成空字串或者是因为一个代换过来的变量就是空字串,这时就用没有隐藏在内层块(或者一个 split,grep,或者 map)里的最后执行成功的正则表达式替代。
在标量环境里,该操作符在成功时返回真(1),失败时返回假("")。这种形式常见于布尔环境:
if($shire =~ m/Baggins/) { ...} # 在 $shire 里找Baggins,译注:shire 知道哪里么?;)
if($shire =~ /Baggins/) { ...} # 在 $shire 里找Baggins
if(m#Baggins#) {...} # 在 $_ 里找
if( /Baggins/ ) {...} # 在 $_ 里找在列表环境里使用,m// 返回一个子字串的列表,这些子字串匹配模式里的捕获圆括弧(也就是 $1,$2,$3 等等),这些捕获圆括弧将在稍后的“捕获和集群”里描述。当列表返回的时候,这些序列数变量仍然是平滑的。如果在列表环境里匹配失败,则返回一个空列表。如果在列表环境中匹配成功,但是没有使用捕获圆括弧(也没有 /g),则返回则返回一列 (1)。因此它在失败时返回一列空列表,所以这种形式的 m// 仍然能用于布尔环境,但是仅限于通过列表赋值间接参与的情况:
if( ($key, $values) = /(\w+): (.*)/) { ... }用于 m//(不管是什么形式)的合法修饰词见表 5-1。
表 5-1。 m// 修饰词
| 修饰词 | 含义 |
|---|---|
| /i | 或略字母大小写 |
| /m | 令 ^ 和 $ 匹配随后嵌入的 \n。 |
| /s | 令 . 匹配换行符并且忽略废弃了的 $*。 |
| /x | 或略(大多数)空白并且允许在模式里的注释 |
| /o | 只编译模式一次 |
| /g | 全局地查找所有匹配 |
| /cg | 在 /g 匹配失败后允许继续查找 |
头五个用于正则表达式的修饰词我们前面描述过了。后面两个修改匹配操作本身的特性。/g 修饰词声明一个全局匹配——也就是说,在该字串里匹配尽可能多的次数。它的具体特性取决于环境。在列表环境里,m//g 返回所有找到的东西的列表。下面的语句找出所有我们提到的 "perl", "Perl","PERL" 的地方:
if( @perls = $paragrapth =~ /perl/gi) {
printf "Perl mentioned %d times.\n", scalar @perls;
}如果在 /g 模式里没有捕获圆括弧,那么返回完整的匹配。如果有捕获圆括弧,那么返回捕获到的字串。想象一下这样的字串:
$string = "password=xyzzy verbose=9 score=0";
并且假设你想用这个字串初始化下面这样的散列:
%hash = (password => "xyzzy", verbose => 9, socre => 0);
当然,你有字串但还没有列表。要获取对应的列表,你可以在列表环境里用 m//g 操作符,从字串里捕获所有的键/值对:
%hash = $string =~ /(\w+)=(\w+)/g;
(\w+) 序列捕获一个字母数字单词。参阅“捕获和集群”节。
在标量环境里使用时,/g 修饰词表明一次渐进地匹配,它令 Perl 从上一次匹配停下来的位置开始一次对同一个变量的新的匹配。\G 断言表示字符串中的那个位置;\G 的描述请参阅本章后面的“位置”一节。如果除了用 /g,你还用了 /c(表示“连续”)修饰词,那么当 /g 运行结束后,失败的匹配不会重置位置指针。
如果分隔符是 ?,就象 ?PATTERN?,那么运行起来和 /PATTERN/ 搜索一样,区别是它在两次 reset 操作符调用之间只匹配一次。如果你只想匹配程序运行中模式出现的第一次,而不是所有的出现,那么这是一个很方便的优化方法。你每次调用此操作符时都会运行搜索,直到它最终匹配了什么东西,然后它就关闭自身,在你明确地用 reset 把它重置之前它一直返回假。Perl 替你跟踪这个匹配状态。
当一个普通模式匹配想找出最后一个匹配而不是第一个,那么 ?? 操作符很好用:
open DICT, "/usr/dict/words" or die "Can't open words: $!\n";
while (<DICT>) {
$first = $1 if ?(^neur.*)?;
$last = $1 if /(^neur.*)/;
}
print $first, "\n"; # 打印"neurad"
print $last, "\n"; # 打印 "neurypnology"在调用 reset 操作符时,reset 只重置那些编译进同一个包的 ?? 记录。你说 m?? 的时候等效于说 ??。
5.2.3 s/// 操作符(替换)
LVALUE =~ s/PATTERN/REPLACEMENT/egimosx s/PATTERN/REPLACEMENT/egimosx
这个操作符在字串里搜索 PATTERN,如果找到,则用 REPLACEMENT 文本替换匹配的子字符串。(修饰词在本节稍后描述。)
$lotr = $hobbit; # 只是拷贝Hobbit 译注:影片魔戒里,Hobbit 人住在 Shire :) $lotr =~ s/Bilbo/Frodo/g; # 然后用最简单的方法写结局,译注:Frodo 代替 Bilbo 成了魔戒的看护人,又是魔戒
一个 s/// 操作符的返回值(在标量和列表环境里都差不多)是它成功的次数(如果与 /g 修饰词一起使用,返回值可能大于一)。如果失败,因为它替换了零次,所以它返回假(""),它等效于数字 0。
if( $lotr =~ s/Bilbo/Frodo/) { print "Successfully wrote sequel." }
$change_count = $lotr =~ s/Bilbo/Frodo/g;替换部分被当作双引号包围的字串看待。你可以在替换字串里使用我们前面描述过的任何动态范围的模式变量($`,$&,$',$1,$2,等等),以及任何其他你准备使用的双引号包围的小发明。比如下面是一个小例子,用于找出所有字串 "revision","version",或者 "release",并且用对应的大写字串替换,我们可以用 \u 逃逸处理替换的目标部分:
s/revision|version|release/\u$&/g; # | 用于表示模式中的“或”
所有的标量变量都在双引号包围的环境中扩展开,而不仅仅是这些特殊的变量。假设你有一个散列 %Names,把版本号映射为内部的项目名;比如,$Name{"3.0"} 可能是名为 "Isengard" 的代码名。你可以用 s/// 找出版本号并且用它们对应的项目名替换掉:
s/version ([0-9.]+)/the $Names{$1} release/g;在在替换字串里,$1 返回第一对(也是唯一的一对)捕获圆括弧。(愿意的话你还可以在模式里用 \1,但是这个用法在替换中已经废弃了,在一个普通的双引号包围的字串里,\1 的意思是 Control-A。)
如果 PATTERN 是一个空字串,则使用上一次成功执行的正则表达式取代。PATTERN 和RELPACEMENT 都需要经受变量代换,不过每次计算 s/// 操作符的时候都进行代换,而 REPLACEMENT 只是在有匹配的时候才做变量代换。(如果你使用了 /g 修饰词,那么 PATTERN 在一次计算中可能匹配多次。)
和前面一样,表 5-2 中的头五个修饰词修改正则表达式的性质;他们与 m// 和 qr// 中的一样。后面两个修改替换操作符本身。
表 5-2 s/// 修饰词
| 修饰词 | 含义 |
|---|---|
| /i | 或略字母大小写 |
| /m | 令 ^ 和 $ 匹配随后嵌入的 \n。 |
| /s | 令 . 匹配换行符并且忽略废弃了的 $*。 |
| /x | 或略(大多数)空白并且允许在模式里的注释 |
| /o | 只编译模式一次 |
| /g | 全局地查找所有匹配 |
| /e | 把右边当作一个表达式计算 |
/g 修饰词用于 s/// 的时候就会把每个匹配 PATTERN 的东西用 REPLACEMENT 值替换,而不仅仅是所找到的第一个。一个 s///g 操作符的作用象一次全局的搜索和替换,令所有修改同时发生,很象 m//g,只不过 m//g 不改变任何东西。(而且 s///g 也和标量 m//g 不一样,它不是递增匹配。)
/e 修饰词把 REPLACEMENT 当作一个 Perl 代码块,而不仅仅是一个替换的字串。执行这段代码后得出的结果当作替换字串使用。比如,s/(0-9)+)/sprintf("%#x", $1)/ge 将把所有数字转换成十六进制,比如,把 2581 变成 0xb23。或者,假设在我们前一个例子里,你不知道是否所有版本都有名称,因此,你希望把这些没有名称的保留不动。可以利用稍微有点创造力的 /x 格式,你可以说:
s{
version
\s+
(
[0-9.]+
)
}{
$Names{$1}
? "the $Names[$1} release"
: $&
}xge;你的 s///e 的右边(或者本例中的下半部分)在编译时与你的程序的其他部分一起做语法检查和编译。在编译过程中,任何语法错误都会被检测到,而运行时例外则被忽略。在第一个 /e 后面每多一个 e(象 /ee,/eee 等等)都等效于对生成的代码调用 eval STRING,每个 /e 相当于一次调用。这么做等于计算了代码表达式的结果并且把例外俘获在特殊变量 $@ 里。参阅本章后面的“编程化模式”获取更多信息。
5.2.3.1 顺便修改一下字串
有时候你想要一个新的,修改过的字串,而不是在旧字串上一阵乱改,新字串以旧字串为基础。你不用写:
$lotr = $hobbit; $lotr =~ s/Bilbo/Frodo/g;
你可以把这些组合成一个语句。因为优先级关系,必须在赋值周围使用圆括弧,因为它们大多和使用了 =~ 的表达式结合在一起。
($lotr = $hobbit ) =~ s/Bilbo/Frodo/g;
如果没有赋值语句周围的圆括弧,你只修改了 $hobbit 并且把替换的个数存储在 $lotr里,那样会得到很傻的结局。
你不能对数组直接使用 s/// 操作符。这时,你需要一个循环。幸运的是,for/foreach 的别名特性加上它把 $_ 当作缺省循环变量,这样就产生了 Perl 标准的用于搜索和替换一个数组里每个元素的俗语:
for (@chapters) { s/Bilbo/Frodo/g } # 一章一章的替换
s/bilbo/Frodo/g for @chapters; # 一样的东西就象一个简单的标量变量一样,如果你想把初始的值保留在其他地方,你也可以把替换和赋值结合在一起:
@oldhues = ('bluebird', 'bluegrass', 'bluefish', 'the blues');
for (@newhues = @oldhues) { s/blue/red/}
print "@newhues\n"; # 打印:redbird redgrass redfish the reds对同一个变量执行重复替换的最经典的方法是用一个单程循环。比如,下面是规范变量里的空白的方法:
for ($string) {
s/^\s+//; # 丢弃开头的空白
s/\s+$//; # 丢弃结尾的空白
s/\s+/ /g; # 压缩内部的空白
}这个方法正好和下面的是一样的:
$string = join(" ", split " ", $string);你还可以把这样的循环和赋值用在一起,就象我们在数组的例子里做的那样:
for( $newshow = $oldshow ) {
s/Fred/Homer/g;
s/Wilma/Marge/g;
s/Pebbles/Lisa/g;
s/Dino/Bart/g;
}5.2.3.2 当全局替换不够“全局”地时候
有时候,你用 /g 不能实现全部修改的发生,这时要么是因为替换是从右向左发生的,要么是因为你要求 $` 的长度在不同的匹配之间改变。通常你可以通过反复调用 s/// 做你想做的事情。不过,通常你希望当 s/// 失败的时候循环停下来,因此你必须把它放进条件里,这样又让循环的主体无所事事。因此我们只写一个 1,这也是一件无聊的事情,不过有时候无聊比没希望好。下面是一些例子,它们又用了一些正则表达式怪兽:
# 把逗号放在一个整数的合理的位置 1 while s/(\d)(\d\d\d)(?!\d)/$1,$2/; # 把水平制表符扩展为八列空间 1 while s/\t+/' ' x (length($&)*8 - length($`)%8)/e; # 删除 (嵌套(甚至深层嵌套(象这样)))的括弧 1 while s/\([^()]*\)//g; # 删除重复的单词(以及三重的(和四重的。。。)) 1 while s/\b(\w+) \1\b/$1/gi;
最后一个需要一个循环是因为如果没有循环,它会把:
Paris in THE THE THE THE spring.
转换成:
Paris in THE THE spring。
这看起来会让那些懂点法文的人觉得巴黎位于一个喷冰茶的喷泉中间,因为"the"(法文)是法文“tea”的单词。当然,巴黎人从来不会上当。
5.2.4 tr/// 操作符(转换)
LVALUE =~ tr/SEARCHLIST/REPLACEMENTLIST/cds tr/SEARCHLIST/REPLACEMENTLIST/cds
对于 sed 的爱好者而言,y/// 就是 tr/// 的同义词。这就是为什么你不能调用名为 y 的函数,同样也不能调用名为 q 或 m 的函数。在所有的其他方面,y/// 都等效于 tr///,并且我们不会再提及它。
把这个操作符放进关于模式匹配的章节看起来其实有点不合适,因为它不使用模式。这个操作符逐字符地扫描一个字串,然后把每个在 SEARCHLIST (不是正则表达式)里出现的字符替换成对应的来自 REPLACEMENTLIST(也不是替换字串)的字符。但是它看上去象 m// 和 s///,你甚至还可以和它一起使用 =~ 和 !~ 绑定操作符,因此我们在这里描述它。(qr// 和 split 都是模式匹配操作符,但是它们不能和绑定操作符一起使用,因此因此在我们本书的别处描述它们。自己找找看。)
转换返回替换或者删除了的字符个数。如果没有通过 =~ 或者 !~ 操作符声明的字串,那么使用 $_ 字串。SEARCHLIST 和 REPLACEMENTLIST 可以用一个划线定义一个顺序字符的范围:
$message =~ tr/A-Za-z/N-ZA-Mn-za-m?; # 旋转13加密
请注意想 A-Z 这样的范围假设你用的是线性字符集,比如 ASCII。但是不同的字符集的字符排列顺序是不一样的。一个合理的原则是,只使用起始点都是相同大小写的字母序列,如 (a-e,A-E),或者数字 (0-4)。任何其他的范围都有问题。如果觉得有问题,他们写成你用的整个字符集:ABCDE。
SEARCHLIST 和 REPLACEMENTLIST 都不会象双引号字串那样进行变量代换;不过,你可以使用那些映射为特殊字符的反斜杠序列,比如 \n 或 \015。
表 5-3 是可用于 tr/// 操作符的修饰词。它们和用于 m//,s///,或 qr// 上的完全不同,即使有些看起来有点象。
表 5-3。tr/// 修饰词
| 修饰词 | 含义 |
|---|---|
| /c | 与SEARCHLIST为补 |
| /d | 删除找到的但是没有替换的字符 |
| /s | 消除重复的字符。 |
如果声明了 /c 修饰词,那么 SEARCHLIST 里的字符会被求补;也就是说,实际的搜索列表包含所有不在 SEARCHLIST 里的字符。如果是 Unicode,这样可能会代表许多字符,不过因为它们是逻辑存储的,而不是物理存储,所以你不用害怕会用光内存。
/d 修饰词把 tr/// 转换成所谓的“过滤吸收”操作符:任何由 SEARCHLIST 声明的但是没有在 RELPACEMENTLIST 里给出替换的字符将被删除。(这样比一些 tr(1) 程序的性质显得更加灵活,那些程序删除它们在 SERACHLIST 里找到的任何东西。)
如果声明了 /s 修饰词,被转换成相同字符的顺序字符将被压缩成单个字符。
如果使用了 /d 修饰词,那么 RELPACEMENTLIST 总是严格地解释成声明的样子。否则,如果 REPLACEMENTLIST 比 SEARCHLIST 短,则复制 REPLACEMENTLIST 的最后一个字符,直到足够长为止。如果 RELPACEMENTLIST 为空,则复制 SEARCHLIST,这一点虽然奇怪,但很有用,尤其是当你只是想计算字符数,而不是改变它们的时候。也有利于用 /s 压缩字符。
tr/aeiou/!/; # 把所有元音字母转换成!
tr{/\\\r\n\b\f. }{_}; # 把怪字符转成下划线
tr/A-Z/a-z/ for @ARGV; # 把字符规则化为小写ASCII
$count = ($para =~ tr/\n//); # 计算$para里的换行符
$count = tr/0-9//; # 计算$_里的位
$word =~ tr/a-zA-Z//s; # bookkeeper -> bokeper
tr/@$%*//d; # 删除这里几个字符
tr#A-Za-z0-9+/##cd; # 删除非base64字符
# 顺便修改
($HOST = $host) =~ tr/a-z/A-Z/;
$pathname =~ tr/a-zA-Z/_/cs; # 把非ASCII字母换成下划线
tr [\200-\377]
{\000-\177]; # 剥除第八位,字节操作如果在 SEARCHLIST 里同一个字符出现的次数多于一次,那么只有第一个有效。因此:
tr/AAA/XYZ/
将只会把($_ 里的)任何单个字符 A 转换成 X。
尽管变量不会代换进入 tr///,但是你还是可以用 eval EXPR 实现同样效果:
$count = eval "tr/$oldlist/$newlist/"; die if $@; # 传播非法eval内容的例外
最后一条信息:如果你想把你的文本转换为大写或者小写,不要用 tr///。用双引号里的 \U 或者 \L 序列(或者等效的 uc 和 lc 函数),因为它们会关心区域设置或 Unicode 信息,而 tr/a-z/A-Z/ 不关心这些。另外在 Unicode 字串里,\u 序列和它的对应 ucfirst 函数能够识别标题格式,对某些语言来说,比简单地转换成大写更突出。
导入论坛 引用链接 收藏 分享给好友 推荐到圈子 管理 举报
TAG:
标题搜索
日历
|
|||||||||
| 日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
| 1 | 2 | 3 | 4 | 5 | |||||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 | |||
| 13 | 14 | 15 | 16 | 17 | 18 | 19 | |||
| 20 | 21 | 22 | 23 | 24 | 25 | 26 | |||
| 27 | 28 | 29 | 30 | 31 | |||||
数据统计
- 访问量: 2844
- 日志数: 555
- 建立时间: 2008-01-07
- 更新时间: 2008-06-24

