很多人,包括我,最开始学习正则表达式的时候,可能或多或少都看过一些,自己当时觉得极其复杂的正则表达式。
对于那些正则表达式,都是觉得,要让自己写,真的是没有头绪,不知道如何下手。
这其中,有部分因素是,对于正则表达式本身,有些语法,还不是很熟悉。
也有些因素是,对于正则表达式,使用的,锻炼的,不是太多,所以也很难写出复杂的正则表达式。
但是,可能还有很多人是,都已经基本学会了正则表达式的语法了,也锻炼过很多了,但是还是对于相对复杂的正则表达式,很难写出来。没有头绪。
所以,便有了此教程的诞生。
因为我写此教程的目的就在于,希望之前对于复杂的正则表达式:
觉得摸不着头脑的,
觉得无从下手的,
不知道如何写的,
能够看完了教程后,有了基本的思路,能够一点点,从简单到复杂,从无到有的,写出满足自己需求的,复杂的正则表达式。
即,本教程目的:
实现在写复杂正则表达式方面的,授人以鱼。
说明:
1.当然,看本教程之前,肯定是那种,对本身的正则表达式,有了一定的了解了。
当然,如果真的不了解,也没关系,可以去看我之前写的各种教程:
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(组的名字)
而此处之前的某个组,就很容易理解了,就是对应的小数点,字符点’.’
所以,此处,也就需要给字符点’.’,去添加括号,变成一个分组:
(\.)?
其中,估计有人会问,为何不写成:
(\.?)
那是因为,如果写成(\.?),那么所表示的意思就是,对于\.?,其作为一个分组
而.\?表示匹配一个点,这个点,可以出现,也可以不出现,都是表示是匹配的;
换句话说,即使此处没有字符点出现,但是此处作为整个分组(\.?)来说,都还是匹配的,
这就导致,后续的,想要通过此处的是否出现小数点,来判断后面是否出现数字的话,就没法实现了。
所以,只能写做:
(\.)?,表示的意思是,对于字符点,是一个分组,这个分组整体的内容,是可以匹配到,或者没有匹配到的。
估计有人看着会晕,那么两者对比起来就是:
当字符点出现了,表达式的值为 | 当字符点没有出现,表达式的值为 | |
(.\) | True | False |
(\.?) | True | True |
这下,应该就很清楚了。
所以,此处小数点以及后半部分的选择性匹配,组合起来就是:
(\.)?(?(小数点对应的组的编号或名字)\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=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=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=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.调试完毕,最终完成,我们所要写的,复杂的正则表达式。