Archive for the ‘PCRE’ tag
环视(二)
肯定逆序环视和否定逆序环视
逆序环视与顺序环视效果作用相同但“看”的方向相反,它要求引擎在目标文本中回溯并检查回溯后得到的字符是否能和逆序环视中的表达式相匹配。使用逆序环视 (?<!a)b 可以匹配字符 b,但 b 不能跟在字符 a 的后面出现,因此它不能匹配目标文本 cab 中 b 字符,但可以匹配 bed 和 debt 中的 b 字符;而肯定逆序环视 (?<=a)b 则可以匹配 cab 中的字符 b,却不能匹配 bed 和 debt 中的字符 b。
肯定逆序环视结构 (?<=text) 以一对圆括号标识,开括号后面依次是问号、小于号、等号,接着再是表达式字符;否定逆序环视像这样 (?<!text),区别仅是以叹号代替了等号。
引擎细节
尝试以 (?<=a)b 匹配目标文本 thingamabob。引擎以逆序环视中的第 1 个表达式字符开始尝试匹配。本例中逆序环视要求引擎在目标文本中回溯一个字符并看回溯后得到的是不是字符 a。一开始引擎无法回溯,因为字符 t 是目标文本中的第 1 个字符,因此逆序环视失败(注意这里如果是否定逆序环视就能匹配成功),接着引擎又从下一个字符 h 再次开始新一轮匹配,再次尝试回溯一个字符并判断能不能找到字符 a,可惜看到的是字符 t 因此肯定逆序环视再次匹配失败。
逆序环视一直匹配失败直到前进到目标文本中的字符 m 为止,当前进到 m 时,引擎回溯一个字符后是字符 a 于是肯定逆序环视匹配成功。接着引擎丢弃获得的字符 a 并返回到目标文本中的 m,表达式也前进到 b,结果 b 和 m 匹配失败。于是引擎继续在目标文上前进到字符 a,回溯后获得 b,逆序环视匹配失败。
目标文本继续前进到字符 b,回溯后获得字符 a,肯定逆序环视再次匹配成功,放弃获得的 a 返回字符 b 后,第二个表达式字符依然匹配成功,终于整个正则表达式匹配成功,而匹配结果就是唯一的一个字符 b。
逆序环视要点
首先,好消息是可以在正则表达式的任意位置使用逆序环视,比如想要匹配一个不以字符 s 结尾的单词就可以使用 \b\w+(?<!s)\b 来实现。这跟 \b\w+[^s]\b 是完全不同的,考虑目标文本 John’s,前者匹配结果是 John,而后者的匹配结果则是 John‘(包括了单引号,原因就留给各位自己参悟了),同样的后者也无法匹配 a 或者 l 这样的单字符。如果想不使用逆序环视而实现同样的需求,那表达式应该写成这样 \b\w*[^s\W]\b,用星号代替加号,并在字符组中添加 \W 元字符。个人认为逆序环视的写法更易于理解,不使用逆序环视的表达式虽然也能正确匹配,但包括了双重否定(\W 也受 ^ 的作用),双重否定让程序员思路混乱,但并不会影响到正则引擎的运转。
糟糕的是多数正则流派都不允许在逆序环视中随意地构造表达式,因为正则引擎需要在检查逆序环视中的表达式之前确定“需要回溯多少个字符”,而其中一些表达式则可能造成引擎回溯失败。
因此很多流派包括 Perl 和 Python 在内,支持在逆序环视中构造“匹配结果定长”的表达式,即可以在逆序环视中使用任意表达式字符,但匹配结果的长度一定要一开始就能够被确定,也就是说逆序环视中可以使用字面值、字符组而不能使用变长量词和可选项,同时也可以使用所有子项长度完全相同的多选分支,在这方面,PCRE 并不和 Perl 完全兼容,虽然 Perl 要求多选分支中的子项长度完全相同,但 PCRE 则并无这个限制,PCRE 允许各子项长度不同但各子项的匹配结果依然要求是定长的。
唯一允许在逆序环视中无限制构造表达式的正则流派只有 JGsoft 正则引擎和 .NET,而 JavaScript, Ruby 和 Tcl 甚至根本就不支持逆序环视结构。
环视(一)
Regex Guru 天牛神作,尝试翻成中文,不过总觉得翻成中文后变得很水。
原文地址: http://www.regular-expressions.info/lookaround.html
顺序环视和逆序环视
Perl 5 引入了两个强大的正则结构:顺序环视和逆序环视,二者合称为环视,也叫做零长度断言。环视和行起始(结束)锚、词起始(结束)锚一样是零长度匹配 (zero-width matches)的。但与锚匹配位置不同,环视匹配的是字符,并且在匹配完成之后,匹配到的字符就被丢弃,而只返回匹配是否成功,这就是为何被称之为断言。因为匹配的字符会被丢弃,因此环视不会消费(consume)字符,而仅仅是判断是否发生了一次成功匹配。借助环视可以创建几乎不可能的、简洁的正则表达式。
肯定顺序环视和否定顺序环视
当需要匹配某字符接下来不是某字符的情况时就需要用到否定顺序环视。像“字符 q 后面跟一个除 u 以外的字符”这样的需求是办法用否定字符组(Negated Character Classes)来实现的,但否定顺序环视 q(?!u) 则可以实现这个需求。否定顺序环视结构由一对圆括号标识,开括号后面跟一个问号和感叹号,再后面就是表达式字符 u。
肯定顺序环视与否定顺序环视原理相同,q(?=u) 匹配字符 q 后面紧跟字符 u 的情况,不过 u 并不作为匹配结果的一部分。肯定顺序环视结果同样由一对圆括号标识,开括号后面跟一个问号,等于号则代替感叹号的位置,表明该顺序环视是“肯定”的。
在顺序环视中可以包括的任意正则表达式(逆序环视则并非如此),如果其中包括分组捕获,那么将会产生与分组捕获对应的反向引用,而环视本身并不会创建任何的反向引用,因而也不会导致反向引用计数地增加。在环视中创建分组捕获也是用圆括号把表达式括起来,像这样:(?=u(regex)),如果错误地将嵌套关系书写为 ((?=regex)) 以期望捕获整个环视匹配的结果是行不通的,因为当引擎试图存储匹配结果到反向引用时,内层的环视已经将匹配结果丢弃,因为反向引用到的值始终都只是空字符。
引擎细节
首先看引擎是如何将表达式 q(?!u) 应用到目标文本 Iraq 的。第 1 个表达式字符是字面量 q,如你所知,这将使引擎发现目标文本中最靠左的 q 字符,q 是目标文本 Iraq 中最后一个字符,当前表达式字符在前推以后就变成空字符。接下来引擎注意到表达式中的顺序环视结构,就直接让环视中的 u 参与匹配,u 与空字符匹配失败,引擎记录下环视中的表达式匹配失败后发现这是个否定顺序环视,因而意味着环视中的表达式匹配失败,环视本身就匹配成功。表达式 q 和顺序环视均匹配成功,此时整个表达式也就匹配成功并返回匹配到的目标字符 q。
接着看看同样的表达式应用到目标文本 quit 的匹配过程。q 匹配 q,然后是环视中表达式 u 和目标文本中的 u 进行比较,匹配成功,环视中的表达式结束,接着引擎前进到目标文本中的字符 i,并记下环视中的表达式匹配成功然后丢弃环匹配结果,于是引擎又从目标文本中的 i 回溯到 u。
因为这是个否定顺序环视,环视中的表达式匹配成功结果导致环视本身匹配失败,而整个表达式的结束导致引擎不得不重新开始新一轮匹配,q 和 u 比较,匹配失败,q 和 i 比较,匹配失败,q 和 t 比较,还是匹配失败,目标文本结束。最终引擎报告整个表达式匹配失败。
再来一个稍复杂的例子,以确保你真的理解了顺序环视的含义。将 q(?=u)i 应用到 quit 的过程中,q 匹配 q,u 匹配 u,之后环视匹配成功,其中匹配到的 u 被再次丢弃,而引擎则从目标文本中的 i 回溯到 u,匹配继续,表达式中的 i 和目标文本中的 u 匹配失败,目标文本结束,于是引擎又从目标文本中的 u 开始新一轮匹配,最后整个表达式还是匹配失败,因为目标文本中已经没有字符 q 能和表达式中的 q 成功匹配了。
PCRE/Python 下的 re 细节(4) — 条件分支(conditional)
所谓条件分支,就是“当 x 成功捕获的话,则执行多选分支 y,否则执行多选分支 z”,也就是说这个东西在表达式中实现了 if/then/else 的功能。条件分支的语法比较类似于多选分支,只是更复杂,像这样:…(?(1)then|else)…
(?(1)… 就表示开始一个条件分支的匹配,数字 1 的意思是测试整个表达式中第 1 个反向引用是否有参与匹配,如果有则匹配表达式 then,否则匹配表达式 else。看 recipe 2.17 中表达式 (a)?b(?(1)c|d) 的匹配过程。(a)?b(?(1)c|d) 如期望的那样可以匹配目标文本 abc、bd、abd,但匹配不到 bc。
匹配目标文本 abc 时,(a)? 匹配成功,\1 捕获到字符 a,然后前推当前表达式字符到 b,再次匹配成功,继续前推当前表达式字符。这时引擎看到 (?(1) 就识别出这是一个条件分支匹配,于是测试反向引用 1 有没有参与匹配,发现有,于是执行 | 前的部分,也就是匹配字符 c,依然匹配成功,于是整个表达式宣告匹配成功。
匹配目标文本 bd 的过程:(a)? 匹配失败,\1 没有捕获到任何内容,然后匹配过程与匹配 abc 时类似,直到当引擎在条件分支中发现反向引用 1 没有参与匹配,会将 | 后的部分 d 拿来进行匹配,还是匹配成功,于是整个表达式匹配成功。匹配 bc 时则是当引擎发现反向引用 1 没有参与匹配时,就将 | 后的 d 拿来匹配时,结果匹配失败,于是整个表达式也相应匹配失败。
目标文本 abd 的匹配过程是比较有趣的,首先 (a)? 匹配成功,\1 捕获到字符 a,前推当前表达式和当前目标文本,b 和 b 匹配成功,继续前推表达式和目标文本,因为 \1 有内容,所以当前表达式字符为 c,c 和 d 匹配失败,但并不意味着整个表达式也匹配失败了,不要忘了 (a)? 中的 ? 是可以回溯的,所以当前表达式字符和当前目标文本字符均发生回溯,接下来的匹配过程就跟匹配 bd 的过程完全一致了,最终整个表达式还是匹配成功。而如果期望 abd 匹配失败的话,只要在 (a)? 前增加一个 ^ 锚就好了。
有个值得注意的地方是表达式 (a)?b(?(1)c|d) 中 (a)? 的 ? 是在括号外面的,如果写成 (a?)b(?(1)c|d) 的话,即使 (a?) 匹配失败,但 (?(1) 还是会返回 true,因为 (a?) 始终都被认为是有参与匹配的,所以 (?(1)c|d) 总是 | 前的 c 参与后续的匹配过程。
PCRE/Python 下的 re 细节(3) — Unicode
本篇是概念流,但个人认为这个 Recipe 2.7 的内容比 Mastering Regular expressions 中关于 Unicode 的部分讲得更精到,相当有看点的说;再就是对 unicode 的完美支持大多集中在 PCRE/Perl 上,所以下面例出的手法在除 Perl 外的大多数语言中都是屠龙之技。(怒了,Perl 6 杀很大,你到底准备在哪年圣诞节君临天下啊…)
代码点(code point)
一个代码点就是 Unicode 字符集中的一个条目,虽然说多数字符都唯一映射到一个对应的代码点,但一个代码点还是不能说完全等同于一个字符,因为有些语言中的单个字符是映射到 Unicode 中的多个代码点组合上的,比如字符 å 就映射到 U+0061 和 U+030A 两个代码点。
可以用 \uXXXX 和 \x{XXXX} 匹配一个代码点,使用 \u 还是 \x 取决于所使用的语言流派,前者在 Python 中使用,后者则应用在 PCRE/Perl 之中。此外,二者还有个区别是 \u 后面的 XXXX 必须是固定的4位16进制数值,范围从 U+0000 到 U+FFFF;而 \x 后面的 XXXX 则支持变长的16进制数,取值从 U+000000 到 U+FFFFFF,即 \x{0000E0} 等价于 \x{E0}。另外就是代码点在字符组(character classes)内外都可以使用。
属性(property)和分类(category)
每个代码点都有一个属性或者说属于某个分类,这个只是说法不同,二者想要表达的意思是完全一样的,这里是所有 Unicode 分类的列表,总共7大类30小类。所谓分类其实就是把所有 Unicode 代码点按其意义分组并形成多个不同的类别。比如 Sc 这个分类下面就全是跟货币符号相关的代码点,分类 Sm 下面全是跟数学符号相关的代码点。PCRE/Perl 下用 \p{Sc} 和 \p{Sm} 来匹配前面提到的两组分类,可惜的是 Python re 不支持按 Unicode 分类来匹配代码点,怒~~~
块(block)
块和分类的概念差不多,也是把代码点划分为 105 个不同的类别,但划分得更细腻,块名字也更适合人类阅读,块名字显示了这个块下面的代码点的主要用途。比如上面的 \p{Sc} 等价的块匹配就是 \p{InCurrency} 明显 InCurrency 比 Sc 可读性高上一百倍呀一百倍。然后让我们继续回到鄙视 Perl 的大路上了,块匹配更是连 PCRE 都不支持了,可挨千刀的 Perl 继续在完美的支持它,\p{InBlockName} 和 \p{IsBlockName} 这两种语法都可以工作,但推荐使用 \p{InBlockName} 这种语法,因为 Is 可能会和 \p{IsScript} 产生混淆。
Script
这个 Script 我真不知道该怎么翻译了,保持原样好了。Script 和 block/category 也都差不多,它按自然语言的不同将代码点分成了汉语(Han)、片假名(Katakana)、希腊语(Greek)等 N 多个分组。但这种划分并不是完全一一对应到自然语言的,有的 Script 下可能包括多种自然语言中的字符,比如 Latin Script;而有的自然语言下的字符所对应的代码点又会被划分到多个不同的 Script 中,比如 Hiragana Script(平假名)、Katakana Script(片假名)、Han Script(汉语)和 Latin Script 这4种 Script 下面均有日文字符的代码点。\p{Script} 和 \p{IsScript} 都用来匹配某个 Script,但推荐使用前者,以避免和 \p{IsBlockName} 发生冲突。是的,你没有猜错,这个不知道是什么东西的 Script 在 Python 下也是不支持的,它仅在 PCRE/Perl 下面生效。
字形(grapheme)
前面有提到代码点和字符并全是一一对应的关系,会有一个字符对应到多个代码点组合的情形发生,比如字符 å 就对应到 U+0061 和 U+030A 两个代码点。当使用点号去匹配 Unicode 字符时,其实只匹配得到一个代码点,也就是只匹配得到 U+0061,这明显不是期望中的结果。要解决这个问题,就需要用到 \X 模式,这里有我以前写的一个例子,现在就不多说了。
最后一句,所有 \p 都有对应的 \P 模式,\P 的意思就是匹配所有非 XXX 的情况。比如 \P{Sc} 就是匹配所有非 Currency 代码点。然后是支持 \p 的流派也都支持 \P 的,所以那些流派可以用 \P{M}\p{M}* 来模拟 PCRE/Perl 的 \X 模式,但由于 Python 连 \p 都不支持,所以建议各位用 Python 童鞋继续望 Perl 兴叹吧,Perl 6深情的凝望…
PCRE/Python 下的 re 细节(2) — 点号、^ 和 $
古时候正则表达式被用来处理一行一行读入的文件内容,那时候是不会存在“匹配一个换行符”这样的需求滴。后来神说这个可以有,于是世间就出现了单行模式。我猜可能是由于拉伯伯是学语言(自然语言,不是计算机语言)出身的缘故,才会给出 single line 这么个让普罗大众有些目眩神迷的称谓。要避免出现大海的感觉,还是得余大的点号通配模式比较靠谱一点(Python re 下面叫 dotall 模式)。关于多行模式和单行模式(点号通配模式)的区别这里就不再赘述了,有兴趣的可以猛击“single line 和 multiline 一毛钱关系都没有”查看详情。下面就直接给出两种实现点号通配模式(单行模式)的办法:
1. 在表达式外部指定点号通配模式,Python 下用 re.S 参数,这个地球人都晓得滴。
2. 在表达式内部用 (?s) 模式修饰符,这个就比较优雅了,从表达式本身就可以看出处于点号通配模式下,推荐使用。
相应的,激活多行模式也有两种手法:
1. 用 re.M 参数
2. 用 (?m) 模式修饰符
PCRE/Python 下的 re 细节(1)
O’Reilly 的 cookbook 系列差不多都是适合摆在手边准备随时查阅的工具书,期间充斥着大量的技术细节。今年七月的时候 rex 同学(一、二)还送了这本热腾腾的 Regular Expressions Cookbook 给我,可惜一直没来得及看,想当初甚至还雄心勃勃准备和 rex 一起翻译这本书的,但最后还是由于这样那样的原因导致计划搁浅,我真是愧对 rex 的信任。为了对得起 rex 同学的美貌与智慧,现在我准备花时间读一读,至少每天一个 recipe 嘛,然后把其中一些关于 PCRE 和 Python 的亮点都记录在这里,以飨诸君。:)
不废话,上 Recipe…
希望匹配目标文本中的 $()*+.?[\^{| 这12个符号时,就必须对表达式中的相应字符进行转义,意即表达式应该是这个样子滴:\$\(\)\*\+\.\?\[\\\^\{\|
除了上面 12 个特殊字符外,其余的特殊字符就无需进行转义了。不过,即使在无需进行转义的特殊字符前添加了转义符 \ 也是不会报错的,只是多余的 \ 可能会使表达式变得难以理解。再就是 PCRE 支持 \Q, \E 对,意思是 \Q 和 \E 间的特殊字符会被自动转义,也就是说表达式 \Q$()*+.?[\^{|\E 和第1个表达式是等价的。
除反斜线 \ 和 ] 外, [...] 中的特殊字符也不必手工转义,比较特殊的情况是 ^ 和 – 这两个。当 ^ 是 [...] 中第一个出现的字符时,表示不匹配后面出现的字符,而 – 出现在文数字中间时,则表示从 xxx 到 xxx 的含义。因此欲匹配这两种情形下的 ^ 和 – 时是需要转义的。
忽略大小写敏感
默认情况下表达式是大小写敏感的,但可以通过 (?i) 来忽略大小写敏感。例如表达式 (?i)ab 可以匹配的目标文本包括 ab, AB, aB 和 Ab
PCRE 同时支持 (?-i) 来关闭 (?i) 的忽略效果,使 (?-i) 后面的表达式字符重回大小写敏感的默认状态。
正则表达式的匹配过程
“匹配过程”应该是学习正则表达式的第一堂课,但国内出版的相关图书大多对这一主题语焉不详,结果导致很多使用正则表达式的程序员都知其然却不知其所以然,想要对表达式进行优化也完全不知道从何入手。所以下面尽量用简单的例子来把匹配过程描述清楚,欢迎大家拍砖。
首先是最简单的情况,用表达式 a 匹配目标文本 django 的过程。正则引擎首先用 a 和目标文本中的 d 进行比较,此时匹配失败,引擎会将当前待匹配目标字符前推到第2个字符 j 后再进行比较,结果还是匹配失败,引擎继续前推当前待匹配目标字符到 a 并进行比较,这回匹配成功,于是引擎返回匹配的内容并退出。当前待匹配目标字符就是第一次等待匹配的实际参与者,如果匹配失败,引擎就会将当前待匹配目标字符符前推到目标文本的下一个字符,然后再与当前待匹配表达式字符进行比较(在第1个例子中,当前待匹配表达式字符始终都是 a。当前待匹配目标字符的说法实在太绕舌,下面分别将之简称为当前表达式和当前字符),一旦目标文本已经结束却依然没有匹配成功,那么引擎就会报告整个正则表达式匹配失败并退出。
接下来将原来的表达式扩展为 an,目标文本依然是 django,首先将当前表达式 a 和当前字符 d 进行比较,匹配失败,所以前推当前字符到 j 并与当前表达式 a 进行比较,还是失败,继续前推当前字符到 a,这次匹配成功,于是将当前表达式和当前字符都向前推进,得到当前表达式 n 和当前字符 n,进行比较时还是匹配,再继续前推当前表达式时,引擎发现表达式已经结束,于是返回匹配的内容并退出。当正则表达式包含多个字符时,如果当前匹配成功,就同时前推当前表达式和当前字符并进行下一次比较,否则就只前推当前字符并将之继续与当前表达式进行比较。
再考虑以 an 匹配 there is a django boy 的情形。首先是当前表达式 a 和当前字符 t 进行比较,匹配失败,前推当前字符,继续比较,还是失败,直到前推当前字符到第1个 a 时,才匹配成功。这时因表达式尚未结束,所以除了前推当前字符到 a 以外,还同时前推当前表达式到 n,结果匹配失败。这时,正则引擎会做两件事,重置当前表达式为整个正则表达式的第1个字符,其次是将当前字符设置为第1次成功匹配字符的下1个字符,这里的第1次成功匹配字符是 a,a 的下一个字符就是空格符,然后再重新开始匹配,也就是将 a 与空格符进行比较,匹配失败,于是前推当前字符,比较、前推,再比较、再前推,直到当前表达式 a 再次与 django 中的 a 匹配成功之后,引擎又同时前推当前表达式与当前字符并进行比较,匹配成功后继续前推时发现表达式已经结束,于是引擎返回匹配的内容并退出。这里的关键点是如果当前表达式和当前字符同时前推后匹配失败,那么当前表达式重置到整个正则表达式的第1个字符,当前字符重置为第1次成功匹配字符的下1个字符,并继续进行比较。
上面几个都是不断前推并进行比较的例子,在遇到需要进行最长匹配的量词元字符时,正则引擎则可能会不断后退当前字符并和当前表达式进行比较,还是以实例来说话,考虑以正则表达式 a.*g 匹配目标文本 there is an elegant django 的情况。当 a 和点号元字符都匹配成功,前推当前表达式后发现是需要最长匹配的元字符 *,于是直接将当前字符彻底前推到整个目标文本的末尾结束符(由于结束符不可见,可以将其想成 C 中的 ‘\0′),并将中间忽略过的字符统统设为匹配成功,这时引擎侦测到表达式并未结束,前推当前表达式到 g 时发现与结束符并不匹配,于是引擎回退当前字符到 o 后再进行比较,还是匹配失败,于是当前字符继续回退到 g,终于匹配成功,之后引擎发现整个正则表达式已经结束,于是返回匹配的内容并退出,匹配的内容就是 an elegant djang
(这里顺便感谢 .rex 同学今天送我 O’Reilly 最新出版的”Regular Expressions Cookbook“一书,你大无畏的奉献精神深深感动了我…)
放之四海而皆准的最早匹配原理
最早匹配和量词的最多匹配是两条基本的正则表达式匹配原则,其直接影响到表达式的运行效率甚至关系到是否能获得期望的匹配结果。重要的是,这也是所有类型的正则引擎都遵循的普适原则之一。
以表达式 go 匹配目标文本 django is elegance, go to it 的情况来说,当匹配到第一个 go 时,引擎就会停止前进,匹配结果也就是 django 中的 go。所谓最早匹配正是“第一次匹配成功,引擎即告退出,绝不多做”的意思。
而在带分支的情况下,正则引擎会先尝试所有分支条件,有一个分支匹配成功即退出,全部分支都匹配失败就前进到目标文本的下一个字符重新开始尝试匹配所有分支。这样就能保证更早地获得匹配结果(这里的“早”是指目标文本中最左边或最右边的空间概念,而非通常意义下运行效率的时间概念)。
举例来说,以表达式 ruby|python|perl 来匹配 there’re python, perl, ruby and etc 的话,结果就是 python 而非 ruby,因为正则引擎总是先尝试所有分支,有一个分支匹配成功就退出,全部分支都匹配失败就前进到目标文本的下一个字符。比较类似于程序中的逻辑短路,但表达式中分支的排列顺序对匹配结果没有丝毫影响,因为正则引擎总是返回目标文本中能满足最早(最左或最右)匹配的结果
正则表达式的单行和多行
星晨大海间无数刚跨入 regex 领域的童鞋们总会混淆 single line 和 multiline 这两种模式,认为这是两种互斥的模式,这都怪 Larry 大叔胡搞瞎搞的起名字,还是小鄙视一下大叔吧,但是大叔的丰功伟绩依然让我辈心向往之。
单行模式主要用于改变点号的匹配规则,而多行模式用于改变 ^ 和 $ 的匹配规则;默认情况下单行模式和多行模式均处于 off 状态,可以分别激活或同时激活这两种模式。
默认情况下,点号能够匹配除换行符外的所有字符,如果想要匹配到换行符的话,就需要激活单行模式,单行模式下点号就能匹配天地间的一切字符了,所以余大将之译为点号通配模式。
而 ^ 和 $ 在默认情况下是匹配整个目标文本起始和结束的锚,也就是说等价于 \A 和 \Z 的,如果激活了多行模式的话,^ 和 $ 就变成了能够匹配到行起始和行结束的锚。
所以,最后说一句,单行模式和多行模式之间,一毛钱关系都没有。
正则表达式 Unicode 编码模式中的点号匹配问题
很多时候我们在构造正则表达式时不会考虑编码的问题,因为默认的 ASCII 编码已经足够我们应对大多数开发场景,但这并不代表对编码的需求就不存在,典型的例子就是用户注册时,要求用户名一定要是中文的情况,这种情况下 ASCII 编码就无能为力了。现在轮到 Unicode 这砣灵丹妙药出场了。
通常情况下我们都认为一个字符就对应到一个 Unicode 代码点,即一一对应的关系。但这里存在理解上的偏差,一般人所谓的单个字符在 Unicode 编码中可能并不和单个代码点对应而是由多个代码点组合而成。比如 å 这种带声调修饰的单个字符在 Unicode 编码中就是由 \u0061 和 \u030A 这两个代码点组合而成。这就又给我们带来了新的问题,正则表达式的点号元字符在匹配这样的由多个代码点组合而成的字符时,是匹配一个代码点映射的单字符还是匹配多个代码点合成后映射到的单字符?
my $str = “å”;
$str =~ m/(.)/;
print “normal: $1\n”;
$str =~ m/(\X.)/;
print “\\X mode: $1\n\n”;
上面代码输出效果如下:
可以看到要想正确的匹配 å 单字,必须通过 \X 来显示通知正则引擎“匹配一个 Unicode 代码点组合”。不幸的是并不是所有编程语言的正则表达式引擎都实现了 \X 模式。(所以到目前为止,在字符处理方面 Perl/PCRE 还是当之无愧的王者啊!)
