Python爬虫02:数据解析工具及类库

  • 正则表达式
  • 正则表达式语法
  • 元字符
  • 贪婪模式与非贪婪模式
  • 在Python中使用正则表达式
  • `re`模块的常用方法
  • `re`模块的常用对象
  • `re.Match`:匹配对象
  • `re.Pattern`:正则对象
  • XPath
  • XPath语法
  • 路径表达式
  • 谓语
  • 通配符
  • 选取多条路径
  • 在Python中使用XPath表达式
  • Beautiful Soup库
  • Beautiful Soup库的简单示例
  • Beautiful Soup的用法
  • 解析HTML文件
  • 操作节点
  • 获取节点内容的属性和方法
  • 节点间移动的属性和方法
  • 查找节点
  • 按标签类型查找节点
  • 使用`find_all()`方法查找节点
  • 按CSS选择器查找节点


正则表达式

正则表达式语法

元字符

  1. 表示字符的元字符

字符

功能

.

匹配除\n以外任意一个字符

[ ]

匹配[ ]中列举的字符

[^ ]

匹配不是[ ]中列举的字符

[0-9]

匹配数字

[5-8]

匹配数字5678

[a-z]

匹配小写字母

[A-Z]

匹配大写字母

\d

匹配数字,即[0-9]

\D

匹配非数字,即不是数字

\s

匹配空格,即[space\t\r\n]

\S

匹配非空格

\w

匹配单词字符,即[a-zA-Z0-9_]

\W

匹配非单词字符

  1. 表示数量的元字符

字符

功能

*

匹配前一个字符出现0次或者无限次,即可有可无

+

匹配前一个字符出现1次或者无限次,即至少有1次

?

匹配前一个字符出现1次或者0次,即要么有1次,要么没有

{m}

匹配前一个字符出现m次

{m,}

匹配前一个字符至少出现m次

{m,n}

匹配前一个字符出现从m到n次

说明: 表示数量的元字符仅表示前面被匹配字符出现的次数,但不要求多次出现的被匹配匹配字符都相同.如\d{3}既可以匹配111,也可以匹配123.

  1. 表示位置的元字符

字符

功能

^

匹配字符串开头(由于re.match()默认从str字符串开头进行匹配, 因此^没作用)

$

匹配字符串结尾

\b

匹配一个单词的边界

\B

匹配非单词边界

  1. 表示分组的元字符

字符

功能

|

匹配左右任意一个表达式

(ab)

将括号中字符作为一个分组

\num

引用分组num匹配到的字符串

(?P<name>)

为分组起别名

(?P=name)

引用别名为name分组匹配到的字符串

说明: 分组号从1开始,第0组表示整个正则表达式匹配上的部分.

贪婪模式与非贪婪模式

在匹配数量时,存在贪婪模式非贪婪模式两种匹配模式.

  • 贪婪模式: 尝试匹配尽可能多的字符.
  • 非贪婪模式: 尝试匹配尽可能少的字符.

正则表达式默认选用的贪婪模式,可以在表示数量的元字符后加上?,将该匹配转为非贪婪模式.

下面例子演示贪婪模式与非贪婪模式的区别:

str = "This is a number 234-235-22-423"

re.match(r".+(\d+-\d+-\d+-\d+)",str).group(1)	# 得到'4-235-22-423'
re.match(r".+(\d+-\d+-\d+-\d+?)",str).group(1)	# 得到'234-235-22-423'

在Python中使用正则表达式

在Python中,我们使用re模块操作正则表达式.可以参考官方文档

re模块的常用方法

re模块中几个常用的方法如下:

  • re.match(pattern, string, flags=0)搜索从字符串string第一个字符开始满足正则表达式patttern的子字符串.若存在匹配则返回一个re.Match对象,否则返回None.
  • re.search(pattern, string, flags=0)搜索从字符串string任意位置开始第一个满足正则表达式pattern的子字符串.若存在匹配则返回一个re.Match对象,否则返回None.
  • re.findall(pattern, string, flags=0)搜索从字符串string任意位置开始所有满足正则表达式pattern的子字符串.以list的形式返回所有匹配上的字符串.
  • re.sub(pattern, repl, string, count=0,flags=0)将字符串string中所有满足正则表达式patttern的子字符串替换为repl.其各参数意义如下:
  • repl: 用来得到替换后的字符串,可以是字符串,也可以是一个函数.
    repl是一个函数,那它会对每个非重复的匹配结果调用.它只能有一个re.Match参数且返回一个替换后字符串.
# repl参数为字符串
re.sub(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):',
	r'static PyObject*\npy_\1(void)\n{',
	'def myfunc():')
