爬虫学习(3)

使用正则表达式来提取信息不是非常方便,而通过html的节点可以方便的定位,通过XPath和CSS选择器可以方便的提取节点,然后调用相应方法来获取想获取的内容。这一过程可以通过解析库来完成。
比较厉害的解析库有lxml、Beautiful Soup、pyquery。

XPth

XML Path Language,XML路径语言,提供了非常简洁的路径选择表达式。其常用规则如下表所示:

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

比如,//title[@lang=’eng’]这条规则,代表选择所有名称为title,同时属性lang的值为eng的节点。
下面来看一下XPath对网页解析的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from lxml import etree
text = '''
<div>
<ul>
<li class="item-0"><a href='link1.html'>1</a></li>
<li class="item-1"><a href='link2.html'>2</a></li>
<li class="item-inactive"><a href='link3.html'>3</a></li>
<li class="item-1"><a href='link4.html'>4</a></li>
<li class="item-0"><a href='link5.html'>5</a>
</ul>
</div>
'''
#注意上面的text缺少一个</li>
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))

首先调用HTML进行初始化,成功构造了XPath解析对象,并且etree可以自动修正文本,然后用tostring输出修正后的文本。
如果是文本文件,则 html = etree.parse(‘./text.html’,etree.HTMLParser())

所有节点 子节点 父节点

所有节点

用//开头的XPath规则来选取所有符合要求的节点,表示匹配所有节点
所以//
表示选取所有节点,//li表示选取所有li节点,

1
2
3
result = html.xpath('//li')
print(result)
#返回的是一个列表形式

子节点

用/或//查找元素的子节点或子孙节点,/表示直接子节点,//表示获取所有的子孙节点
//li/a表示所有li节点的直接a子节点,//li//a表示li节点的所有子孙a节点

父节点

用..来查找父节点
也可以用parent::

属性匹配

