参考 《Python 3网络爬虫开发实战 》崔庆才著
对于网页的节点来说,它可以定义id、 class或其他属性。而且节点之间还有层次关系,在网页中可以通过 XPath或CSS选择器来定位一个或多个节点。那么,在页面解析时,利用 XPath或CSS选择器来提取某个节点,然后再调用相应方法获取它的正文内容或者属性,可以提取我们想要的信息了
比较强大的库有lxml、Beautiful Soup、pyquery
xpath
即XML路径语言,它是一门在XML文档中查找信息的语言。
XPath的选择功能十分强大,它提供了非常简洁明了的路径选择表达式。另外,它还提供了超过100个内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理等。几乎所有我们想要定位的节点,都可以用 XPath来选择。
常用规则
//title[@lang='eng']
这就是一个 XPath规则,它代表选择所有名称为 title,同时属性lang的值为eng的节点。
后面会通过 Python的lxml库,利用 XPath进行HTML的解析。
from lxml import etree
text = '''
<div>
<ul>
<li class="item-O"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html"> second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text) # 构造了一个 XPath解析对象
result = etree.tostring(html) # etree 模块可以自动修正 HTML 文本 补齐了fifth item的</li> 同时增加body 和html节点
print(result.decode('utf-8 '))
文本获取
使用Xpath中的text()
方法
html.xpath('//li[@class="item-0"]//text()')
获取Xpath对象html中li节点(class为"item-0“的li节点)下所有子孙节点下的文本
即first item
和fifth item
和\n
这里的\n
出现是因为最后一个节点在etree模块自动修正HTML文本,添加尾标签的时候换行了,所以提取文本得到的结果包含 li 节点的尾标签和 a节点的尾标签之间的换行符
属性获取
使用@
符号
html.xpath('//li/a/@href')
这里我们通过@href
即可获取节点的href
属性。
这里结果为
['link1.html','link2.html','link3.html','link4.html','link5.html']
可以看到,我们成功获取了所有1i节点下a节点的hre属性,它们以列表形式返回。
注意,此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如
[@href="link1.html"]
,限制了节点的href
属性为item-0
而此处的@href
指的是获取节点的某个属性,二者需要做好区分。
属性多值匹配contains()
函数
有时候,某些节点的某个属性可能有多个值,例如
<li class="li li-first"><a href="link.html">first item</a></li>
html.xpath('//li[@class="li"]/a/text()')
这里HTML文本中li
节点的class
属性有两个值li
和li-first
,此时如果还想用之前的属性匹配获取,就无法匹配了,此时的运行结果为空列表
这时需要使用contains()
函数了
html.xpath('//li[contains(@class,"li")]/a/text()')
第一个参数传入属性名,第二个参数传入属性值。这样只要此属性包含所传入的属性值,就可以完成匹配了。此时结果为['first item']
多属性匹配
此时使用运算符and
连接
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
这里的li
节点又增加了一个属性name
。要确定这个节点,需要同时根据class
和name
属性来选择,一个条件是class
属性里面包含1i字符串,另一个条件是name
属性为item
字符串,二者需要同时满足,需要用and
操作符相连,相连之后置于中括号内进行条件筛选。
html.xpath('//li[contains(@class,"li") and @name="item"]/a/text()')
运行结果如下
['first item']
按序选择
有时候,我们在选择的时候某些属性可能同时匹配了多个节点,但是只想要其中的某个节点,如第二个节点或者最后一个节点,
这时该怎么办呢这时可以利用中括号传入索引的方法获取特定次序的节点,示例如下
# 选择第一个li节点,注意xpath这里的排序不是从0开始的
result = html.xpath('//li[1]/a/text()')
# 选择最后一个节点
result = html.xpath('//li[last()]/a/text()')
# 选择位置小于3的节点
result = html.xpath('//li[position()<3]/a/text()')
# 选择倒数第三个节点
result = html.xpath('//li[last()-2]/a/text()')
节点轴选择
XPath提供了很多节点轴选择方法,包括获取子元素、兄弟元素、父元素、祖先元素等
# 第一个li节点的所有祖先节点
result = html.xpath('//li[1]/ancestor::*')
print(result)
# 加入限定条件,祖先为div的节点
result = html.xpath('//li[1]/ancestor::div')
print(result)
# 获取所有的属性值
result = html.xpath('//li[1]/attribute::*')
print(result)
# 获取所有的直接子节点,但限定条件为属性href为link1.html的直接a子节点
result = html.xpath('//li[1]/child::a[@href="link1.html"]')
print(result)
# 获取所有的子节点,限定条件为span节点,意思是只包含span节点的所有子节点
result = html.xpath('//li[1]/descendant::span')
print(result)
# 获取当前节点之后的所有节点,但限制条件为[2] 即是第二个节点
result = html.xpath('//li[1]/following::*[2]')
print(result)
# 获取当前节点之后的所有同级节点
result = html.xpath('//li[1]/following-sibling::*')
print(result)
Beautiful Soup
简单来说, Beautiful Soup就是 Python的一个HTML或XML的解析库,可以用它来方便地从网页中提取数据。官方解释如下:
Beautiful Soup提供一些简单的、 Python式的函数来处理导航、搜索、修改分析树等功能。
它是一个工具箱,通过解析文档为用户提供需要抓取的数据,因为简单,所以不需要多少代码就可以写出一个完整的应用程序。
Beautiful Soup自动将输入文档转换为 Unicode编码,输出文档转换为UTF-8编码。你不需要考虑编码方式,除非文档没有指定一个编码方式,这时你仅仅需要说明一下原始编码方式就可以了。
Beautiful Soup已成为和lxml、html6lib一样出色的 Python解释器,为用户灵活地提供不同的解析策略或强劲的速度。
- 支持的解析器
from bs4 import BeautifulSoup
html = """
<html><head><title> The Dormouse's story</title></head>
<p class="title" name="dromouse"><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, 'lxml')
print(soup.prettify())
print(soup.title.string)
这里首先声明变量html
,它是一个HTML
字符串。但是需要注意的是,它并不是一个完整的HTML字符串,因为body
和html
节点都没有闭合。接着,我们将它当作第一个参数传给 Beautiful Soup
对象,该对象的第二个参数为解析器的类型(这里使用lxml
),此时就完成了 BeaufulSoup
对象的初始化。
然后,将这个对象赋值给soup
变量。
接下来,就可以调用soup
的各个方法和属性解析这串HTML
代码了,首先,调用 prettify()
方法。这个方法可以把要解析的字符串以标准的缩进格式输出。这里需要注意的是,输出结果里面包含body
和html
节点,也就是说对于不标准的HTML字符串 BeautifulSoup
,可以自动更正格式。这一步不是由 prettify()
方法做的,而是在初始化 Beautiful Soup时就完成了。
然后调用soup.title.string
,这实际上是输出HTML
中 title
节点的文本内容。所以,soup.title
可以选出HTML
中的 title
节点,再调用 string属性就可以得到里面的文本了,所以我们可以通过简单调用几个属性完成文本提取,这是不是非常方便?
节点选择器
直接调用节点的名称就可以选择节点,再调用string属性就可以得到节点内的文本了
html ="""
<html><head><title>The Dormouse ’s story</title></head>
<body>
<p class="title" name="dromouse" >< 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 >< la>
<a href http://example.com/lacie" class ="sister" id="link2" >Lacie< /a> and
<a href=“ http://example.com/tillie" class="sister" id="link3">Tillie< la>;
and they lived at the bottom of a well. </p>
<p class="story") ... </p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html,'lxml')
print(soup.title)
print(type(soup.title)) # 输出类型是bs4.element.Tag类型
print(soup.title.string)
print(soup.head)
print(soup.p) # 这里只会有第一个节点的内容,后面的p节点并未有选到,
# 也就是说,多个节点只会选第一个,其余被忽略
-
提取信息
可以调用string
属性获取文本的值,-
获取结点属性的值
使用name
获取节点的名称print(soup.title.name) # 输出:title
使用
attrs
获取 所有属性及值,类型为字典print(soup.p.attrs) # 输出: {‘class’:[title],'name':'dromouse'} print(soup.p.attrs['name']) # 输出:dromouse
还可以直接使用数组下标
print(soup.p['class']) # 输出:[title]
这里需要注意的是,有的返回结果是字符串,有的返回结果是字符串组成的列表 比如,
name
性的值是唯一的,返回的结果就是单个字符串 而对于class
一个节点元素可能有多个class
, 所以返回的是列表 在实际处理过程中,我们要注意判断类型。
-
-
关联选择
在做选择的时候,有时候不能做到一步就选到想要的节点元素,需要先选中某一个节点元素,然后以它为基准再选择它的子节点、父节点、 兄弟节点等,这里就来介绍如何选择这些节点元素。- 子节点和子孙节点
- 获取直接子节点 调用contens属性,返回列表形式。
- 使用children属性,返回生成器类型
- 如果得到所有子孙节点的话,调用descendants属性,返回生成器类型
- 子节点和子孙节点
-
父节点和祖先节点
- 获取某个节点元素的直接父节点,可以调用parent属性
-
兄弟节点
next_sibling
,previous_sibling
分别获取节点的下一个和上一个兄弟属性next_siblings
,previous_siblings
获取所有的兄弟节点生成器- 提取信息
string
和attrs
属性
-
方法选择器
前面所讲的选择方法都是通过属性来选择的,这种方法非常快,但进行复杂的选择的话,他就比较繁琐- find_all()
find_all(name, attrs, recursive, text, **kwargs)
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">]ay</li> </ul> <Ul class="list list-small" id="list-2"> <li class="element"> Foo</li> <li class="element">Bar</li> </ul> </div> </div> """
-
find_all(name, attrs, recursive, text, **kwargs)
查询所有符合条件的元素- name:节点名
soup.find_all(name='li')
- attrs:属性
soup.find_all(attrs={'id','list-1})
对于一些常用的属性,id,class可以直接传
eg:soup.find_all(id='list-1')
soup.find_all(class_='element')
这里由于class为关键字,所有后面需要加_ - text
匹配节点文本,传入形式可以是字符串,也可是正则表达
- name:节点名
-
find()
返回单个元素 -
其他
- find_all()
-
CSS选择器
beautiful Soup 使用CSS选择器时只需要调用**select()**方法。传入想应的CSS选择器就可print(soup.select('.panel .panel-heading')) print(soup.select('ul li'))
* select()方法同样支持嵌套选择
* 例如,先选择所有 ul 节点,再遍历每个 ul 节点,选择其 li 节点
```python
for ul in soup.select('ul'):
print(ul.select('li'))
-
获取文本
可以使用string属性,也可使用
get_text()
方法print('Get Text :', li.get_ text()) print('String:', li.string)
总结
- 推荐使用lxml解析库,必要时使用html.parser
- 节点选择筛选功能弱但是速度快
- 建议使用
find()
或者find_all()
查询匹配单个结果或者多个结果 - 如果对css选择器熟悉,可以使用
select()
方法
pyquery
如果你比较喜欢用 css 选择器,如果你对 jQuery 有所了解,那么这里有一个更适合你的解析库–pyquery
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">]ay</li>
</ul>
<Ul class="list list-small" id="list-2">
<li class="element"> Foo</li>
<li class="element">Bar</li>
</ul>
</div>
</div>
"""
from pyquery import PyQuery as pq
doc = pq(html) # 使用字符串形式初始化
print(doc('li'))
-
URL初始化
除了使用字符串形式初始化,还可以传入网页的URL,doc = pq(url='http://cuiqingcai.com')
这样的话, PyQuery 对象会首先请求这个 URL 然后用得到的 HTML 内容完成初始 ,这其实就相当于用网页的源代码以字符串的形式传递给 pyquery 类来初始化。
-
文件初始化
还可以传递本地的文件名,此时参数指定为filename即可
doc = pd(filename='demo.html')
基本CSS选择器
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>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class ="item-0"><a href= "link5.html"> fifth item</a></li>
</ul>
</div> """
from pyquery import PyQuery as pq
doc = pq(html)
print(doc (’#container .list li’ ))
print(type(doc('#container .list li')))
这里我们初始化 PyQuery 对象之后,传入了 css 选择器#container .list li ,它的意思是先选取 id为container 的节点,然后再选取其内部的 class为list 的节点内部的所有 li 节点 然后,打印输出
-
查找节点
有些函数与jQuery中的函数用法完全相同
-
子节点
find()
方法,此时传入的参数是CSS选择器,查找范围是节点的所有子孙节点doc = pq(html) item = doc('.list') lis = item.find('li')
如果只想找子节点,那么可以用
children()
方法lis = item.children()
如果要筛选所有子节点中符合条件的节点,比如想筛选出子节点中 class为active 的节点,可以向
childre ()
方法传入 css 选择器.active
:lis = item.children('.active')
-
父节点
parent()
方法获取某个节点的父节点,直接父节点parents()
获取祖先节点 -
兄弟节点
siblings()
方法
-
遍历 item()
刚才可以观察到, pyquery 的选择结果可能是多个节点,也可能是单个节点,类型都是 PyQuery类型,并没有返回像 beautiful Soup 那样的列表
对于多个节点的结果,我们就需要遍历来获取了,调用items()方法
lis = doc('li').items()
print(type(lis))
for li in lis:
print(li, type(li))
可以发现,调用 items ()方法后,会得到 个生成器,遍历下,就可以逐个得到 li 节点对象了
获取信息 attr() text()
-
属性
可以使用attr()方法获取属性
当我们调用 attr()方法时,返 回结果却只是第一个 这是因为,当返回结果包含多个节点时,调用 attr () 方法,只会得到第一个节点的属性。那么,遇到这种情况时,如果想获取所有的节点的属性,就要用到前面所说的遍历了
-
获取文本
调用 text () 方法,忽略掉节点内部包含的所有 HTML只返回纯文字内容。
如果要获取这个节点的HTML文本就使用html()
值得注意,如果得到的结果是多个节点,并且想要获取每个节点的内部 HTML 文本,则需要遍历每个节点
text() 方法不需要遍历就可以获取,它将所有节点取文本之后合并成一个字符串
节点操作
pyquery 提供了一系列方法来对节点进行动态修改。比如为某个节点添加一个 class ,移除某个节点等,这些操作有时候会为提取信息带来极大的便利
-
addClass() 和removeClass()
可以动态修改节点的class属性值
-
attr、text、html
li = doc('.item-0 .active')
print(li)
li.attr('name', 'link') # 增加了不存在的属性name,值为link
print(li)
li.text('changed item') # 传入文本,文本全被改为传入的字符串文本了
print(li)
li.html('<span>changed item</span>') # 传入html文本
print(li)
<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><lli> <li class="item-0 active" name=”link”>changed item</li> <li class="item-0 active" name=”link”>< span>changed item</span></li>
如果 attr ()方法只传入第一个参数的属性名,则是获取这个属性值,如果传入第二个参数,可以用来修改
属性值 text ()和 html ()方法如果不传参数 ,则是获取节点内纯文本和 HTML 文本,如果传入参数 ,则进行赋值
- remove
伪类选择器
css 选择器之所以强大,还有 个很重要的原因,那就是它支持多种多样的伪类选择器