使用正则表达式来提取信息不是非常方便,而通过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
16from 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
3result = 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
11from 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
8from 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节点3>
节点轴选择
有很多轴,比如ancestor:: attribute:: child:: desendant:: following:: following-sibling等,具体的可以参考w3school
豆瓣电影Top250
之前采用re提取信息,现在采用XPath来获取我们想要的信息:1
2
3
4
5
6
7
8
9
10
11items = 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
4from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>','lxml') #使用lxml解析器
print(soup.prettify())#把要解析的字符串以标准的缩进格式输出,自动更正格式
print(soup.p.string)#p节点的文本
节点选择器
直接调用节点的名称就可以选择节点元素,在调用string属性就可以得到节点内的文本。
选择元素
1 | html = """ |
提取信息
1.获取节点名称
用name属性1
2print(soup.title.name)
#title
2.获取节点属性的值
选取节点元素后,调用attrs获取所有的属性,返回的是字典,然后获取相应的属性值1
2
3
4print(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
2print(soup.head.title)
# <title>A Story</title>
关联选择
有时候不能一步选到想要的节点元素,需要选一个节点元素作为基准,然后选择其子节点、父节点、兄弟节点等。
1.子节点和子孙节点
用contents属性,获取直接子节点1
print(soup.p.contens)
也可以用children属性:1
2
3
4
5
6
7
8
9
10for 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
2for 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.name1
2soup.find_all(name='ul')#返回列表,所有的ul节点及其内部所有内容。
#可以嵌套
2.attrs1
2
3soup.find_all(attrs={'id':'list-1'})
#查询id为list-1的节点
#返回列表,包含的内容是符合id=list-1d的所有节点
对于id、class等常用属性,可以不用attrs:1
2soup.find_all(id='list-1')
soup.find_all(class_='element')#class为关键字,所以加下划线
3.text
匹配节点的文字,可以传入字符串,也可以是正则表达式1
2soup.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
34html = """
<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 | for ul in soup.select('ul'): |
获取文本
除了string属性,还有get_text方法1
2
3
4
5
6
7
8for 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
15def 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
4from 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
15from 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
20html = '''
<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
18html = '''
<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
2a = doc('.item-0.active a')
print(a.text())
如果想要保留内部的html,则用html()方法
同样,如果是多个节点,html()返回第一个节点的内容,而text()则返回所有节点的,是一个用空格隔开的字符串
节点操作
pyquery提供了很多方法可以对节点进行动态修改,比如为节点添加一个class、删除某个节点等操作。
addClass和removeClass
1 | html = ''' |
attr、text、html
1 | html = ''' |
attr(),第一个参数是属性名,第二个参数是属性值,可以用来添加或者修改某个属性,如果只传入第一个参数,则结果就是获取其属性值
text()、html(),如果传入参数,则变为修改文本,如果没有参数,则是获取文本
remove()
1 | html = ''' |
另外,还有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
29html = '''
<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
15def 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')