最新消息:20210816 当前crifan.com域名已被污染,为防止失联,请关注(页面右下角的)公众号

【教程】以Python中的re模块为例,手把手教你,如何从无到有,写出相对复杂的正则表达式

Python re crifan 3738浏览 0评论

很多人,包括我,最开始学习正则表达式的时候,可能或多或少都看过一些,自己当时觉得极其复杂的正则表达式。

对于那些正则表达式,都是觉得,要让自己写,真的是没有头绪,不知道如何下手。

这其中,有部分因素是,对于正则表达式本身,有些语法,还不是很熟悉。

也有些因素是,对于正则表达式,使用的,锻炼的,不是太多,所以也很难写出复杂的正则表达式。

但是,可能还有很多人是,都已经基本学会了正则表达式的语法了,也锻炼过很多了,但是还是对于相对复杂的正则表达式,很难写出来。没有头绪。

所以,便有了此教程的诞生。

因为我写此教程的目的就在于,希望之前对于复杂的正则表达式:

觉得摸不着头脑的,

觉得无从下手的,

不知道如何写的,

能够看完了教程后,有了基本的思路,能够一点点,从简单到复杂,从无到有的,写出满足自己需求的,复杂的正则表达式。

即,本教程目的:

实现在写复杂正则表达式方面的,授人以鱼。

 

 

说明:

1.当然,看本教程之前,肯定是那种,对本身的正则表达式,有了一定的了解了。

当然,如果真的不了解,也没关系,可以去看我之前写的各种教程:

正则表达式学习心得

Python的系列教程:

【教程】详解Python正则表达式

2.本教程,是以Python语言中用于处理正则表达式的的re模块为例,写的代码,用于演示。

但是并不影响对于正则表达式的逻辑和写法本身,从无到有的过程的演示。

即,如果你本身熟悉python,那么最好,更容易看懂本教程;

如果不熟悉python,那么也基本上不影响你学习此处介绍的逻辑,教你如何从无到有写出复杂的正则表达式的逻辑。

 

好的,现在开始了。


我们此处举例所用的正则表达式,是:

【教程】详解Python正则表达式之: (?(id/name)yes-pattern|no-pattern) 条件性匹配

中所最终写出来的:

^(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))$

此处,再重复一遍我们的需求:

类似于检测(最多两位小数的)数字的合法性:
所有的字符都是数字
如果有小数点,那么小数点后面最多2位数字

而再给出一些测试所用的例子,就更容易明白了:

testNumStrList = {
    # 合法的数字
    '12.34',
    '123.4',
    '1234',
    
    #非法的数字
    '1.234',
    '12.',
    '12.ab',
    '12.3a',
    '123abc',
    '123abc456',
}

 

下面,就来看看,这样的,相对算是有些复杂的正则表达式,是如何写出来的:

所以,我们很容易想到,对于输出的内容,都是数字,这个最基本的要求,对应的正则表达式是:

\d+

其中:

  • \d:指的是,数字
  • +:表示尽可能多的

-> 放在一起,\d+就表示,匹配尽可能多的数字。

这个,不难理解。

 

接着再来看需求中的描述是,

有可能有小数点,即字符’.’

也可能没有小数点。

 

对此,

先说小数点的写法:

字符点’.’,在正则表达时候中,本身是表示任意字符的意思,想要匹配点这个字符本身,需要加上反斜杠,所以应该写成:

\.

而对于此小数点,可以有,也可以没有,所以要后面加上一个问号。变成:

\.?

 

然后,再说整个这部分的逻辑如何实现:

如果你之前对于选择性匹配,有所了解,或者说听说过的话,那么就容易想到,此种需求,利用选择性匹配去实现,是最合适的。

当然,你也会发现,如果想要尝试用别的方式去实现,最终也还是很难,最终还是会返回来,选择通过选择性匹配,去实现对应的需求。

而选择性匹配的语法是:

(?(id/name)yes-pattern|no-pattern)

因此,也容易理解,我们所要实现的,也就是类似于:

(?(小数点对应的组的编号或名字)存在小数点的话,去匹配最多两个数字 | 不存在的话,就啥也不匹配 )

将此描述性的语言,再进一步转化为正则表达式,就是

(?(小数点对应的组的编号或名字)\d{1,2})

其中:

  • \d{1,2}:表示去匹配数字,最少一个,最多2个,也就是我们所希望的,最多有2个数字(但是如果有小数点,则必须至少有一个数字出现,那才合理,否则类似于 12. 这样的数字,也不合法)
  • 中间没有了竖杠’|’,是因为选择性匹配的语法中,就已经说明了,对于no-pattern部分,如果不需要,也是可以省略的。
  • (小数点对应的组的编号或名字):接下来会解释,如何将此部分,转化为真正的正则表达式

 

接下来,继续解释:

对于上面的“(小数点对应的组的编号或名字)”,根据选择性匹配的语法,是(id/name),即是之前某个分组的id(组的编号)或name(组的名字)

而此处之前的某个组,就很容易理解了,就是对应的小数点,字符点’.’

所以,此处,也就需要给字符点’.’,去添加括号,变成一个分组:

(\.)?

其中,估计有人会问,为何不写成:

(\.?)

那是因为,如果写成(\.?),那么所表示的意思就是,对于\.?,其作为一个分组

而.\?表示匹配一个点,这个点,可以出现,也可以不出现,都是表示是匹配的;

换句话说,即使此处没有字符点出现,但是此处作为整个分组(\.?)来说,都还是匹配的,

这就导致,后续的,想要通过此处的是否出现小数点,来判断后面是否出现数字的话,就没法实现了。

所以,只能写做:

(\.)?,表示的意思是,对于字符点,是一个分组,这个分组整体的内容,是可以匹配到,或者没有匹配到的。

估计有人看着会晕,那么两者对比起来就是:

 当字符点出现了,表达式的值为当字符点没有出现,表达式的值为
(.\)TrueFalse
(\.?)TrueTrue

这下,应该就很清楚了。

 

所以,此处小数点以及后半部分的选择性匹配,组合起来就是:

(\.)?(?(小数点对应的组的编号或名字)\d{1,2})

而此处的“点是否存在”,具体如何写,取决于自己是要写成组的编号id还是组的名字name

  • 想要写成组的编号id:则需要知道对应的(\.)所对应的组编号,此处由于暂时只有一个组,所以,此处(\.)的组编号是1,所以变成:(\.)(?(1) \d{1,2})
  • 想要写成组的名字name:此处前面的写法中,没有给小数点这个组命名。如果是写成命名的组的话,根据命名的组的语法,(?P<name>xxx),就是写成(?P<foundPoint>\.)了,这点也不难理解。对应的foundPoint,就是小数点这个组的名字了,然后再把foundPoint放到后面的那个选择性匹配的括号中,就变成了我们所需要的:(?P<foundPoint>\.)(?(foundPoint)\d{1,2})

到此,关于选择性匹配的写法,终于完成了。

但是总体的正则表达式,还是没有结束。

现在接着再把之前已经写好的:\d+,和上面的写法组合起来,就变成:

\d+(\.)?(?(1)\d{1,2})

\d+(?P<foundPoint>\.)?(?(foundPoint)\d{1,2})

 

此处:

(1)为了演示如何继续写出,不带命名的组的最终写法,所以接着再对

\d+(\.)?(?(1)\d{1,2})

去改进。

此处,为了使得整数部分本身更加容易识别,所以也被整数部分,写成一个,不带命名的组,所以变成:

(\d+)(\.)?(?(1)\d{1,2})

但是,需要注意的是,此时,由于整数部分添加了组了,则导致编号为1的分组,就是(\d+)而不是之前我们所写的(\.)了,

而(\.)这个分组的编号,对应的就变成了2了,所以,整个表达式,就应该改为:

(\d+)(\.)?(?(2)\d{1,2})

了。

 

(2)为了使得正则表达式,从形式上,更加容易看懂,所以,还是倾向于选择第二种,给组命了名的写法:

\d+(?P<foundPoint>\.)?(?(foundPoint)\d{1,2})

并且,再次为了使得正则表达式的含义更明确,也把整数部分,写成一个带命名的分组的形式,所以就变成了:

(?P<integerPart>\d+)(?P<foundPoint>\.)?(?(foundPoint)\d{1,2})

此处,很明显,可以看到给组命名的好处了,即使小数点部分的分组:

(?P<foundPoint>\.)

之前又添加了整数部分的分组:

(?P<integerPart>\d+)

但是,也不会造成上面的那种情况:由于分组编号变化,而要修改选择性匹配中,分组号码,从1变成2。

而且,由于添加了分组名,则更加一眼就看出,每一个部分的值,都是什么含义。每一部分的值,都包括哪些内容。

 

再进一步优化一下,为了逻辑上更加明确,以及后期测试,可以提取出小数点的部分的内容,再把小数点部分的,全部的内容,再变成一个命名的分组:

(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))

 

至此,才算基本写完整个我们所需要的正则表达式,但是,别高兴的太早,别以为就完工了。

因为,实际上,我们用这个表达式:

(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))

去测试的话,就会发现,所得到的结果,和我们所期望的不太一样。

即用如下代码:

import re;

testNumStrList = {
    #合法的数字
    '12.34',
    '123.4',
    '1234',
    
    #非法的数字
    '1.234',
    '12.',
    '12.ab',
    '12.3a',
    '123abc',
    '123abc456',
};

for eachNumStr in testNumStrList:
    foundValidNumStr = re.search("(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))", eachNumStr);

    if(foundValidNumStr):
        integerPart = foundValidNumStr.group("integerPart");
        decimalPart = foundValidNumStr.group("decimalPart");
        print "eachNumStr=%s\tis valid numebr ^_^, integerPart=%s, decimalPart=%s"%(eachNumStr, integerPart, decimalPart);
    else:
        print "eachNumStr=%s\tis invalid number !!!"%(eachNumStr);

所得到的结果却是:

eachNumStr=1234 is valid numebr ^_^, integerPart=1234, decimalPart=

eachNumStr=1.234        is valid numebr ^_^, integerPart=1, decimalPart=23

eachNumStr=123.4        is valid numebr ^_^, integerPart=123, decimalPart=4

eachNumStr=12.34        is valid numebr ^_^, integerPart=12, decimalPart=34

eachNumStr=12.  is valid numebr ^_^, integerPart=12, decimalPart=

eachNumStr=123abc456    is valid numebr ^_^, integerPart=123, decimalPart=

eachNumStr=12.ab        is valid numebr ^_^, integerPart=12, decimalPart=

eachNumStr=123abc       is valid numebr ^_^, integerPart=123, decimalPart=

eachNumStr=12.3a        is valid numebr ^_^, integerPart=12, decimalPart=3

很明显,其中误判了:

1.234

123abc456

12.ab

123abc

12.3a

然后,就是所谓的,去调试正则表达式,看看到底错在哪里了。

 

【调试正则表达式期间的分析和思考过程】

对于上述结果,拿1.234来分析的话,其实,仔细看看我们的表达式,也就能发现问题所在。

对于1.234来说,的确如程序输出的结果一样,的确是对于1来说,符合了整数部分,然后后面出现了小数点,然后接着再去匹配,最多有2个数字,即23。

但是很明显,我们此处的小数部分是234,是3位数,按道理来说,我都写了正则表达式,\d{1,2}了,只去匹配最多2位了,为何此处有3位数字,却还会出现匹配的结果呢?

对此,我开始也是很疑惑。

最后,才想起来,原来是,我们忽略了,去判断字符串的末尾,

即应该判断,从小数点往后,到整个字符串的末尾部分,最多只有2位数字,即:

\d{1,2}$

所以,上述正则表达式就应该改为:

(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))$

然后再去调试一下。

结果发现用对应的代码:

import re;