# 得到'static PyObject*\npy_myfunc(void)\n{'
#  repl参数为函数
def dashrepl(matchobj):
    if matchobj.group(0) == '-': return ' '
    else: return '-'
    
re.sub('-{1,2}', dashrepl, 'pro----gram-files')
# 得到'pro--gram files'
  • count: 替换的最大次数,必须为一个非负整数,默认为0,表示替换所有匹配的字符串.

上述几个函数中的参数意义如下:

  • pattern: 正则表达式,一般在字符串前加r变为原始字符串(raw string),使得其中的\不表示转义.
  • string: 被处理的字符串.
  • flags: 模式修正符,对正则匹配的默认行为进行修改,常用的模式修正符如下:

模式修正符

作用

re.Ire.IGNORECASE

匹配英文字符时忽略大小写

re.Mre.MULTILINE

多行模式,^匹配每行的开头,$匹配每行的结尾.(默认情况下^只匹配字符串的开头,$只匹配字符串的结尾)

re.Sre.DOTALL

.匹配所有字符(包括换行符\n)

re.Are.ASCII

\w,\W,\b,\B,\d,\D,\s\S只匹配ASCII,而不是Unicode

re模块的常用对象

re.Match:匹配对象

re.match()方法和re.search()方在存在匹配时使用re.Match对象来返回匹配结果,在不存在匹配时返回None.re.Match对象总是存在一个布尔值True,可以使用if判断匹配是否存在再进行操作.

match = re.search(pattern, string)
if match:
    process(match)

re.Match对象的常用方法如下:

  • Match.group([groupIndex1, ...]): 返回一个或多个匹配的子组。如果只有一个参数,则返回一个字符串,如果有多个参数,则返回一个元组.参数默认为0,表示正则表达式匹配上的所有部分.
m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
m.group(0)       # 以字符串形式返回正则表达式匹配的所有部分: 'Isaac Newton'
m.group(1)       # 以字符串形式返回正则表达式匹配的第一组: 'Isaac'
m.group(2)       # 以字符串形式返回正则表达式匹配的第二组: 'Newton'
m.group(1, 2)    # 以元组形式返回正则表达式匹配的第一组和第二组: ('Isaac', 'Newton')
  • Match.groups(default=None): 以元组形式返回所有匹配的子组.default参数用于替代未参与匹配的子组,默认为None.
m = re.match(r"(\d+)\.?(\d+)?", "24")
m.groups()      # 得到('24', None).第二组没参与匹配,设为None
m.groups('0')   # 得到('24', '0').第二组没参与匹配,设为'0'
re.Pattern:正则对象

对于大多数重要的应用场景中,我们都预先使用re.compile(pattern, flags=0)方法将正则表达式编译为re.Pattern对象,再对其调用match(),search(),findall()等方法.

  • Pattern.search(string[, pos[, endpos]]): 等价于re.search()方法
  • Pattern.match(string[, pos[, endpos]]): 等价于re.match()方法
  • Pattern.findall(string[, pos[, endpos]]): 等价于re.findall()方法

上面三个方法增加了可选参数posendpos,表示string中被匹配区域的起始位置和终止位置.

XPath

XPath语法

XPath是一门在XML文档中查找信息的语言,可以根据标签名和属性查找对应节点.下面介绍其常用语法,具体内容参考其官方文档和W3Cschool的XPath教程.

下面我们以文档books.xml为例,展示XPath表达式的用法:

<?xml version="1.0" encoding="ISO-8859-1"?>

<bookstore>

<book category="COOKING">
	<title lang="en">Everyday Italian</title>
	<author>Giada De Laurentiis</author>
	<year>2005</year>
	<price>30.00</price>
</book>

<book category="CHILDREN">
	<title lang="en">Harry Potter</title>
	<author>J K. Rowling</author>
	<year>2005</year>
	<price>29.99</price>
</book>

<book category="WEB">
	<title lang="en">XQuery Kick Start</title>
	<author>James McGovern</author>
	<author>Per Bothner</author>
	<author>Kurt Cagle</author>
	<author>James Linn</author>
	<author>Vaidyanathan Nagarajan</author>
	<year>2003</year>
	<price>49.99</price>
</book>

<book category="WEB">
	<title lang="en">Learning XML</title>
	<author>Erik T. Ray</author>
	<year>2003</year>
	<price>39.95</price>
</book>

</bookstore>

路径表达式

表达式

描述

标签名

选取所有该类型的标签节点

/

从当前节点(根节点)的直接子节点中选取

//

从当前节点(根节点)的所有后代节点中选取

.

选取当前节点

..

