正则表达式
正则表达式
Tips:本章内容量很多,因为正则表达式的功能十分强大,笔者曾经在一本《自制编译器》中看到作者使用正则表达式进行语法分析,学好了正则表达式,绝对是一把趁手的尖刀利器。
一、正则表达式的简介
正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符")。
正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。
正则表达式的功能非常强大。
二、内容介绍
编写爬虫的步骤
第1步,抓取Web资源
第2步,对Web资源进行分析
1、正则表达式
2、用match方法匹配字符串
3、用search方法搜索满足条件的字符串
4、用findall方法和finditor方法查找字符串
5、用sub方法和subn方法搜索和替换
6、split方法分割字符串
7、常用的正则表达式表示法
8、实战案例,分别使用urllib,urllib3和requests抓取不同的Web资源
三、干货
7.1 match_group
- 正则表达式中的match方法有两个参数,第一个参数表示文本模式,第二个参数表示待匹配的字符串。
- 如果匹配成功,match方法返回SRE_Match方法,然后可以调用该对象中的group方法获取匹配成功的字符串,
- 如果文本模式就是一个普通的字符串,那么group方法按返回的就是文本模式字符串本身
import re # 导入re模块
m = re.match('hello','hello') # 进行文本匹配,匹配成功
if m is not None:
print(m.group()) # 运行结果:hellow
print(m.__class__.__name__) # 输出m的类名,运行结果:SRE_Match
m = re.match('hellow','world')
if m is not None:
print(m.group()) # 运行结果:None
print(m.__class__.__name__)
m = re.match('hello','hello, world.')
if m is not None:
print(m.group()) # 运行结果:hello
# <re.Match object; span=(0, 5), match='hello'>
print(m)
7.2 match和select方法
使用match方法和search方法对文本模式进行匹配和搜索,并对这两个方法做一个对比
import re
# 进行文本模式匹配,匹配失败,match方法返回None
m = re.match('python','I love python')
if m is not None:
print(m.group())
# 匹配失败,要完全相等或者在开头
print(m)
# 进行文本模式搜索,搜索成功
m = re.search('python','I love python')
if m is not None:
print(m.group()) # 匹配成功
# <re.Match object; span=(7, 13), match='python'>
print(m)
# .*代表匹配任意字符,后面会讲到正则表达式的规则
m = re.search('.*','/177913/37117198/')
if m is not None:
print(m.group()) # 匹配成功
7.3 SelectOne择一匹配符号"|"
使用带择一匹配符号的文本模式字符串,并通过match方法和search方法分别匹配和搜索指定字符串
import re
s = 'Bill|Mike|John'
# 指定使用择一匹配符号的文本模式字符串
# 满足其中之一则匹配成功
m = re.match(s, 'Bill') # 匹配成功
if m is not None:
print(m.group()) # 运行结果Bill
m = re.match(s, "Bill is my friend") # 匹配成功
if m is not None:
print(m.group())
m = re.match(s, "John is my friend")
if m is not None:
print(m.group())
# <re.Match object; span=(0, 4), match='John'>
print(m)
7.4 MatchAny
正则表达式匹配规则:
.字符,可以匹配1个任意字符
\.转义字符,可以匹配到真正的.字符
例子主要针对match、select、.字符和.\字符的使用
import re
s = '.ind' # 使用了点(.)符号的文本模式字符串
m = re.match(s, 'bind') # 匹配成功
if m is not None:
print(m.group()) # 运行结果:bind
m = re.match(s,'binding')
# 运行结果:<<_sre.SRE_Match object; span=(0, 4), match='bind'>
print("<" + str(m))
m = re.match(s,'bin') # 匹配失败
print(m) # 运行结果:None
m = re.search(s,'<bind>') # 搜索成功
print(m.group()) # 运行结果:bind
# 运行结果:<_sre.SRE_Match object; span=(1, 5), match='bind'>
print(m)
s1 = '3.14' # 使用了点(.)符号的文本模式字符串
s2 = '3\.14' # 使用了转义符将点(.)变成真正的点字符
m = re.match(s1, '3.14') # 匹配成功,因为点字符同样也是一个字符
# 运行结果:<_sre.SRE_Match object; span=(0, 4), match='3.14'>
print(m)
m = re.match(s1, '3314') # 匹配成功,3和14之间可以是任意字符
# 运行结果:<_sre.SRE_Match object; span=(0, 4), match='3314'>
print(m)
m = re.match(s2, '3.14') # 匹配成功
# 运行结果:<_sre.SRE_Match object; span=(0, 4), match='3.14'>
print(m)
m = re.match(s2, '3314') # 匹配失败,因为中间的3并不是点(.)字符
print(m) # 运行结果:None
7.5 CharSet字符集
一个字符集【】内只能匹配一个字符,等同于多个 | (择一匹配符)
import re
# 使用字符集,匹配成功
m = re.match('[ab][cd][ef][gh]','adfh')
# 运行结果:adfh
print(m.group())
# 使用字符集,匹配成功
m = re.match('[ab][cd][ef][gh]','bceg')
# 运行结果:bceg
print(m.group())
# 使用字符集,匹配不成功,因为a和b是或的关系
m = re.match('[ab][cd][ef][gh]','abceg')
# 运行结果:None
print(m)
# 字符集和普通文本模式字符串混合使用,匹配成功,ab相当于前缀
m = re.match('ab[cd][ef][gh]','abceh')
# 运行结果:abceh
print(m.group())
# <re.Match object; span=(0, 5), match='abceh'>
print(m)
# 使用择一匹配符,匹配成功,abcd和efgh是或的关系,只要满足一个即可
m = re.match('abcd|efgh','efgh')
# 运行结果:efgh
print(m.group())
# <re.Match object; span=(0, 4), match='efgh'>
print(m)
7.6 SpecificSymbol特殊匹配
通过模式字符串中使用* + ?符号以及特殊字符\w \d
*:0-n个字符
+:1-n个字符
?:前缀或者后缀
\w:一个字母或者一个数字
\d:任意一个数字
import re
# 匹配'a''b''c'三字母按顺序从左到右排列,而且这3个字母都必须至少有一个
# abc aabc abbbcc都可以匹配成功
s = 'a+b+c+'
strList = ['abc','aabc','bbabc','aabbbcccxyz']
# 只有'bbabc'无法匹配成功,因为开头没有'a'
for value in strList:
m = re.match(s, value)
if m is not None:
print(m.group())
else:
print('{}不匹配{}'.format(value, s))
print('''---------------------''')
# 匹配任意3个数字-任意3个小写字母
# 123-abc 433-xyz都可以成功
# 下面采用了两种设置模式字符串的方式
# [a-z]是设置字母之间或关系的简化形式,表示a到z的26个字母可以选择任意一个,相当于“a|b|c|…|z”
# s = '\d\d\d-[a-z][a-z][a-z]'
# {3}表示让前面修饰的特殊字符“\d”重复3次,相当于“\d\d\d”
s = '\d{3}-[a-z]{3}'
strList = ['123-abc','432-xyz','1234-xyz','1-xyzabc','543-xyz^%ab']
# '1234-xyz'和'1-xyzabc'匹配失败
for value in strList:
m = re.match(s, value)
if m is not None:
print(m.group())
else:
print('{}不匹配{}'.format(value, s))
print('''---------------------''')
# 匹配以a到z的26个字母中的任意一个作为前缀(也可以没有这个前缀),后面是至少1个数字
s = '[a-z]?\d+'
strList = ['1234','a123','ab432','b234abc']
# 'ab432'匹配失败,因为前缀是两个字母
for value in strList:
m = re.match(s, value)
if m is not None:
print(m.group())
else:
print('{}不匹配{}'.format(value,s))
print('''---------------------''')
# 匹配一个email
email = '\w+@(\w+\.)*\w+\.com'
emailList =['abc@126.com','test@mail.geekori.com','test-abc@geekori.com','abc@geekori.com.cn']
# 'test-abc@geekori.com'匹配失败,因为“test”和“abc”之间有连字符(-)
for value in emailList:
m = re.match(email,value)
if m is not None:
print(m.group())
else:
print('{}不匹配{}'.format(value,email))
strValue = '我的email是lining@geekori.com,请发邮件到这个邮箱'
# 搜索文本中的email,由于“\w”对中文也匹配,所以下面对email模式字符串进行改进
m = re.search(email, strValue)
print(m)
# 规定“@”前面的部分必须是至少1个字母(大写或小写)和数字,不能是其他字符
email = '[a-zA-Z0-9]+@(\w+\.)*\w+\.com'
m = re.search(email, strValue)
print(m)
7.7 group分组匹配
用括号()表示一组匹配,案例如下:
import re
# 分成3组:(\d{3})(\d{4})([a-z]{2})
m = re.match('(\d{3})-(\d{4})-([a-z]){2}','123-4567-xy')
if m is not None:
print(m.group()) # 123-4567-xy
print(m.group(1)) # 123
print(m.group(2)) # 4567
print(m.group(3)) # y
print(m.group()) # 123-4567-xy
print('---------------------------')
# 分成2组:(\d{3}-\d{4})和([a-z]{2})
m = re.match('(\d{3}-\d{4})-([a-z]{2})', '123-4567-xy')
if m is not None:
print(m.group()) # 运行结果:123-4567-xy
print(m.group(1)) # 获取第1组的值,运行结果:123-4567
print(m.group(2)) # 获取第2组的值,运行结果:xy
print(m.groups()) # 获取每组的值组成的元组,运行结果:('123-4567', 'xy')
print('-----------------')
# 分了1组:([a-z]{2})
m = re.match('\d{3}-\d{4}-([a-z]{2})', '123-4567-xy')
if m is not None:
print(m.group()) # 运行结果:123-4567-xy
print(m.group(1)) # 获取第1组的值,运行结果:xy
print(m.groups()) # 获取每组的值组成的元组,运行结果:('xy',)
print('-----------------')
# 未分组,因为模式字符串中没有圆括号括起来的部分
m = re.match('\d{3}-\d{4}-[a-z]{2}', '123-4567-xy')
if m is not None:
print(m.group()) # 运行结果:123-4567-xy
print(m.groups()) # 获取每组的值组成的元组,运行结果:()
7.8 start和end-匹配单词的开始和结束
规则:
^用于表示匹配字符串的开始
$用于表示匹配字符串的结束
\b用于表示单词的边界
```
```python
import re
# 匹配成功
m = re.search('^The','The end.')
print(m)
if m is not None:
print(m.group())
# The在匹配字符串的最后,不匹配
m = re.search('^The','end. The')
print(m)
if m is not None:
print(m.group())
m = re.search('The$','The end.')
print(m)
if m is not None:
print(m.group())
# this的左侧必须有边界,成功匹配,this左侧是空格
m = re.search(r'\bthis',"What's this?")
print(m)
if m is not None:
print(m.group())
# 不匹配,因为this左侧是“s”,没有边界
# 字符串前面的r表示该字符中的特殊字符(如“\b”)不进行转义
m = re.search(r'\bthis',"What'sthis?")
print(m)
if m is not None:
print(m.group())
# this的左右要求都有边界,匹配成功,因为this左侧是空格,右侧是符号?
m = re.search(r'\bthis\b',"What's this?")
print(m)
if m is not None:
print(m.group())
# 不匹配,因为this右侧是a,a也是单词,不是边界
m = re.search(r'\bthis\b',"What's thisa")
print(m)
if m is not None:
print(m.group())
7.9 findall函数
findall函数用于查询字符串中某个正则表达式模式全部的非重复出现
finditem与findall类似,前者更灵活,后者更节省内存资源
共同的第三个参数re.I: 大小写不敏感
.*:贪婪匹配,尽可能长,在findall里可能只获得一个
.*?:非贪婪,尽可能短,findall里可以获得多组数据
小案例如下:
import re
# 待匹配的字符串
s = '12.数据库存储-a-abc54-a-xyz---78-A-ytr'
# 匹配以2个数字开头,结尾是3个小写字母,中间用“-a”分隔的字符串,对大小写敏感
# 下面的代码都使用了同样的模式字符串
result = re.findall(r'\d\d-a-[a-z]{3}',s)
# ['12.数据库存储-a-abc', '54-a-xyz']
print(result)
# 将模式字符串加了两个分组(用圆括号括起来的部分),findall方法也会以分组形势返回
result = re.findall(r'(\d\d-a-[a-z]{3})',s)
# ['12.数据库存储-a-abc', '54-a-xyz']
print(result)
# 忽略大小写(最后一个参数值,re.I)
result = re.findall(r'\d\d-a-[a-z]{3}',s,re.I)
# ['12.数据库存储-a-abc', '54-a-xyz', '78-A-ytr']
print(result)
# 忽略大小写,并且加了2个分组
result = re.findall(r'(\d\d-a-[a-z]{3})',s,re.I)
# ['12.数据库存储-a-abc', '54-a-xyz', '78-A-ytr']
print(result)
# 使用finditer函数匹配模式字符串,并返回匹配迭代器
# finditer返回字符串中匹配成功的迭代器,对于每一个匹配,这个迭代器都返回一个Match Object
it = re.finditer(r'(\d\d)-a-([a-z]{3})',s,re.I)
for each in it:
print(each.group(),end='< ')
# 获取每一个迭代结果中组的所有的值
groups = each.groups()
# 对分组进行迭代
for i in groups:
print(i,end=' ')
print('>')
7.10 sub函数和subn函数的替换和搜索
sub函数与subn函数用于实现搜索和替换功能,将某个字符串中所有匹配正则表达式的部分替换成其他字符串
sub返回替换后的结果,subn函数返回一个元组:元组的第一个元素替换后的结果,第二个元素是替换的总数
案例:sub和subn配合正则表达式进行搜索和替换
import re
# sub函数第1个参数是模式字符串,第2个参数是要替换的字符串,第3个参数是被替换的字符串
# 匹配'Bill is my son'中的'Bill',并用'Mike'替换'Bill'
result = re.sub('Bill', 'Mike', 'Bill is my son')
# 运行结果:Mike is my son
print(result)
# 返回替换结果和替换总数
result = re.subn('Bill', 'Mike', 'Bill is my son,I like Bill')
# 运行结果:('Mike is my son,I like Mike', 2)
print(result)
# 运行结果:Mike is my son,I like Mike
print(result[0])
# 运行结果:替换总数 = 2
print('替换总数','=',result[1])
# 使用“\N”的形式引用匹配字符串中的分组
result = re.sub('([0-9])([a-z]+)',r'产品编码(\1-\2)','01-1abc,02-2xyz,03-9hgf')
# 01-产品编码(1-abc),02-产品编码(2-xyz),03-产品编码(9-hgf)
print(result)
# 该函数返回要替换的字符串
def fun():
return r'产品编码(\1-\2)'
result = re.subn('([0-9])([a-z]+)',fun(),'01-1abc,02-2xyz,03-9hgf')
# ('01-产品编码(1-abc),02-产品编码(2-xyz),03-产品编码(9-hgf)', 3)
print(result)
# 01-产品编码(1-abc),02-产品编码(2-xyz),03-产品编码(9-hgf)
print(result[0])
# 替换总数 = 3
print('替换总数','=',result[1])
7.11 split分割
split函数根据正则表达式分割字符串
第一个参数式模式字符串,第二个参数式待分隔的字符串
案例:split配合正则表达式分割字符串
import re
result = re.split(';','Bill;Mike;John')
# 运行结果:['Bill', 'Mike', 'John']
print(result)
# 用至少1个逗号(,),分号(;),点(.)和空白符(\s)分隔字符串
result = re.split('[,;.\s]+','a,b,,d,d;x c;d. e')
# 运行结果:['a', 'b', 'd', 'd', 'x', 'c', 'd', 'e']
print(result)
# 用以3个小写字母开头,紧接着一个连字符(-),并以2个数字结尾的字符串作为分隔符对字符串进行分隔
result = re.split('[a-z]{3}-[0-9]{2}','testabc-4312productxyz-49abill')
# 运行结果:['test', '12product', 'abill']
print(result)
# 使用maxsplit参数限定分隔的次数,这里限定为1,也就是只分隔一次
result = re.split('[a-z]{3}-[0-9]{2}','testabc-4312productxyz-43abill',maxsplit=1)
# 运行结果:['test', '12productxyz-43abill']
print(result)
7.12 三种常用的匹配格式
电子邮箱 1、Email:'[0-9a-zA-Z]+@[0-9a-zA-Z]+\.[a-zA-Z]{2,3}'
IP地址 2、IP(IPV4):'\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}'
网址匹配 3、Web address: 'https?:/{2}\w.+'
import re
# 匹配Email的正在表达式
email = '[0-9a-zA-Z]+@[0-9a-zA-Z]+\.[a-zA-Z]{2,3}'
result = re.findall(email, 'lining@geekori.com')
# 运行结果:['lining@geekori.com']
print(result)
result = re.findall(email, 'abcdefg@aa')
# “@”后面不是域名形式,匹配失败。运行结果:[]
print(result)
result = re.findall(email, '我的email是lining@geekori.com,不是bill@geekori.cn,请确认输入的Email是否正确')
# 运行结果:['lining@geekori.com', 'bill@geekori.cn']
print(result)
# 匹配IPV4的正则表达式
# 此处应该有限制,0-255.0-255.0-255.0-255
ipv4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
result = re.findall(ipv4, '这是我的IP地址:33.12.数据库存储.54.34,你的IP地址是100.32.53.13吗')
# 运行结果:['33.12.数据库存储.54.34', '100.32.53.13']
print(result)
# 匹配Url的正则表达式
url = 'https?:/{2}\w.+'
url1 = 'https://geekori.com'
url2 = 'ftp://geekori.com' # ftp文件传输协议
# 运行结果:<_sre.SRE_Match object; span=(0, 19), match='https://geekori.com'>
print(re.match(url,url1))
# 运行结果:None
print(re.match(url,url2))
最终案例:糗事网笑话合集抓取
本来是由三个案例,但由于篇幅问题,只能放一个,学习足矣。
import requests
import re
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
}
jokeLists = []
# 判断性别
def verifySex(class_name):
if class_name =='womenIcon':
return '女'
else:
return '男'
def getJoke(url):
# 获取页面的HTML代码
res = requests.get(url)
# 获取用户ID
ids = re.findall('<h2>(.*?)</h2>', res.text, re.S)
# re.S = '如果不使用re.S参数,则只在每一行内进行匹配,如果一行没有,就换下一行重新开始。' \
# '而使用re.S参数以后,正则表达式会将这个字符串作为一个整体,在整体中进行匹配。'
# 获取用户级别
levels = re.findall('<div class="articleGender \D+Icon">(.*?)</div>', res.text, re.S)
# 获取性别
sexs = re.findall('<div class="articleGender (.*?)">', res.text, re.S)
# 获取段子内容
contents = re.findall('<div class="content">.*?<span>(.*?)</span>',res.text,re.S)
# 获取好笑数
laughs = re.findall('<span class="stats-vote"><i class="number">(.*?)</i>',res.text,re.S)
# 获取评论数
comments = re.findall('<i class="number">(\d+)</i>',res.text,re.S)
# 使用zip函数将上述获得的数据的对应索引元素放到一起
# 如将[1,2]、['a','b']编程[(1,'a'),(2,'b')],便于对元素迭代
for id,level,sex,content,laugh,comment in zip(ids,levels,sexs,contents,laughs,comments):
# 获得每一个段子相关的数据
info = {
'id':id,
'level':level,
'sex':verifySex(sex),
'content':content,
'laugh':laugh,
'comment':comment
}
print(info)
jokeLists.append(info)
# 产生1-30页的URL
urls = ['http://www.qiushibaike.com/text/page/{}/'.format(str(i)) for i in range(1,31)]
# 对这30个URl进行迭代,获取这30页的段子
for url in urls:
getJoke(url)
# 将抓取结果保存到jokes.txt文件中
for joke in jokeLists:
f = open('jokes.txt','a+')
try:
f.write(joke['id']+'\n')
f.write(joke['level'] + '\n')
f.write(joke['sex'] + '\n')
f.write(joke['content'] + '\n')
f.write(joke['laugh'] + '\n')
f.write(joke['comment'] + '\n')
f.close()
except UnicodeEncodeError:
pass
抓取的joke.txt内容