爬虫解析库(7. 正则表达式)

正则表达式

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

  1. 正则表达式中的match方法有两个参数,第一个参数表示文本模式,第二个参数表示待匹配的字符串。
  2. 如果匹配成功,match方法返回SRE_Match方法,然后可以调用该对象中的group方法获取匹配成功的字符串,
  3. 如果文本模式就是一个普通的字符串,那么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内容
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值