正则表达式学习笔记

本文是《正则表达式30分钟入门教程》的学习笔记,主要目的是完成对于 html 标签的匹配。

元字符,metacharacter

正则表达式语言由两种基本字符类型组成:原义(正常)文本字符和元字符。元字符使正则表达式具有处理能力。所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符,可以用来规定其前导字符(即位于元字符前面的字符)在目标对象中的出现模式。对于“元”这个概念,有必要引用一下不鳥萬如一在知乎上的一篇回答来帮助理解一下

錯譯的例子肯定是很多很多的,但我想單獨把 meta 拿出來講。這個概念本身因爲某種原因沒太能進入中文,或許和翻譯得不好有關,但也不一定。 Meta 這個前綴,通常只有在看學術類、專業書籍的時候纔會碰到,大陸通譯「元」。例如「元數據」(meta data)。我沒考據過當年是怎麼把 meta 翻譯成元的,但可以確定的是大部分人不懂元是什麼意思。 Meta 的意思其實一點也不玄,就是「關於什麼的什麼」。Meta data 就是「關於數據的數據」。例如一個 MP3 文件,它本身是音樂數據(data),但 MP3 文件裡顯示的歌手名字、歌曲名字、碼率等等,則是「關於這個 MP3 文件的數據」,所以叫 meta data。同樣,一張照片的 EXIF 信息也是這張照片的 meta data。 這個概念在英語世界的當代文化生活裡很常見,已經成爲了一種趣味。比如我可以說 Glenn Fleishman 主編的 The Magazine 是一本 meta-magazine,因爲它本身就是在探索雜誌這種形態的未來,是一本關於雜誌的雜誌。但在中文世界,不論是大陸的「元」還是臺灣的譯法「後設」都沒能普及。

代码 说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

字符转义

老生常谈
如果你想查找元字符本身的话,比如你查找.,或者*,就出现了问题:你没办法指定它们,因为它们会被解释成别的意思。这时你就得使用\来取消这些字符的特殊意义。因此,你应该使用\.和\*。当然,要查找\本身,你也得用\\.

例如:unibetter\.com匹配_unibetter.com_,C:\\Windows匹配_C:\Windows_。

重复

直接上表格

代码/语法 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

字符类

可以把我们想要匹配的内容直接在方括号中 ([ ]) 列出来,像[aeiou]就匹配任何一个英文元音字母,[.?!]匹配标点符号(.或?或!)。 我们也可以轻松地指定一个字符范围,像[0-9]代表的含意与\d就是完全一致的:一位数字;同理[a-z0-9A-Z_]也完全等同于\w(如果只考虑英文的话)。

分枝条件

这一段写的很明白,直接照抄。 不幸的是,刚才那个表达式也能匹配010)12345678或(022-87654321这样的“不正确”的格式。要解决这个问题,我们需要用到分枝条件。正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用|把不同的规则分隔开。听不明白?没关系,看例子:

**0\d{2}-\d{8}|0\d{3}-\d{7}**这个表达式能匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)。

**(0\d{2})[- ]?\d{8}|0\d{2}[- ]?\d{8}**这个表达式匹配3位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。你可以试试用分枝条件把这个表达式扩展成也支持4位区号的。

**\d{5}-\d{4}|\d{5}这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题:使用分枝条件时,要注意各个条件的顺序。如果你把它改成\d{5}|\d{5}-\d{4}**的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。

分组

正确的IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
理解这个表达式的关键是理解2[0-4]\d|25[0-5]|[01]?\d\d?,那么我们知道,一个正确的地址的格式要求,每个数字都不能大于255,所以我们可以知道这个表达式使用了分支条件:

  • 2[0-4]\d:这个是匹配以2开头,第二位是0~4,第三位任意,也就是指200~249
  • 25[0-5] :这个匹配250~255
  • [01]?\d\d?: 这个匹配0或者1开头,后面两位任意,也就是匹配了0~199

最后我们再回过头来看这个表达式**((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)**,对前面循环三次进行匹配0~255的数字,最后再匹配一次结束匹配。

反义

这部分写的也比较明确,直接照抄。
有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义

代码/语法 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

例子:

  • \S+匹配不包含空白符的字符串。
  • <a[^>]+>匹配用尖括号括起来的以a开头的字符串。

后向引用

使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。 这个就是说我们可以利用正则表达式默认分配好的组号来高效的复用。

这一段还需要在以后的实践中补充

零宽断言

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言

正向零宽断言

零宽度正预测先行断言

(?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找*I’m singing while you’re dancing.*时,它会匹配sing和danc。

零宽度正回顾后发断言

(?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找reading a book时,它匹配ading。

负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果我们想查找这样的单词–它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样:

\b\w*q[u]\w*\b匹配包含后面不是字母u的字母q的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像**Iraq,Benq**,这个表达式就会出错。这是因为[u]总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[u]将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\w*\b将会匹配下一个单词,于是\b\w*q[u]\w*\b就能匹配整个Iraq fighting。负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\b\w*q(?!u)\w*\b。

零宽度负预测先行断言

(?!exp),断言此位置的后面不能匹配表达式exp。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。

零宽度负回顾后发断言

我们可以用(?<!exp),零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp:(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。

一个更复杂的例子

一个更复杂的例子:(?<=<(\w+)>).*(?=</\1>)匹配不包含属性的简单HTML标签内里的内容。(?<=<(\w+)>)指定了这样的前缀:被尖括号括起来的单词(比如可能是<b>),然后是.*(任意的字符串),最后是一个后缀(?=</\1>)。注意后缀里的/,它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)匹配的内容,这样如果前缀实际上是<b>的话,后缀就是了。整个表达式匹配的是<b>和</b>之间的内容(再次提醒,不包括前缀和后缀本身)。

注释

小括号的另一种用途是通过语法(?#comment)来包含注释。例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)。

要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:

  (?<=    # 断言要匹配的文本的前缀
  <(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
  )       # 前缀结束
  .*      # 匹配任意文本
  (?=     # 断言要匹配的文本的后缀
  <\/\1>  # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
  )       # 后缀结束

贪婪与懒惰

贪婪与懒惰是两个匹配的模式,要在匹配的时候注意选择使用哪一个。 当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:*a.b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。

为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

代码/语法 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

平衡组/递归匹配

TBD.