第四部分 解析库的使用(XPath、Beautiful Soup、PyQuery)
在网页节点中,可以定义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对网页的解析过程,代码如下:
1 from lxml import etree 2 text = ''' 3 <div> 4 <ul> 5 <li class="item-0"><a href="link1.html">first item</a></li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-inactive"><a href="link2.html">third item</a></li> 8 <li class="item-1"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a> 10 </ul> 11 </div> 12 ''' 13 html = etree.HTML(text) # 调用HTML类初始化,自动修正HTML文本 14 result = etree.tostring(html) # 输出修正后的HTML代码,是bytes类型 15 print(result.decode("utf-8"))
在上面代码中,首先导人lxml库的etree模块,然后声明了一段HTML文本,调用HTML类进行初始化,这样就成功构造了一个XPath解析对象。注意HTML文本中的最后一个li节点是没有闭合的,但是etree模块可以自动修正HTML文本。
调用tostring()方法即可输出修正后的HTML代码,结果是bytes类型。利用decode()方法将其转成str类型,在输出结果中,经过处理后,li节点的标签被补全,并且还自动添加了body、html节点。输出结果如下:
1 <html><body><div> 2 <ul> 3 <li class="item-0"><a href="link1.html">first item</a></li> 4 <li class="item-1"><a href="link2.html">second item</a></li> 5 <li class="item-inactive"><a href="link2.html">third item</a></li> 6 <li class="item-1"><a href="link4.html">fourth item</a></li> 7 <li class="item-0"><a href="link5.html">fifth item</a> 8 </li></ul> 9 </div> 10 </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的声明,对解析没有任何影响。输出如下所示:
1 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> 2 <html><body><div> 3 <ul> 4 <li class="item-0"><a href="link1.html">first item</a></li> 5 <li class="item-1"><a href="link2.html">second item</a></li> 6 <li class="item-inactive"><a href="link2.html">third item</a></li> 7 <li class="item-1"><a href="link4.html">fourth item</a></li> 8 <li class="item-0"><a href="link5.html">fifth item</a> 9 </li></ul> 10 </div></body></html>
3、 所有节点
可用 // 开头的XPath规则来选取所有符合要求的节点。用前面的HTML文本,如果要选取所有节点,可以这样实现:
1 from lxml import etree 2 html = etree.parse('./test.html', etree.HTMLParser()) 3 result = html.xpath('//*') # *表示选取所有节点 4 print(result) # 输出如下所示: 5 [<Element html at 0x2555e650208>, <Element body at 0x2555e650308>, 6 <Element div at 0x2555e650348>, <Element ul at 0x2555e650388>, 7 <Element li at 0x2555e6503c8>, <Element a at 0x2555e650448>, 8 <Element li at 0x2555e650488>, <Element a at 0x2555e6504c8>, 9 <Element li at 0x2555e650508>, <Element a at 0x2555e650408>, 10 <Element li at 0x2555e650548>, <Element a at 0x2555e650588>, 11 <Element li at 0x2555e6505c8>, <Element a at 0x2555e650608>]
这里使用 * 代表匹配所有节点,也就是整个HTML文本中的所有节点都会被获取。输出返回形式是一个列表,每个元素是Element类型,其后跟了节点的名称,如html、body、div、ul、li、a等,所有节点都包含在列表中。
也可以匹配指定节点名称。如果想获取所有li节点,示例如下:
1 from lxml import etree 2 html = etree.parse('./test.html', etree.HTMLParser()) 3 result = html.xpath('//li') # 匹配所有的li节点 4 print(result) 5 print(result[0]) #输出如下所示: 6 [<Element li at 0x199d8940308>, <Element li at 0x199d8940348>, <Element li at 0x199d8940388>, 7 <Element li at 0x199d89403c8>, <Element li at 0x199d8940408>] 8 <Element li at 0x199d8940308>
要选取所有li节点,可以使用 //,直接加上节点名称即可,调用时直接使用xpath()方法即可。输出结果是列表,每个元素是一个Element对象,要取出某个对象,使用索引即可,如[0]。
4、 子节点
通过 / 或 // 即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接a子节点,可以这样实现:
1 from lxml import etree 2 html = etree.parse('./test.html', etree.HTMLParser()) 3 result = html.xpath('//li/a') # 匹配所有的li节点下的a节点 4 print(result) # 输出如下所示: 5 6 [<Element a at 0x1e2198b0308>, <Element a at 0x1e2198b0348>, 7 <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) # 输出是:['\r\n ']
在输出结果中,没有获取到文本内容,获取到是一个换行符。在上面代码中,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', '\r\n ']
输出的结果有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 运算符及其介绍
关于此表可参考:http://www.w3school.com.cn/xpath/xpath_operators.asp
11、 按序选择
在一些匹配任务中,可能在某些属性会同时匹配多个节点,但是只想要其中的某个节点,如第二个节点或第一个节点,这时可利用中括号传入索引的方法获取特定次序的节点。注意这里的索引是从1开始。代码如下:
1 from lxml import etree 2 3 text = ''' 4 <div> 5 <ul> 6 <li class="item-0"><a href="link1.html">first item</a></li> 7 <li class="item-1"><a href="link2.html">second item</a></li> 8 <li class="item-inactive"><a href="link3.html">third item</a></li> 9 <li class="item-1"><a href="link4.html">fourth item</a></li> 10 <li class="item-0"><a href="link5.html">fifth item</a> 11 </ul> 12 </div> 13 ''' 14 html = etree.HTML(text) 15 result = html.xpath('//li[1]/a/text()') # [1]表示第一个li节点 16 print(result) # 输出:['first item'] 17 result = html.xpath('//li[last()]/a/text()') # 选取最后一个li节点 18 print(result) # 输出:['fifth item'] 19 result = html.xpath('//li[position()<3]/a/text()') # 选取位置(索引)小于3的节点 20 print(result) # 输出:['first item', 'second item'] 21 result = html.xpath('//li[last()-2]/a/text()') # 选取最后一个节点减2的节点 22 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提供了很多节点轴选择方法,包括获取子元素、兄弟元素、父元素、祖先元素等,示例如下:
1 from lxml import etree 2 3 text = ''' 4 <div> 5 <ul> 6 <li class="item-0"><a href="link1.html"><span>first item</span></a></li> 7 <li class="item-1"><a href="link2.html">second item</a></li> 8 <li class="item-inactive"><a href="link3.html">third item</a></li> 9 <li class="item-1"><a href="link4.html">fourth item</a></li> 10 <li class="item-0"><a href="link5.html">fifth item</a> 11 </ul> 12 <a></a> 13 </div> 14 ''' 15 html = etree.HTML(text) 16 result = html.xpath('//li[1]/ancestor::*') 17 print(result) # 第一个li节点的所有父节点 18 result = html.xpath('//li[1]/ancestor::div') 19 print(result) # 第一个li节点的div父节点 20 result = html.xpath('//li[1]/attribute::*') 21 print(result) # 第一个li节点的所有属性 22 result = html.xpath('//li[1]/child::a[@href="link1.html"]') 23 print(result) # 第一个li节点的子节点a且属性是href="link1.html"的子节点 24 result = html.xpath('//li[1]/descendant::span') 25 print(result) # 第一个li节点的所有span子孙节点 26 result = html.xpath('//li[1]/following::*[2]') 27 print(result) # 第一个li节点后的结束标签之后的所有节点,但限定索引为第二个 28 result = html.xpath('//li[1]/following-sibling::*') 29 print(result) # 第一个li节点的所有同级(兄弟)节点 30 输出如下所示: 31 [<Element html at 0x1a26413e2c8>, <Element body at 0x1a26413e248>, <Element div at 0x1a26413e208>, <Element ul at 0x1a26413e308>] 32 [<Element div at 0x1a26413e208>] 33 ['item-0'] 34 [<Element a at 0x1a26413e308>] 35 [<Element span at 0x1a26413e208>] 36 [<Element a at 0x1a26413e308>] 37 [<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轴常用轴选择表
上面是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支持的解析器
通过以上对比可以看出,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的基本用法:
1 html = """ 2 <html><head><title>The Dormouse's story</title></head> 3 <body> 4 <p class="title" name="dromouse"><b>The Dormouse's story</b></p> 5 <p class="story">Once upon a time there were three little sisters; and their names were 6 <a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>, 7 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and 8 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>; 9 and they lived at the bottom of a well.</p> 10 <p class="story">...</p> 11 """ 12 from bs4 import BeautifulSoup as bs 13 soup = bs(html, 'lxml') 14 print(soup.prettify()) 15 print(soup.title.string) 16 17 运行结果如下: 18 <html> 19 <head> 20 <title> 21 The Dormouse's story 22 </title> 23 </head> 24 <body> 25 <p class="title" name="dromouse"> 26 <b> 27 The Dormouse's story 28 </b> 29 </p> 30 <p class="story"> 31 Once upon a time there were three little sisters; and their names were 32 <a class="sister" href="http://example.com/elsie" id="link1"> 33 <!-- Elsie --> 34 </a> 35 , 36 <a class="sister" href="http://example.com/lacie" id="link2"> 37 Lacie 38 </a> 39 and 40 <a class="sister" href="http://example.com/tillie" id="link3"> 41 Tillie 42 </a> 43 ; 44 and they lived at the bottom of a well. 45 </p> 46 <p class="story"> 47 ... 48 </p> 49 </body> 50 </html> 51 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 选择元素
用一个例子详细说明选择元素的方法:
1 html = """ 2 <html><head><title>The Dormouse's story</title></head> 3 <body> 4 <p class="title" name="dromouse"><b>The Dormouse's story</b></p> 5 <p class="story">Once upon a time there were three little sisters; and their names were 6 <a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>, 7 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and 8 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>; 9 and they lived at the bottom of a well.</p> 10 <p class="story">...</p> 11 """ 12 from bs4 import BeautifulSoup as bs 13 soup = bs(html, 'lxml') 14 print(soup.title) # 输出title标签及内容 15 print(type(soup.title)) # 是bs4的标签元素类型 16 print(soup.title.string) # 输出title标签文本内容 17 print(soup.head) # 输出head标签及其下面的子标签 18 print(soup.p) # 输出p标签及其下面的子标签 19 20 输出结果如下: 21 <title>The Dormouse's story</title> 22 <class 'bs4.element.Tag'> 23 The Dormouse's story 24 <head><title>The Dormouse's story</title></head> 25 <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属性,例如:
1 html = """ 2 <html> 3 <head> 4 <title>The Dormouse's story</title> 5 </head> 6 <body> 7 <p class="story"> 8 Once upon a time there were three little sisters; and their names were 9 <a href="http://example.com/elsie" class="sister" id="link1"> 10 <span>Elsie</span> 11 </a> 12 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 13 and 14 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a> 15 and they lived at the bottom of a well. 16 </p> 17 <p class="story">...</p> 18 """ 19 from bs4 import BeautifulSoup as bs 20 soup = bs(html, 'lxml') 21 print(soup.p.contents) 22 23 运行结果如下: 24 ['\n Once upon a time there were three little sisters; and their names were\n ', 25 <a class="sister" href="http://example.com/elsie" id="link1"> 26 <span>Elsie</span> 27 </a>, '\n', <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, '\n and\n ', 28 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, 29 '\n and they lived at the bottom of a well.\n ']
从输出可知,结果是列表形式。在p节点里有文本、节点,输出将它们以列表统一返回。要注意的是,列表中的每个元素都是p节点的直接子节点。比如第一个a节点里面包含一层span节点,这相当于孙节点了,但是返回结果并没有单独把span节点选出来。所以说,contents属性得到的结果是直接子节点的列表。
还可以调用children属性得到相应的结果:
1 from bs4 import BeautifulSoup as bs 2 soup = bs(html, 'lxml') 3 print(soup.p.children) # 结果是一个迭代器类型 4 for i, child in enumerate(soup.p.children): 5 print(i, child) 6 7 运行结果如下: 8 <list_iterator object at 0x0000014EEF1662B0> 9 0 10 Once upon a time there were three little sisters; and their names were 11 12 1 <a class="sister" href="http://example.com/elsie" id="link1"> 13 <span>Elsie</span> 14 </a> 15 2 16 17 3 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> 18 4 19 and 20 21 5 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a> 22 6 23 and they lived at the bottom of a well.
调用children属性选择器,结果是生成器类型。接着用for循环输出相应内容。但子孙节点没有单独选出。要得到所有的子孙节点,可以调用descendants属性:
1 from bs4 import BeautifulSoup 2 soup = BeautifulSoup(html, 'lxml') 3 print(soup.p.descendants) # 选取子孙节点,结果是一个生成器类型 4 for i, child in enumerate(soup.p.descendants): 5 print(i, child) 6 7 运行结果如下: 8 <generator object descendants at 0x000001D287BEC360> 9 0 10 Once upon a time there were three little sisters; and their names were 11 12 1 <a class="sister" href="http://example.com/elsie" id="link1"> 13 <span>Elsie</span> 14 </a> 15 2 16 17 3 <span>Elsie</span> 18 4 Elsie 19 5 20 21 6 22 23 7 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> 24 8 Lacie 25 9 26 and 27 28 10 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a> 29 11 Tillie 30 12 31 and they lived at the bottom of a well.
descendants属性返回的结果仍然是生成器。遍历输出可以看到,这次输出结果包含了span节点。descendants会递归查询所有子孙节点,得到所有的子孙节点。
父节点和祖先节点
调用parent属性可以获取某个节点元素的父节点。例如:
1 html = """ 2 <html> 3 <head> 4 <title>The Dormouse's story</title> 5 </head> 6 <body> 7 <p class="story"> 8 Once upon a time there were three little sisters; and their names were 9 <a href="http://example.com/elsie" class="sister" id="link1"> 10 <span>Elsie</span> 11 </a> 12 </p> 13 <p class="story">...</p> 14 """ 15 from bs4 import BeautifulSoup as bs 16 soup = bs(html, 'lxml') 17 print(soup.a.parent) 18 19 运行结果如下: 20 <p class="story"> 21 Once upon a time there were three little sisters; and their names were 22 <a class="sister" href="http://example.com/elsie" id="link1"> 23 <span>Elsie</span> 24 </a> 25 </p>
上面代码中选择的是第一个a节点的父节点元素。它的父节点是p节点,输出结果是p节点及其内部的内容。parent属性仅仅是找某个节点的直接父节点,没有继续向外层寻找父节点的祖先节点。要获取祖先节点,就调用parents属性:
1 html = """ 2 <html> 3 <body> 4 <p class="story"> 5 <a href="http://example.com/elsie" class="sister" id="link1"> 6 <span>Elsie</span> 7 </a> 8 </p> 9 """ 10 from bs4 import BeautifulSoup as bs 11 soup = bs(html, 'lxml') 12 print(soup.a.parents) # 结果是一个生成器类型 13 print(list(enumerate(soup.p.parents))) 14 15 运行结果如下: 16 <generator object parents at 0x0000017E847CC360> 17 [(0, <body> 18 <p class="story"> 19 <a class="sister" href="http://example.com/elsie" id="link1"> 20 <span>Elsie</span> 21 </a> 22 </p> 23 </body>), 24 (1, <html> 25 <body> 26 <p class="story"> 27 <a class="sister" href="http://example.com/elsie" id="link1"> 28 <span>Elsie</span> 29 </a> 30 </p> 31 </body></html>), (2, <html> 32 <body> 33 <p class="story"> 34 <a class="sister" href="http://example.com/elsie" id="link1"> 35 <span>Elsie</span> 36 </a> 37 </p> 38 </body></html>)]
输出可知,parents返回结果也是生成器类型。这里用列表输出它的索引和内容,列表中的元素就是a节点的祖先节点。
兄弟节点
前面的方法是获取当前节点的子节点或者父节点。如要获取当前节点的同级节点(兄弟节点),可使用next_sibling、
previous_siblings等属性。例如:
1 html = """ 2 <html> 3 <body> 4 <p class="story"> 5 Once upon a time there were three little sisters; and their names were 6 <a href="http://example.com/elsie" class="sister" id="link1"> 7 <span>Elsie</span> 8 </a> 9 Hello 10 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 11 and 12 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a> 13 and they lived at the bottom of a well. 14 </p> 15 """ 16 from bs4 import BeautifulSoup as bs 17 soup = bs(html, 'lxml') 18 print('Next Sibling', soup.a.next_sibling) 19 print('Prev Sibling', soup.a.previous_sibling) 20 print('Next Siblings', list(enumerate(soup.a.next_siblings))) 21 print('Prev Siblings', list(enumerate(soup.a.previous_siblings))) 22 23 输出如下所示: 24 Next Sibling 25 Hello 26 27 Prev Sibling 28 Once upon a time there were three little sisters; and their names were 29 30 Next Siblings [(0, '\n Hello\n '), 31 (1, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>), 32 (2, '\n and\n '), 33 (3, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>), 34 (4, '\n and they lived at the bottom of a well.\n ')] 35 Prev Siblings [(0, '\n Once upon a time there were three little sisters; and their names were\n ')]
在上面代码中,调用了4个属性,其中next_sibling和previous_sibling分别获取节点的下一个和上一个兄弟元素,next_siblings和previous_siblings则分别获取所有后面和前面的兄弟节点的生成器。
提取信息
要获取相关节点的一些信息,比如文本、属性等,也可用同样的方法。例如:
1 html = """ 2 <html> 3 <body> 4 <p class="story"> 5 Once upon a time there were three little sisters; and their names were 6 <a href="http://example.com/elsie" class="sister" id="link1">Bob</a><a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 7 </p> 8 """ 9 from bs4 import BeautifulSoup as bs 10 soup = bs(html, 'lxml') 11 print('Next Sibling:') 12 print(type(soup.a.next_sibling)) # 单个节点类型是bs4.element.Tag 13 print(soup.a.next_sibling) # 输出下一个兄弟节点 14 print(soup.a.next_sibling.string) # 获取下一个兄弟节点的文本 15 print('Parent:') 16 print(type(soup.a.parents)) # 第一个a节点的所有父节点类型是一个生成器类型,注意和单个节点类型区分 17 print(list(soup.a.parents)[0]) # 需要先将生成类型转换为列表方可获取元素。获取第一个 18 print(list(soup.a.parents)[0].attrs['class']) # 获取第一个元素的class属性值,因class属性值可以有多个,所以结果是列表。 19 20 运行结果如下: 21 Next Sibling: 22 <class 'bs4.element.Tag'> 23 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> 24 Lacie 25 Parent: 26 <class 'generator'> 27 <p class="story"> 28 Once upon a time there were three little sisters; and their names were 29 <a class="sister" href="http://example.com/elsie" id="link1">Bob</a><a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> 30 </p> 31 ['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节点,示例如下:
1 html=''' 2 <div class="panel"> 3 <div class="panel-heading"> 4 <h4>Hello</h4> 5 </div> 6 <div class="panel-body"> 7 <ul class="list" id="list-1"> 8 <li class="element">Foo</li> 9 <li class="element">Bar</li> 10 <li class="element">Jay</li> 11 </ul> 12 <ul class="list list-small" id="list-2"> 13 <li class="element">Foo</li> 14 <li class="element">Bar</li> 15 </ul> 16 </div> 17 </div> 18 ''' 19 from bs4 import BeautifulSoup as bs 20 soup = bs(html, 'lxml') 21 print(soup.find_all(name='ul')) # 查找所有的ul节点 22 print(type(soup.find_all(name='ul')[0])) # 类型是bs4.element.Tag类型 23 24 运行结果如下所示: 25 [<ul class="list" id="list-1"> 26 <li class="element">Foo</li> 27 <li class="element">Bar</li> 28 <li class="element">Jay</li> 29 </ul>, <ul class="list list-small" id="list-2"> 30 <li class="element">Foo</li> 31 <li class="element">Bar</li> 32 </ul>] 33 <class 'bs4.element.Tag'>
这里调用find_all()方法,传入参数name,参数值是ul。这样可查询所有ul节点,返回结果是列表类型,长度为2。每个元素依然是bs4.element.Tag类型。
由于还是Tag类型,还可以进行嵌套查询。下面查询出所有ul节点后,再查询其内部的li节点:
1 for ul in soup.find_all(name='ul'): 2 print(ul.find_all(name='li')) 3 输出如下所示: 4 [<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>] 5 [<li class="element">Foo</li>, <li class="element">Bar</li>]
输出结果是2个列表,列表的每个元素还是Tag类型。再一次遍历每个li,就可获取它的文本:
1 for ul in soup.find_all(name='ul'): 2 print(ul.find_all(name='li')) 3 for li in ul.find_all(name='li'): 4 print(li.string) 5 输出如下所示: 6 [<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>] 7 Foo 8 Bar 9 Jay 10 [<li class="element">Foo</li>, <li class="element">Bar</li>] 11 Foo 12 Bar
attrs参数:在做查询时,还可以用属性来查询,如attrs={'id':'i1'},示例如下:
1 html=''' 2 <div class="panel"> 3 <div class="panel-heading"> 4 <h4>Hello</h4> 5 </div> 6 <div class="panel-body"> 7 <ul class="list" id="list-1" name="elements"> 8 <li class="element">Foo</li> 9 <li class="element">Bar</li> 10 <li class="element">Jay</li> 11 </ul> 12 <ul class="list list-small" id="list-2"> 13 <li class="element">Foo</li> 14 <li class="element">Bar</li> 15 </ul> 16 </div> 17 </div> 18 ''' 19 from bs4 import BeautifulSoup as bs 20 soup = bs(html, 'lxml') 21 print(soup.find_all(attrs={'id': 'list-1'})) 22 print(soup.find_all(attrs={'name': 'elements'})) 23 24 运行结果如下: 25 [<ul class="list" id="list-1" name="elements"> 26 <li class="element">Foo</li> 27 <li class="element">Bar</li> 28 <li class="element">Jay</li> 29 </ul>] 30 [<ul class="list" id="list-1" name="elements"> 31 <li class="element">Foo</li> 32 <li class="element">Bar</li> 33 <li class="element">Jay</li> 34 </ul>]
这里查询时用的是attrs参数,参数的类型是字典。如传入attrs={'id': 'list-1'}表示查询id为list-1的节点,结果是列表形式,内容就是符合id为list-1的所有节点。在这里符合条件的元素个数是1,所以结果是长度为1的列表。
对于常用属性,如id和class等,可不用attrs来传递,可以直接传入id这个参数。用上面上HTML代码换种方式查询:
1 from bs4 import BeautifulSoup as bs 2 soup = bs(html, 'lxml') 3 print(soup.find_all(id='list-1')) 4 print(soup.find_all(class_='element')) 5 6 输出如下所示: 7 [<ul class="list" id="list-1" name="elements"> 8 <li class="element">Foo</li> 9 <li class="element">Bar</li> 10 <li class="element">Jay</li> 11 </ul>] 12 [<li class="element">Foo</li>, <li class="element">Bar</li>, 13 <li class="element">Jay</li>, <li class="element">Foo</li>, 14 <li class="element">Bar</li>]
直接传入id='list-1',可查询id为list-1的节点元素。对于class来说,由于class在Python里是一个关键字,所以后面需要加一个下划线,即class_='element',返回的结果依然还是Tag组成的列表。
text参数:可匹配节点的文本,传入的形式可以是字符串,可以是正则表达式对象,示例如下:
1 import re 2 html=''' 3 <div class="panel"> 4 <div class="panel-body"> 5 <a>Hello, this is a link</a> 6 <a>Hello, this is a link, too</a> 7 </div> 8 </div> 9 ''' 10 from bs4 import BeautifulSoup as bs 11 soup = bs(html, 'lxml') 12 print(soup.find_all(text=re.compile('link'))) 13 输出如下所示: 14 ['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参数选项。例如:
1 html=''' 2 <div class="panel"> 3 <div class="panel-heading"> 4 <h4>Hello</h4> 5 </div> 6 <div class="panel-body"> 7 <ul class="list" id="list-1"> 8 <li class="element">Foo</li> 9 <li class="element">Bar</li> 10 <li class="element">Jay</li> 11 </ul> 12 <ul class="list list-small" id="list-2"> 13 <li class="element">Foo</li> 14 <li class="element">Bar</li> 15 </ul> 16 </div> 17 </div> 18 ''' 19 from bs4 import BeautifulSoup as bs 20 soup = bs(html, 'lxml') 21 print(soup.find(name='ul')) # 查找第一个ul节点 22 print(type(soup.find(name='ul'))) # 查看类型,仍然是:bs4.element.Tag类型 23 print(soup.find(class_='list')) # 查找第一个class属性为list的节点 24 25 运行结果如下所示: 26 <ul class="list" id="list-1"> 27 <li class="element">Foo</li> 28 <li class="element">Bar</li> 29 <li class="element">Jay</li> 30 </ul> 31 <class 'bs4.element.Tag'> 32 <ul class="list" id="list-1"> 33 <li class="element">Foo</li> 34 <li class="element">Bar</li> 35 <li class="element">Jay</li> 36 </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选择器即可。例如下面示例:
1 html=''' 2 <div class="panel"> 3 <div class="panel-heading"> 4 <h4>Hello</h4> 5 </div> 6 <div class="panel-body"> 7 <ul class="list" id="list-1"> 8 <li class="element">Foo</li> 9 <li class="element">Bar</li> 10 <li class="element">Jay</li> 11 </ul> 12 <ul class="list list-small" id="list-2"> 13 <li class="element">Foo</li> 14 <li class="element">Bar</li> 15 </ul> 16 </div> 17 </div> 18 ''' 19 from bs4 import BeautifulSoup as bs 20 soup = bs(html, 'lxml') 21 print(soup.select('.panel .panel-heading')) # 层级选择器 22 print(soup.select('ul li')) # 层级选择器,ul节点下的所有li节点 23 print(soup.select('#list-2 .element')) 24 print(type(soup.select('ul')[0])) # 仍然是:bs4.element.Tag类型 25 26 输出如下所示: 27 [<div class="panel-heading"> 28 <h4>Hello</h4> 29 </div>] 30 [<li class="element">Foo</li>, <li class="element">Bar</li>, 31 <li class="element">Jay</li>, <li class="element">Foo</li>, 32 <li class="element">Bar</li>] 33 [<li class="element">Foo</li>, <li class="element">Bar</li>] 34 <class 'bs4.element.Tag'>
在上面代码使用了3次CSS选择器,输出结果均是符合CSS选择器节点组成的列表。最后一个输出类型仍然是bs4.element.Tag类型
2.5.1 嵌套选择
select()方法支持嵌套选择。例如,可先选择全部的ul节点,遍历每个ul节点,选择其下的li节点。例如:
1 from bs4 import BeautifulSoup as bs 2 soup = bs(html, 'lxml') 3 for ul in soup.select('ul'): # 先遍历所有的ul节点 4 print(ul.select('li')) # 输出每个li节点 5 输出如下所示: 6 [<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>] 7 [<li class="element">Foo</li>, <li class="element">Bar</li>]
2.5.2 获取属性
由于节点类型是Tag类型,获取属性还可以用原来的方法。例如要获取每个li节点的id属性,可以直接使用中括号传入属性名(例:ul['id']),也可使用attrs属性方式(例:ul.attrs['id'])。如下面代码所示:
1 from bs4 import BeautifulSoup as bs 2 soup = bs(html, 'lxml') 3 for ul in soup.select('ul'): 4 print(ul['id']) # 直接使用中括号获取属性 5 print(ul.attrs['id']) # 通过attrs获取属性 6 输出如下所示: 7 list-1 8 list-1 9 list-2 10 list-2
2.5.3 获取文本
除了可用string属性外,还可以用get_text()方法。例如获取li节点的文本可用:li.get_text()。
1 from bs4 import BeautifulSoup as bs 2 soup = bs(html, 'lxml') 3 for li in soup.select('li'): 4 print("Get Text:", li.get_text()) 5 print("String:", li.string) 6 输出如下所示: 7 Get Text: Foo 8 String: Foo 9 Get Text: Bar 10 String: Bar 11 Get Text: Jay 12 String: Jay 13 Get Text: Foo 14 String: Foo 15 Get Text: Bar 16 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节点,例如:
1 html = ''' 2 <div> 3 <ul> 4 <li class="item-0">first item</li> 5 <li class="item-1"><a href="link2.html">second item</a></li> 6 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 7 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 8 <li class="item-0"><a href="link5.html">fifth item</a></li> 9 </ul> 10 </div> 11 ''' 12 from pyquery import PyQuery as pq 13 doc = pq(html) 14 print(doc('li')) # 输出如下所示: 15 <li class="item-0">first item</li> 16 <li class="item-1"><a href="link2.html">second item</a></li> 17 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 18 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 19 <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选择器用法:
1 html = ''' 2 <div id="container"> 3 <ul class="list"> 4 <li class="item-0">first item 成都</li> 5 <li class="item-1"><a href="link2.html">second item</a></li> 6 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 7 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 8 <li class="item-0"><a href="link5.html">fifth item</a></li> 9 </ul> 10 </div> 11 ''' 12 from pyquery import PyQuery as pq 13 doc = pq(html) 14 print(doc('#container .list li')) 15 print(type(doc('#container .list li'))) 16 输出如下所示: 17 <li class="item-0">first item 成都</li> 18 <li class="item-1"><a href="link2.html">second item</a></li> 19 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 20 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 21 <li class="item-0"><a href="link5.html">fifth item</a></li> 22 <class 'pyquery.pyquery.PyQuery'>
上面代码中初始化PyQuery对象后,传入一个CSS选择器#container .list li,该选择器的意思是先取id为container的节点,然后再选取其内部的class为list的节点内部的所有li节点。第二个输出可知类型是PyQuery类型。
3.3 查找节点
可以查找子节点、父节点、兄弟节点等,对应着相关的查询函数,使用方法与jQuery中函数用法相同。
3.3.1 子节点
用find()方法查找子节点,参数是CSS选择器。以前面的HTML为例:
1 from pyquery import PyQuery as pq 2 doc = pq(html) 3 items = doc('.list') 4 print(type(items)) # 输出是PyQuery对象 5 print(items) 6 lis = items.find('li') 7 print(type(lis)) # 输出是PyQuery对象 8 print(lis) 9 输出如下所示: 10 <class 'pyquery.pyquery.PyQuery'> 11 <ul class="list"> 12 <li class="item-0">first item 成都</li> 13 <li class="item-1"><a href="link2.html">second item</a></li> 14 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 15 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 16 <li class="item-0"><a href="link5.html">fifth item</a></li> 17 </ul> 18 19 <class 'pyquery.pyquery.PyQuery'> 20 <li class="item-0">first item 成都</li> 21 <li class="item-1"><a href="link2.html">second item</a></li> 22 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 23 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 24 <li class="item-0"><a href="link5.html">fifth item</a></li>
首先,选取class为list的节点,调用find()方法,传入css选择器,选取其内部的li节点,最后打印输出。输出中find()方法将符合条件的所有节点选择出来,结果的类型是PyQuery类型。
find()查找范围是节点的所有子孙节点,只想查找子节点,可以用children()方法:
1 lis = items.children() 2 print(type(lis)) # 输出是PyQuery对象 3 print(lis) # 输出如下所示: 4 <class 'pyquery.pyquery.PyQuery'> 5 <li class="item-0">first item 成都</li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 8 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a></li>
还可以向children()方法传入CSS选择器,做进一步筛选。如选出子节点中class为active的节点:
1 lis = items.children('.active') 2 print(lis) # 输出如下所示: 3 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 4 <li class="item-1 active"><a href="link4.html">fourth item</a></li>
3.3.2 父节点和祖先节点
用parent()方法可以获取某个节点的直接父节点,用parents()方法可以获取某个节点的祖先节点。例如:
1 html = ''' 2 <div class="wrap"> 3 <div id="container"> 4 <ul class="list"> 5 <li class="item-0">first item</li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 8 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a></li> 10 </ul> 11 </div> 12 </div> 13 ''' 14 from pyquery import PyQuery as pq 15 doc = pq(html) 16 items = doc('.list') 17 print('以下是直接父节点') 18 container = items.parent() # 找直接父节点 19 print(type(container)) # 类型是PyQuery类型 20 print(container) 21 print('以下是祖先节点') 22 parents = items.parents() # 查找祖先节点 23 print(type(parents)) # 类型是PyQuery类型 24 print(parents) 25 26 输出如下所示: 27 以下是直接父节点 28 <class 'pyquery.pyquery.PyQuery'> 29 <div id="container"> 30 <ul class="list"> 31 <li class="item-0">first item</li> 32 <li class="item-1"><a href="link2.html">second item</a></li> 33 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 34 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 35 <li class="item-0"><a href="link5.html">fifth item</a></li> 36 </ul> 37 </div> 38 以下是祖先节点 39 <class 'pyquery.pyquery.PyQuery'> 40 <div class="wrap"> 41 <div id="container"> 42 <ul class="list"> 43 <li class="item-0">first item</li> 44 <li class="item-1"><a href="link2.html">second item</a></li> 45 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 46 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 47 <li class="item-0"><a href="link5.html">fifth item</a></li> 48 </ul> 49 </div> 50 </div><div id="container"> 51 <ul class="list"> 52 <li class="item-0">first item</li> 53 <li class="item-1"><a href="link2.html">second item</a></li> 54 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 55 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 56 <li class="item-0"><a href="link5.html">fifth item</a></li> 57 </ul> 58 </div>
在祖先节点的输出中有两个:一个是class为wrap的节点,一个是id为container的节点。parents()方法会返回所有的祖先节点。要单独提取某个祖先节点,可向parents()方法传入CSS选择器,只查找满足CSS选择器的节点。例如:
parent = items.parents('.wrap')
print(parent) # 输出省略,参考上面的输出
3.3.3 兄弟节点
使用siblings()方法可以查找某个节点的所有兄弟节点。向siblings()方法传入CSS选择器,可以查找特定条件的兄弟节点。使用前面的HTML代码为例:
1 from pyquery import PyQuery as pq 2 doc = pq(html) 3 li = doc('.list .item-0.active') # 查找满足“.list .item-0.active”这个条件的兄弟节点,有4个 4 print("查找满足‘.list .item-0.active’这个条件的兄弟节点") 5 print(li.siblings()) 6 print("在兄弟节点中找有class属性为active属性的节点") 7 print(li.siblings(".active")) 8 9 运行结果如下: 10 查找满足‘.list .item-0.active’这个条件的兄弟节点 11 <li class="item-1"><a href="link2.html">second item</a></li> 12 <li class="item-0">first item</li> 13 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 14 <li class="item-0"><a href="link5.html">fifth item</a></li> 15 在兄弟节点中找有class属性为active属性的节点 16 <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='\n')
输出结果如下所示:
<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()方法获取属性:
1 html = ''' 2 <div class="wrap"> 3 <div id="container"> 4 <ul class="list"> 5 <li class="item-0">first item</li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 8 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a></li> 10 </ul> 11 </div> 12 </div> 13 ''' 14 from pyquery import PyQuery as pq 15 doc = pq(html) 16 a = doc('.item-0.active a') # 获取满足条件的a节点 17 print(a, type(a), sep=',') 18 print(a.attr('href')) 19 20 输出如下所示: 21 <a href="link3.html"><span class="bold">third item</span></a>, <class 'pyquery.pyquery.PyQuery'> 22 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()方法。例如下面代码所示:
1 html = ''' 2 <div class="wrap"> 3 <div id="container"> 4 <ul class="list"> 5 <li class="item-0">first item</li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 8 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a></li> 10 </ul> 11 </div> 12 </div> 13 ''' 14 from pyquery import PyQuery as pq 15 doc = pq(html) 16 a = doc('.item-0.active a') 17 print(a) 18 print(a.text()) 19 20 输出如下所示: 21 <a href="link3.html"><span class="bold">third item</span></a> 22 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
通过下面的实例进行理解:
1 html = ''' 2 <div class="wrap"> 3 <div id="container"> 4 <ul class="list"> 5 <li class="item-0">first item</li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 8 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a></li> 10 </ul> 11 </div> 12 </div> 13 ''' 14 from pyquery import PyQuery as pq 15 doc = pq(html) 16 li = doc('.item-0.active') # 选取满足条件的节点,选取到的是第三个li节点 17 print(li) 18 li.removeClass('active') # 删除class属性值active 19 print(li) 20 li.addClass('active') # 添加class属性值active 21 print(li) 22 23 输出结果如下所示: 24 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 25 <li class="item-0"><a href="link3.html"><span class="bold">third item</span></a></li> 26 <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()方法改变节点内部的内容。
示例如下:
1 html = ''' 2 <ul class="list"> 3 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 4 </ul> 5 ''' 6 from pyquery import PyQuery as pq 7 doc = pq(html) 8 li = doc('.item-0.active') 9 print(li) 10 li.attr('name', 'link') # 参数依次是属性名和属性值 11 print(li) 12 li.text('changed item') # 修改节点内部的文本 13 print(li) 14 li.html('<span>changed item</span>') # 修改节点内部的html文本 15 print(li) 16 17 输出结果如下所示: 18 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 19 <li class="item-0 active" name="link"><a href="link3.html"><span class="bold">third item</span></a></li> 20 <li class="item-0 active" name="link">changed item</li> 21 <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文本:
1 html = ''' 2 <div class="wrap"> 3 Hello, World 4 <p>This is a paragraph.</p> 5 </div> 6 ''' 7 from pyquery import PyQuery as pq 8 doc = pq(html) 9 wrap = doc('.wrap') # 获取满足条件的节点 10 print(wrap.text()) # 获取所有节点的文本,输出如下所示: 11 Hello, World 12 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选择器的强大,还有一个重要原因,就是它支持多种多样的伪类选择器,例如选择第一个节点、最后一个节点、奇偶数节点、包含某一文本的节点等。示例如下:
1 html = ''' 2 <div class="wrap"> 3 <div id="container"> 4 <ul class="list"> 5 <li class="item-0">first item</li> 6 <li class="item-1"><a href="link2.html">second item</a></li> 7 <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> 8 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 9 <li class="item-0"><a href="link5.html">fifth item</a></li> 10 </ul> 11 </div> 12 </div> 13 ''' 14 from pyquery import PyQuery as pq 15 doc = pq(html) 16 li = doc('li:first-child') # 选择第一个li节点 17 print(li) 18 li = doc('li:last-child') # 选择最后一个li节点 19 print(li) 20 li = doc('li:nth-child(2)') # 选择第二个li节点 21 print(li) 22 li = doc('li:gt(2)') # 选择第三个li节点之后的li节点 23 print(li) 24 li = doc('li:nth-child(2n)') # 选择偶数位置的li节点,注意索引从1开始 25 print(li) 26 li = doc('li:contains(second)') # 包含second文本的li节点 27 print(li) 28 29 输出如下所示: 30 <li class="item-0">first item</li> 31 32 <li class="item-0"><a href="link5.html">fifth item</a></li> 33 34 <li class="item-1"><a href="link2.html">second item</a></li> 35 36 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 37 <li class="item-0"><a href="link5.html">fifth item</a></li> 38 39 <li class="item-1"><a href="link2.html">second item</a></li> 40 <li class="item-1 active"><a href="link4.html">fourth item</a></li> 41 42 <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。