testNumStrList = {
    #合法的数字
    '12.34',
    '123.4',
    '1234',
    
    #非法的数字
    '1.234',
    '12.',
    '12.ab',
    '12.3a',
    '123abc',
    '123abc456',
};

for eachNumStr in testNumStrList:
    foundValidNumStr = re.search("(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))$", eachNumStr);

    if(foundValidNumStr):
        integerPart = foundValidNumStr.group("integerPart");
        decimalPart = foundValidNumStr.group("decimalPart");
        print "eachNumStr=%s\tis valid numebr ^_^, integerPart=%s, decimalPart=%s"%(eachNumStr, integerPart, decimalPart);
    else:
        print "eachNumStr=%s\tis invalid number !!!"%(eachNumStr);

测试出来的结果,还是有问题的:

eachNumStr=1234 is valid numebr ^_^, integerPart=1234, decimalPart=

eachNumStr=1.234        is valid numebr ^_^, integerPart=234, decimalPart=

eachNumStr=123.4        is valid numebr ^_^, integerPart=123, decimalPart=4

eachNumStr=12.34        is valid numebr ^_^, integerPart=12, decimalPart=34

eachNumStr=12.  is invalid number !!!

eachNumStr=123abc456    is valid numebr ^_^, integerPart=456, decimalPart=

eachNumStr=12.ab        is invalid number !!!

eachNumStr=123abc       is invalid number !!!

eachNumStr=12.3a        is invalid number !!!

很明显,其中还是误判了:

123abc456

123abc456当成了456了,所以,结果也认为是有效的数字了。

所以,此时就明白了,此处虽然去对于字符串末尾做了限定,但是却忘了对于字符串开始的位置做限定。

所以,再去限定一下,是从字符串最开始的位置,去检查,匹配到字符串的最末尾。

所以正则表达式就又改变为:

^(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))$

对应的代码为:

import re;

testNumStrList = {
    #合法的数字
    '12.34',
    '123.4',
    '1234',
    
    #非法的数字
    '1.234',
    '12.',
    '12.ab',
    '12.3a',
    '123abc',
    '123abc456',
};

for eachNumStr in testNumStrList:
    foundValidNumStr = re.search("^(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))$", eachNumStr);

    if(foundValidNumStr):
        integerPart = foundValidNumStr.group("integerPart");
        decimalPart = foundValidNumStr.group("decimalPart");
        print "eachNumStr=%s\tis valid numebr ^_^, integerPart=%s, decimalPart=%s"%(eachNumStr, integerPart, decimalPart);
    else:
        print "eachNumStr=%s\tis invalid number !!!"%(eachNumStr);

对应的输出结果为:

eachNumStr=1234 is valid numebr ^_^, integerPart=1234, decimalPart=

eachNumStr=1.234        is invalid number !!!

eachNumStr=123.4        is valid numebr ^_^, integerPart=123, decimalPart=4

eachNumStr=12.34        is valid numebr ^_^, integerPart=12, decimalPart=34

eachNumStr=12.  is invalid number !!!

eachNumStr=123abc456    is invalid number !!!

eachNumStr=12.ab        is invalid number !!!

eachNumStr=123abc       is invalid number !!!

eachNumStr=12.3a        is invalid number !!!

终于,得到了我们最终期望输出的结果了。

至此,才算最终完成我们的正则表达式:

^(?P<integerPart>\d+)(?P<foundPoint>\.)?(?P<decimalPart>(?(foundPoint)\d{1,2}))$

 

【总结:如何一步步写出相对复杂的正则表达式】

下面简单总结一下,对于从无到有,白手起家,是如何一步步写出复杂的正则表达式的:

1.明确需求

2.从最简单的写起

3.一点点,根据需求中的限定条件,去增加正则表达式的复杂性

4.最后,再去验证正则表达式,是否正确,是否是按照我们所期望的方式去工作了。

5.调试完毕,最终完成,我们所要写的,复杂的正则表达式。

转载请注明:在路上 » 【教程】以Python中的re模块为例,手把手教你,如何从无到有,写出相对复杂的正则表达式

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
89 queries in 0.176 seconds, using 22.12MB memory