「第十三章」 非结构化数据提取
在爬取数据的过程中,需要对页面解析和数据提取。
一般来讲对我们而言,需要抓取的是某个网站或者某个应用的内容,提取有用的价值。内容一般分为两部分,非结构化的数据和结构化的数据。
非结构化数据:先有数据,再有结构。
结构化数据:先有结构、再有数据。
不同类型的数据,我们需要采用不同的方式来处理。
13.1 正则表达式
13.1.1 为什么要学正则表达式
实际上爬虫一共就四个主要步骤:
1. 明确目标 (要知道你准备在哪个范围或者网站去搜索)
2. 爬 (将所有的网站的内容全部爬下来)
3. 取 (去掉对我们没用处的数据)
4. 处理数据(按照我们想要的方式存储和使用)
之前的案例里实际上省略了第3步,也就是"取"的步骤。因为我们down下了的数据是全部的网页,这些数据很庞大并且很混乱,大部分的东西使我们不关心的,因此我们需要将之按我们的需要过滤和匹配出来。
那么对于文本的过滤或者规则的匹配,最强大的就是正则表达式,是Python爬虫世界里必不可少的神兵利器。
13.1.2 什么是正则表达式
正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:
1. 给定的字符串是否符合正则表达式的过滤逻辑(“匹配”);
2. 通过正则表达式,从文本字符串中获取我们想要的特定部分(“过滤”)。
13.1.3正则表达式匹配规则
1. 字符匹配规则。
2. 预定义字符集(可以写在字符集[…]中)。
3. 数词量(用在字符或者(...)之后)
4.边界匹配。
13.1.4 Python3下正则表达式的模块的加载
在 Python 中,我们可以使用内置的 re 模块来使用正则表达式。
import re
有一点需要特别注意的是,正则表达式使用 对特殊字符进行转义,所以如果我们要使用原始字符串,只需加一个 r 前缀。
例子:
import re
#例子一
str1='nihaoinghai'
print(str1)
#例子二
str2=r'nihaoinghai'
print(str2)
运行结果:
nihao inghai
nihaoinghai
13.1.5 compile 函数
compile 函数用于编译正则表达式,生成一个正则表达式( Pattern )对象,供 match() 和 search() 这两个函数使用。
语法格式为:
re.compile(pattern[, flags])
参数:
pattern : 一个字符串形式的正则表达式
flags 可选,表示匹配模式,比如忽略大小写,多行模式等,具体参数为:
re.I 忽略大小写
re.L 表示特殊字符集 w, W, b, B, s, S 依赖于当前环境
re.M 多行模式
re.S 即为' . '并且包括换行符在内的任意字符(' . '不包括换行符)
re.U 表示特殊字符集 w, W, b, B, d, D, s, S 依赖于 Unicode 字符属性数据库
re.X 为了增加可读性,忽略空格和' # '后面的注释
例子:
import re
pattern = re.compile(r'd+') # 用于匹配至少一个数字
m = pattern.match('one12twothree34four') # 查找头部,没有匹配
print(m)
m = pattern.match('one12twothree34four', 2, 10) # 从'e'的位置开始匹配,没有匹配
print(m)
m = pattern.match('one12twothree34four', 3, 10) # 从'1'的位置开始匹配,正好匹配
print(m)
运行结果:
None
None
13.1.6 正则表达式对象
re.compile() 返回 RegexObject 对象。
re.MatchObject
group() 返回被 RE 匹配的字符串。
start() 返回匹配开始的位置。
end() 返回匹配结束的位置。
span() 返回一个元组包含匹配 (开始,结束) 的位置。
13.1.7 Python3 re模块的2种使用方式
第一种方式:使用compile 函数
1.使用 compile() 函数将正则表达式的字符串形式编译为一个 Pattern 对象
2.通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果,一个 Match 对象。
3.最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作
compile 函数用于编译正则表达式,生成一个 Pattern 对象,它的一般使用形式如下:
import re
# 将正则表达式编译成 Pattern 对象。
pattern = re.compile(r'd+')
在上面,我们已将一个正则表达式编译成 Pattern 对象,接下来,我们就可以利用 pattern 的一系列方法对文本进行匹配查找了。
Pattern 对象的一些常用方法主要有:
match 方法:从起始位置开始查找,一次匹配
search 方法:从任何位置开始查找,一次匹配
findall 方法:全部匹配,返回列表
finditer 方法:全部匹配,返回迭代器
split 方法:分割字符串,返回列表
sub 方法:替换
第二种方式:直接使用re. search()/re. findall ()方式。
例子:
import re
old_url = 'http://www.jikexueyuan.com/course/android/?pageNum=2'
total_page =20
html = """
爬虫测试
- 这是第一条
- 这是第二条
- 这是第三条
"""
# f.close()
# #任务一:爬取网页标题
#
# title = re.search('
(.*?)',html,re.S).group(1)
# print(title)
#
# #任务二:爬取链接
# links=re.findall('href="(.*?)">',html)
# print(links)
# #任务三:爬取部分文字内容
# u_text =re.findall('
- (.*?)
',html,re.S)[0]
# texts =re.findall('">(.*?)',u_text,re.S)
# for every_text in texts:
# print(texts)
#任务四:sub实现翻页
for i in range(2,total_page+1):
new_link =re.sub('pageNum=d','pageNum=%d'%i,old_url,re.S)
print(new_link)
13.1.8 re模块之match 方法
match 方法用于查找字符串的头部(也可以指定起始位置),它是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果。它的一般使用形式如下:
match(string,begin,end)
其中,string 是待匹配的字符串,begin 和end 是可选参数,指定字符串的起始和终点位置,当你指定begin 和end 时,match 方法会根据指定的范围去查询,如果不指定begin 和end 时,match 方法默认匹配字符串的头部。
当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。
综合例子:
import re
#例子一
str1='ting123hai456'
pattern = re.compile(r'd+') # 用于匹配至少一个数字
m1 = pattern.match(str1) # 查找头部,没有匹配
print(m1)
#例子二
str2='ting123hai456'
pattern = re.compile(r'd+') # 用于匹配至少一个数字
m2 = pattern.match(str2,3,8) # 从'g'的位置开始匹配,没有匹配
print(m2)
#例子三
str3='ting123hai456'
pattern = re.compile(r'd+') # 用于匹配至少一个数字
m3 = pattern.match(str3,4,8) # 从'1'的位置开始匹配,正好匹配
print(m3) # 返回一个 Match 对象
print(m3.group(0))
print(m3.start(0))
print(m3.end(0))
print(m3.span(0))
运行结果:
None
None
123
4
7
(4, 7)
在上面,当匹配成功时返回一个 Match 对象,其中:
group([group1, …]) 方法:用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group() 或 group(0);
start([group]) 方法:用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;
end([group]) 方法:用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;
span([group]) 方法:返回 (start(group), end(group))。
re.I 与re.S
1. re.I 表示忽略大小写。
2. re.S 表示全文匹配。
例子一:re.I 表示忽略大小写。
import re
pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I) # re.I 表示忽略大小写
m = pattern.match('Welcome To Reptiles')
print(m) # 匹配成功,返回一个 Match 对象
print(m.group(0)) # 返回匹配成功的整个子串
print(m.span(0)) # 返回匹配成功的整个子串的索引
print(m.group(1)) # 返回第一个分组匹配成功的子串
print(m.span(1)) # 返回第一个分组匹配成功的子串的索引
print(m.group(2)) # 返回第二个分组匹配成功的子串
print(m.span(2)) # 返回第二个分组匹配成功的子串
print(m.groups()) # 等价于 (m.group(1), m.group(2), ...)
print(m.group(3)) # compile(r'([a-z]+) ([a-z]+)')只是匹配了2组规则,不存在第三个分组
运行结果:
Welcome To
(0, 10)
Welcome
(0, 7)
To
(8, 10)
('Welcome', 'To')
IndexError: no such group
re.S表示全文匹配,讲findall()方法的时候,再用具体的例子展示。
13.1.9 re模块之search 方法
search 方法用于查找字符串的任何位置,它也是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果,它的一般使用形式如下:
search(string,begin,end)
其中,string 是待匹配的字符串,begin 和end 是可选参数,指定字符串的起始和终点位置,当你指定begin 和end 时,search 方法会根据指定的范围去查询,如果不指定begin 和end 时,match 方法默认任何位置,只要找到了一个匹配的结果就返回。
当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。
综合例子1:
import re
#例子一
str1='ting123hai456'
pattern = re.compile('d+')
m1 = pattern.search(str1) # 查找字符串任意位置,这里如果使用 match 方法则不匹配
print(m1)
print(m1.group())
print(m1.span())
#例子二
str2='ting123hai456'
pattern = re.compile('d+')
m2 = pattern.search(str2,4,8) # 指定字符串区间
print(m2)
print(m2.group())
print(m2.span())
运行结果:
123
(4, 7)
123
(4, 7)
综合例子2:
import re
#例子一
str1='ting123hai456'
pattern = re.compile('d+')
m1 = pattern.search(str1) # 查找字符串任意位置,这里如果使用 match 方法则不匹配
print(m1)
print(m1.group())
print(m1.span())
#例子二
str2='ting123hai456'
pattern = re.compile('d+')
m2 = pattern.search(str2,7,13) # 指定字符串区间
print(m2)
print(m2.group())
print(m2.span())
运行结果:
123
(4, 7)
456
(10, 13)
13.1.10 re模块之findall 方法
上面的 match 和 search 方法都是一次匹配,只要找到了一个匹配的结果就返回。然而,在大多数时候,我们需要搜索整个字符串,获得所有匹配的结果。
findall 方法的使用形式如下:
findall(string,begin,end)
其中,string 是待匹配的字符串,begin 和end 是可选参数,指定字符串的起始和终点位置,当你指定begin 和end 时,findall 方法会根据指定的范围去查询,以列表形式返回全部能匹配的子串,如果不指定begin 和end 时,match 方法会全文搜索,以列表形式返回全部能匹配的子串。
findall 以列表形式返回全部能匹配的子串,如果没有匹配,则返回一个空列表。
综合例子:
import re
#例子一
str1 = 'hello123hell world456hel'
pattern = re.compile('hel') # 查找数字
m1 = pattern.findall(str1)
print(m1)
#例子二
str2 = 'hello123hell world456hel'
pattern = re.compile('hel') # 查找 hel
m2 = pattern.findall(str2, 7, 14)
print(m2)
#例子三
str3 = 'hello123hell world456hel'
pattern = re.compile('hel') # 查找 hel
m3 = pattern.findall(str3, 7, 25)
print(m3)
运行结果:
['hel', 'hel', 'hel']
['hel']
['hel', 'hel']
13.1.11 re模块之finditer 方法
finditer 方法的行为跟 findall 的行为类似,也是搜索整个字符串,获得所有匹配的结果。但它返回一个顺序访问每一个匹配结果(Match 对象)的迭代器。
例子:
import re
pattern = re.compile(r'd+')
m1 = pattern.finditer('hello 123456 789')
m2 = pattern.finditer('one1two2three3four4', 0, 10)
print(type(m1))
print(type(m2))
print('----- m1 ------')
for a1 in m1: # a1 是 Match 对象
print('matching string: {}, position: {}'.format(a1.group(), a1.span()))
print('----- m2 ------')
for a2 in m2:
print('matching string: {}, position: {}'.format(a2.group(), a2.span()))
运行结果:
----- m1 ------
matching string: 123456, position: (6, 12)
matching string: 789, position: (13, 16)
----- m2 ------
matching string: 1, position: (3, 4)
matching string: 2, position: (7, 8)
13.1.12 split 方法
split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下:
split(string[, maxsplit])
其中,maxsplit 用于指定最大分割次数,不指定将全部分割。
例子:
import re
p = re.compile(r'[s,;]+')
print(p.split('a,b;; c d'))
运行结果:
['a', 'b', 'c', 'd']
13.1.13 sub 方法
sub 方法用于替换。它的使用形式如下:
sub(repl, string[, count])
其中,repl 可以是字符串也可以是一个函数:
如果 repl 是字符串,则会使用 repl 去替换字符串每一个匹配的子串,并返回替换后的字符串,另外,repl 还可以使用 id 的形式来引用分组,但不能使用编号 0;
如果 repl 是函数,这个方法应当只接受一个参数(Match 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。
count 用于指定最多替换次数,不指定时全部替换。
例子一:
import re
p = re.compile('123(.*?)123')
s = '123asdfxxIxxxxLovexxded123'
f = p.sub('123456789',s)
print(f)
运行结果:
123456789
例子二:
import re
p = re.compile(r'(w+) (w+)') # w = [A-Za-z0-9]
s = 'hello 123, hello 456'
print(p.sub(r'hello world', s)) # 使用 'hello world' 替换 'hello 123' 和 'hello 456'
print(p.sub(r'2 1', s)) # 引用分组
def func(m):
return 'hi' + ' ' + m.group(2)
print(p.sub(func, s))
print(p.sub(func, s, 1)) # 最多替换一次
运行结果:
hello world, hello world
123 hello, 456 hello
hi 123, hi 456
hi 123, hello 456
13.1.14 贪婪模式与非贪婪模式
在使用正则匹配的时候,有2种模式:
【贪婪模式】:在整个表达式匹配成功的前提下,尽可能多的匹配 ( * );
【非贪婪模式】:在整个表达式匹配成功的前提下,尽可能少的匹配 ( ? );
Python里数量词默认是贪婪的。
综合例子一:
import re
#例子一 贪婪模式
s = 'abbbc'
p = re.compile('ab*')
f1 = p.findall(s)
print(f1)
#例子二 非贪婪模式
s = 'abbbc'
p = re.compile('ab*?')
f2 = p.findall(s)
print(f2)
运行结果:
['abbb']
['a']
运行结果说明:
使用贪婪的数量词的正则表达式 ab* ,匹配结果: abbb。
* 决定了尽可能多匹配 b,所以a后面所有的 b 都出现了。
使用非贪婪的数量词的正则表达式ab*?,匹配结果: a。
即使前面有 *,但是 ? 决定了尽可能少匹配 b,所以没有 b。
综合例子二:
import re
html="aa
test1
bb
test2
cc"
#例子一 贪婪模式
p = re.compile('
.*
')
f1 = p.findall(html)
print(f1)
#例子二 非贪婪模式
p = re.compile('
.*?
')
f2 = p.findall(html)
print(f2)
运行结果:
['
test1
bb
test2
']
['
test1
', '
test2
']
运行结果说明:
使用贪婪的数量词的正则表达式:
.*
匹配结果:
test1
bb
test2
这里采用的是贪婪模式。在匹配到第一个“
”时已经可以使整个表达式匹配成功,但是由于采用的是贪婪模式,所以仍然要向右尝试匹配,查看是否还有更长的可以成功匹配的子串。匹配到第二个“”后,向右再没有可以成功匹配的子串,匹配结束,匹配结果为“