爬虫系列-解析库

概述

在前面的实例中,我们采用正则表达式来提取相关的信息,但正则表达式过于复杂,容易写错,一旦写错就可能匹配不到我们想要的东西。所以这次博客我将介绍另一种提取信息的方法-解析库。
对于网页的节点来说,它可以定义id、class或其他属性。而且节点之间还有层次关系,在网页中可以通过XPath或CSS选择器来定位一个或多个节点。那么,在页面解析时,利用XPath或CSS选择器来提取某个节点,然后再调用相应方法获取它的正文内容或者属性,不就可以提取我们想要的任意信息。
python中里面有很多强大的解析库,其中常用的有lxml、Beautiful Soup、pyquery等,但我常用的主要的是lxml、Beautiful Soup,这篇博客主要讲的也是这两种,第三种待我后续有需要的时候,我会专门来写一篇。

1.Xpath

XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言。它最初是用来搜寻XML文档的,但是它同样适用于HTML文档的搜索。在我们看来它就是把网页当做一张地图,然后需求的信息当做地图上某一个,然后通过搜索定位来找到相应的信息。
所以在做爬虫时,我们完全可以使用XPath来做相应的信息抽取。首先我们就来介绍XPath的基本用法。

xpath常用规则

下面我列出几个常用的匹配规则:

nodenam  选取此节点的所有子节点
/        从当前节点选取直接子节点
//       从当前节点选取子孙节点
.        选取当前节点
..       选取当前节点的父节点
@        选取属性

我举个具体例子让大家看看怎么写xpath:

//title[@class='item']

这个xpath规则代表选择的是所有名称为title,同时属性class的值为item的节点。
看完这上面大致有了个了解吧,我们下面就可以通过python的解析库来实践一下了。

示例

python中常用来作为xpath解析库的是lxml库。下面我声明一个HTML文本来做测试

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 = etree.tostring(html)
print(result.decode('utf-8'))

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

这里我们调用tostring()方法即可输出修正后的HTML代码,但是结果是bytes类型。这里利用decode()方法将其转成str类型,其实这个一般在大多数网页解析是不存在的,网页一般都是完整的。但我们还是可以看看修正之后的结果:

<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="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>
     </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'))

运行结果,我就不想展示了,就是一段HTML文本而已,然后和上面多一个声明,不过对解析没任何影响。

所有节点

还是上面的HTML文本,我们要选取所有节点,可以这样来实现

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//*')
print(result1)

我们可以看一下返回的结果:

[<Element html at 0x162425e3e48>, <Element body at 0x1624272ad48>, <Element div at 0x162426ad508>, <Element ul at 0x16242758488>, <Element li at 0x16242758288>, <Element a at 0x16242758848>, <Element li at 0x16242758888>, <Element a at 0x162427588c8>, <Element li at 0x16242758908>, <Element a at 0x16242758808>, <Element li at 0x16242758948>, <Element a at 0x16242758988>, <Element li at 0x162427589c8>, <Element a at 0x16242758a08>]

我们用*代表匹配所有的节点,也就是整个HTML文本中所有节点都会获取,我们可以看到返回的是一个列表形式。其实所以节点都包括在列表中。

指定节点

在刚才那个匹配,我们也可以指定节点名称,比如获取所有的a标签

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//a')
print(result1)

输出结果为:

[<Element a at 0x16242758c48>, <Element a at 0x16242758c88>, <Element a at 0x16242758cc8>, <Element a at 0x16242758d08>, <Element a at 0x16242758d48>]
子节点

我们通过/或//即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接a子节点,具体实现:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li/a')
print(result1)

然后输出的也是所有直接a字节点,//li用于选中的所有的li节点,/a用于选中li节点的所有直接子节点a,二者组合在一起所有直接子节点a,
此处的/用于选取直接子节点,如果要获取所有子孙节点,就可以使用//。例如,要获取ul节点下的所有子孙a节点,可以这样实现:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li//a')
print(result1)

运行结果和上面是相同的。但如果我们采用//ul/a就没结果,因为ul下面没有直接的子节点。
所以我们平时在用的时候要注意//和/的区别,其中/用于获取直接子节点,//用于获取子孙节点。

父节点

我们知道连续的/和//可以查找子节点或子孙节点,我们知道子节点希望获取其父节点可以通过…来实现。
比如下面这个例子,我们希望获取到a节点的父亲节点,并获取到它的class属性,其实,相关代码如下:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//a[@href="link4.html"]/../@class')
print(result1)

运行结果如下:

['item-1']

刚好达到我们的要求,我们还可以通过parent::来获取父节点,代码如下:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//a[@href="link4.html"]/parent::*/@class')
print(result1)

结果也是一样的。

属性匹配

在选取的时候,我们还可以用@符号进行属性过滤。比如,这里如果要选取class为item-1的li节点,可以这样实现:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li[@class="item-0"]')
print(result1)

