如果你在课后有勤加练习,那么你对于字符串的查找应该是已经深恶痛绝了,你发现下载一个网页是很容易的,但是要在网页中查找到你需要的内容,那就是困难的,你发现字符串查找并没有你想象的那么简单,并不是说直接使用 find 方法找到匹配字符串的位置就可以了。
我们来举个例子,学习了前面几节课你应该已经尝试过写一个脚本来自动获取最新的代理 ip 地址,但是呢,你肯定会遇到困难,我现在来重现一下大家会遇到的困难。
大家肯定会先踩点,在 https://www.xicidaili.com/wt 网点审查元素,找一下代理 ip 前后有什么标签,例如:
ip 为 61.135.217.7 前后的标签为 td ,但是呢,别的地方也会有 td,但是里面包括的不是 ip 地址,你可能会费了九牛二虎之力,先找 table,再找 tbody,再找td,终于找到了 ip 地址的唯一特性,找到了一个 ip 地址,但是这样写不仅麻烦,而且不具有通用性,你在这个网站可行,在另外一个网站就不可行了,而且,万一站长哪天心血来潮,改了一下网页,那你更是心塞啊。
这时候你就会琢磨,可不可以按照我们需要的内容特征来进行自动查找呢?也就是说,如果我要找一个 ip 地址,那 ip 地址的特征是什么呢?
就是由 4 段数字组成,每段数字的范围是 0-255,分别是由3个点号隔开,这就是 ip 地址的特征嘛。根据这个特征,它去网页里面查找。
很抱歉,字符串所附带的方法你无法做到。
但是呢,我们遇到的问题,计算机老前辈们也早就已经想到了,并且已经帮我们设计出了非常优秀的解决方案,就是我们今天要讲的 正则表达式。
今天,我们将来学习使用 正则表达式来匹配 ip 地址。
关于正则表达式有一个非常经典的美式笑话:
有些人面临一个问题的时候会想:"我知道,可以使用正则表达式来解决这个问题。"于是,现在他就有两个问题了。
没错,正则表达式的确是很难学,但却非常有用。在编写字符串网页或程序的时候,经常会有查找某些符合复杂规则字符串的需求,例如,我们需要查找的 ip 地址的规则。那么,使用 Pythob 自带的字符串方法,你一定会恼羞成怒,那么这时候,如果你懂得正则表达式,你会发现,这真是灵丹妙药啊。
因为 正则表达式就是为了描述这些复杂规则的工具。正则表达式本身就是用于描述这些规则的。不同的语言均有使用正则表达式的方法,但各不相同,Python 是使用 re 模块来实现的,因为这一部分比较难,所以我们边举例子边讲解。
-
>>> import re
-
>>> re.search(r'Python', 'I love Python')
-
<_sre.SRE_Match object; span=(7, 13), match='Python'>
-
>>> re.search(r'Python', 'I love Python').span()
-
(7, 13)
re.search方法
re.search 扫描整个字符串并返回正则表达式模式第一次成功匹配的位置。
++函数语法:++
++re.search(pattern, string, flags=0)++
函数参数说明:
参数 描述 pattern 匹配的正则表达式 string 要匹配的字符串。 flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。 匹配成功re.search方法返回一个匹配的对象,否则返回None。
有人就会认为,你说了这么多,和 find() 方法也没有什么区别啊,使用 find() 方法也可以实现上面的功能啊。如下:
-
>>> ("I love Python".find('Python'), "I love Python".find('Python') + len('Python'))
-
(7, 13)
那我们来一个 find() 方法没办法实现的:
大家都知道 通配符 (就是我们实际中经常使用的 * 和 ?这一类可以表示任何字符的符号),例如我们想找到 word 类型的文件的时候,我们就会搜索 *.docx。
正则表达式也有所谓的通配符,这里是使用点号(.),它可以匹配除了 换行符 以外的任何字符。
-
>>> re.search(r'.', 'I love Python')
-
<_sre.SRE_Match object; span=(0, 1), match='I'>
这里它就匹配到了字符串中的 'I'。
-
>>> re.search(r'Pytho.', 'I love Python')
-
<_sre.SRE_Match object; span=(7, 13), match='Python'>
这里 点号(.) 就匹配到了'c',然后正则表达式就匹配到了 'Python'。
看了上面的例子,会思考的小伙伴们就有了一个问题了,既然 点号(. )可以匹配任何字符,那我如果想要匹配 点号(.)字符本身,那你要怎么办?
正如 Python 的字符串规则一样,想要消除一个字符串的特殊功能,就在前面加上 反斜杠(\),如:
-
>>> re.search(r'\.', 'I love Python.com')
-
<_sre.SRE_Match object; span=(13, 14), match='.'>
这里 '\.' 匹配的就是 点号(.)本身了,这时候,点号不代表任何其他字符,它只代表点号,前面的反斜杠已经将其解译了。
也就是说,在正则表达式中,反斜杠同样具有剥夺元字符的特殊功能的能力,什么是元字符,就是这个字符它本身代表着其他含义、有特殊功能的字符,例如 点号(.)。
同样呢,反斜杠还可以使得普通的字符具有特殊能力,例如 我们想要匹配数字,我们可以使用 '\d' 来匹配任何数字,如:
-
>>> re.search(r'\d', 'I love Python35.com')
-
<_sre.SRE_Match object; span=(13, 14), match='3'>
-
>>> re.search(r'\d\d', 'I love Python35.com')
-
<_sre.SRE_Match object; span=(13, 15), match='35'>
OK,我们结合以上两点知识,就可以匹配一个 ip 地址大概会这么写:
-
>>> re.search(r'\d\d\d\.\d\d\d\.\d\d\d\.\d\d\d', '192.168.111.123')
-
<_sre.SRE_Match object; span=(0, 15), match='192.168.111.123'>
大家会看到,匹配成功了,但是我们这么写是有问题的。首先,\d 表示匹配的数字是 0~9,但是 ip 地址的约定范围每组数字的范围是 0~255,那你这里 \d\d\d 最大匹配数字是 999 ,而 ip 地址的最大范围是 255;然后,你这里要求 ip 地址每组必须是三位数字,但实际上有些 ip 地址中的某组数字只有 1 位或者 2 位,像这种情况,我们就匹配不了了。
那我们怎么解决呢?
为了表示一个字符串的范围,我们可以创建一个叫做 字符类 的东西,使用中括号[] 来创建一个字符类,字符类的含义就是你只要匹配字符类中的一个字符,那么就算匹配,举例:
我们想要匹配 元音字母(aeiou),我们就可以这样写:
-
>>> re.search(r'[aeiou]', 'I love Python')
-
<_sre.SRE_Match object; span=(3, 4), match='o'>
我们匹配到了 'o',那大家可能会有疑惑了,为什么没有匹配大写字母 'I' 呢,这是因为 正则表达式 是默认开启 大小字母敏感 模式的,所以 大写 'I' 和小写 'i' 会区分开来。解决的方案有两种,一种是关闭大小写敏感模式(后边进行讲解),另一种是修改我们的字符类[aeiou]为[aeiouAEIOU]。
你还可以在字符类中使用 横杆 '-' 表示一个范围,例如:
-
>>> re.search(r'[a-z]', 'I love Python')
-
<_sre.SRE_Match object; span=(2, 3), match='l'>
-
>>> re.search(r'[0-9]', 'I love 123 Python')
-
<_sre.SRE_Match object; span=(7, 8), match='1'>
-
>>> re.search(r'[2-9]', 'I love 123 Python')
-
<_sre.SRE_Match object; span=(8, 9), match='2'>
数字范围的问题我们解决了,那接下来我们处理的第二个问题就是匹配个数的问题:
限定重复匹配的次数,我们可以使用 大括号 来解决,举例:
-
>>> re.search(r'ab{3}c', 'abbbc')
-
<_sre.SRE_Match object; span=(0, 5), match='abbbc'>
因为这个大括号里面的数字表示前面要匹配的字符要重复多少次。上面的 {3} 就表示前面的 b 匹配时要重复3次,即匹配 abbbc。
-
>>> re.search(r'ab{3}c', 'abbbbc') #这样子就匹配不了,返回 None
-
>>>
大括号里还可以给出重复匹配次数的范围,例如{a, b} 表示匹配 a 到 b 次。
-
>>> re.search(r'ab{3,10}c', 'abbbbbbbc')
-
<_sre.SRE_Match object; span=(0, 9), match='abbbbbbbc'>
接下来我们来考虑一下,如何使用正则表达式来匹配 0~255 ?
有些人想都不用想,就写给你看:
-
>>> re.search(r'[0-255]', '188')
-
<_sre.SRE_Match object; span=(0, 1), match='1'>
本来我们想匹配 188 ,结果只是匹配到了 1。
还有的同学会这样写:
-
>>> re.search(r'[0-2][0-5][0-5]', '188')
-
>>>
结果匹配不到。
跟你想象的不一样啊,要记住,正则表达式 匹配的是字符串,所以呢,数字对于字符来说,只有 0~9,例如 188,就是由1、8、8 这三个字符来组成的,并没有说 百十千 这些单位。所以呢,[0-255] 这个字符类(其中0-2,就是 0 1 2,后面两个 5 重复了)表示的是[0125]这四个数字中的某一个,所以 re.search(r'[0-255]', '188') 就只匹配到了一个 1。
所以呢,要匹配 0~255 这个范围里的数字,我们使用正则表达式应该这么写:
-
>>> re.search('[01]\d\d|2[0-4]\d|25[0-5]', '188')
-
<_sre.SRE_Match object; span=(0, 3), match='188'>
这里写的匹配的正则表达式就是 [01]\d\d 或者 2[0-4]\d 或者 25[0-5],这其中任何一个成立都是 ok 的。这里的 "或 " 和 C语言中的 "或" 是一样的。
但是上面的写法还是存在问题,要求匹配的数字必须是 3 位的,例如:
-
>>> re.search('[01]\d\d|2[0-4]\d|25[0-5]', '8')
-
>>>
-
>>> re.search('[01]\d\d|2[0-4]\d|25[0-5]', '18')
-
>>>
可以像下面这样改写,让前面的两位可以重复 0 次(因为默认是重复1次嘛):
-
>>> re.search('[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '8')
-
<_sre.SRE_Match object; span=(0, 1), match='8'>
-
>>> re.search('[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '80')
-
<_sre.SRE_Match object; span=(0, 2), match='80'>
-
>>> re.search('[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '118')
-
<_sre.SRE_Match object; span=(0, 3), match='118'>
按照这样的话,我们就可以来匹配一个 ip 地址啦:
-
>>> re.search('(([01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5])\.){3}[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '192.168.42.1')
-
<_sre.SRE_Match object; span=(0, 12), match='192.168.42.1'>
上面的小括号的意思就是分组,首先 ([01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]) 作为一个组,然后加上 点号,(([01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5])\.) 作为新的组,然后这个组重复3次,然后再加一组数字。这样就完成了对 ip 地址的匹配啦。
现在应该可以充分理解,"当你发现一个问题可以使用正则表达式来解决的时候,于是你就会有两个问题。"这句话的含义了。
但是大家也充分理解到掌握正则表达式的重要性,因为我们这里主要是讲 python爬虫,所以并没有花太多时间来讲正则表达式的隐藏技能,大家可以看一下Python正则表达式更深层次的知识:-> Python3 如何优雅地使用正则表达式