用@符号进行属性过滤
例如,选取class为item-0的li节点:html.path(‘//li[@class=”item-0”]’)

文本获取

用XPath的text()方法获取节点中的文本。
result = html.xpath(‘//li[@class=”item-1”]//text()’)
result = html.xpath(‘//li[@class=”item-1”]/a/text()’)

属性获取

用@符号
获取所有li节点下a节点的href属性:result = html.xpath(‘//li/a/@href’)

属性多值匹配

某些属性可能有多个值
例如下面li节点的class属性包含两个值,用之前的属性匹配就无法匹配,这时候得加上contains()函数,第一个参数是属性名字,第二个参数是属性值

1
2
3
4
5
6
7
8
9
10
11
from lxml import etree
text = '''
<li class = "li li-firsr"><a href = 'link.html'>first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[@class = "li"]/a/text()')
print(result)
#[]
result = html.xpath('//li[contains(@class,"li")]/a/text()')
print(result)
#['first item']

多属性匹配

有时候需要通过多个属性确定一个节点,这时候可以用and进行连接

1
2
3
4
5
6
7
8
from lxml import etree
text = '''
<li class = "li li-firsr" name="item"><a href = 'link.html'>first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class,"li") and @name="item"]/a/text()')
print(result)
#['first item']

事实上还有很多其他运算,比如or、mod、|等

按序选择

有时候匹配了很多节点,但是只想要特定节点,可以传入索引来获取特定节点
注意,这里的索引从1开始
//li[1] 选取第一个li节点
//li[last()] 选取最后一个li节点
//li[position()<3>] 选取位置小于3的li节点,也就是1和2
//li[last()-2] 选取倒数第三个li节点

节点轴选择

有很多轴,比如ancestor:: attribute:: child:: desendant:: following:: following-sibling等,具体的可以参考w3school

豆瓣电影Top250

之前采用re提取信息,现在采用XPath来获取我们想要的信息:

1
2
3
4
5
6
7
8
9
10
11
items = html.xpath('//div[@class="item"]')
for each in items:
rank = each.xpath('./descendant::em[@class=""]/text()')
pic = each.xpath('./descendant::div[@class="pic"]/a/img/@src')
title = each.xpath('./descendant::span[@class="title"]/text()')
title = [eachtitle.strip().replace('\xa0', '') for eachtitle in title]
print(title)
info = each.xpath('./descendant::div[@class="bd"]/p[1]/text()')
info = [eachinfo.strip().replace('\xa0', '') for eachinfo in info]
score = each.xpath('./descendant::div[@class="star"]/span[2]/text()')
quote = each.xpath('./descendant::p[@class="quote"]/span/text()')

Beautiful Soup

借助网页的结构和属性等特性来解析网页,是一个十分强大的解析库。
Beautiful Soup在解析时依赖解析器,除了python标准库中的HTML解析器,还有lxml、html5lib等第三方解析器

1
2
3
4
from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>','lxml') #使用lxml解析器
print(soup.prettify())#把要解析的字符串以标准的缩进格式输出,自动更正格式
print(soup.p.string)#p节点的文本

节点选择器

直接调用节点的名称就可以选择节点元素,在调用string属性就可以得到节点内的文本。

选择元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
html = """
<html><head><title>A Story</title></head>
<body>
<p class="title" name="Dormouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html,'lxml')
print(soup.title.string)#title节点内部的内容
print(type(soup.title))#类型,是Tag类型
print(soup.head)#head节点+其内部的内容
print(soup.p)#只选择第一个匹配到的节点p
# A Story
# <class 'bs4.element.Tag'>
# <head><title>A Story</title></head>
# <p class="title" name="Dormouse"><b>The Dormouse's story</b></p>

提取信息

1.获取节点名称
用name属性

1
2
print(soup.title.name)
#title

2.获取节点属性的值
选取节点元素后,调用attrs获取所有的属性,返回的是字典,然后获取相应的属性值

1
2
3
4
print(soup.p.attrs)
print(soup.p.attrs['name'])
#{'class': ['title'], 'name': 'Dormouse'}
#Dormouse

更为简单的方式是:

1
print(soup.p['name'])

3.获取内容
string属性获取节点元素包含的文本内容

1
print(soup.p.string) #只取第一个p节点

嵌套选择

可以继续调用节点来进行下一步的选择

1
2
print(soup.head.title)
# <title>A Story</title>

关联选择

有时候不能一步选到想要的节点元素,需要选一个节点元素作为基准,然后选择其子节点、父节点、兄弟节点等。
1.子节点和子孙节点
用contents属性,获取直接子节点

1
print(soup.p.contens)

也可以用children属性:

1
2
3
4
5
6
7
8
9
10
for i ,child in enumerate(soup.body.children):
print(i,child)
# 0
#
# 1 <p class="title" name="Dormouse"><b>The Dormouse's story</b></p>
# 2
#
# 3 <p class="story">Once upon a time...</p>
# 4
#

children属性,返回是生成器类型
如果要返回所有的子孙节点,则用descendants属性

1
2
for i,child in enumerate(soup.body.descendants):
print(i,child)

2.父节点和祖先节点
获取某元素的父节点,调用parent属性

1
print(soup.p.parent)#输出父节点及其内部内容

如果要获取所有的祖先节点,则用parents属性,返回结果也是一个生成器类型。
3.兄弟节点
获取同级节点:

1
2
3
4
5
#以节点p为例
soup.p.next_sibling#上一个
soup.p.previous_sibling#下一个
soup.p.next_siblings#生成器类型 所有的上面的同级
soup.p.previous_siblings#生成器类型 所有的下面的同级

4.提取信息
如果返回的是单个节点,可以直接调用string\attrs等属性获取对应节点的文本和属性。
如果返回的是多个节点的生成器,转化成列表后取出某个元素,在调用string\attrs等属性获取对应节点的文本和属性。

方法选择器

提供了一些方法可以灵活查询

find_all()

查询所有符合条件的元素,API如下:
find_all(name,attrs,recursive,text,**kwargs)
1.name

1
2
soup.find_all(name='ul')#返回列表,所有的ul节点及其内部所有内容。
#可以嵌套

2.attrs

1
2
3
soup.find_all(attrs={'id':'list-1'})
#查询id为list-1的节点
#返回列表,包含的内容是符合id=list-1d的所有节点

对于id、class等常用属性,可以不用attrs:

1
2
soup.find_all(id='list-1')
soup.find_all(class_='element')#class为关键字,所以加下划线

3.text
匹配节点的文字,可以传入字符串,也可以是正则表达式

1
2
soup.find_all(text=re.compile('link'))
#返回所有匹配正则表达式的节点文本组成的列表

find()

返回的是第一个匹配的元素,不再是列表形式

其他方法

find_parents()返回所有祖先节点 find_parent()返回直接父节点
find_next_siblings()返回后面所有兄弟节点 find_next_sibling()返回后面的第一个兄弟节点
find_previous_siblings() find_previous_sibling()
find_all_next()返回节点后所有符合条件的节点 find_next()返回第一个符合条件的节点
find_all_previous() find_previous()

CSS选择器

只要调用select()方法,传入相应的CSS选择器即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
html = """
<div class="panel">
<div class="panel-heading">
<h4>Hello</h4>
</div>
<div class="panel-body">
<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>
<ul class="list" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>
</div>
</div>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html,'lxml')
print(soup.select('.panel .panel-heading'))
# [<div class="panel-heading">
# <h4>Hello</h4>
# </div>]
print(soup.select('ul li'))
# [<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>, <li class="element">Foo</li>, <li class="element">Bar</li>]
print(soup.select('#list-2 .element'))
#[<li class="element">Foo</li>, <li class="element">Bar</li>]
print(soup.select('ul')[0])
# <ul class="list" id="list-1">
# <li class="element">Foo</li>
# <li class="element">Bar</li>
# <li class="element">Jay</li>
# </ul>

嵌套选择

支持嵌套

获取属性

1
2
3
4
for ul in soup.select('ul'):
print(ul['id'])
# list-1
# list-2

获取文本

除了string属性,还有get_text方法

1
2
3
4
5
6
7
8
for li in soup.select('li'):
print(li.get_text())
#print(li.string)
# Foo
# Bar
# Jay
# Foo
# Bar

总结

Beautiful Soup推荐使用lxml解析库,必要时使用html.parser
节点选择筛选功能弱但是速度快
建议使用find()、find_all()
如果熟悉CSS,可以用select()

豆瓣电影Top250

下面来看一下利用bs4爬取信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def parse_one_page(soup):
items = soup.find_all(class_='item')
for item in items:
rank = item.find(class_='').get_text()
pic = item.find(name='img')['src']
title = item.find(class_='title').get_text()
other_title = item.find(class_='other').get_text()

info = item.find(class_='bd')
info1 = info.find(name='p').get_text().strip().split('\n')

actor = info1[0]
time_country = info1[1].strip()
score = info.find(class_='rating_num').get_text()
quote = info.find(class_='quote').get_text()

pyquery

首先看一下pyquery的初始化

1
2
3
4
from pyquery import PyQuery as pq
#pq里面可以传字符串、URL也可以传文件filename=''
doc = pq(url='https://www.baidu.com')
print(doc('title'))

CSS选择器十分强大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pyquery import PyQuery as pq
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
</div>
'''
doc = pq(html)
print(doc('#container .list li'))
# <li class="item-0">first item</li>
# <li class="item-1"><a href="link2.html">second item</a></li>
# <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

首先初始化一个pq,然后传入CSS选择器#container .list li 意思是说选取id为container的节点,再选取其内部class为list的节点内部的所有li节点。

查找节点

查询函数与jQuery中函数的用法完全相同
1.子节点
find(),传入CSS选择器,选择所有符合要求的子孙节点,如果要找子节点,则用children()方法
2.父节点
parent()获取某个节点的父节点
parents()获取祖先节点
3.兄弟节点
siblings(),获取所有的兄弟节点
如果要筛选出某一个兄弟节点,则向方法中传入CSS选择器即可

遍历

有时候筛选完后可能是多个节点的结果,如果需要对结果进行遍历,需要调用items()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
</div>
'''
from pyquery import PyQuery
doc = PyQuery(html)
lis = doc('li').items()
for li in lis:
print(li,type(li))
# <li class="item-0">first item</li>
# <class 'pyquery.pyquery.PyQuery'>
# <li class="item-1"><a href="link2.html">second item</a></li>
# <class 'pyquery.pyquery.PyQuery'>
# <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
# <class 'pyquery.pyquery.PyQuery'>

获取信息

获取属性

提取到某个节点后,用attr()方法来获取属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
</div>
'''
from pyquery import PyQuery
doc = PyQuery(html)
a = doc('.item-0.active a')
print(a)
print(a.attr('href'))
print(a.attr.href)
# <a href="link3.html"><span class="bold">third item</span></a>
# link3.html
# link3.html

如果有多个节点,attr只会返回第一个节点的属性

获取文本

用text(),会忽略内部所有的html只返回纯文本

1
2
a = doc('.item-0.active a')
print(a.text())

如果想要保留内部的html,则用html()方法
同样,如果是多个节点,html()返回第一个节点的内容,而text()则返回所有节点的,是一个用空格隔开的字符串

节点操作

pyquery提供了很多方法可以对节点进行动态修改,比如为节点添加一个class、删除某个节点等操作。

addClass和removeClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
</div>
'''
from pyquery import PyQuery
doc = PyQuery(html)
a = doc('.item-0.active')
print(a)
print(a.removeClass('active'))
print(a.addClass('active'))
#<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
#<li class="item-0"><a href="link3.html"><span class="bold">third item</span></a></li>
#<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

attr、text、html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
</div>
'''
from pyquery import PyQuery
doc = PyQuery(html)
a = doc('.item-0.active')
print(a)
print(a.attr('name','link'))
print(a.text('change the text'))
print(a.html('<span>change again</span>'))
#<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
#<li class="item-0 active" name="link"><a href="link3.html"><span class="bold">third item</span></a></li>
#<li class="item-0 active" name="link">change the text</li>
#<li class="item-0 active" name="link"><span>change again</span></li>

attr(),第一个参数是属性名,第二个参数是属性值,可以用来添加或者修改某个属性,如果只传入第一个参数,则结果就是获取其属性值
text()、html(),如果传入参数,则变为修改文本,如果没有参数,则是获取文本

remove()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html = '''
<div class="wrap">
Hello World
<p>This is a gragraph</p>
</div>
'''
from pyquery import PyQuery
doc = PyQuery(html)
wrap = doc('.wrap')
print(wrap.text())
# Hello World
# This is a gragraph
wrap.find('p').remove()
print(wrap.text())
#Hello World

另外,还有append()、empty()、prepend()等方法

伪类选择器

支持多种多样的伪类选择器,例如选择第一个节点、最后一个节点、奇偶数节点、包含某一文本的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
</div>
'''
from pyquery import PyQuery
doc = PyQuery(html)
li = doc('li:first-child') #第一个li节点
print(li)
#<li class="item-0">first item</li>
li = doc('li:last-child') #最后一个li节点
print(li)
#<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
li = doc('li:nth-child(2)') #第二个li节点
print(li)
#<li class="item-1"><a href="link2.html">second item</a></li>
li = doc('li:gt(1)') #第二个li节点之后的li节点
print(li)
#<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
li = doc('li:nth-child(2n)') #偶数位置的li节点
print(li)
#<li class="item-1"><a href="link2.html">second item</a></li>
li = doc('li:contains(second)') #包含second文本的li节点
print(li)
#<li class="item-1"><a href="link2.html">second item</a></li>

豆瓣电影Top250榜单

下面就用pyquery来提取一下影片信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def parse_one_page(doc):
for item in doc('.item').items():

film = {
'rank':item('.pic em').text(),
'pic':item('.pic img').attr('src'),
'title':item('.title').text().strip().replace('\xa0',''),
'actor':item('.bd p:first-child').text().strip().split('\n')[0].replace('\xa0',''),
'time':item('.bd p:first-child').text().strip().split('\n')[1].replace('\xa0',''),
'score':item('.rating_num').text(),
'quote':item('.quote').text()
}
print(film)
with open('result3.txt','a+',encoding='utf-8') as f:
f.write(str(film)+'\n')