输出的结果是:

[<Element li at 0x20fc35e9948>, <Element li at 0x20fc35e9308>]

匹配结果正是两个,刚好符合我们的要求。

文本获取

我们用XPath中的text()方法获取节点中的文本,接下来尝试获取前面li节点中的文本,相关代码如下:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li[@class="item-0"]/text()')
print(result1)

但是我们发现输出的结果只有一个换行符,因为li的直接子节点是a标签,而li标签中的文本信息只有/n,但是我们如果还是想要其中的文本应该这样写:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li[@class="item-0"]/a/text()')
print(result1)

输出结果刚好达到了我们的要求

['first item', 'fifth item']

我们还可以采用另一种方式写,也就是//与/的区别,我们可以来看一下:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li[@class="item-0"]//text()')
print(result1)

但是这样也存在一个问题,结果不是很整洁,包括了换行那一些字符,如果想要数据比较简洁,推荐使用第一种方法,不然那结果可能类似是这样的

['first item', 'fifth item', '\n     ']
属性获取

比如有的时候,我们在爬取网页相关的链接的时候,url的链接可能在某个标签中的href属性中,这个时候我们就需要提取属性值了,我们可以看看怎么用,还是用上一个为例子:

from lxml import etree
html = etree.HTML(result)
result1 = html.xpath('//li/a/@href')
print(result1)

结果是:

