正则表达式学习

正则表达式(regular expression)

本文根据 菜鸟编程,以及《精通正则表达式》为基础学习,产生的笔记

1. 作用

  • 用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

2. 构成

  • 由普通字符(例如字符 a 到 z)以及特殊字符(称为"元字符")组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。

1. 普通字符

普通字符包括非元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

字符作用
abc匹配所有 ‘abc’ 的字符
[abc]匹配[...]中所有的单个字符,例:[ab] 匹配 ’acb’ 中的 a 和 c。
[^abc]匹配除了 […] 中字符的所有单个字符,
[A-Z][A-Z] 表示一个区间,匹配所有单个大写字母,[a-z] 表示所有单个小写字母。
.匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^\n\r]。
[\s\S]匹配所有单个字符。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行。
\w匹配字母、数字、下划线。等价于 [A-Za-z0-9_]

Tips:“[]”中除了不在首位的"-",其余的特殊符号都变为普通字符。

2. 非打印字符

字符作用
\cx匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\d匹配一个数字字符。等价于 [0-9]。
\D匹配一个非数字字符。等价于 [^0-9]。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\w匹配字母、数字、下划线。等价于’[A-Za-z0-9_]'。

3. 特殊字符

字符作用
$匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 \$。
^匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配 ^ 字符本身,请使用 \^
( )标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 \( 和 \)。
*匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*。
+匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 ?。
.匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \. 。
|将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。‘\n’ 匹配换行符。序列 ‘\’ 匹配 “”,而 ‘(’ 则匹配 “(”。
{ }标记限定符表达式的开始。
|指明两项之间的一个选择。

4. 限定符

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。

字符作用
*0 或多次
+1 或多次
?0 或 1 次
{n}连续匹配n次
{n,}至少连续匹配n次
{n,m}匹配n-m次

tip:*、+ 或 ? 本身是贪婪的,后置 ? 转变为非贪婪
例如 <.*?> 可匹配一个<>标签

5. 定位符

能够将正则表达式固定到行首或行尾。

字符作用
^匹配输入字符串开始的位置。
$匹配输入字符串结尾的位置。
\b匹配一个单词边界,即字与空格间的位置。
\B非单词边界匹配。

注意:

  1. ^ 表示定位符时,只能在正则表达式的开始,且 ^ 后不能紧跟*, +, ? 。(在中括号中 [^…] ,表示取反)
  2. $ 只能在正则表达式的结束。
  3. \bCha 匹配 Chapter 前3个字母,\b代表了单词边界,是单词和空格之间的位置;ter\b 匹配 Chapter 后3个字母。
  4. \Bapt匹配 Chapter 中间3个字母,但不匹配 aptitude 的 apt。

6. 选择符

用圆括号 ( ) 将所有选择项括起来,相邻的选择项之间用|分隔
( ) 表示捕获分组,( ) 会把每个分组里的匹配的值保存起来, 多个匹配值可以通过数字 n 来查看(n 是一个数字,表示第 n 个捕获组的内容)。
但用圆括号会有一个副作用,使相关的匹配会被缓存。此时可用 ?:来消除副作用。

字符作用
?:(?:X) 在正则中表示所匹配的子组 X 不作为结果输出,但是 X 被匹配
?=exp1(?=exp2):查找后面是 exp2 的 exp1,先查找 exp2 但不匹配,并把查找指针放置在 exp2 的开头,然后向前匹配 exp1。(?=exp2)exp1 则不行,因为是向前匹配。
?!exp1(?!exp2):查找后面不是 exp2 的 exp1。(?!a)表示指针移到不是 a 的前面的位置
?<=(?<=exp2)exp1:查找前面是 exp2 的 exp1。(?<=a) 表示指针移到 a 的后面,向后匹配。
?<!(?<!exp2)exp1:查找前面不是 exp2 的 exp1。(?<!a) 表示指针移到不是 a 的后面
例子(?=exp1)(?<=exp2):表示匹配前面是 exp1 后面是 exp2 的这个位置,并将指针移位。表达式前后括号内容互换,结果也不会变化。

这些符号不会占据匹配的位置
这些符号称为"环视",他们匹配的是匹配指针的位置,并不是最终匹配文本,因此可以进行多次匹配。例:
(\d)((\d\d\d)+\b)只能匹配一次,左边有一个数字,右边的数字个数是 3 的倍数的文本。可能是 1234 ,也可能是 1234567。
(\d)(?=(\d\d\d)+(?!\d))则可以匹配多个,原理是:正则匹配是迭代操作,一次迭代后,下一次迭代会从上一次匹配的终点开始,以 1234567890 为例。
上一种方法匹配完成后,匹配指针在 0 的后面。下一种方法第一次匹配位置是 1_234567890 。第二次匹配接着这个位置迭代,是 1234_567890 。以此类推。

all the words
解析:
 \ball\b\b \b\bthe\b\b \b\bwords\b 
 一个空格符号两边也有 \b。
(?:\bthe\b )\b\S*\b		the words
(?=\bthe\b )\b\S*\b		the
(?<=\bthe\b )\b\S*\b	words
\b\S*\b (?=\bthe\b )	all (此处有空格)
\b\S*\b (?<=\bthe\b )	the
\b\S*\b (?:\bthe\b )	all the (此处有空格)

悟:
1.(?=)括号内不会放入结果
2.\b\b中间可以无符号,会计入结果,所以使用 + 不用 * 
3.\b\S+\b(?= \bthe\b) 结果就不会有空格

对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 \n 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

7. 修饰符

标记也称为修饰符,正则表达式的标记用于指定额外的匹配策略。
标记不写在正则表达式里,标记位于表达式之外,例:/pattern/flags

字符含义作用
iignore - 不区分大小写将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
gglobal - 全局匹配查找所有的匹配项。
mmulti line - 多行匹配使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
s特殊字符圆点 . 中包含换行符 \n默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。

8. 优先级顺序

  1. \
  2. (), (?😃, (?=), []
  3. *, +, ?, {n}, {n,}, {n,m}
  4. ^, $, \任何元字符、任何字符
  5. |

匹配原理

规则1:有限最左端匹配

正则表达式会从文本开头,逐个进行匹配,并且优先显示最左边的匹配结果。
如:indicates bird dog cat使用cat进行匹配,则会先匹配并显示indicates,再匹配cat。若使用[dog|bird]匹配,则会先匹配bird。
因为正则表达式匹配时,会进行多次尝试,每一次尝试匹配时,会将正则表达式的所有可能都试一遍,所以,在匹配bird时,第一个dog不成功,试第二个bird,成功之后返回。
如果不能在字符串开始的位置找到匹配结果,匹配引擎会从字符串下一个位置开始尝试。从上面例子看,就是"i"不能匹配,会从"n"开始,再尝试一遍表达式。如果加了"^"表示匹配字符串开头,那么匹配引擎就不会再向后尝试匹配了。

规则2:标准量词是匹配优先的

标准量词就是由元字符代替的字符(如:“\s.*.+”)。这些量词是有匹配数量上下限的。但它们总是按最长长度匹配。
当正则表达式中出现常字符(如"abc[edf]“)时,标准量词就会酌情少匹配一些字符,以使常字符能够匹配。
例:regexes使用\b\w+s\b来匹配,”\w+"就会让出"s"来让常字符匹配。

标准量词之间:先来先服务

标准量词会在尽可能让出少的字符的情况下匹配。当多个标准量词按顺序相邻排列时,满足了后面的标准量词最基本的需求后,剩下的字符都是前面的标准量词匹配的对象了。
例:mar1899使用\b.*[0-9]+\b匹配,“.*“会匹配"mar189”,剩下一个"9"给”[0-9]+“满足基本需求"至少匹配一个数字”。
如果是"[0-9]*“那么整个字符串就全由”.*"匹配。

NFA 与 DFA

注:以下使用 regx 代表正则表达式,str 代表待匹配字符串。

1:NFA(非确定型有穷自动机)

例:regx = “abc[0-9]+”; str = “23abc900”;

NFA 是"表达式主导"的引擎:以 regx 为主体,来匹配文本。
1:从 regx 第一个字符开始,匹配 str 字符,若失败,则"str 匹配指针"向右移一位,然后继续与 regx 第一个字符匹配。
2:当匹配到 str 的"a"时,“regx 匹配指针"向右移到"b”。再从 str 的"b"开始匹配。也就是说,regx 扫描的次数,要小于 str 扫描的次数。
3:当遇到分支"|"时,NFA 会记录下 regx 的产生分支的地方,然后选择一个分支进行下去。若匹配失败,再返回上一个记录的分支。也就是说,越接近 regx 匹配指针的分支,越早被进行。

例:regx = “a(b|c)d(e|f)g”
那么匹配的时候,会在"_(b"产生标记,然后继续向后匹配,匹配到"(e|f)"又会在括号前产生标记,若后面匹配失败,则 “(e|f)” 处的标记先被重启扫描。(也称 “回溯”)
以上特性和堆栈类似,是 “后进先出” 的特性。

2:DFA(确定型又穷自动机)

例:regx = “abc(901|902|900)”; str = “23abc900”;

DFA 是"文本主导"的引擎:以 str 为主体,来匹配文本。它在扫描 str 时,会记录 “当前有效” 的所有匹配可能。
1:从 str 第一个字符开始,在 regx 中标记可能匹配的位置,当 “str 匹配指针” 移动到 “a” 前时,regx 中会在 “a” 前设置一个标记,表示在标记处可能存在匹配内容。
2:等 “str 匹配指针” 移动到 “9” 时,会在 “(_901|_902|_900)” 处设置标记。每向后匹配一位,标记都会向后移动。当
3:“str 匹配指针” 到第二个 0 时,标记就只剩下 900 前面的标记了,然后完成匹配。
4:在这个过程中,str 只扫描一次,regx 会扫描多次,直到匹配到,或字符串扫描完成还没有结果。

DFA 的好处就是匹配速度极快。

回溯的问题

1:从头回溯:整个 regx 匹配失败,从头回溯,从 str 下一个字符重新开始。
2:分支回溯:正在匹配的分支失败,从最后标记的分支开始继续匹配。

分支回溯的几个要点:
1:匹配的时候,在每个字符都要从 regx 开头匹配。
regx = “abc(901|902|900)”; str = “23abc900”;

匹配顺序:
1:str 从 "2" 开始,扫描 regx ,不匹配。
2:"str 匹配指针" 跳到 "\_3" ,从头扫描 regx ,不匹配。
3:"str 匹配指针" 跳到 "\_a" ,从头扫描 regx ,匹配;继续向右扫描,直到括号前,产生标记;选第一个分支,匹配不成功,然后跳回标记,走第二个分支;不成功,跳回标记,走第三个分支,成功。

2:若无法匹配成功,那么匹配引擎会将 str 的每个字符,使用 regx 匹配一遍。
regx = “abc”; str = “123”;

匹配顺序:
1:str 从 "1" 开始,扫描 regx ,不匹配。
2:"str 匹配指针" 跳到 "\_2" ,从头扫描 regx ,不匹配。
2:"str 匹配指针" 跳到 "\_3" ,从头扫描 regx ,不匹配。
4:返回结果:无匹配。

3:匹配优先量词,忽略优先量词
匹配优先量词即:.*+?
忽略优先量词即:*?,+?,也是我们经常说的非贪婪匹配。
机制:在面对这些匹配优先量词时,引擎会设置标记,然后判断是进行 “尝试匹配”,还是 “跳过尝试”。对于匹配优先量词,会进行尝试;对于忽略优先量词,会进行跳过,将 "regx 匹配指针"跳过这个量词。

不忽略优先的匹配:

例1:
regx = “ab?c”; str = “abc”;

匹配顺序:
1:匹配 "a";
2:决定是进行 "尝试匹配",还是 "跳过尝试"。因为 "?" 是优先匹配量词,所以 "尝试匹配" ,并在 regx "ab?_c" 做标记,在 str "a_bc" 做标记。
3:若 b? 未匹配成功,就会从标记处回溯匹配。
4:匹配 b 成功,匹配 c 。
5:匹配完毕,将标记放弃。

例2:
regx = “ab?c”; str = “ac”;

匹配顺序:
1:匹配 "a";
2:决定是进行 "尝试匹配",还是 "跳过尝试"。因为 "?" 是优先匹配量词,所以 "尝试匹配" ,并在 regx "ab?_c" 做标记,在 str "a_c" 做标记。
3:b? 匹配 c 未成功,从标记处回溯匹配。
4:匹配 c 成功。

例3:
regx = “ab?c”; str = “abx”;

匹配顺序:
1:匹配 "a";
2:决定是进行 "尝试匹配",还是 "跳过尝试"。因为 "?" 是优先匹配量词,所以 "尝试匹配" ,并在 regx "ab?_c" 做标记,在 str "a_bx" 做标记。
3:b? 匹配 b 成功,继续 c 匹配 x ,未成功,从标记处回溯匹配。
4:c 匹配 b 未成功,分支尝试完毕,regx 从头开始,str 从 b 开始再循环上述过程。
5:循环完也不能匹配,报告匹配失败。
忽略优先的匹配:

例1:
regx = “ab??c”; str = “abc”;

匹配顺序:
1:匹配 "a";
2:决定是进行 "尝试匹配",还是 "跳过尝试"。因为 "??" 是忽略优先匹配量词,所以 "跳过匹配" ,并在 regx "a_b??c" 做标记,在 str "a_bc" 做标记。
3:若 c 未匹配成功,就会从标记处回溯匹配。
4:c 匹配 b 未成功,回溯标记处,由 b?? 匹配 b ,匹配成功。
5:c 匹配 c 成功,返回结果。

例2:
regx = “[0-9]+”; str = “a-1234-dsf”;
“x*” 这种多字符匹配的优先匹配量词,可以等同于 “(x(x(x…?)?)?)”

匹配:
1:regx 可以匹配到 4,在这时,引擎保存了4个 + 能够回溯的状态状态,分别是:
a-1_234-dsf
a-12_34-dsf
a-123_4-dsf
a-1234_-dsf
2:若是 regx 为 [0-9]+4-d 则在 [0-9]+ 匹配 - 失败后,继续使用 4 匹配 -。
3:4 匹配 - 失败后,会进行 + 回溯,到 a-123_4-dsf 然后继续用 4 匹配。
4:4 匹配 4 成功,- 匹配 - 成功,d 匹配 d 成功。然后正则匹配完成,返回结果。

例3:
regx = <B>.*?</B>; str = <B>Bill</B> and <B>Zooo</B>;
如果不使用忽略优先匹配 ? 会将头和尾的 <B> 标签匹配。

匹配:
1:regx 匹配到 .* 的时候,会判断 "尝试" 或者 "暂时跳过" 使用了忽略优先 ? 则暂时跳过并标记。让后面的 </B> 进行匹配
2:</B> 匹配 B 失败,返回标记,由 .* 匹配 B,然后再重复 .*? 跳过并标记的步骤,由 </B> 匹配 i 失败,返回标记,由 .* 匹配 i,一直循环,直到 </B> 匹配成功
匹配优先和忽略优先都为全局匹配服务

含义:当不能完成匹配时,匹配优先和忽略优先,都会进行回溯,也就是交出一部分字符,来给匹配失败的部分再次尝试匹配。
例如:regx = “(.\d\d[1-9]?)\d+”; str = “1.625”
在 (.\d\d[1-9]?) 匹配 “.625” 之后,“\d+” 无法匹配,那么 “[1-9]?” 就会回溯到不匹配 “5” 交给 “\d+” 来匹配,以完成全局匹配。

这里要注意的是:无论是匹配优先还是忽略优先,只要全局匹配失败,他就必然尝试了所有可能。

可以看出,匹配优先和忽略优先是无法影响匹配结果的,只能改变匹配尝试的次数。

占有优先量词 和 固化分组

1:介绍:固化分组 和 占有优先量词 会在匹配完成 他们所修饰的那部分 regx 后,将 那部分 匹配时设置的标记全部清除。固化分组非常有用

2:例如:regx = “(.\d\d[1-9]?)\d+”; str = “1.625”
在 [1-9] 匹配到 5 的时候,会有一个标记产生,标记是:[1-9] 不去匹配 5 ,然后继续向右匹配。
如果对 [1-9] 使用了 固化分组,那么在 [1-9] 匹配完 5 之后,将标记删除,继续向右匹配。

3:使用:
固化分组:(?>…) ;比如 “(?>[1-9]?)”
占有优先量词:+ ;比如 “[1-9]?+”,“[1-9]*+”,“[1-9]++”。

4:使用场景:
在我们人能一眼看出无法匹配的地方比如 “^\w+:” 匹配 “subject” 少个冒号,但是正则引擎会回溯去尝试。这时,我们使用固化分组,可以加快匹配失败的速度,让引擎跳过这段尝试。

用肯定环视 (?=…) 模拟固化分组

有些引擎不支持固化分组,但大多数都支持环视。
(?>regex) ==> (?=(regex))\1
理解:当肯定环视匹配了 regex 代表的表达式后,环视会结束,str 匹配指针会放到匹配到的字符串之前,并且清空环视匹配时创建的回溯标记。然后加个 \1 让匹配从环视匹配到的字符串那里继续进行。

这部分需要仔细理解。

多选分支的顺序原则

(a|b|c|d) 这种多选分支结构,只会按照我们写的顺序进行尝试,当有一个分支全局匹配成功之后,剩下的分支都会放弃,不再尝试;若全局匹配未成功,会选下一个分支尝试,直到所有分支都试过,才会匹配失败。

正则的技巧

  1. 避免重新编译:表达式内最好不要有变量,那样会导致每次变量改变都需要重新编译
  2. 正则表达式数目不要大于可以缓存的条数
  3. 尽量使用(?:),不滥用()、[]
  4. 尽量使用起始锚点
  5. 从量词中提取必须元素,使用xx*,而不用x+
  6. 从多选分支中提取公共元素
  7. 锚点尽量独立,不在多选结构内
  8. 排除型字符组效率高于忽略优先:^[^8]*8高于^.*?8
  9. 先匹配固定字符串,再使用环视,可以大大减少环视的时间
  10. 如果一个长的正则表达式匹配速度太慢,可以分成若干个小的表达式,逐层筛选会更快
  11. 最可能匹配的多选分支放在最前面
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值