正则表达式知识点梳理

    虽然看过《正则表达式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;

- email

$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和这个,如果有人知道怎么直接写,欢迎告诉我^_^


- 匹配含有转义引号的字符串

      "(\\.|[^\\"])*"

posted @ 2011-06-02 23:30  花花的肥羊  阅读(388)  评论(0编辑  收藏  举报