「第十三章」 非结构化数据提取

在爬取数据的过程中,需要对页面解析和数据提取。

一般来讲对我们而言,需要抓取的是某个网站或者某个应用的内容,提取有用的价值。内容一般分为两部分,非结构化的数据和结构化的数据。

非结构化数据:先有数据,再有结构。

结构化数据:先有结构、再有数据。

不同类型的数据,我们需要采用不同的方式来处理。

13.1 正则表达式

13.1.1 为什么要学正则表达式

实际上爬虫一共就四个主要步骤:

1. 明确目标 (要知道你准备在哪个范围或者网站去搜索)

2. 爬 (将所有的网站的内容全部爬下来)

3. 取 (去掉对我们没用处的数据)

4. 处理数据(按照我们想要的方式存储和使用)

之前的案例里实际上省略了第3步,也就是"取"的步骤。因为我们down下了的数据是全部的网页,这些数据很庞大并且很混乱,大部分的东西使我们不关心的,因此我们需要将之按我们的需要过滤和匹配出来。

那么对于文本的过滤或者规则的匹配,最强大的就是正则表达式,是Python爬虫世界里必不可少的神兵利器。

13.1.2 什么是正则表达式

正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

给定一个正则表达式和另一个字符串,我们可以达到如下的目的:

1. 给定的字符串是否符合正则表达式的过滤逻辑(“匹配”);

2. 通过正则表达式,从文本字符串中获取我们想要的特定部分(“过滤”)。




spark 非结构化处理 python 非结构化数据_字符串


13.1.3正则表达式匹配规则

1. 字符匹配规则。


spark 非结构化处理 python 非结构化数据_子串_02


2. 预定义字符集(可以写在字符集[…]中)。


spark 非结构化处理 python 非结构化数据_正则提取特定后的数字_03


3. 数词量(用在字符或者(...)之后)


spark 非结构化处理 python 非结构化数据_spark 非结构化处理_04


4.边界匹配。


spark 非结构化处理 python 非结构化数据_正则提取特定后的数字_05


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


这里采用的是贪婪模式。在匹配到第一个“

”时已经可以使整个表达式匹配成功,但是由于采用的是贪婪模式,所以仍然要向右尝试匹配,查看是否还有更长的可以成功匹配的子串。匹配到第二个“”后,向右再没有可以成功匹配的子串,匹配结束,匹配结果为“