选取当前节点的父节点

@

选取属性内容

text()

选取节点的文字内容

示例如下:

路径表达式

结果

bookstore

选取所有bookstore元素

/bookstore

选取根元素bookstore

bookstore/book

选取属于bookstore子元素的所有book元素

//book

选取所有book子元素,而不论它们在文档中的位置

bookstore//book

选择属于bookstore元素的后代的所有book元素,而不管它们位于bookstore之下的什么位置

//@lang

选取名为lang的所有属性

谓语

谓语被嵌在方括号中,对节点进行筛选.

谓语可以是数字表达式,表示选取元素的位置(从1开始);也可以是条件表达式,表示对元素进行筛选的条件.

路径表达式

结果

/bookstore/book[1]

选取属于根bookstore元素的子元素的第一个book元素

/bookstore/book[last()]

选取属于根bookstore元素的子元素的最后一个book元素

/bookstore/book[last()-1]

选取属于根bookstore元素的子元素的倒数第二个book元素

/bookstore/book[position()<3]

选取属于根bookstore元素的子元素的第一个和第二个book元素

/bookstore/book[position()>1 and position()<4]

选取属于根bookstore元素的子元素的第二个和第三个book元素

//title[@lang]

选取所有拥有lang的属性的title元素

//title[@lang='en']

选取所有lang属性为entitle元素

/bookstore/book[price>35.00]

选取属于bookstore子元素的所有子price元素值大于35的book元素

/bookstore/book[price>35.00]/title

选取属于bookstore子元素的所有子price元素值大于35的book元素的title元素

通配符

通配符

描述

*

匹配任何元素节点

@*

匹配任何属性节点

node()

匹配任何类型的节点

示例如下:

路径表达式

结果

