python爬蟲之BeautifulSoup4使用

鋼鐵知識庫,一個學習python爬蟲、數據分析的知識庫。人生苦短,快用python。

上一章我們講解針對結構化的htmlxml數據,使用Xpath實現網頁內容爬取。本章我們再來聊另一個高效的神器:Beautiful Soup4。相比於傳統正則表達方式去解析網頁源程式碼,這個就簡單得多,實踐是檢驗真理的唯一標準,話不多說直接上號開搞驗證。

Beautiful Soup 簡介

首先說說BeautifulSoup是什麼。簡單來說,這是Python的一個HTML或XML的解析庫,我們可以用它方便從網頁中提取數據,官方解釋如下:

BeautifulSoup 提供一些簡單的、Python 式的函數用來處理導航、搜索、修改分析樹等功能。它是一個工具箱,通過解析文檔為用戶提供需要抓取的數據,因為簡單,所以不需要多少程式碼就可以寫出一個完整的應用程式。 BeautifulSoup 自動將輸入文檔轉換為 Unicode 編碼,輸出文檔轉換為 utf-8 編碼。你不需要考慮編碼方式,除非文檔沒有指定一個編碼方式,這時你僅僅需要說明一下原始編碼方式就可以了。 BeautifulSoup 已成為和 lxml、html5lib 一樣出色的 Python 解釋器,為用戶靈活地提供不同的解析策略或強勁的速度。

所以,利用它可以省去很多繁瑣的提取工作,提高解析效率。

BeautifulSoup 安裝

BeautifulSoup3 目前已經停止開發,推薦使用 BeautifulSoup4,不過它也被移植到bs4了,也就是說導入時我們需要import bs4

在開始之前,請確保已經正確安裝beautifulsoup4lxml,使用pip安裝命令如下:

pip install beautifulsoup4
pip install lxml

解析器

BeautifulSoup在解析時實際上依賴解析器。除了支援Python標準庫中的HTML解析器,還支援一些第三方的解析器,如果不安裝它,則Python會使用默認的解析器。

下面列出BeautifulSoup支援的解析器

解析器 使用方法 優勢 劣勢
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的功能,相比默認的HTML解析器更加強大,速度,容錯能力強。

推薦使用它,下面統一使用lxml進行演示。使用時只需在初始化時第二個參數改為 lxml 即可。

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

基本使用

下面舉個實例來看看BeautifulSoup的基本用法:

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

首先聲明一個html變數,它是一個HTML字元串,注意html和body標籤都沒有閉合。

經過初始化,使用prettify()方法把要解析的字元串以標準縮進格式輸出,發現結果中自動補全了html和body標籤。這一步不是prettify()方法做的,而是在初始化BeautifulSoup時就完成了。然後調用soup.title.string拿到title裡面的文本內容。

通過簡單調用幾個屬性完成文本提取,是不是非常方便呢?

節點選擇器

直接調用節點的名稱就可以選擇節點元素,再調用 string 屬性就可以得到節點內的文本了,這種選擇方式速度非常快。如果單個節點結構層次非常清晰,可以選用這種方式來解析。

選擇元素

還是以上面的HTML程式碼為例,詳細說明選擇元素的方法:

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

首先輸出title節點的選擇結果,包含標籤。

接下來輸出它的類型,是一個bs4.element.Tag類型,Tag具有一些屬性,比如string。

調用string屬性可以看到輸出節點的文本內容。

繼續嘗試head、p節點。發現p只取了第一個匹配的節點。說明當有多個節點時只取一個。

獲取屬性

每個節點可能有多個屬性比如id 、class等,選擇元素後可以調用attrs獲取所有屬性:

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

可以看到attrs返回結果是字典,它把選擇節點所有屬性都組合成一個字典。取值直接按字典方式即可。

當然還有一種更簡單的獲取方式:不寫attrs,直接在元素後面中括弧取值也行:

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

但是注意區分:有的返回字元串、有的返回字元串組成的列表。

