正则表达式知识点梳理
虽然看过《正则表达式30分钟入门教程》,但总觉得还是不够系统。最近翻了翻《精通正则表达式》,这里梳理下知识点,用这种方式加深下学习效果。
[初学推荐的宿主工具/语言]
egrep/perl或者任何你熟悉的带有正则的语言
解释下为什么叫宿主,因为正则表达式已经成了公认的处理字符串的好方法,所以被语言设计者加入到各种编程语言中,当然这些语言设计者常常加入一些自己的想法,但正则基本的用法是相通的。
[基础语法]
. 单个任意字符
[...] 字符组,有时可以通过`-`少写点,比如[0-9]代表0-9共10个数字;[a-z]代表a到z共26个字母。在字符组内部,一些元字符被降为普通字符了,比如.就代表点号,而不是“单个任意字符了”。
[^...] 排除型字符组,就是字符组的反义了
^ 行的起始位置
$ 行的结束位置
\< 单词的起始位置(比如[ .,!;]等等)
\> 单词的结束位置
(...)限制和分割表达式的范围,还有个分组的作用
| 多选分支,选择结构,比如(a|b)代表a或b
* 重复0到多次
+ 重复1到多次
? 重复0或1次
{min, max} 至少重复min次,最多重复max次
\ 转义符号,有时会拿来把元字符降为平民字符,比如想表达`.`而不是`单个任意字符`,可以写`\.`(不包含`).有时会拿来表达些特殊意义,比如\<就包含了特殊意思,而不是字符`<`
[进阶语法]
\1 反向引用,记忆子表达式(用括号确定的)的匹配结果。\1, \2 分别代表第一,第二组括号匹配的文本。开括号`(`从左至右的出现的顺序决定组号(从1开始算)。在perl里面分组会被perl记住,存储在$1,$2里
(?:...) 非捕获型括号,当不想被反向引用记住的时候使用。
(?<name>...) 命名捕获,给分组起个名字,python和php中的写法是(?p<name>...)
(?=....) 肯定顺序环视,子表达式能够匹配右侧的(后面的)文本
(?<=...) 肯定逆序环视,子表达式能够匹配左侧的(前面的)文本
(?!...) 否定顺序环视,自表达式不能匹配右侧的(后面的)文本
(?<!...) 否定逆序环视,子表达式不能匹配左侧的(前面的)文本
(?if then|else) 条件判断,比如(?(?=NUM:)\d+|\w+),if后面的是`NUM:`,then用`\d+`,else用`\w+`.值得注意的是,当if为真时,\d+会去匹配`NUM:`后面的字符串。
其他: 固化分组(?>...),忽略优先量词 *?, +?, ??, {num,num}? 占有优先量词 *+, ++, ?+, {num,num}+
[常用字符组]
\b 单词分界线,包含\<和\>
\B \b的非
\d 数字,等价[0-9]
\s 匹配所有空白,等价[ \t]
\S 大写的s,\s的非
\w 单词中字符,等价[a-zA-Z0-9_],有些工具会特殊点,比如没有`_`
\W \w的非,等价[^\w]
[模式]
i 不区分大小写,这个并不包含在正则的那串定义中,一般会提出来。
x 宽松排列和注释模式
s 点号通配(单行模式)
m 增强的行锚点(多行模式)
不同宿主的表达方式可能会有些差异,有些功能可能会不支持,比如反向引用。或者会要求加各种\,比如分组的时候得写\(....\),使用的时候最好参考下相关的说明。
[一些例子]
- 给数字字符串从右到左每3位插入`,`号
$text = "The population of 2828128821 is growing"; $text =~ s/(\d)(?=(\d\d\d)+(?!\d))/$1,/g;
$hostname_regex = qr/[-a-z0-9]+(\.[-a-z0-9]+)*\.(com|edu|info)/i; $text =~ s{ \b ( \w[-.\w]* \@ $hostname_regex ) \b }{<a href="mailto:$1">$1</a>}gix;
-第1章开头题目:找出html相邻的重复单词
$/ = ".\n"; # chuck mode while (<>) { next if !s{ \b ([a-z]+) ((?:\s|<[^>]+>)+) (\1\b) } {\e[7m$1\e[m$2\e[7m$3\e[m}igx; # find and mark s/^(?:[^e]*\n)+//mg; # del all unmark line s/^/$ARGV: /mg; # add filename print; }
[引擎与回溯]
理解匹配过程能对正则表达式的优化有所帮助
NFA:非确定有穷自动机(一个状态对应多个分支)
DFA :确定有穷自动机(一个状态对应一个分支)详情见恐龙书
一般来说DFA的速度快,结果比较一致,NFA能够提供更多的功能,如反向引用。混合性引擎能拥有两者优点的(tcl, grep, awk)
eg.正则 a.*c 对 daabcdecf 的匹配
> 找到第一个a: d aabcdecf
> 找尽可能多的字符 d a abcdecf (因为.*是优先匹配的,或者说是贪婪的)
> 匹配c失败,需要吐出一些字符,吐出f,不行,吐出c,ok。得到 d a abcde c f
当然这是比较传统的过程,现代正则引擎有时会进行一些优化措施。
这里涉及到两个回溯要点,P159:
1.如果需要在`进行尝试`和`跳过尝试`之间选择,对匹配优先量词,会选择`进行尝试`,对忽略优先量词,会选择`跳过尝试`。
2.当失败需要强制回溯时,使用的LIFO(last in frist out,后进先出,栈)。
可以想想把a.*c换成a.*c$会是个怎样的过程。
eg.截取float型小数点后3位,比如1.234->"1.234"; 1.23023 -> "1.23",不考虑四舍五入,那不是正则该干的,下面有两个正则:
$price =~ s/(\.\d\d[1-9]?)\d*/$1;
$price =~ s/(\.\d\d[1-9]?\d+/$1;
来测试下:
- 1.234000001: 第一个匹配成功,$1="1.234"; 第二个匹配成功,$1=1.234
- 1.23:第一个匹配成功,$1="1.23"; 第二个匹配失败,不用替换,目标也不需要替换
- 1.234: 第一个匹配成功,$1="1.234"; 第二个匹配成功,$1="1.23",如果使用?,?+,??都会被强制回溯,??则一开始就不会匹配4,看来优化不是那么简单的事。
固化分组(?>...)可以做到不吐出已匹配的字符组,这样$price =~ s/(\.\d\d(?>[1-9]?)\d+/$1;
多选结构既不是匹配优先,也不是忽略优先,而是按顺序排列的,至少对传统型NFA是这样的(P175)
[更多的例子]
- 在vim中去除代码文件行尾多余的空格和tab:
%s/[ \t]\+$//g (vim中+得转义后才能生效,用set list可以看到换行符和制表符)
- IP,请考虑1234.123.2.3, 999.999.999.999, :
m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ 这样考虑了位数,但没考虑数值范围
关键是如何匹配0-255呢,需要规划下,这个看个人喜好:
0-199:([01]?\d\d)
200-249: (2[0-4]\d)
250-255: (25[0-5])
合并起来就是([01]?\d\d)|(2[0-4]\d)|(25[0-5]),然后这样写4次- -!
`^(([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)|(2[0-4]\d)|(25[0-5]))$`
可惜的是还是会匹配0.0.0.0
- 不以.py,.c,.h结尾的文件名,请考虑abc, abc..c:
m/.*(?!\.(py|c|h)$)/ 将与.*差不多
m/.*\.(?!(py|c|h)$)/ 这样能匹配大部分情况,不过无法匹配 abc
建议遇到比较复杂的情况,借助宿主语言,而不是全部依赖正则,如果比如IP和这个,如果有人知道怎么直接写,欢迎告诉我^_^
- 匹配含有转义引号的字符串
"(\\.|[^\\"])*"