/bookstore/*

选取根bookstore元素的所有子元素

//*

选取文档中所有元素

//title[@*]

选取文档中所有带有属性的title元素

选取多条路径

使用|运算符可以使若干个路径之间做‘或‘的关系

路径表达式

结果

//book/title | //book/price

选取所有book元素的所有titleprice元素

//title | //price

选取文档中所有titleprice元素

/bookstore/book/title | //price

选取属于bookstore元素的book元素的所有title元素,以及文档中所有price元素

在Python中使用XPath表达式

在Python中,我们可以用lxml模块的etree以使用XPath表达式解析HTML文档,基本用法如下:

from lxml import etree

HTML_TEXT = """
<bookstore>
<book category="COOKING">
	<title lang="en">Everyday Italian</title>
	<author>Giada De Laurentiis</author>
...
"""
selector = etree.HTML(HTML_TEXT)

selector.xpath('//@lang')	# 得到 ['en', 'en', 'en', 'en']
selector.xpath('//bookstore/book/title/text()') 	# 得到 ['Everyday Italian', 'Harry Potter', 'XQuery Kick Start', 'Learning XML']
selector.xpath('//bookstore/book[1]/title/text()')	# 得到 ['Everyday Italian']
selector.xpath('//bookstore/book/price/text()')		# 得到 ['30.00', '29.99', '49.99', '39.95']
selector.xpath('//bookstore/book[price>35]/price/text()')	# 得到 ['49.99', '39.95']

Beautiful Soup库

Beautiful Soup是一个可以从HTML或XML文件中提取数据的Python库,其下面介绍其常用语法,具体内容参考其官方文档.

Beautiful Soup库的简单示例

下面例子演示如何使用Beautiful Soup库解析HTML文件.

  1. 将HTML字符串解析为BeautifulSoup对象
from bs4 import BeautifulSoup

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

soup = BeautifulSoup(html_doc)
  1. BeautifulSoup的基本属性和方法
soup.title				
# <title>The Dormouse's story</title>

soup.title.name			
# u'title'

soup.title.string		
# u'The Dormouse's story'

soup.title.parent.name	
# u'head'

soup.p					
# <p class="title"><b>The Dormouse's story</b></p>

soup.p['class']			
# u'title'

soup.a					
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.find(id="link3")
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
  1. 从文档中找到所有<a />标签指向的URL
for link in soup.find_all('a'):
    print(link.get('href'))
    # http://example.com/elsie
    # http://example.com/lacie
    # http://example.com/tillie
  1. 获取文档中的所有文字内容
print(soup.get_text())
# The Dormouse's story
#
# The Dormouse's story
#
# Once upon a time there were three little sisters; and their names were
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.
#
# ...

Beautiful Soup的用法

解析HTML文件

使用BeautifulSoup类的构造方法,可以得到一个文档的对象,该方法有两个常用参数:

  • markup: 待解析的HTML,可以是字符串,也可以是文件句柄
from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))

soup = BeautifulSoup("<html>data</html>")
  • features: 解析器,默认为"html.parser",推荐使用"lxml",这需要提前安装lxml库.

操作节点

在Beautiful Soup库中,节点被封装成为Tag对象,包括BeautifulSoup对象也继承自Tag对象,它们有一系列的属性和方法用于操作节点.

获取节点内容的属性和方法
  • name: 返回节点的标签名
  • __getattr__(): 获取节点的属性.其中"class"属性被认为是多值属性,会以list类型返回,其它属性都会以str类型返回.
# "class"属性被认为是多值属性,会以列表形式返回
css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.p['class']	
#得到 ["body", "strikeout"]

css_soup = BeautifulSoup('<p class="body"></p>')
css_soup.p['class']
#得到 ["body"]	

# 其他属性都会以字符串形式返回,即使该属性看起来有多个值
id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
#得到 'my id'
  • .string: 若该节点没有子节点,则该节点标签内的内容,否则返回None.
  • .strings: 以生成器的形式返回当前节点标签内所有字符串内容.
节点间移动的属性和方法
  • 移动到子节点
  • .content: 将当前节点的直接子节点以列表的方式返回
  • .children: 将当前节点的直接子节点以生成器的方式返回
  • 移动到所有后代节点
  • .descendants: 将当前节点的所有后代节点以生成器的方式返回
  • 移动到兄弟节点
  • .next_sibling.previous_sibling: 获取当前节点的前一个兄弟节点后一个兄弟节点
  • .next_siblings.previous_siblings: 将当前节点的前面所有兄弟节点后面所有兄弟节点以生成器的方式返回
  • 按解析顺序移动节点
  • .next_element.previous_element: 获取当前节点的前一个被解析的对象后一个被解析的对象.
  • .next_elements.previous_elements: 将当前节点的所有比当前节点早被解析的对象所有比当前节点晚被解析的对象以生成器的方式返回

查找节点

按标签类型查找节点

可以直接按照标签名查找节点,这样会返回第一个该类型的节点.

soup.p		# 得到 <p class="title"><b>The Dormouse's story</b></p>
soup.p.b	# 得到 <b>The Dormouse's story</b>
使用find_all()方法查找节点

使用Tag对象的find_all(self, name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs)方法可以在该节点的所有后代节点中按条件过滤查找.

在了解各参数之前,我们首先需要了解上述参数所需的参数类型:

  • 字符串
  • 正则表达式
for tag in soup.find_all(name=re.compile("t")):
print(tag.name)
# html
# title
  • True: 匹配任何值
  • 函数: 按照该函数过滤所有子孙节点,该函数接受一个Tag对象并返回bool值
# 定义函数判断标签是否含有class属性且不含有id属性
def has_class_but_no_id(tag):
	return tag.has_attr('class') and not tag.has_attr('id')

soup.find_all(has_class_but_no_id)
# [<p class="title"><b>The Dormouse's story</b></p>,
#  <p class="story">Once upon a time there were...</p>,
#  <p class="story">...</p>]
  • 列表: 匹配列表中任一元素的子孙节点都会被返回

各参数意义如下:

  • name: 搜索标签名满足某条件的节点,其参数类型可以是字符串,正则表达式,列表,True.
  • attrs: 搜索属性名满足某条件的节点,其参数类型可以是字符串,正则表达式,列表,True.
  • text: 搜索文字内容满足某条件的节点,其参数类型可以是字符串,正则表达式,列表,True.
  • limit: 限制搜索到的结果数量
  • recursive: 指定是否搜索所有后代节点.默认为True,表示搜索所有后代节点.若为False则表示搜索直接子节点.
  • kwargs: 如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字的属性来搜索,如果包含一个名字为id的参数,Beautiful Soup会搜索每个tag的id属性.但class属性与Python的关键字冲突,所以使用class_参数搜索class属性.
# 查找所有含有id属性的标签
soup.find_all(id=True)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

# 查找href属性满足正则表达式的标签
soup.find_all(href=re.compile("elsie"), id='link1')
# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]

# 查找sister类的a标签
soup.find_all("a", class_="sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

因为find_all()方法使用得如此频繁,Tag对象可以被当作一个方法来使用,执行该方法的结果和调用其find_all()方法的结果完全相同.

# 下面两行是等价的
soup.find_all("a")
soup("a")
按CSS选择器查找节点

使用Tag对象的select()方法即可根据CSS选择器语法查找节点.

soup.select("body a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

soup.select("#link1 ~ .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie"  id="link3">Tillie</a>]

soup.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]