9  网页数据解析提取

图 9.1 网页解析库的对比

9.1 XPath

https://cuiqingcai.com/202231.html

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

表 9.1 XPath 常用规则
表 达 式 描  述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

这里列出了 XPath 的常用匹配规则,示例如下:

//title[@lang='eng']

这就是一个 XPath 规则,它代表选择所有名称为 title,同时属性 lang 的值为 eng 的节点。

使用之前,首先要确保安装好 lxml 库。如尚未安装,可以使用 pip3 来安装:

pip3 install lxml

更详细的安装说明可以参考:https://setup.scrape.center/lxml

9.1.1 实例引入

现在通过实例来感受一下使用 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="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'))
<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>

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

这里我们调用 tostring 方法即可输出修正后的 HTML 代码,但是结果是 bytes 类型。这里利用 decode 方法将其转成 str 类型

可以看到,经过处理之后,li 节点标签被补全,并且还自动添加了 bodyhtml 节点。

另外,也可以直接读取文本文件进行解析,示例如下

from lxml import etree
html = etree.parse('./data/sample1.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))

9.1.2 所有节点

我们一般会用 // 开头的 XPath 规则来选取所有符合要求的节点。这里以前面的 HTML 文本为例,如果要选取所有节点,可以这样实现:

from lxml import etree
result = html.xpath('//*')
print(result)
[<Element html at 0x19e5448d3c0>, <Element body at 0x19e244543c0>, <Element div at 0x19e56dd1280>, <Element ul at 0x19e56dd1300>, <Element li at 0x19e56dd1700>, <Element a at 0x19e56dd1740>, <Element li at 0x19e56dd1400>, <Element a at 0x19e56dd16c0>, <Element li at 0x19e56dd1780>, <Element a at 0x19e56dd1140>, <Element li at 0x19e56dd17c0>, <Element a at 0x19e56dd1800>, <Element li at 0x19e56dd1840>, <Element a at 0x19e56dd1880>]

这里使用 * 代表匹配所有节点,也就是整个 HTML 文本中的所有节点都会被获取。可以看到,返回形式是一个列表,每个元素是 Element 类型,其后跟了节点的名称,如 htmlbodydivullia 等,所有节点都包含在列表中了。

当然,此处匹配也可以指定节点名称。如果想获取所有 li 节点,示例如下:

result = html.xpath('//li')
print(result)
print(result[0])
[<Element li at 0x19e56dd1700>, <Element li at 0x19e56dd1400>, <Element li at 0x19e56dd1780>, <Element li at 0x19e56dd17c0>, <Element li at 0x19e56dd1840>]
<Element li at 0x19e56dd1700>

这里要选取所有 li 节点,可以使用 //,然后直接加上节点名称即可,调用时直接使用 xpath 方法即可。

9.1.3 子节点

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

result = html.xpath('//li/a')
print(result)
[<Element a at 0x19e56dd1140>, <Element a at 0x19e56dd1bc0>, <Element a at 0x19e56dd1880>, <Element a at 0x19e56dd1c80>, <Element a at 0x19e56dd1c00>]

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

result = html.xpath('//ul//a')
print(result)
[<Element a at 0x19e56dd1140>, <Element a at 0x19e56dd1bc0>, <Element a at 0x19e56dd1880>, <Element a at 0x19e56dd1c80>, <Element a at 0x19e56dd1c00>]

运行结果是相同的。

但是如果这里用 //ul/a,就无法获取任何结果了。因为 / 用于获取直接子节点,而在 ul 节点下没有直接的 a 子节点,只有 li 节点,所以无法获取任何匹配结果,代码如下:

result = html.xpath('//ul/a')
print(result)
[]

因此,这里我们要注意 /// 的区别,其中 / 用于获取直接子节点,// 用于获取子孙节点。

9.1.4 父节点

我们知道通过连续的 /// 可以查找子节点或子孙节点,那么假如我们知道了子节点,怎样来查找父节点呢?这可以用 .. 来实现。

比如,现在首先选中 href 属性为 link4.htmla 节点,然后获取其父节点,再获取其 class 属性,相关代码如下:

result = html.xpath('//a[@href="link4.html"]/../@class')
print(result)
['item-1']

检查一下结果发现,这正是我们获取的目标 li 节点的 class 属性。

同时,我们也可以通过 parent:: 来获取父节点,代码如下:

result = html.xpath('//a[@href="link4.html"]/parent::*/@class')
print(result)
['item-1']

9.1.5 属性匹配

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

result = html.xpath('//li[@class="item-0"]')
print(result)
[<Element li at 0x19e56dd7280>, <Element li at 0x19e56dd7140>]

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

可见,匹配结果正是两个,至于是不是那正确的两个,后面再验证。

9.1.6 文本获取

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

result = html.xpath('//li[@class="item-0"]/text()')
print(result)
['\n     ']

奇怪的是,我们并没有获取到任何文本,只获取到了一个换行符,这是为什么呢?因为 XPath 中 text 方法前面是 /,而此处 / 的含义是选取直接子节点,很明显 li 的直接子节点都是 a 节点,文本都是在 a 节点内部的,所以这里匹配到的结果就是被修正的 li 节点内部的换行符,因为自动修正的 li 节点的尾标签换行了。

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

因此,如果想获取 li 节点内部的文本,就有两种方式,一种是先选取 a 节点再获取文本,另一种就是使用 //。接下来,我们来看下二者的区别。

首先,选取 a 节点再获取文本,代码如下:

result = html.xpath('//li[@class="item-0"]/a/text()')
print(result)
['first item', 'fifth item']
result = html.xpath('//li[@class="item-0"]//text()')
print(result)
['first item', 'fifth item', '\n     ']

这里的返回结果是 3 个。可想而知,这里是选取所有子孙节点的文本,其中前两个就是 li 的子节点 a 内部的文本,另外一个就是最后一个 li 节点内部的文本,即换行符。

9.1.7 属性获取

我们知道用 text 方法可以获取节点内部文本,那么节点属性该怎样获取呢?其实还是用 @ 符号就可以。例如,我们想获取所有 li 节点下所有 a 节点的 href 属性,代码如下:

result = html.xpath('//li/a/@href')
print(result)
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

这里我们通过 @href 即可获取节点的 href 属性。注意,此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如 [@href="link1.html"],而此处的 @href 指的是获取节点的某个属性,二者需要做好区分。

可以看到,我们成功获取了所有 li 节点下 a 节点的 href 属性,它们以列表形式返回。

9.1.8 多属性匹配

另外,我们可能还遇到一种情况,那就是根据多个属性确定一个节点,这时就需要同时匹配多个属性。此时可以使用运算符 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']
result = html.xpath('//li[contains(@class, "ul") or @name="item"]/a/text()')
print(result)
['first item']

还有很多运算符, http://www.w3school.com.cn/xpath/xpath_operators.asp

9.1.9 节点轴选择

XPath 提供了很多节点轴选择方法,包括获取子元素、兄弟元素、父元素、祖先元素等,示例如下:

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::*')
print(result)
[<Element html at 0x19e24406d00>, <Element body at 0x19e56de5880>, <Element div at 0x19e56de58c0>, <Element ul at 0x19e56de5900>]
[<Element div at 0x19e56de58c0>]
['item-0']
[<Element a at 0x19e56de5800>]
[<Element span at 0x19e56de58c0>]
[<Element a at 0x19e56de5900>]
[<Element li at 0x19e56de5800>, <Element li at 0x19e56de59c0>, <Element li at 0x19e56de5980>, <Element li at 0x19e56de5940>]

9.2 Beautiful Soup

pip install beautifulsoup4

9.2.1 解析器

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

表 9.2 Beautiful Soup 支持的解析器
解析器 使用方法 优势 劣势
Python 标准库 BeautifulSoup(markup, “html.parser”) Python 的内置标准库、执行速度适中 、文档容错能力强 Python 2.7.3 or 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 即可:

from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
print(soup.p.string)
Hello

在后面,Beautiful Soup 的用法实例也统一用这个解析器来演示。

9.2.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
soup = BeautifulSoup(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

9.2.3 节点选择器

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

选择元素

下面再用一个例子详细说明选择元素的方法:

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></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.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
print(soup.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 类型,这是 Beautiful Soup 中一个重要的数据结构。经过选择器选择后,选择结果都是这种 Tag 类型。Tag 具有一些属性,比如 string 属性,调用该属性,可以得到节点的文本内容,所以接下来的输出结果正是节点的文本内容。

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

提取信息

上面演示了调用 string 属性来获取文本的值,那么如何获取节点属性的值呢?如何获取节点名呢?下面我们来统一梳理一下信息的提取方式。

获取名称

可以利用 name 属性获取节点的名称。这里还是以上面的文本为例,选取 title 节点,然后调用 name 属性就可以得到节点名称:

print(soup.title.name)
title
获取属性

每个节点可能有多个属性,比如 id 和 class 等,选择这个节点元素后,可以调用 attrs 获取所有属性:

print(soup.p.attrs)
print(soup.p.attrs['name'])
{'class': ['title'], 'name': 'dromouse'}
dromouse

可以看到,attrs 的返回结果是字典形式,它把选择的节点的所有属性和属性值组合成一个字典。接下来,如果要获取 name 属性,就相当于从字典中获取某个键值,只需要用中括号加属性名就可以了。比如,要获取 name 属性,就可以通过 attrs[‘name’] 来得到。

print(soup.p['name'])
print(soup.p['class'])
dromouse
['title']
获取内容

可以利用 string 属性获取节点元素包含的文本内容,比如要获取第一个 p 节点的文本:

print(soup.p.string)
print(soup.p.get_text())
The Dormouse's story
The Dormouse's story

再次注意一下,这里选择到的 p 节点是第一个 p 节点,获取的文本也是第一个 p 节点里面的文本。

嵌套选择

在上面的例子中,我们知道每一个返回结果都是 bs4.element.Tag 类型,它同样可以继续调用节点进行下一步的选择。比如,我们获取了 head 节点元素,我们可以继续调用 head 来选取其内部的 head 节点元素:

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
"""
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

关联选择

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

子节点和子孙节点

选取节点元素之后,如果想要获取它的直接子节点,可以调用 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
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 节点的直接子节点。比如第一个 a 节点里面包含一层 span 节点,这相当于孙子节点了,但是返回结果并没有单独把 span 节点选出来。所以说,contents 属性得到的结果是直接子节点的列表。

同样,我们可以调用 children 属性得到相应的结果:

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 0x0000019E5706B130>
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.
        
print(soup.p.descendants)
for i, child in enumerate(soup.p.descendants):
    print(i, child)
<generator object Tag.descendants at 0x0000019E5706D510>
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.
        
父节点和祖先节点

如果要获取某个节点元素的父节点,可以调用 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
soup = BeautifulSoup(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>
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
soup = BeautifulSoup(html, 'lxml')
print(type(soup.a.parents))
print(list(enumerate(soup.a.parents)))
<class 'generator'>

[(0, <p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>), (1, <body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body>), (2, <html>
<body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body></html>), (3, <html>
<body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body></html>)]
兄弟节点

上面说明了子节点和父节点的获取方式,如果要获取同级的节点(也就是兄弟节点),应该怎么办呢?示例如下:

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)))
提取信息

前面讲解了关联元素节点的选择方法,如果想要获取它们的一些信息,比如文本、属性等,也用同样的方法,示例如下:

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
soup = BeautifulSoup(html, 'lxml')
print('Next Sibling:')
print(type(soup.a.next_sibling))
print(soup.a.next_sibling)
print(soup.a.next_sibling.string)
print('Parent:')
print(type(soup.a.parents))
print(list(soup.a.parents)[0])
print(list(soup.a.parents)[0].attrs['class'])

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

9.2.4 方法选择器

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

find_all

find_all,顾名思义,就是查询所有符合条件的元素,可以给它传入一些属性或文本来得到符合条件的元素,功能十分强大。

它的 API 如下:

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

我们可以根据节点名来查询元素,下面我们用一个实例来感受一下:

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_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'>
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>]
attrs

除了根据节点名查询,我们也可以传入一些属性来进行查询,我们用一个实例感受一下:

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
soup = BeautifulSoup(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>]
text

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
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(text=re.compile('link')))
['Hello, this is a link', 'Hello, this is a link, too']
C:\Users\xinlu\AppData\Local\Temp\ipykernel_5920\2083358073.py:12: DeprecationWarning:

The 'text' argument to find()-type methods is deprecated. Use 'string' instead.

find

除了 find_all 方法,还有 find 方法,只不过 find 方法返回的是单个元素,也就是第一个匹配的元素,而 find_all 返回的是所有匹配的元素组成的列表。

9.2.5 CSS 选择器

Beautiful Soup 还提供了另外一种选择器,那就是 CSS 选择器。如果对 Web 开发熟悉的话,那么对 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
soup = BeautifulSoup(html, 'lxml')
print(soup.select('.panel .panel-heading'))
print(soup.select('ul li'))
print(soup.select('#list-2 .element'))
print(type(soup.select('ul')[0]))
[<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 选择器的节点组成的列表。例如,select(‘ul li’) 则是选择所有 ul 节点下面的所有 li 节点,结果便是所有的 li 节点组成的列表。

最后一句我们打印输出了列表中元素的类型,可以看到类型依然是 Tag 类型。

嵌套选择

select 方法同样支持嵌套选择,例如我们先选择所有 ul 节点,再遍历每个 ul 节点选择其 li 节点,样例如下:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
    print(ul.select('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>]

获取属性

我们知道节点类型是 Tag 类型,所以获取属性还可以用原来的方法。仍然是上面的 HTML 文本,这里尝试获取每个 ul 节点的 id 属性:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
    print(ul['id'])
    print(ul.attrs['id'])
list-1
list-1
list-2
list-2

到此 BeautifulSoup 的使用介绍基本就结束了,最后做一下简单的总结:

  • 推荐使用 LXML 解析库,必要时使用 html.parser。
  • 节点选择筛选功能弱但是速度快。
  • 建议使用 find、find_all 方法查询匹配单个结果或者多个结果。
  • 如果对 CSS 选择器熟悉的话可以使用 select 选择法。

9.3 pyquery

强大而又灵活的网页解析库,如果你觉得正则写起来太麻烦, 如果你觉得beutifulsoup 语法太难记,如果你熟悉jquery的语法,那么pyquery是最佳选择

http://www.w3school.com.cn/css/index.asp

http://pyquery.readthedocs.io/

html = '''
</div><div class="account-signin">
    <ul class="navigation menu" aria-label="Social Media Navigation">
        Hello World
        <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    </ul>
</div>
'''
from pyquery import PyQuery as pq
doc=pq(html)
print(doc('.tier-2')) #默认就是css选择器
<li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
from pyquery import PyQuery as pq
try:
    doc = pq(url='https://elaborate-heliotrope-296857.netlify.app/')
    print(doc('title'))
except Exception:
    pass
url = 'https://www.feixiaohao.com/currencies/bitcoin/'
headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
                            '(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
        }
doc = pq(url=url, headers=headers, method='get')

# 也支持发送表单数据
# pq(your_url, {'q': 'foo'}, method='post', verify=True)
doc=pq(filename='./data/sample1.html')
print(doc('li')[0])
<Element li at 0x19e56fcdae0>
doc = pq(html)
print(doc('.tier-2')) #默认就是css选择器

#子元素
print(doc('li').find('li')) #这里的find是查找所有,但是不一定是直接子元素
print('==>',doc('li').children('li')) #查找直接子元素

#父元素
print(doc('.tier-2').parent())

#祖先元素:爹,爹的爹
print(doc('.tier-2').parents())
print(doc('.tier-2').parents('.account-signin')) #从祖先里筛选

#先补充:并列选择
print(doc('.tier-1 .tier-2'))
print(doc('.tier-1 .tier-2.element-1'))

#兄弟元素
print(doc('.tier-2.element-1').siblings())
print(doc('.tier-2.element-1').siblings('li a'))
<li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
<li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
==> 
<ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        
<html><body><div class="account-signin">
    <ul class="navigation menu" aria-label="Social Media Navigation">
        Hello World
        <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    </ul>
</div>
</body></html><body><div class="account-signin">
    <ul class="navigation menu" aria-label="Social Media Navigation">
        Hello World
        <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    </ul>
</div>
</body><div class="account-signin">
    <ul class="navigation menu" aria-label="Social Media Navigation">
        Hello World
        <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    </ul>
</div>
<ul class="navigation menu" aria-label="Social Media Navigation">
        Hello World
        <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    </ul>
<li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        
<div class="account-signin">
    <ul class="navigation menu" aria-label="Social Media Navigation">
        Hello World
        <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    </ul>
</div>

<li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
<li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                
<li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
<a href="/accounts/login/">Sign In</a>
# 遍历
lis=doc('li').items()
print(lis)
#
for i,j in enumerate(lis):
    print(i,j)

# 获取属性
print(doc('li').attr('class'))
print(doc('a').attr.href)

# 获取文本
print(doc('a').text())

# html
print(doc('.subnav.menu'))
print(doc('.subnav.menu').html())

# DOM
# addclass, removeclass
tag=doc('.subnav.menu')
print(tag)
#
tag.addClass('active')
print(tag)
#
tag.removeClass('active')
print(tag)

tag=doc('.tier-2.element-1 a')
tag.attr('name','link')
tag.css('font-size','14px')
print(tag)

tag = doc('.navigation.menu')
print(tag.text()) #获取的是tag下所有的文本,

tag.find('li').remove()
print(tag.text())
<generator object PyQuery.items at 0x0000019E57254E40>
0 <li class="tier-1 last" aria-haspopup="true">

            <a href="/accounts/login/" title="Sign Up or Sign In to Python.org">Sign In</a>
            <ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        </li>
    
1 <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                
2 <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
tier-1 last
/accounts/login/
Sign In Sign Up / Register Sign In
<ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        

                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            
<ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        
<ul class="subnav menu active">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        
<ul class="subnav menu">
                <li class="tier-2 element-1" role="treeitem"><a href="/accounts/signup/">Sign Up / Register</a></li>
                <li class="tier-2 element-2" role="treeitem"><a href="/accounts/login/">Sign In</a></li>
            </ul>
        
<a href="/accounts/signup/" name="link" style="font-size: 14px">Sign Up / Register</a>
Hello World
Sign In
Sign Up / Register
Sign In
Hello World
print(doc('li:first-child'))  #选择li标签的第一个
print(doc('li:last-child'))  #选择li标签的最后一个
print(doc('li:nth-child(2)'))  #选择li标签的第2个
print(doc('li:gt(2)'))  #选择li标签第2个以后的
print(doc('li:nth-child(2n)'))  #选择li标签的偶数标签
print(doc('li:nth-child(2n+1)'))  #选择li标签的奇数标签
print(doc('li:contains(second)'))  #选择li标签中包含second文本的标签

#更多css选择器可以查看

#






9.4 parsel

https://cuiqingcai.com/202232.html

前文我们了解了 lxml 使用 XPath 和 pyquery 使用 CSS Selector 来提取页面内容的方法,不论是 XPath 还是 CSS Selector,对于绝大多数的内容提取都足够了,大家可以选择适合自己的库来做内容提取。 parsel可以二者结合起来使用

parsel 这个库可以对 HTML 和 XML 进行解析,并支持使用 XPath 和 CSS Selector 对内容进行提取和修改,同时它还融合了正则表达式提取的功能。功能灵活而又强大,同时它也是 Python 最流行爬虫框架 Scrapy 的底层支持。

pip3 install parsel

更详细的安装说明可以参考:https://setup.scrape.center/parsel。

9.4.1 初始化

首先我们还是用上一节的示例 HTML,声明 html 变量如下:

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

接着,一般我们会用 parsel 的 Selector 这个类来声明一个 Selector 对象,写法如下:

from parsel import Selector
selector = Selector(text=html)

这里我们创建了一个 Selector 对象,传入了 text 参数,内容就是刚才声明的 HTML 字符串,赋值为 selector 变量。

有了 Selector 对象之后,我们可以使用 css 和 xpath 方法分别传入 CSS Selector 和 XPath 进行内容的提取,比如这里我们提取 class 包含 item-0 的节点,写法如下:

items = selector.css('.item-0')
print(len(items), type(items), items)
items2 = selector.xpath('//li[contains(@class, "item-0")]')
print(len(items2), type(items), items2)
3 <class 'parsel.selector.SelectorList'> [<Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')]" data='<li class="item-0">first item</li>'>, <Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')]" data='<li class="item-0 active"><a href="li...'>, <Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')]" data='<li class="item-0"><a href="link5.htm...'>]
3 <class 'parsel.selector.SelectorList'> [<Selector xpath='//li[contains(@class, "item-0")]' data='<li class="item-0">first item</li>'>, <Selector xpath='//li[contains(@class, "item-0")]' data='<li class="item-0 active"><a href="li...'>, <Selector xpath='//li[contains(@class, "item-0")]' data='<li class="item-0"><a href="link5.htm...'>]

不过这里可能大家有个疑问,第一次我们不是用 css 方法来提取的节点吗?为什么结果中的 Selector 对象还输出了 xpath 属性而不是 css 属性呢?这是因为 css 方法背后,我们传入的 CSS Selector 首先被转成了 XPath,XPath 才真正被用作节点提取。其中 CSS Selector 转换为 XPath 这个过程是在底层用 cssselect 这个库实现的,比如 .item-0 这个 CSS Selector 转换为 XPath 的结果就是 descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' item-0 ')],因此输出的 Selector 对象有了 xpath 属性了。不过这个大家不用担心,这个对提取结果是没有影响的,仅仅是换了一个表示方法而已。

9.4.2 提取文本

好,既然刚才提取的结果是一个可迭代对象 SelectorList,那么要获取提取到的所有 li 节点的文本内容就要对结果进行遍历了,写法如下:

from parsel import Selector

selector = Selector(text=html)
items = selector.css('.item-0')
for item in items:
    text = item.xpath('.//text()').get()
    print(text)
first item
third item
fifth item

这里我们遍历了 items 变量,赋值为 item,那么这里 item 又变成了一个 Selector 对象,那么此时我们又可以调用其 css 或 xpath 方法进行内容提取了,比如这里我们就用 .//text() 这个 XPath 写法提取了当前节点的所有内容,此时如果不再调用其他方法,其返回结果应该依然为 Selector 构成的可迭代对象 SelectorList。SelectorList 有一个 get 方法,get 方法可以将 SelectorList 包含的 Selector 对象中的内容提取出来。

这里 get 方法的作用是从 SelectorList 里面提取第一个 Selector 对象,然后输出其中的结果。

result = selector.xpath('//li[contains(@class, "item-0")]//text()').get()
print(result)
first item

其实这里我们使用 //li[contains(@class, "item-0")]//text() 选取了所有 class 包含 item-0 的 li 节点的文本内容。应该来说,返回结果 SelectorList 应该对应三个 li 对象,而这里 get 方法仅仅返回了第一个 li 对象的文本内容,因为其实它会只提取第一个 Selector 对象的结果。

那有没有能提取所有 Selector 的对应内容的方法呢?有,那就是 getall 方法。

result = selector.xpath('//li[contains(@class, "item-0")]//text()').getall()
print(result)
['first item', 'third item', 'fifth item']

9.4.3 正则提取

除了常用的 css 和 xpath 方法,Selector 对象还提供了正则表达式提取方法,我们用一个实例来了解下:

from parsel import Selector

selector = Selector(text=html)
result = selector.css('.item-0').re('link.*')
print(result)
['link3.html"><span class="bold">third item</span></a></li>', 'link5.html">fifth item</a></li>']

这里我们先用 css 方法提取了所有 class 包含 item-0 的节点,然后使用 re 方法,传入了 link.*,用来匹配包含 link 的所有结果。

可以看到,re 方法在这里遍历了所有提取到的 Selector 对象,然后根据传入的正则表达式查找出符合规则的节点源码并以列表的形式返回。

parsel 是一个融合了 XPath、CSS Selector 和正则表达式的提取库,功能强大又灵活。

9.5 other resources