['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

这样我们就实现了提取多个属性值了。

属性多值匹配

有的时候,某些节点的某个属性可能有多搁置,我们要用到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']

这种方法适合某个节点有多个属性值时经常用到。

多属性匹配

有的时候我们可能会遇到根据多个属性确定一个节点,但单独一个属性我们可能匹配不到,所以我们需要同时匹配多个属性,我们可以用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节点包括两个属性,我们要确定这个节点需要两个属性匹配。
这里面and是xpath中的运算符,下面有很多运算符,我介绍几个常用的:

or  或   age=19 or age=20   如果age是19,则返回true。如果age是21,则返回false
and  与   age>19 and age<21  如果age是20,则返回true。如果age是18,则返回false
|  计算两个节点集    //book | //cd 返回所有拥有book和cd元素的节点集

想了解更多可以查看官方文档。

按序选择

有时候我们在选择某些属性可能同时匹配多个节点,但是只想要其中的某个节点,如果只想要最后一个或者其他节点该怎么办?比如找下一页的标签时候,我们可能需要定位最后一个,这个时候就需要按序选择。

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()')
print(result)
result = html.xpath('//li[last()]/a/text()')
print(result)
result = html.xpath('//li[position()<3]/a/text()')
print(result)
result = html.xpath('//li[last()-2]/a/text()')
print(result)

运行结果是:

['first item']
['fifth item']
['first item', 'second item']
['third item']

第一次选择时,我们选择了第一个li节点,括号中传入1就行啦,注意区分这个不是0开头和列表等不同。
最后一个一般用last()来表示。
倒数第二个可以用last()-1来表示。
假如我们选择位置小于3的节点,可以用position()<3来表示。

补充用法

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>
 </div>
'''
html = etree.HTML(text)
result = html.xpath('//li[1]/ancestor::*')
print(result)
result = html.xpath('//li[1]/ancestor::div')
print(result)
result = html.xpath('//li[1]/attribute::*')
print(result)
result = html.xpath('//li[1]/child::a[@href="link1.html"]')
print(result)
result = html.xpath('//li[1]/descendant::span')
print(result)
result = html.xpath('//li[1]/following::*[2]')
print(result)
result = html.xpath('//li[1]/following-sibling::*')

运行结果是:

[<Element html at 0x24aa34ea448>, <Element body at 0x24aa3500e48>, <Element div at 0x24aa351d248>, <Element ul at 0x24aa351d308>]
[<Element div at 0x24aa351d248>]
['item-0']
[<Element a at 0x24aa351d348>]
[<Element span at 0x24aa351d148>]
[<Element a at 0x24aa3500e48>]

1.ancestor轴,可以获取所有祖先节点。其后需要跟两个冒号,然后是节点的选择器,这里我们直接使用*,表示匹配所有节点,因此返回结果是第一个li节点的所有祖先节点,包括html、body、div和ul。
2.attribute轴,可以获取所有属性值,其后跟的选择器还是*,这代表获取节点的所有属性,返回值就是li节点的所有属性值。
3.child轴,可以获取所有直接子节点。这里我们又加了限定条件,选取href属性为link1.html的a节点。
4.descendant轴,可以获取所有子孙节点。这里我们又加了限定条件获取span节点,所以返回的结果只包含span节点而不包含a节点。
5.following轴,可以获取当前节点之后的所有节点。这里我们虽然使用的是匹配,但又加了索引选择,所以只获取了第二个后续节点。
6.following-sibling轴,可以获取当前节点之后的所有同级节点。这里我们使用
匹配,所以获取了所有后续同级节点。
如果想了解更多关于xpath语法,我们可以去官网查阅。

2.Beautiful Soup

上面介绍了一种通过XPath选择器来定位一个或多个节点。那么,在页面解析时,利用XPath选择器来提取某个节点,然后再调用相应方法获取它的正文内容或者属性,不就可以提取我们想要的任意信息。这里我们介绍另外一种相似的通过网页的的特殊结构和层级关系等,下面我介绍另外一种解析工具Beautiful Soup。

概述

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间.(这段话我引用的是官方文档的)

解析器

Beautiful Soup在解析时实际上依赖解析器,它除了支持Python标准库中的HTML解析器外,还支持一些第三方解析器(比如lxml)。下面我列出Beautiful Soup支持的几种解析器。

Python标准库      BeautifulSoup(markup, "html.parser")                 Python的内置标准库、执行速度适中、文档容错能力强         Python 2.7.3及Python 3.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即可,其他都是一致的。
后面我也是以这个解析器为参考。
下面看看具体用法,我们就能了解大概怎么用了。

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
soup = BeautifulSoup(html, 'lxml')
print(soup.prettify())
print(soup.title.string)

输出结果一个是修正后的完整的上面HTML文本,并且以标准缩进输出和一个titile标签中的文本,但prettify()这个方法可以把要解析的字符串以标准的缩进格式输出。这里需要注意的是,输出结果里面包含body和html节点,也就是说对于不标准的HTML字符串BeautifulSoup,可以自动更正格式。这一步不是由prettify()方法做的,而是在初始化BeautifulSoup时就完成了。
然后调用soup.title.string,这实际上是输出HTML中title节点的文本内容。所以,soup.title可以选出HTML中的title节点,再调用string属性就可以得到里面的文本了。

节点选择器
选择元素

还是以上面那段HTML文本为例,下面我来直接调用节点名称就可以选择节点元素,甚至提取元素,这种选择方式速度非常快,可以用这种方法怎么用:

soup = BeautifulSoup(html, 'lxml')
print(soup.title)#打印title节点的选择结果
print(type(soup.title))#打印title节点的类型
print(soup.title.string)#打印title节点中的文本
print(soup.p)#选择p标签,但只能选择第一个匹配到的内容,后面的都会忽略
print(soup.head)

返回结果是:

<title>The Dormouse's story</title>
<class 'bs4.element.Tag'>
The Dormouse's story
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<head><title>The Dormouse's story</title></head>
提取信息

我们可以通过string属性来获取文本的值,我们有的时候需要获取节点中属性的名称和节点的名称。但其实节点名称这个有的不是很多,但我也简单介绍一下,还是以上一段HTML文本为例
1.获取节点名称,我们获取p节点的名称,可以这样做:

print(soup.p.name)

结果就不用说了吧,就是p。
2.获取属性,每个节点可能有多个属性,常有class,href,id等,选择节点元素后,调用attr获取所有属性:

print(soup.p.attrs)#返回的是一个字典,属性和属性值键值对形式
print(soup.p.attrs['name'])#类似从字典中获取某个键值

返回结果:

{'class': ['title'], 'name': 'dromouse'}
dromouse

其实我们还可以稍微简化一下,毕竟这个还是有一点繁琐的。其实我们可以不用写attrs,直接在节点元素后面加入中括号,传入属性名就可以获取属性值。

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

运行结果:

dromouse
['title']

这样看,也达到上面的效果但我们要注意属性值返回的类型,防止数据格式不同。

获取内容

上面的样例也展示过了,用string提取节点元素包括的文本,比如要需要获取第一个p节点里面的文本。

print(soup.p.string)

返回的结果是:

The Dormouse's story

这里我们还是要注意返回的是第一个匹配的结果,后面的都不会返回。

嵌套选择

结合上面,我们可以知道返回的结果都是bs4.element.Tag类型,它同时可以调用节点进行下一步的选择。比如我们捕获了head节点元素,我们可以调用head来选择器head内部的head节点元素:

from bs4 import BeautifulSoup
soup = BeautifulSoup(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类型。也就是说,我们在Tag类型的基础上再次选择得到的依然还是Tag类型,每次返回的结果都相同,所以这样就可以做嵌套选择了。
最后,输出它的string属性,也就是节点里的文本内容。

关联选择

在做选择的时候,有时候不能做到一步就选到想要的节点元素,需要先选中某一个节点元素,然后以它为基准再选择它的子节点、父节点、兄弟节点等,这里就来介绍如何选择这些节点元素。

选取子节点或子孙节点

选取节点元素之后,如果想获取它的直接子节点,可以利用contents属性,示例如下:

from bs4 import BeautifulSoup
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>
"""
soup = BeautifulSoup(html, 'lxml')
print(soup.p.contents)

返回的结果:

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

从结果中我们可以看到返回的都是p标签的直接子节点,p标签中的孙子节点span没有保存下来,也说明contents返回都是直接节点。
还有一种方法也可以得到这样的效果,就是children属性,我们可以调用children属性来得到相应结果,只不过它返回的是生成器类型,我们需要for循环输出:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.children)
for i, child in enumerate(soup.p.children):
    print(i, child)

返回结果是:

<list_iterator object at 0x000001667B0772B0>

            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.

不同的方法但最终的结果都是一样的。

获取所有的子孙节点

要得到所有的子孙节点的话,可以调用descendants属性:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.descendants)
for i, child in enumerate(soup.p.descendants):
    print(child)

返回的结果是:

<generator object descendants at 0x000001667ABFD4C0>

            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>


<span>Elsie</span>
Elsie




<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
Lacie
 
            and
            
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
Tillie

            and they lived at the bottom of a well.
        
父节点和祖先节点

要获取某个节点的父亲和祖先节点用的是parent和parents属性。parent属性返回的是某个节点的父亲节点,parents返回的是某个节点的祖先节点,是一个生成器。

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
soup = BeautifulSoup(html, 'lxml')
print(soup.a.parent)#返回的仅仅是父节点
print(list(soup.a.parents))#返回的是祖先节点,一个生成器,需要我们先将其转成列表

返回结果是:

<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>
[<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>, <body>
<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>
<p class="story">...</p>
</body>, <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 class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
<p class="story">...</p>
</body></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 class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
<p class="story">...</p>
</body></html>]
兄弟节点

获取兄弟节点采用的是这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">
                <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
soup = BeautifulSoup(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, '\n            Hello\n            '), (1, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>), (2, ' \n            and\n            '), (3, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>), (4, '\n            and they lived at the bottom of a well.\n        ')]
Prev Siblings [(0, '\n            Once upon a time there were three little sisters; and their names were\n            ')]

上面这几个我们要记住,如果结果是一个生成器,记得先转为列表,再来进行后续的操作,比如提取属性、文本,这和前面是一样的,我就不重复了。

方法选择器

前面所讲的选择方法都是通过属性来选择的,这种方法非常快,但是如果进行比较复杂的选择的话,它就比较烦琐,不够灵活了。幸好,Beautiful Soup还为我们提供了一些查询方法,比如find_all()和find()等,调用它们,然后传入相应的参数,就可以灵活查询了。

find_all()

看这个函数我们就知道查询所有符合条件的元素,我们给它传入一些参数,就可以得到符合条件的元素。函数形式:

find_all(name , attrs , recursive , text , **kwargs)

我们先找一段测试HTML文本:

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>
'''

我们根据函数形式,对它举一些具体实例来了解它的参数用法。
1.直接获取节点

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(name='ul'))
print(type(soup.find_all(name='ul')[0]))

