代码改变世界

正则学习笔记

2017-11-24 09:38  紫日残月  阅读(206)  评论(0编辑  收藏  举报

正则学习笔记

元字符

元字符可以先大概的分为两类, 一类用来匹配字符或者位置, 一类用来匹配重复次数.

字符或位置

字符

代码

说明

示例

\b

代表单词的开头或者结尾,也就是单词的分界处. 通常用来精确匹配单词的时候会用到.

如我们希望匹配'hi'这个单词, 那么可以用 \bhi\b来匹配.这样只会匹配一个单独的hi单词,其它的history/him/fadhifa等都不会匹配. 类似的, 可以使用\bhi 和 hi\b 来分别匹配以hi开头或者以hi结尾的字符

 

 

 

\d

匹配一个数字(0-9).

比如0\d\d-\d\d\d\d\d\d\d\d 用这个表示0开头,然后是任意两位数字, 然后是中划线-, 然后是8位数字.

.

代表除了换行符以外的任意字符

\bhi.oo\b 匹配以hi开头中间有一个任意字符,同时以oo结尾

\w

匹配正常字符, 即 字母/数字/下划线/汉字 注意此处w为小写 不包含特殊字符,比如#$&*?>)之类

\whi 匹配hi前面任意字符

\s

匹配任意空白字符

\shi\b 匹配任意以hi结尾,并且hi前面有空格的字符

^

匹配字符串开始

之前的都是字符/单词, 这个用来匹配字符串

$

匹配字符结尾

和^共同组成字符串的开头结尾|^hi\w$ 匹配以hi开头,然后是一位任意字符结尾的字符串

位置(限定符)

代码

说明

示例

*

重复零次或者任意次数

此重复包含零次, 比如可以用 a\w*来匹配a开头的任意为字符,包括字符a

+

重复一次或者更多次,是*的子集,即不包括零次

比如可以用 a\w+来匹配a开头的任意为字符,但是不包括字符a

?

重复零次或者一次

?和+ 一起共同组成*

{n}

重复n次数,必须是很精确的n次

hi{3} 匹配hiii这样的字符而不是 hihihi

{n,}

重复n次或者跟多次, 就是时候至少n次

fa{2} 会匹配faa 而不会匹配faaa faaaaa, 但是fa{n,} 会匹配这些

{n,m}

重复n-m次之间的次数,包括n和m

fa{2,3} 会匹配faa faaa 但不会匹配fa faaaa

字符类

上面我们已经有了一些对应字符集合的元字符来代表数字字母或者数字, 但是如果我们希望匹配那些没有预定义元字符的字符集合的时候应该字母办呢?

一个简单的方法就是把需要匹配的字符在方括号里列出来, 比如[?><]可以匹配 ?>< 在方括号中的都会陪匹配到.

比如说[0-9]代表的含义其实和 \d 是一样的, 都是一位数字;[a-z0-9A-Z_]也完全等同于\w, 不考虑汉字的情况,因为它包括大小写字母/数字和下划线.

(?0\d{2}[) -]?\d{8} 这个复杂的正则的解析如下: (? 表示零个或者一个(,然后是一个0, 然后是两个数字\d{2}. [) -]? 表示零个或者一个 )或者-或者空格 , 然后是八位数字. 那么以下都会匹配到 (012)12345678/(01212345678/(012-12345678/012)12345678/01212345678/012-12345678

分支条件

还是上面这个 (?0\d{2}[) -]?\d{8} 其实我们是想匹配三位区号+八位电话号码, 但是不希望出现012)12345678或者(012-12345678 这样的格式. 那怎么才能把这样的排除呢?

这里我们需要引入分支条件的概念,指的是一个正则有几种规则, 只要满足任意一个都可以完成匹配,具体方式就是把多个规则用|分隔开来.

0\d{2}[- ]\d{8}|[(]0\d{2}[)]-\d{8} 这样可以分拆成两部分, 一部分0\d{2}[- ]\d{8} 以0开头的三位数字, 中间是-或者空格, 然后是八位数字; 另一部分[(]0\d{2}[)[- ]\d{8} 圆括号包裹的三位数字, 中间是-或者空格, 然后是八位数字. 只要符合这样标准的都会被上面的正则匹配到

分组

我们之前已经知道怎么重复单个字符, 直接在字符或者元字符后面叫上* ? + 等限定符就好了, 那么如果我们希望重复多个字符怎么办?

可以用小括号(圆括号)来指定子表达式,也就是分组,然后就可以指定重复次数了. 比如 hi{3} 匹配hiii (hi){3} 匹配hihihi.

(\d{1,3}.){3}\d{1,3} 是一个初始的ip地址匹配. 首先是\d{1,3} 表示1-3位数字, 然后是 \d{1,3}. 一到三位数字叫一个点,比如256. 365. 256. 等. (\d{1,3}.){3} 表示重复三次 ,也就是 236.236.258.这样的字符, 然后是\d{1,3} 最后再跟上三位数字. 这样的正则也会匹配000.203.014.0 这样的地址

((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?).){3} 和(2[0-4]\d|25[0-5]|[01]?\d\d?) 第一个又可以拆分为 (2[0-4]\d|25[0-5]|[01]?\d\d?)和. 那么核心已经很明显了, 就是 (2[0-4]\d|25[0-5]|[01]?\d\d?) 又可以分为三个分支条件 2[0-4]\d 和25[0-5] 和[01]?\d\d? 第一个表示从200-249的数字, 第二个是250-255的数字, 第三个是从000-199的数字, 结合起来就是0-255的数字,也就是ip地址的范围. 然后前面三个加点. 最后一个没有点.就是我们的ip地址了

反义

上面已经提到过可以用\d来表示数字, \b表示字符开头或者结尾,\w表示数字字母或者汉字,那如果表示这些东西的反义怎么办?

那就是把他们[大写], 比如 \W 匹配任意不是数字字母下划线汉字的字符; \S 匹配任意不是空白符的字符 \D 匹配任意不是数字的字符 \B 匹配不是单词开始或者结尾的位置 [^x] 匹配x以外的任意字符 [^aeiou] 匹配除了aeiou这几个字符以外的所有字符

后向引用

这时候我们需要匹配一些重复的内容,这个时候可能就需要用后后向引用了, 顾名思义,就是前面定义的正则, 后面直接通过一种方式来引用. 当然也不是任意匹配都可以引用,只能是小括号里的表达式才能引用.具体表示如下:使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。 此处重点是:1.小括号的表达式才能被引用, 2. 匹配的是字表达式的文本,也就是内容,而不是继续匹配.

每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。1.分组0对应整个正则表达式,2.实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号3.你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权.

\b(\w+)\b\s+\1\b 可以用来匹配重复的单词, 比如像go go, 或者kitty kitty。这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b),这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\1)。

你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?\w+)(或者把尖括号换成'也行:(?'Word'\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容,你可以使用\k,所以上一个例子也可以写成这样:\b(?\w+)\b\s+\k\b

常用的分组

代码/语法

说明

示例

(exp)

匹配exp,并捕获文本到自动命名的组里

 

(?exp)

匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp)

 

?:exp)

匹配exp,不捕获匹配的文本,也不给此分组分配组号

 

(?=exp)

匹配exp前面的位置

 

(?<=exp)

匹配exp后面的位置

 

(?!exp)

匹配后面跟的不是exp的位置

 

(?<!exp)

匹配前面不是exp的位置

 

(?#comment)

这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读

 

零宽断言

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\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>),这个表达式最能表现零宽断言的真正用途。

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

贪婪和懒惰

当正则表达式中包含能够接受重复的限定符时,通常的行为是尽可能多的匹配字符, 以a.*b为例, 它会匹配以a开始,b结束的字符, 如果用来搜索aabab的话,它匹配整个字符,这叫做贪婪匹配.

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

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