第四部分 解析库的使用(XPathBeautiful SoupPyQuery)

Posted micro0623

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第四部分 解析库的使用(XPathBeautiful SoupPyQuery)相关的知识,希望对你有一定的参考价值。

在网页节点中,可以定义id、class或其他属性。节点间有层次关系,网页中要通过XPath或CSS选择器定位一个或多个节点。在页面解析时,可利用XPath或CSS选择器提取某个节点,再调用相应方法获取它的正文内容或者属性,就可提取到想要的信息。在python中常用的解析库有lxml、Beautiful Soup、pyquery等。使用这些库可以很大程度上提高效率。

一 使用XPath解析库

XPath,全称XML Path Language,即XML路径语言,是一门在XML文档中查找信息的语言。适用于XML文档和html文档搜索。

XPath的选择功能十分强大,提供了非常简洁明了的路径选择表达式。另外,还提供了超过100个内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理等。几乎所有想要定位的节点,都可以用XPath来选择。

XPath于1999年11月16日成为W3C标准,被设计为供XSLT、XPointer以及其他XML解析软件使用,更多的文档可以访问其官方网站:https://www.w3.org/TR/xpath/。

XPath对应的库是lxml库。

1、 XPath的常用规则
下表是XPath的几个常用规则
表4-1 XPath常用规则
表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

下面是XPath的匹配示例,如下所示:
//title[@lang=‘eng‘]
这个实例是XPath规则 ,代表选择所有名称为title,同时属性lang的值为eng的节点。

2、 第一个lxml库实例

下面用一个简单的代码了解下XPath对网页的解析过程,代码如下:
from lxml import etree
text = ‘‘‘
<div>
<ul>
<li class="item-0"><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="link2.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) # 调用HTML类初始化,自动修正HTML文本
result = etree.tostring(html) # 输出修正后的HTML代码,是bytes类型
print(result.decode("utf-8"))

在上面代码中,首先导人lxml库的etree模块,然后声明了一段HTML文本,调用HTML类进行初始化,这样就成功构造了一个XPath解析对象。注意HTML文本中的最后一个li节点是没有闭合的,但是etree模块可以自动修正HTML文本。

调用tostring()方法即可输出修正后的HTML代码,结果是bytes类型。利用decode()方法将其转成str类型,在输出结果中,经过处理后,li节点的标签被补全,并且还自动添加了body、html节点。输出结果如下:
<html><body><div>
<ul>
<li class="item-0"><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="link2.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>
</li></ul>
</div>
</body></html>

还可以直接读取文本文件进行解析,示例如下:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = etree.tostring(html)
print(result.decode(‘utf-8‘))

上面代码中test.html的内容是前面例子中的HTML代码。这次的输出结果中多了DOCTYPE的声明,对解析没有任何影响。输出如下所示:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div>&#13;
<ul>&#13;
<li class="item-0"><a href="link1.html">first item</a></li>&#13;
<li class="item-1"><a href="link2.html">second item</a></li>&#13;
<li class="item-inactive"><a href="link2.html">third item</a></li>&#13;
<li class="item-1"><a href="link4.html">fourth item</a></li>&#13;
<li class="item-0"><a href="link5.html">fifth item</a>&#13;
</li></ul>&#13;
</div></body></html>

3、 所有节点
可用 // 开头的XPath规则来选取所有符合要求的节点。用前面的HTML文本,如果要选取所有节点,可以这样实现:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//*‘) # *表示选取所有节点
print(result) # 输出如下所示:
[<Element html at 0x2555e650208>, <Element body at 0x2555e650308>,
<Element div at 0x2555e650348>, <Element ul at 0x2555e650388>,
<Element li at 0x2555e6503c8>, <Element a at 0x2555e650448>,
<Element li at 0x2555e650488>, <Element a at 0x2555e6504c8>,
<Element li at 0x2555e650508>, <Element a at 0x2555e650408>,
<Element li at 0x2555e650548>, <Element a at 0x2555e650588>,
<Element li at 0x2555e6505c8>, <Element a at 0x2555e650608>]

这里使用 * 代表匹配所有节点,也就是整个HTML文本中的所有节点都会被获取。输出返回形式是一个列表,每个元素是Element类型,其后跟了节点的名称,如html、body、div、ul、li、a等,所有节点都包含在列表中。

也可以匹配指定节点名称。如果想获取所有li节点,示例如下:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//li‘) # 匹配所有的li节点
print(result)
print(result[0]) #输出如下所示:
[<Element li at 0x199d8940308>, <Element li at 0x199d8940348>, <Element li at 0x199d8940388>,
<Element li at 0x199d89403c8>, <Element li at 0x199d8940408>]
<Element li at 0x199d8940308>

要选取所有li节点,可以使用//,直接加上节点名称即可,调用时直接使用xpath()方法即可。输出结果是列表,每个元素是一个Element对象,要取出某个对象,使用索引即可,如[0]。

4、 子节点
通过 / 或 // 即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接a子节点,可以这样实现:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//li/a‘) # 匹配所有的li节点下的a节点
print(result) # 输出如下所示:
[<Element a at 0x1e2198b0308>, <Element a at 0x1e2198b0348>,
<Element a at 0x1e2198b0388>, <Element a at 0x1e2198b03c8>, <Element a at 0x1e2198b0408>]

这里通过追加 /a 即选择所有li节点的所有直接a子节点。因为 //li 用于选中所有li节点,/a 用于选中li节点的所有直接子节点a,二者组合在一起即获取所有li节点的所有直接a子节点。

此处的 / 用于选取直接子节点,如果要获取所有子孙节点,可以使用 // 。例如,要获取ul节点下的所有子孙a节点,可以这样实现:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//ul//a‘) # 匹配所有的li节点下的a节点
print(result) # 输出结果与前面相同

如果这里用 //ul/a ,就不能获取任何结果。因为 / 用于获取直接子节点,而在 ul 节点下没有直接的a子节点,只有li节点,所以无法获取任何匹配结果。例如:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//ul/a‘)
print(result) # 输出结果为空列表:[]

所以要注意 / 和 // 的区别,其中 / 用于获取直接子节点, // 用于获取子孙节点。

5、 父节点

用/或者//可以查找子节点或子孙节点。如果知道一个子节点要查找父节点,可用 .. 来实现。

比如,现在首先选中href属性为link4.html的a节点,然后再获取其父节点,然后再获取其class属性,相关代码如下:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath("//a[@href=‘link4.html‘]/../@class")
print(result) # 输出是:[‘item-1‘]

输出结果正是获取的目标li节点的class属性。也可以通过parent::来获取父节点,代码如下:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath("//a[@href=‘link4.html‘]/parent::*/@class")
print(result) # 输出结果与前面一样:[‘item-1‘]

6、 属性匹配