返回结果是:

[<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'>

2.根据属性(attrs)l来查询:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(id='list-1'))
print(soup.find_all(class_='element'))
print(soup.find_all(attrs={'id': 'list-1'}))
print(soup.find_all(attrs={'class': 'element'}))

返回结果为:

[<ul class="list" id="list-1">
<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>]
[<ul class="list" id="list-1">
<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>]

3.提取节点中的文本:

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
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(text=re.compile('link')))
print(soup.find_all('a')[0].text)

返回结果是:

['Hello, this is a link', 'Hello, this is a link, too']
Hello, this is a link

两个a节点,其内部包含文本信息。这里在find_all()方法中传入text参数,该参数为正则表达式对象,结果返回所有匹配正则表达式的节点文本组成的列表。我们还可以直接用.text形式提取文本。

find()

除了find_all()方法,还有find()方法,只不过后者返回的是单个元素,也就是第一个匹配的元素,而前者返回的是所有匹配的元素组成的列表。示例如下:

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
soup = BeautifulSoup(html, 'lxml')
print(soup.find(name='ul'))
print(type(soup.find(name='ul')))
print(soup.find(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_parents()find_parent():前者返回所有祖先节点,后者返回直接父节点。
find_next_siblings()find_next_sibling():前者返回后面所有的兄弟节点,后者返回后面第一个兄弟节点。
find_previous_siblings()find_previous_sibling():前者返回前面所有的兄弟节点,后者返回前面第一个兄弟节点。
find_all_next()find_next():前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点。
find_all_previous()find_previous():前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点。
想了解更多,可以去官网查阅。这里我只大致介绍一下,上面的选择器都是通过节点等结构方式来处理,其实还有很多选择器,比如CSS选择器。这里我不做讲解,但一般掌握正则表达式,Beautiful Soup,Xpath三种也就差不多,有兴趣的可以去了解CSS选择器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章