對於class,一個節點元素可能有多個class,所以返回的是列表。

子節點和子孫節點

選取節點元素之後,如果想要獲取它的直接子節點,可以調用 contents 屬性,示例如下:

html4 = """
<html>
    <head>
        <title>The Dormouse's story</title>
    </head>
    <body>
        <p class="story">
            鋼鐵知識庫
            <a href="//a.com" class="鋼鐵學數據分析" id="link1">
                <span>Elsie</span>
            </a>
            <a href="//b.com" class="鋼鐵學自動化" id="link2">Lacie</a> 
            and
            <a href="//example.com" class="cccc" id="link3">Tillie</a>
            鋼鐵學爬蟲.
        </p>
        <p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html4, 'lxml')
print(soup.p.contents)
'''
['\n            鋼鐵知識庫\n            ', <a class="鋼鐵學數據分析" href="//a.com" id="link1">
<span>Elsie</span>
</a>, '\n', <a class="鋼鐵學自動化" href="//b.com" id="link2">Lacie</a>, ' \n            and\n            ', <a class="cccc" href="//example.com" id="link3">Tillie</a>, '\n            鋼鐵學爬蟲.\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 0x0000000001D9A1C0>
0 
            鋼鐵知識庫
            
1 <a class="鋼鐵學數據分析" href="//a.com" id="link1">
<span>Elsie</span>
</a>
2 

3 <a class="鋼鐵學自動化" href="//b.com" id="link2">Lacie</a>
4  
            and
            
5 <a class="cccc" href="//example.com" id="link3">Tillie</a>
6 
            鋼鐵學爬蟲.
'''

還是同樣的 HTML 文本,這裡調用了 children 屬性來選擇,返回結果是生成器類型。接下來,我們用 for 循環輸出相應的內容。

如果要得到所有的子孫節點的話,可以調用 descendants 屬性:

<generator object Tag.descendants at 0x000001D77A90E570>
0 
            鋼鐵知識庫
            
1 <a class="鋼鐵學數據分析" href="//a.com" id="link1">
<span>Elsie</span>
</a>
2 

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

6 

7 <a class="鋼鐵學自動化" href="//b.com" id="link2">Lacie</a>
8 Lacie
9  
            and
            
10 <a class="cccc" href="//example.com" id="link3">Tillie</a>
11 Tillie
12 
            鋼鐵學爬蟲.

此時返回結果還是生成器。遍歷輸出一下可以看到,這次的輸出結果就包含了 span 節點。descendants 會遞歸查詢所有子節點,得到所有的子孫節點。

除此之外,還有父節點parent 和祖先節點parents,兄弟節點next_siblingprevious_siblings 日常用得少不再演示,後續需要自行查官方文檔即可。

方法選擇器

前面聊的通過屬性選擇節點,但如果進行比較複雜的話還是比較繁瑣。幸好BeautifulSoup還為我們提供另外一些查詢方法,比如find_all 和 find ,調用他們傳入相應參數就可以靈活查詢。

find_all

顧名思義,就是查詢所有符合條件的元素,可以給它傳入一些屬性或文本來得到符合條件的元素,功能十分強大。

它的 API 如下:

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

我們可以根據節點名來查詢元素,下面我們用一個實例來感受一下:

html5='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">鋼鐵</li>
            <li class="element">知識</li>
            <li class="element">倉庫</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">python</li>
            <li class="element">java</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, 'lxml')
print(soup.find_all(name='ul'))
print(type(soup.find_all(name='ul')[0]))
'''
[<ul class="list" id="list-1">
<li class="element">鋼鐵</li>
<li class="element">知識</li>
<li class="element">倉庫</li>
</ul>, <ul class="list list-small" id="list-2">
<li class="element">python</li>
<li class="element">java</li>
</ul>]
<class 'bs4.element.Tag'>
'''

可以看到返回了一個列表,分別是兩個ul長度為2,且類型依然是bs4.element.Tag類型。

因為都是Tag類型,所以依然可以繼續嵌套查詢,還是同樣文本,查詢ul節點後再繼續查詢內部li節點。

from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, 'lxml')
for ul in soup.find_all(name='ul'):
    print(ul.find_all(name='li'))
'''
[<li class="element">鋼鐵</li>, <li class="element">知識</li>, <li class="element">倉庫</li>]
[<li class="element">python</li>, <li class="element">java</li>]
'''

返回結果是列表類型,元素依然是Tag類型。

接下來我們可以遍歷每個li獲取它的文本:

for ul in soup.find_all(name='ul'):
    print(ul.find_all(name='li'))
    for li in ul.find_all(name='li'):
        print(li.string)
'''
[<li class="element">鋼鐵</li>, <li class="element">知識</li>, <li class="element">倉庫</li>]
鋼鐵
知識
倉庫
[<li class="element">python</li>, <li class="element">java</li>]
python
java
'''

find

除了 find_all 方法,還有 find 方法,不過 find 方法返回的是單個元素,也就是第一個匹配的元素,而 find_all 返回的是所有匹配的元素組成的列表。示例如下:

html5='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">鋼鐵</li>
            <li class="element">知識</li>
            <li class="element">倉庫</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">python</li>
            <li class="element">java</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, '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">鋼鐵</li>
<li class="element">知識</li>
<li class="element">倉庫</li>
</ul>
<class 'bs4.element.Tag'>
<ul class="list" id="list-1">
<li class="element">鋼鐵</li>
<li class="element">知識</li>
<li class="element">倉庫</li>
</ul>
'''

返回結果不再是列表形式,而是第一個匹配的節點元素,類型依然是 Tag 類型。

其它方法

另外還有許多的查詢方法,用法與前面介紹的 find_all、find 方法完全相同,只不過查詢範圍不同,在此做一下簡單的說明。

find_parents 和 find_parent:前者返回所有祖先節點,後者返回直接父節點。

find_next_siblings 和 find_next_sibling:前者返回後面所有的兄弟節點,後者返回後面第一個兄弟節點。

find_previous_siblings 和 find_previous_sibling:前者返回前面所有的兄弟節點,後者返回前面第一個兄弟節點。

find_all_next 和 find_next:前者返回節點後所有符合條件的節點,後者返回第一個符合條件的節點。

find_all_previous 和 find_previous:前者返回節點前所有符合條件的節點,後者返回第一個符合條件的節點。

CSS選擇器

BeautifulSoup還提供了另外一種選擇器,CSS選擇器。如果對 Web 開發熟悉的話,那麼對 CSS 選擇器肯定也不陌生。如果不熟悉的話,可以參考 //www.w3school.com.cn/cssref/css_selectors.asp 了解。

使用 CSS 選擇器,只需要調用 select 方法,傳入相應的 CSS 選擇器即可,我們用一個實例來感受一下:

html5='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">鋼鐵</li>
            <li class="element">知識</li>
            <li class="element">倉庫</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">python</li>
            <li class="element">java</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, '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">鋼鐵</li>, <li class="element">知識</li>, <li class="element">倉庫</li>, <li class="element">python</li>, <li class="element">java</li>]
[<li class="element">python</li>, <li class="element">java</li>]
<class 'bs4.element.Tag'>
'''

結果為所有匹配的節點。例如select('ul li')則是所有ul節點下面的所有li節點,返回結果是列表。

select 方法同樣支援嵌套選擇(soup.select(‘ul’))、屬性獲取(ul[‘id’]),以及文本獲取(li.string/li.get_text())

—- 鋼鐵知識庫 2022.08.22

結語

到此 BeautifulSoup 的使用介紹基本就結束了,最後鋼鐵知識庫做一下簡單的總結:

  • 推薦使用 LXML 解析庫,速度快、容錯能力強。
  • 建議使用 find、find_all 方法查詢匹配單個結果或者多個結果。
  • 如果對 CSS 選擇器熟悉的話可以使用 select 匹配,可以像Xpath一樣匹配所有。