在选取的时候,还可以用 @ 符号进行属性过滤。比如,这里如果要选取class为item-0的li节点,可以这样实现:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath("//li[@class=‘item-0‘]")
print(result) # 输出如下所示:
[<Element li at 0x1e53b240308>, <Element li at 0x1e53b240348>]

通过加入 [@class=‘item-0‘] ,限制了节点的class属性为item-0 ,而HTML文本中符合条件的li节点有两个,所以结果返回两个匹配到的元素。

7、 获取文本

用XPath中的text()方法可获取节点中的文本,下面尝试获取前面li节点中的文本,相关代码如下:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//li[@class="item-0"]/text()‘)
print(result) # 输出是:[‘ ‘]

在输出结果中,没有获取到文本内容,获取到是一个换行符。在上面代码中,XPath中text()前面是 / ,在此处 / 的含义是选取直接子节点,而li的直接子节点都是a节点,文本是在a节点内部,所以这里匹配到的结果是被修正的li节点内部的换行符,因为自动修正的li节点的尾标签换行了。即选中的是这两个节点:
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</li>

因其中一个节点自动修正, li节点的尾标签添加的时候换行了,所以提取文本得到的唯一结果是li节点的尾标签和a节点的尾标签之间的换行符。

要获取li节点内部的文本,有两种方式,一是先选取a节点再获取文本,二是使用 // 。下面看下这两个的区别:

from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//li[@class="item-0"]/a/text()‘) # 获取li节点下的a子节点文本
print(result) # 输出如下所示:
[‘first item‘, ‘fifth item‘]
上面的输出返回了两个值,内容是属性为item-0的li节点的文本,这说明前面的属性匹配是正确的。这里是逐层选取的,先选取li节点,又利用 / 选取其直接子节点 a ,再选取其文本。得到的结果是预期的结果。

接下来用另一种方式(使用 //)选取结果,如下所示:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//li[@class="item-0"]//text()‘) # 获取li节点下的文本
print(result) 输出如下所示:
[‘first item‘, ‘fifth item‘, ‘ ‘]

输出的结果有3个,这里选取了所有子孙节点的文本,其中前两个是li的子节点a节点内部的文本,另一个是最后一个li节点内部的文本,即换行符。

通过上面分析得出,要获取某个节点下的子孙节点内部的所有文本,可直接用 // 加text()的方式,这样获取到最全面的文本信息,但可能会有一些换行符和特殊字符。如果要获取某些特定的子孙节点下的所有文本,可以先选取到特定的子孙节点,再利用text()方法获取其内部文本,这样获取的结果是整洁的。

8、 属性获取

用text()方法可以获取节点内部文本,用 @ 符号可以获取节点的属性值。例如要获取所有li节点下所有a节点的href属性,代码如下:
from lxml import etree
html = etree.parse(‘./test.html‘, etree.HTMLParser())
result = html.xpath(‘//li/a/@href‘)
print(result) # 输出结果如下:
[‘link1.html‘, ‘link2.html‘, ‘link2.html‘, ‘link4.html‘, ‘link5.html‘]

输出结果是以列表形式返回,成功的获取了所有li节点下的a节点的href属性。在代码中,通过 @href获取节点的href属性。要注意的是,此处和属性匹配方法不同,属性匹配是中括号加属性名和值来限定某个属性,如 [@href="link1.html"],而此处的 @href 指的是获取节点的某个属性值,注意二者的区分

9、 属性多值匹配

一个节点的某个属性可能有多个值,用单纯的属性方式获取,会无法匹配。例如:
from lxml import etree
text = ‘‘‘
<li class="li li-first"><a href="link.html">first item</a></li>
‘‘‘
html = etree.HTML(text)
result = html.xpath(‘//li[@class="li"]‘)
print(result) # 输出结果为空:[]

因为在HTML文本中li节点的class属性有两个值li和li-first,用前面的属性匹配方式就不能匹配到结果。这时要用contains()函数,修改代码如下:
from lxml import etree
text = ‘‘‘
<li class="li li-first"><a href="link.html">first item</a></li>
‘‘‘
html = etree.HTML(text)
result = html.xpath(‘//li[contains(@class, "li")]/a/text()‘)
print(result) # 输出结果是:[‘first item‘]

通过contains()方法,第一个参数传入属性名称,第二个参数传入属性值,只要此属性包含所传入的属性值,就可以完成匹配。在上面代码中,先匹配到li节点,接着匹配子节点a的文本。在节点有多个属性值时经常用到contains()方法,因为class属性值通常有多个。

10、 多属性匹配

根据多个属性确定一个节点,需要同时匹配多个属性,可使用运行符 and 来连接。示例如下:
from lxml import etree
text = ‘‘‘
<li class="li li-first" 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‘]

这里的li节点增加了一个属性name。要确定这个节点,需要同时根据class和name属性来选择,一个条件是class属性里面包含li字符串,另一个条件是name属性为item字符串,二者需要同时满足,需要用and操作符相连,相连之后置于中括号内进行条件筛选。

上面代码中的and是XPath中的运算符。此处,还有很多运算符,如or, mod等。表4-2做了一简短总结。

表4-2 运算符及其介绍
运算符 描述 实例 返回值
or 或 age=19 or age=20 如果age是19,则返回true。如果age是21,则返回false
and 与 age>19 and age<21 如果age是20,则返回true。如果age是18,则返回false
mod 计算除法的余数 5 mod 2 1
| 计算两个节点集 //book | //cd 返回所有拥有book和cd元素的节点集
+ 加法 6 + 4 10
- 减法 6 - 4 2
* 乘法 6 * 4 24
div 除法 8 div 4 2
= 等于 age=19 如果age是19,则返回true。否则返回false
!= 不等于 age!= 如果age不等于19,则返回true。否则返回false
< 小于 age<19 如果age小于19,则返回true。否则返回false
<= 小于或等于 age<=19 如果age小于等于19,则返回true。否则返回false
> 大于 age>19 如果age大于19,则返回true。否则返回false
>= 大于或等于 age>=19 如果age大于等于19,则返回true。否则返回false
关于此表可参考:http://www.w3school.com.cn/xpath/xpath_operators.asp

11、 按序选择

在一些匹配任务中,可能在某些属性会同时匹配多个节点,但是只想要其中的某个节点,如第二个节点或第一个节点,这时可利用中括号传入索引的方法获取特定次序的节点。注意这里的索引是从1开始。代码如下:
from lxml import etree
text = ‘‘‘
<div>
<ul>
<li class="item-0"><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)
result = html.xpath(‘//li[1]/a/text()‘) # [1]表示第一个li节点
print(result) # 输出:[‘first item‘]
result = html.xpath(‘//li[last()]/a/text()‘) # 选取最后一个li节点
print(result) # 输出:[‘fifth item‘]
result = html.xpath(‘//li[position()<3]/a/text()‘) # 选取位置(索引)小于3的节点
print(result) # 输出:[‘first item‘, ‘second item‘]
result = html.xpath(‘//li[last()-2]/a/text()‘) # 选取最后一个节点减2的节点
print(result) # 输出:[‘third item‘]

在上面代码中,第一次选择的是第一个li节点,中括号传入的是数字1。注意这里的序号是从1开始。第二次选择的是最后一个li节点,中括号传入的是last(),输出是最后一个li节点。第三次选择时,选取位置小于3的li节点,也就是序号为1和2的节点,输出的是前两个li节点。第四次选择时,中括号传入的参数是last()-2,表示选取倒数第三个li节点,因为last()是最后一个,last()-2就是倒数第三个。

这里使用了last()、position()等函数。在XPath中,提供了100多个函数,包括存取、数值、字符串、逻辑、节点、序列等处理功能,它们的具体作用可以参考:http://www.w3school.com.cn/xpath/xpath_functions.asp 。

12、 节点轴选择

XPath提供了很多节点轴选择方法,包括获取子元素、兄弟元素、父元素、祖先元素等,示例如下:
from lxml import etree
text = ‘‘‘
<div>
<ul>
<li class="item-0"><a href="link1.html"><span>first item</span></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>
<a></a>
</div>
‘‘‘
html = etree.HTML(text)
result = html.xpath(‘//li[1]/ancestor::*‘)
print(result) # 第一个li节点的所有父节点
result = html.xpath(‘//li[1]/ancestor::div‘)
print(result) # 第一个li节点的div父节点
result = html.xpath(‘//li[1]/attribute::*‘)
print(result) # 第一个li节点的所有属性
result = html.xpath(‘//li[1]/child::a[@href="link1.html"]‘)
print(result) # 第一个li节点的子节点a且属性是href="link1.html"的子节点
result = html.xpath(‘//li[1]/descendant::span‘)
print(result) # 第一个li节点的所有span子孙节点
result = html.xpath(‘//li[1]/following::*[2]‘)
print(result) # 第一个li节点后的结束标签之后的所有节点,但限定索引为第二个
result = html.xpath(‘//li[1]/following-sibling::*‘)
print(result) # 第一个li节点的所有同级(兄弟)节点
输出如下所示:
[<Element html at 0x1a26413e2c8>, <Element body at 0x1a26413e248>, <Element div at 0x1a26413e208>, <Element ul at 0x1a26413e308>]
[<Element div at 0x1a26413e208>]
[‘item-0‘]
[<Element a at 0x1a26413e308>]
[<Element span at 0x1a26413e208>]
[<Element a at 0x1a26413e308>]
[<Element li at 0x1a26413e248>, <Element li at 0x1a26413e348>, <Element li at 0x1a26413e388>, <Element li at 0x1a26413e3c8>]

上面代码中,第一次选择时,调用ancestor轴,可以获取当前节点的所有祖先节点。其后需要跟两个冒号,然后是节点的选择器,这里直接使用*,表示匹配所有节点,因此返回结果是第一个li节点的所有祖先节点,包括
html、body、div和ul。

第二次选择时,又加限定条件,在冒号后面加div,得到的结果就只有div这个祖先节点了。

第三次选择时,调用了attribute轴,可以获取所有属性值,其后跟的选择器还是 * ,这代表获取节点的所有属性,返回值就是li 节点的所有属性值。

第四次选择时,调用了child轴,可以获取所有直接子节点。这里又加了限定条件,选取href属性为link1.html的a节点。

第五次选择时,调用了descendant轴,可以获取所有子孙节点。这里又加了限定条件获取span节点,所以返回的结果只包含span节点而不包含a节点。

第六次选择时,调用了following轴,选取文档中当前节点的结束标签之后的所有节点。这里虽然使用的是 * 匹配,但又加了索引选择,所以只获取了第二个后续节点。

第七次选择时,调用了following-sibling轴,可以获取当前节点之后的所有同级(兄弟)节点。这里使用 * 匹配,所以获取了所有后续同级节点。

XPath轴常用轴选择表
轴名称 结果
ancestor 选取当前节点的所有先辈(父、祖父等)。
ancestor-or-self 选取当前节点的所有先辈(父、祖父等)以及当前节点本身。
attribute 选取当前节点的所有属性。
child 选取当前节点的所有子元素。
descendant 选取当前节点的所有后代元素(子、孙等)。
descendant-or-self 选取当前节点的所有后代元素(子、孙等)以及当前节点本身。
following 选取文档中当前节点的结束标签之后的所有节点。
namespace 选取当前节点的所有命名空间节点。
parent 选取当前节点的父节点。
preceding 选取文档中当前节点的开始标签之前的所有节点。
preceding-sibling 选取当前节点之前的所有同级节点。
self 选取当前节点。

上面是XPath的简单用法,更多轴的用法参考:http://www.w3school.com.cn/xpath/xpath_axes.asp

XPath功能强大,内置函数多,熟练后,可大大提升HTML信息的提取效率。
更多XPath的用法:http://www.w3school.com.cn/xpath/index.asp
如果想查询更多Python lxml库的用法,可以查看http://lxml.de/

二 使用Beautiful Soup

Beautiful Soup,借助网页的结构和属性等特性来解析网页

Beautiful Soup是Python的一个HTML或XML的解析库,用它可方便地从网页中提取数据。官方解释如下:

Beautiful Soup提供一些简单的、Python式的函数来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为用户提供需要抓取的数据,由于简单,不需要多少代码就可写出一个完整的应用程序。

Beautful Soup自动将输入文档转换为Unicode编码,输出文档转换为UTF-8编码。不需要考虑编码方式,除非文档没有指定一个编码方式,这时仅需要说明一下原始编码方式就可行。

Beautiful Soup 已成为和lxml、html6lib一样出色的Python解释器,为用户灵活地提供不同的解析策略或强劲的速度。

在使用之前确保已经安装了Beautiful Soup和lxml。

1、 解析器

Beautiful Soup在解析时要依赖解析器,支持Python标准库中的HTML解析器,还支持一些第三方解析器(比如lxml)。表4-3列出了Beautiful Soup支持的解析器。

表4-3 Beautiful Soup支持的解析器
解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, "html.parser") Python的内置标准库,执行 Python 2.7.3及Python3.2.2之前的
速度适中、文档容错能力强 版本文档容错能力差
lxml HTML解析器 BeautifulSoup(markup, "lxml") 速度快、文档容错能力强 需要安装C语言库
lxml XML解析器 BeautifulSoup(markup, "xml") 速度快,唯一支持XML的解析器 需要安装C语言库
html5lib BeautifulSoup(markup, "html5lib") 最好的容错性、以浏览器的方式 速度慢、不依赖外部扩展
解析文档、生成HTML5格式文档

通过以上对比可以看出,lxml解析器有解析HTML和XML的功能,而且速度快,容错能力强。要使用lxml,在初始化Beautiful Soup时,第二个参数设为lxml即可,例如:
from bs4 import BeautifulSoup
soup = BeautifulSoup(‘<p>Hello</p>‘, ‘lxml‘)
print(soup.p.string) # 输出:Hello

下面的介绍就使用这个lxml解析器。

2、 基本用法

先通过实例了解Beautiful Soup的基本用法:
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 --></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>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.prettify())
print(soup.title.string)

运行结果如下:
<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 class="sister" href="http://example.com/elsie" id="link1">
<!-- Elsie -->
</a>
,
<a class="sister" href="http://example.com/lacie" id="link2">
Lacie
</a>
and
<a class="sister" href="http://example.com/tillie" id="link3">
Tillie
</a>
;
and they lived at the bottom of a well.
</p>
<p class="story">
...
</p>
</body>
</html>
The Dormouse‘s story

在代码中声明的html变量,是一个HTML字符串。但它并不是一个完整的HTML字符串,因为body和html节点没有闭合。接着将它作为第一个参数传递给BeautifulSoup对象,该对象的第二个参数为解析器类型(这里使用lxml)此时完成了BeautifulSoup对象的初始化。然后将这个对象赋值给soup变量。下面就可以调用soup的各个方法和属性解析这串HTML代码。

首先,调用prettify()方法。这个方法可以把要解析的字符串以标准的缩进格式输出。这里需要注意的是,输出结果里面包含body和html节点,也就是说对于不标准的HTML字符串BeautifolSoup,可以自动更正格式。这一步不是由prettify()方法做的,而是在初始化BeautifolSoup时就完成了。

然后调用soup.title.string,输出的是HTML中title节点的文本内容。所以soup.title可以选出HTML中的title节点,再调用string属性就可以得到里面的文本,所以可以通过简单调用几个属性完成文本提取,非常方便。

3、 节点选择器

直接调用节点的名称就可以选择节点元素,再调用string属性就可以得到节点内的文本,这种选择方式速度非常快。如果单个节点结构层次非常清晰,可以选用这种方式来解析。

2.3.1 选择元素
用一个例子详细说明选择元素的方法:
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 --></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>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.title) # 输出title标签及内容
print(type(soup.title)) # 是bs4的标签元素类型
print(soup.title.string) # 输出title标签文本内容
print(soup.head) # 输出head标签及其下面的子标签
print(soup.p) # 输出p标签及其下面的子标签

输出结果如下:
<title>The Dormouse‘s story</title>
<class ‘bs4.element.Tag‘>
The Dormouse‘s story
<head><title>The Dormouse‘s story</title></head>
<p class="title" name="dromouse"><b>The Dormouse‘s story</b></p>

选用前面相同的HTML代码,这次首先打印输出title节点的选择结果,输出结果正是title节点加里面的文字内容。接下来,输出它的类型,是bs4.element.Tag类型,这是BeautifulSoup中一个重要的数据结构。经过选择器选择后,选择结果都是这种Tag类型。Tag具有一些属性,比如string属性,调用该属性,可以得到节点的文本内容,所以接下来的输出结果正是节点的文本内容。

接着,又尝试选择head节点,结果也是节点加其内部的所有内容。最后,选择p节点。不过这次情况比较特殊,结果是第一个p节点的内容,后面的几个p节点并没有选到。也就是说,当有多个节点时,这种选择方式只会选择到第一个匹配的节点,其他的后面节点都会忽略。

2.3.2 提取信息
前面用到的string属性可以获取文本的值。要获取节点名称和节点属性,就要使用name、attrs属性

获取名称:利用name属性获取节点名称。以前面的HTML代码为例,选取title节点,调用其name属性就可得到节点名称:
print(soup.title.name) # 输出:title

获取属性:每个节点可能有多个属性,比如id和class等,选择这个节点元素后,可调用attrs获取所有属性:
print(soup.p.attrs) # 获取第一个p节点的所有属性,以字典形式输出
print(soup.p.attrs[‘name‘]) # 输出第一个p节点的name属性值
输出结果如下:
{‘class‘: [‘title‘], ‘name‘: ‘dromouse‘}
dromouse

从上面输出可知,attrs是以字典形式返回结果,它把选择的节点的所有属性和属性值组合成一个字典。如果要获取name属性,向字典提供键名即可。所以要获取name属性,可通过attrs[‘name‘]方式得到。这种方式获取属性相对比较麻烦。另一种方式是不用写attrs,直接在节点元素后面加中括号,传入属性名就可以获取属性值。例如:
print(soup.p[‘name‘]) # 输出name属性值,是字符串类型
print(soup.p[‘class‘]) # 输出class属性值,是列表类型
输出如下所示:
dromouse
[‘title‘]

要注意的是,返回结果有的是字符串,有的是字符串组成的列表。在上面代码中,name属性的值是唯一的,返回结果就是单个字符串。对于class属性可以有多个属性值,所以返回的是列表。注意这点区别。

获取内容:用string属性可获取节点元素包含的文本内容,比如要获取第一个p节点的文本:
print(soup.p.string) # 输出是:The Dormouse‘s story
注意这里选择的是第一个p节点,文本也是第一个p节点的文本。

2.3.3 嵌套选择
在前面的输出中已经知道每个返回结果都是bs4.element.Tag类型,该类型可以继续调用节点进行下一步的选择。比如,获取了head节点元素,可以继续调用head来选取其内部的head节点元素:
html = """
<html><head><title>The Dormouse‘s story</title></head>
<body>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.head.title)
print(type(soup.head.title))
print(soup.head.title.string)

输出如下所示:
<title>The Dormouse‘s story</title>
<class ‘bs4.element.Tag‘>
The Dormouse‘s story

第一个输出结果是调用head之后再次调用title而选择的title节点元素。接着输出类型仍然是bs4.element.Tag类型。所以可以继续做嵌套选择。最后输出它的string属性,就是节点里的文本内容。

2.3.4 关联选择
在做选择时,不能一步就选到想要的节点元素,需要先选中某一个节点元素,然后以它为基准再选择它的子节点、父节点、兄弟节点等。

选择子节点和子孙节点:要获取直接子节点,调用contents属性,例如:
html = """
<html>
<head>
<title>The Dormouse‘s story</title>
</head>
<body>
<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">
<span>Elsie</span>
</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>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.p.contents)

运行结果如下:
[‘ Once upon a time there were three little sisters; and their names were ‘,
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>, ‘ ‘, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, ‘ and ‘,
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>,
‘ and they lived at the bottom of a well. ‘]

从输出可知,结果是列表形式。在p节点里有文本、节点,输出将它们以列表统一返回。要注意的是,列表中的每个元素都是p节点的直接子节点。比如第一个a节点里面包含一层span节点,这相当于孙节点了,但是返回结果并没有单独把span节点选出来。所以说,contents属性得到的结果是直接子节点的列表。

还可以调用children属性得到相应的结果:
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.p.children) # 结果是一个迭代器类型
for i, child in enumerate(soup.p.children):
print(i, child)

运行结果如下:
<list_iterator object at 0x0000014EEF1662B0>
0
Once upon a time there were three little sisters; and their names were

1 <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
2

3 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
4
and

5 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
6
and they lived at the bottom of a well.

调用children属性选择器,结果是生成器类型。接着用for循环输出相应内容。但子孙节点没有单独选出。要得到所有的子孙节点,可以调用descendants属性
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, ‘lxml‘)
print(soup.p.descendants) # 结果是一个生成器类型
for i, child in enumerate(soup.p.descendants):
print(i, child)

运行结果如下:
<generator object descendants at 0x000001D287BEC360>
0
Once upon a time there were three little sisters; and their names were

1 <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
2

3 <span>Elsie</span>
4 Elsie
5

6

7 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
8 Lacie
9
and

10 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
11 Tillie
12
and they lived at the bottom of a well.

descendants属性返回的结果仍然是生成器。遍历输出可以看到,这次输出结果包含了span节点。descendants会递归查询所有子孙节点,得到所有的子孙节点。

父节点和祖先节点
调用parent属性可以获取某个节点元素的父节点。例如:
html = """
<html>
<head>
<title>The Dormouse‘s story</title>
</head>
<body>
<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">
<span>Elsie</span>
</a>
</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.a.parent)

运行结果如下:
<p class="story">
Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>

上面代码中选择的是第一个a节点的父节点元素。它的父节点是p节点,输出结果是p节点及其内部的内容。parent属性仅仅是找某个节点的直接父节点,没有继续向外层寻找父节点的祖先节点。要获取祖先节点,就调用parents属性
html = """
<html>
<body>
<p class="story">
<a href="http://example.com/elsie" class="sister" id="link1">
<span>Elsie</span>
</a>
</p>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.a.parents) # 结果是一个生成器类型
print(list(enumerate(soup.p.parents)))

运行结果如下:
<generator object parents at 0x0000017E847CC360>
[(0, <body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body>),
(1, <html>
<body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body></html>), (2, <html>
<body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body></html>)]

输出可知,parents返回结果也是生成器类型。这里用列表输出它的索引和内容,列表中的元素就是a节点的祖先节点。

兄弟节点
前面的方法是获取当前节点的子节点或者父节点。如要获取当前节点的同级节点(兄弟节点),可使用next_sibling、
previous_siblings等属性
。例如:
html = """
<html>
<body>
<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">
<span>Elsie</span>
</a>
Hello
<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>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(‘Next Sibling‘, soup.a.next_sibling)
print(‘Prev Sibling‘, soup.a.previous_sibling)
print(‘Next Siblings‘, list(enumerate(soup.a.next_siblings)))
print(‘Prev Siblings‘, list(enumerate(soup.a.previous_siblings)))

输出如下所示:
Next Sibling
Hello

Prev Sibling
Once upon a time there were three little sisters; and their names were

Next Siblings [(0, ‘ Hello ‘),
(1, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>),
(2, ‘ and ‘),
(3, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>),
(4, ‘ and they lived at the bottom of a well. ‘)]
Prev Siblings [(0, ‘ Once upon a time there were three little sisters; and their names were ‘)]

在上面代码中,调用了4个属性,其中next_sibling和previous_sibling分别获取节点的下一个和上一个兄弟元素,next_siblings和previous_siblings则分别获取所有后面和前面的兄弟节点的生成器。

提取信息
要获取相关节点的一些信息,比如文本、属性等,也可用同样的方法。例如:
html = """
<html>
<body>
<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">Bob</a><a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
</p>
"""
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(‘Next Sibling:‘)
print(type(soup.a.next_sibling)) #单个节点类型是bs4.element.Tag
print(soup.a.next_sibling) # 输出下一个兄弟节点
print(soup.a.next_sibling.string) # 获取下一个兄弟节点的文本
print(‘Parent:‘)
print(type(soup.a.parents)) # 第一个a节点的所有父节点类型是一个生成器类型,注意和单个节点类型区分
print(list(soup.a.parents)[0]) # 需要先将生成类型转换为列表方可获取元素。获取第一个
print(list(soup.a.parents)[0].attrs[‘class‘]) # 获取第一个元素的class属性值,因class属性值可以有多个,所以结果是列表。

运行结果如下:
Next Sibling:
<class ‘bs4.element.Tag‘>
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
Lacie
Parent:
<class ‘generator‘>
<p class="story">
Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Bob</a><a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
</p>
[‘story‘]

如果返回的是单个节点,可直接调用string、attrs等属性获取其文本和属性值;如果返回结果是多个节点的生成器,可先转为列表后取出某个元素,然后再利用string、attrs等属性获取其对应节点的文本和属性。

4、 方法选择器

属性选择的优点是速度快,但对复杂的选择不灵活。Beautiful Soup还提供一些查询方法,如find_all()、find()等,调用这些方法,传入相应参数就可灵活查询。

2.4.1 find_all()方法

find_all查询所有符合条件的元素。给它传入一些属性或文本,就可以得到符合条件的元素,功能强大。对应的参数如下:
find_all(name, attrs, recursive, text, **kwargs)

name参数:可根据节点名查询元素,如name="ul"查找所有ul节点,示例如下:
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 list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>
</div>
</div>
‘‘‘
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.find_all(name=‘ul‘)) # 查找所有的ul节点
print(type(soup.find_all(name=‘ul‘)[0])) # 类型是bs4.element.Tag类型

运行结果如下所示:
[<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 list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>]
<class ‘bs4.element.Tag‘>

这里调用find_all()方法,传入参数name,参数值是ul。这样可查询所有ul节点,返回结果是列表类型,长度为2。每个元素依然是bs4.element.Tag类型。

由于还是Tag类型,还可以进行嵌套查询。下面查询出所有ul节点后,再查询其内部的li节点:
for ul in soup.find_all(name=‘ul‘):
print(ul.find_all(name=‘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>]

输出结果是2个列表,列表的每个元素还是Tag类型。再一次遍历每个li,就可获取它的文本:
for ul in soup.find_all(name=‘ul‘):
print(ul.find_all(name=‘li‘))
for li in ul.find_all(name=‘li‘):
print(li.string)
输出如下所示:
[<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>]
Foo
Bar
Jay
[<li class="element">Foo</li>, <li class="element">Bar</li>]
Foo
Bar

attrs参数:在做查询时,还可以用属性来查询,如attrs={‘id‘:‘i1‘},示例如下:
html=‘‘‘
<div class="panel">
<div class="panel-heading">
<h4>Hello</h4>
</div>
<div class="panel-body">
<ul class="list" id="list-1" name="elements">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>
<ul class="list list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>
</div>
</div>
‘‘‘
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.find_all(attrs={‘id‘: ‘list-1‘}))
print(soup.find_all(attrs={‘name‘: ‘elements‘}))

运行结果如下:
[<ul class="list" id="list-1" name="elements">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>]
[<ul class="list" id="list-1" name="elements">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>]

这里查询时用的是attrs参数,参数的类型是字典。如传入attrs={‘id‘: ‘list-1‘}表示查询id为list-1的节点,结果是列表形式,内容就是符合id为list-1的所有节点。在这里符合条件的元素个数是1,所以结果是长度为1的列表。

对于常用属性,如id和class等,可不用attrs来传递,可以直接传入id这个参数。用上面上HTML代码换种方式查询:
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.find_all(id=‘list-1‘))
print(soup.find_all(class_=‘element‘))

输出如下所示:
[<ul class="list" id="list-1" name="elements">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>]
[<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>]

直接传入id=‘list-1‘,可查询id为list-1的节点元素。对于class来说,由于class在Python里是一个关键字,所以后面需要加一个下划线,即class_=‘element‘,返回的结果依然还是Tag组成的列表。

text参数:可匹配节点的文本,传入的形式可以是字符串,可以是正则表达式对象,示例如下:
import re
html=‘‘‘
<div class="panel">
<div class="panel-body">
<a>Hello, this is a link</a>
<a>Hello, this is a link, too</a>
</div>
</div>
‘‘‘
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.find_all(text=re.compile(‘link‘)))
输出如下所示:
[‘Hello, this is a link‘, ‘Hello, this is a link, too‘]

这里在find_all()方法中传入text参数,该参数为正则表达式对象,结果返回所有匹配正则表达式的节点文本组成的列表。

2.4.2 find()方法

find()方法返回的是第一个匹配的元素。find_all()方法返回所有匹配的元素组成的列表。find()方法同样有name, attrs, text参数选项。例如:
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 list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>
</div>
</div>
‘‘‘
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.find(name=‘ul‘)) # 查找第一个ul节点
print(type(soup.find(name=‘ul‘))) # 查看类型,仍然是:bs4.element.Tag类型
print(soup.find(class_=‘list‘)) # 查找第一个class属性为list的节点

运行结果如下所示:
<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>
<class ‘bs4.element.Tag‘>
<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>

这次的返回结果不再是列表形式, 而是第一个匹配的节点元素,类型依然是Tag类型。

除了find_all() 和 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():返回当前节点前第一个符合条件的节点。

5、 CSS选择器

Beautiful Soup还提供了css选择器。可参考下面这个网站:
http://www.w3school.com.cn/cssref/css_selectors.asp

使用CSS选择器时,调用select()方法,传入相应的CSS选择器即可。例如下面示例:
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 list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>
</div>
</div>
‘‘‘
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
print(soup.select(‘.panel .panel-heading‘)) # 层级选择器
print(soup.select(‘ul li‘)) # 层级选择器,ul节点下的所有li节点
print(soup.select(‘#list-2 .element‘))
print(type(soup.select(‘ul‘)[0])) # 仍然是:bs4.element.Tag类型

输出如下所示:
[<div class="panel-heading">
<h4>Hello</h4>
</div>]
[<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>]
[<li class="element">Foo</li>, <li class="element">Bar</li>]
<class ‘bs4.element.Tag‘>

在上面代码使用了3次CSS选择器,输出结果均是符合CSS选择器节点组成的列表。最后一个输出类型仍然是bs4.element.Tag类型

2.5.1 嵌套选择
select()方法支持嵌套选择。例如,可先选择全部的ul节点,遍历每个ul节点,选择其下的li节点。例如:
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
for ul in soup.select(‘ul‘): # 先遍历所有的ul节点
print(ul.select(‘li‘)) # 输出每个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>]

2.5.2 获取属性
由于节点类型是Tag类型,获取属性还可以用原来的方法。例如要获取每个li节点的id属性,可以直接使用中括号传入属性名(例:ul[‘id‘]),也可使用attrs属性方式(例:ul.attrs[‘id‘])。如下面代码所示:
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
for ul in soup.select(‘ul‘):
print(ul[‘id‘]) # 直接使用中括号获取属性
print(ul.attrs[‘id‘]) # 通过attrs获取属性
输出如下所示:
list-1
list-1
list-2
list-2

2.5.3 获取文本
除了可用string属性外,还可以用get_text()方法。例如获取li节点的文本可用:li.get_text()。
from bs4 import BeautifulSoup as bs
soup = bs(html, ‘lxml‘)
for li in soup.select(‘li‘):
print("Get Text:", li.get_text())
print("String:", li.string)
输出如下所示:
Get Text: Foo
String: Foo
Get Text: Bar
String: Bar
Get Text: Jay
String: Jay
Get Text: Foo
String: Foo
Get Text: Bar
String: Bar

在使用Beautiful Soup解析库时,注意以下几点:
尽量使用lxml解析库,必要时可用html.parser。
节点选择筛选功能弱但是速度快。
多使用find()或者find_all()查询匹配单个结果或者多个结果。
熟悉CSS选择器,可用select()方法选择。

三 使用pyquery解析库

Beautiful Soup是强大的网页解析库,但对CSS选择器功能不够强大。另外一个对CSS选择器更适合的解析库是pyquery,它与jQuery有一定的相似性。

在使用pyquery前先正确安装pyquery。

3.1 初始化

初始化pyquery时,需要传入HTML文本初始化一个PyQuery对象。初始化方式有多种,可直接传入字符串,传入URL,传入文件名等

3.1.1 字符串方式初始化
首先引入PyQuery对象,声明HTML字符串,并将其当做参数传递给PyQuery类,这样就完成了初始化。接着将初始化对象传入CSS选择器,就可根据选择器选择所有相应的节点。下面代码中选择所有的li节点,例如:
html = ‘‘‘
<div>
<ul>
<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(‘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>
<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>

3.1.2 URL初始化
初始化参数可以传入网址的URL,需要指定参数为url即可。例如:
from pyquery import PyQuery as pq
doc = pq(url=‘https://www.baidu.com‘, encoding="utf-8")
print(doc(‘title‘)) # 获取title标签,输出结果如下所示:
<title>百度一下,你就知道</title>

当传入URL时,PyQuery对象会先请求这个URL,然后用得到的HTML内容完成初始化,就是用网页的源代码以字符串的形式传递给PyQuery类来初始化。与下面的作用是相同的:
from pyquery import PyQuery as pq
import requests
text = requests.get(‘https://www.baidu.com‘).text
doc = pq(text)
print(doc(‘title‘)) # 输出中文是乱码,后面找解决方法

3.1.3 文件初始化
指定参数filename可以传递本地文件名。本地文件要求是html文件,内容是待解析HTML字符串。先读取文件内容,用文件内容以字符串形式传递给PyQuery类初始化。例如:
from pyquery import PyQuery as pq
doc = pq(filename=‘jq1.html‘)
print(doc(‘title‘)) # 输出中文仍然是乱码

3.2 CSS选择器

先用实例来了解pyquery的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‘)))
输出如下所示:
<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>
<class ‘pyquery.pyquery.PyQuery‘>

上面代码中初始化PyQuery对象后,传入一个CSS选择器#container .list li,该选择器的意思是先取id为container的节点,然后再选取其内部的class为list的节点内部的所有li节点。第二个输出可知类型是PyQuery类型。

3.3 查找节点
可以查找子节点、父节点、兄弟节点等,对应着相关的查询函数,使用方法与jQuery中函数用法相同。

3.3.1 子节点
用find()方法查找子节点,参数是CSS选择器。以前面的HTML为例:
from pyquery import PyQuery as pq
doc = pq(html)
items = doc(‘.list‘)
print(type(items)) # 输出是PyQuery对象
print(items)
lis = items.find(‘li‘)
print(type(lis)) # 输出是PyQuery对象
print(lis)
输出如下所示:
<class ‘pyquery.pyquery.PyQuery‘>
<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>

<class ‘pyquery.pyquery.PyQuery‘>
<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>

首先,选取class为list的节点,调用find()方法,传入css选择器,选取其内部的li节点,最后打印输出。输出中find()方法将符合条件的所有节点选择出来,结果的类型是PyQuery类型。

find()查找范围是节点的所有子孙节点,只想查找子节点,可以用children()方法
lis = items.children()
print(type(lis)) # 输出是PyQuery对象
print(lis) # 输出如下所示:
<class ‘pyquery.pyquery.PyQuery‘>
<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>

还可以向children()方法传入CSS选择器,做进一步筛选。如选出子节点中class为active的节点:
lis = items.children(‘.active‘)
print(lis) # 输出如下所示:
<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>

3.3.2 父节点和祖先节点
用parent()方法可以获取某个节点的直接父节点,用parents()方法可以获取某个节点的祖先节点。例如:
html = ‘‘‘
<div class="wrap">
<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>
</div>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
items = doc(‘.list‘)
print(‘以下是直接父节点‘)
container = items.parent() # 找直接父节点
print(type(container)) # 类型是PyQuery类型
print(container)
print(‘以下是祖先节点‘)
parents = items.parents() # 查找祖先节点
print(type(parents)) # 类型是PyQuery类型
print(parents)

输出如下所示:
以下是直接父节点
<class ‘pyquery.pyquery.PyQuery‘>
<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>
以下是祖先节点
<class ‘pyquery.pyquery.PyQuery‘>
<div class="wrap">
<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>
</div><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>

在祖先节点的输出中有两个:一个是class为wrap的节点,一个是id为container的节点。parents()方法会返回所有的祖先节点。要单独提取某个祖先节点,可向parents()方法传入CSS选择器,只查找满足CSS选择器的节点。例如:
parent = items.parents(‘.wrap‘)
print(parent) # 输出省略,参考上面的输出

3.3.3 兄弟节点
使用siblings()方法可以查找某个节点的所有兄弟节点。向siblings()方法传入CSS选择器,可以查找特定条件的兄弟节点。使用前面的HTML代码为例:
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘.list .item-0.active‘) # 查找满足“.list .item-0.active”这个条件的兄弟节点,有4个
print("查找满足‘.list .item-0.active’这个条件的兄弟节点")
print(li.siblings())
print("在兄弟节点中找有class属性为active属性的节点")
print(li.siblings(".active"))

运行结果如下:
查找满足‘.list .item-0.active’这个条件的兄弟节点
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0">first item</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>
在兄弟节点中找有class属性为active属性的节点
<li class="item-1 active"><a href="link4.html">fourth item</a></li>

在上面代码中选择class为list的节点内部class为item-0和active的节点,就是第三个li节点。它的兄弟
节点有4个,是第一、二、四、五个li节点。接着向兄弟节点传入CSS选择器,选择了class为active的节点。
此时在兄弟节点中找到一个,就是第三个兄弟节点。所以输出的是第三个兄弟节点。

3.4 遍历
pyquery选择的结果可能有多个节点,也可能是单个节点,它们的类型都是PyQuery类型。不是像Beautiful Soup那样返回列表。对于单个节点可直接打印输出,也可以直接转成字符串。例如:
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘.item-0.active‘) # 获取满足“.item-0.active”这个条件的节点
print(li)
print(str(li)) # 转换成字符串后输出,结果和直接输出是一样的

输出结果如下:
<li class="item-0 active"><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>

对于多个节点的结果,可使用遍历来获取。例如,把每一个li节点进行遍历,调用items()方法,此时的类型是生成器类型。例如:
from pyquery import PyQuery as pq
doc = pq(html)
lis = doc(‘li‘).items() # 转换为生成器类型,即可迭代
print(type(lis)) # 生成器类型
for li in lis:
print(str(li).strip(),type(li), sep=‘ ‘)

输出结果如下所示:
<class ‘generator‘>
<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‘>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<class ‘pyquery.pyquery.PyQuery‘>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<class ‘pyquery.pyquery.PyQuery‘>

由输出可知,调用items()方法后,得到一个生成器,遍历生成器可以得到每个li节点对象。每个li节点对象依然是PyQuery类型,该对象可以调用前面的方法进行选择,如查询子节点、祖先节点等。

3.5 获取信息
获取到节点后,接着就要提取节点包含的信息。有两类重要的信息:获取属性和获取文本

3.5.1 获取属性
获取到某个节点后,可调用attr()方法获取属性:
html = ‘‘‘
<div class="wrap">
<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>
</div>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
a = doc(‘.item-0.active a‘) # 获取满足条件的a节点
print(a, type(a), sep=‘,‘)
print(a.attr(‘href‘))

输出如下所示:
<a href="link3.html"><span class="bold">third item</span></a>, <class ‘pyquery.pyquery.PyQuery‘>
link3.html

在上面代码中选取的是class为item-0和active的li节点内的a节点,类型是PyQuery类型。接着调用attr()方法传入属性名称参数,就得到了属性值。还可以直接调用attr属性来获取属性值,例如:
print(a.attr.href) # 输出:link3.html

a.attr(‘href‘)和a.attr.herf获取到的结果是完全一样的。

如果选中的是多个元素,调用attr()方法只能获取到第一个节点的属性值,例如下面代码所示:
a = doc(‘a‘)
print(a, type(a))
print(a.attr(‘href‘))
print(a.attr.href)

输出如下所示:
<a href="link2.html">second item</a>
<a href="link3.html"><span class="bold">third item</span></a>
<a href="link4.html">fourth item</a>
<a href="link5.html">fifth item</a> <class ‘pyquery.pyquery.PyQuery‘>
link2.html
link2.html

在上面的输出中,找到了4个a节点,但属性值只获取到第一个节点的。如果要获取所有的a节点属性,需使用遍历:
from pyquery import PyQuery as pq
doc = pq(html)
a = doc(‘a‘)
for item in a.items():
print(item.attr(‘href‘))

输出如下所示:
link2.html
link3.html
link4.html
link5.html

通过属性获取节点时要注意,如果返回的节点有多个,就需要遍历才能获取每个节点的属性。

3.5.2 获取文本
前面的方法是获取节点和属性,要获取节点内部的文本,可使用text()方法。例如下面代码所示:
html = ‘‘‘
<div class="wrap">
<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>
</div>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
a = doc(‘.item-0.active a‘)
print(a)
print(a.text())

输出如下所示:
<a href="link3.html"><span class="bold">third item</span></a>
third item

在上面代码中,先获取满足条件的a节点,然后调用text()方法获取其内部的文本信息。该方法忽略掉节点内部包含的所有HTML,只返回纯文字内容。要获取这个节点内部的HTML文本,就使用html()方法。如下所示:
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘.item-0.active‘)
print(li)
print(li.html())

输出如下所示:
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<a href="link3.html"><span class="bold">third item</span></a>

上面的输出可知,获取到第三个li节点,调用html()方法获取li节点内的所有HTML文本。如果获取到的结果是多个节点,text()或html()返回的内容会有所不一样。html()方法返回第一个获取到节点的内部HTML文本,text()则返回所有获取到的节点内部的纯文本,中间用空格隔开,即返回结果是一个字符串。如下面代码所示:
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘li‘) # 获取所有的li节点
print(li.html()) # 输出的是第一个li节点的html文本
print(li.text()) # 输出的是所有节点的文本
print(type(li.text())) # 输出是字符串类型

输出如下所示:
first item
first item second item third item fourth item fifth item
<class ‘str‘>

所以当获取到多个节点时,要获取每个节点内部的HTML文本,需要遍历每个节点。而text()方法不用遍历,该方法将获取的所有文本合并成一个字符串。

3.6 节点操作
pyquery有一系列方法来对节点进行动态修改,比如为某个节点添加一个class,移除某个节点等,这些操作有时候会为提取信息带来极大的便利。节点操作方法太多,下面说几个常用的。

3.6.1 addClass和removeClass
通过下面的实例进行理解:
html = ‘‘‘
<div class="wrap">
<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>
</div>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘.item-0.active‘) # 选取满足条件的节点,选取到的是第三个li节点
print(li)
li.removeClass(‘active‘) # 删除class属性值active
print(li)
li.addClass(‘active‘) # 添加class属性值active
print(li)

输出结果如下所示:
<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>

从上面代码的输出可知,根据class属性获取到节点后,使用removeClass()方法和addClass()方法可以动态改变节点的class属性。

3.6.2 attr 、text 和html
除了操作class属性外,也可用attr()方法对属性进行操作。还可用text()和html()方法改变节点内部的内容。
示例如下:
html = ‘‘‘
<ul class="list">
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
</ul>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘.item-0.active‘)
print(li)
li.attr(‘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></li>
<li class="item-0 active" name="link">changed item</li>
<li class="item-0 active" name="link"><span>changed item</span></li>

在上面代码中,首先选中li节点,接着调用attr()方法来修改属性,这个方法的第一个参数是属性名,第二个参数是属性值。接着,调用text()和html()方法来改变节点内部的内容。三次操作后,分别输出当前的li节点。从输出可以看出,调用attr()方法后,li节点多了一个原来没有的属性name,其值为link。接着调用text()方法,传入文本后,li节点内部的文本全被改为传入的字符串文本了。最后调用html()方法传入HTML文本后,li节点内部变为传入的HTML文本。

小结:如果attr()方法只传入第一个参数的属性名,是获取这个属性值;如果传入第二个参数,可以用来修改属性值。text()和html()方法如果不传参数,则是获取节点内纯文本和HTML文本;如果传人参数,则进行赋值。

3.6.3 remove()方法
remove()方法是删除,有时会为信息的提取带来非常大的便利。例如下面这段HTML文本:
html = ‘‘‘
<div class="wrap">
Hello, World
<p>This is a paragraph.</p>
</div>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
wrap = doc(‘.wrap‘) # 获取满足条件的节点
print(wrap.text()) # 获取所有节点的文本,输出如下所示:
Hello, World
This is a paragraph.

上面的代码输出同时获取了满足条件的所有文本,如果不要p节点内的文本,这时就要用到remove()方法。先选中p节点,再调用remove()方法将其移除。这时wrap内部就只剩下Hello, World这个文本,此时再利用text()方法提取即可。
wrap.find(‘p‘).remove() # 找到wrap内部的p节点并删除
print(wrap.text()) # 获取wrap内部删除p节点后的文本
输出是:Hello, World

此外,还有很多的节点操作方法,如append(),empty(),prepend()等方法,用法与jQuery的用法完全一致。
详细用法参考官方文档:http://pyquery.readthedocs.io/en/latest/api.html。

3.7 伪类选择器
css选择器的强大,还有一个重要原因,就是它支持多种多样的伪类选择器,例如选择第一个节点、最后一个节点、奇偶数节点、包含某一文本的节点等。示例如下:
html = ‘‘‘
<div class="wrap">
<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>
</div>
‘‘‘
from pyquery import PyQuery as pq
doc = pq(html)
li = doc(‘li:first-child‘) # 选择第一个li节点
print(li)
li = doc(‘li:last-child‘) # 选择最后一个li节点
print(li)
li = doc(‘li:nth-child(2)‘) # 选择第二个li节点
print(li)
li = doc(‘li:gt(2)‘) # 选择第三个li节点之后的li节点
print(li)
li = doc(‘li:nth-child(2n)‘) # 选择偶数位置的li节点,注意索引从1开始
print(li)
li = doc(‘li:contains(second)‘) # 包含second文本的li节点
print(li)

输出如下所示:
<li class="item-0">first item</li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

<li class="item-1"><a href="link2.html">second item</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>

<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-1"><a href="link2.html">second item</a></li>

上面使用了CSS3的伪类选择器,依次选择了第一个li节点、最后一个li节点、第二个li节点、第三个li之后的li节
点、偶数位置的li节点、包含second文本的li节点。

CSS选择器的更多用法参数:http://www.w3school.com.cn/css/index.asp。
pyquery官方文档:http://pyquery.readthedocs.io。















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































以上是关于第四部分 解析库的使用(XPathBeautiful SoupPyQuery)的主要内容,如果未能解决你的问题,请参考以下文章

使用 WIFE Java 库的 SWIFT MT 消息解析

python解析HTML之:PyQuery库的介绍与使用

寒假第四次作业

Java中哪个JSON库的解析速度是最快的

python中pyquery库的css选择器实战解析

第四次作业之jieba库的应用