前面兩篇文章我們已經學習了網頁請求的利器---requests,還學習了強大的字元串處理工具---正則表達式,並且以一個實例小試牛刀。但是可能這麼一試,你會說:requests簡直完美,但正則表達式卻不是我喜歡的類型。咋辦勒?後面幾篇文章會依次向你介紹三個高質量的解析庫,如果還找不到心儀的解析庫,那麼,「親,這邊建議您自己打造一個解析庫呢!」。

這一篇我們首先介紹BeautifulSoup,並在下一篇給出其應用的實例。

一、認識Beautiful Soup

1、一些預備知識

在第一篇中我們簡單講到網頁是由一個個的標籤按一定的佈局來呈現的,如下圖

一般而言,任何一個源代碼的大多數標籤中,我們都可以看到<>中會包含一些名為id、class或者其他名字的屬性,這就意味著我們可以用網路結構、節點名稱及其包含的屬性來定位各個節點了。其中需要注意的是每一個id屬性的值都是唯一的,即含有id的節點可以用id來精確定位。

2、Beautiful Soup

  • 基本簡介

Beautiful Soup是一個簡單方便的HTML解析庫,它能夠自動地將輸入的HTML文檔轉化為Unicode編碼,輸出文檔轉換為utf-8,利用它,我們對網頁源代碼進行解析時不需要指定編碼格式。(這是非常非常方便的)

但是需要注意的是Beautiful Soup在對HTML文檔進行解析時,需要依賴於別的解析器,如Python標準庫中的HTML解析庫、lxml等。下面列一些爬蟲常用的解析器:

(1)Python標準庫:BeautifulSoup(markup,html.parse)

(2)lxml HTML解析器:BeautifulSoup(markup,lxml)

一般而言,使用lxml解析器是主流,所以在後面的文章中我們使用BeautifulSoup時都選擇使用lxml。

  • 用法講解

要使用BeautifulSoup,首先必須導入

from bs4 import BeautifulSoup

可以看到,這和其他庫的導入不太一樣,這是因為BeautifulSoup僅僅是bs4中的一個類。既然是一個類,那麼我們使用它時必須初始化BeautifulSoup對象,見代碼

from bs4 import BeautifulSoup #導入BeautifulSoup

html =
<html><head>
<meta charset="utf-8">
</head>
<body>
<div><h4>Course Materials
</h4>
<li><a href="qe/intro.html">Lec 0</a></li>
<div>
<h5>Data</h5>
<!--<a href="qe/data_text.zip">data_text<a>-->
</div></div>
#故意定義一個雜亂的不算HTML文檔的HTML文檔

soup = BeautifulSoup(html,lxml) #BeautifulSoup對象初始化
print(soup.prettify()) 將解析的HTML文檔按標準縮進格式輸出

輸出結果如圖

從輸出結果看,BeautifulSoup將預設的標籤自動閉合了(注意下原始html的<html>與<body>標籤是沒有閉合的),這個工作並非由prettify()完成,而是在BeautifulSoup對象初始化的時候就完成了,prettify()方法的功能是將解析好的HTML文檔按標準縮進格式輸出。

下面回到我們的根本問題:如何使用BeautifulSoup解析提取需要的信息。前面我們說過BeautifulSoup能夠定位到HTML文檔中的節點,所以BeautifulSoup是先根據一定的信息從整個HTML文本中選擇特定節點,然後從該節點中提出所需要的信息。接下來我們就介紹幾種BeautifulSoup選擇節點的方式:節點選擇器、方法選擇器和CSS選擇器。

(1)節點選擇器

這種方式的精髓:直接調用節點的名稱(標籤名)就可以選擇節點,再通過各種獲取屬性的方法就能獲得該節點內包含的信息。優勢:這種方式簡單速度快,適用於層次結構清晰的單節點的選擇。缺點:對於結構複雜的節點選擇很繁瑣。下面以代碼演示:

from bs4 import BeautifulSoup

html =
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div>
<h4>Course Materials</h4>
<li><a href="qe/intro.html" class="item sister">Lec 0</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<div>
<h5>Data</h5>
</div></div>

soup = BeautifulSoup(html,lxml)
print(soup.h4) #選擇h4節點
print(soup.li) #選擇li節點
print(type(soup.li))
print(soup.h4.string) #調用soup.h4的string屬性獲取h4節點內的文本
print(soup.li.attrs)
print(soup.li.a.attrs)
print(soup.li.a.attrs[href])
print(soup.li[href])
print(soup.li.[class])

輸出結果為

第一個輸出結果應該沒有很大問題,第二個輸出就有一些說法了。注意到html中有兩個<li>節點,soup.li只選擇了第一個<li>節點而忽略後面的<li>節點。第三個輸出了選擇的節點是一個Tag類型的數據,這個Tag類有很多的屬性,如string屬性可以獲取節點包含的文本信息,見第三個輸出。每個節點可能會有很多屬性,而獲取屬性有兩種方式,首先可以用attrs屬性獲取節點的所有屬性,如代碼中的

print(soup.li.a.attrs)

輸出了一個有屬性名和屬性值構成的字典。這裡注意一下

print(soup.li.attrs)

選中的是<li>節點,而<li>節點沒有任何屬性,所以輸出為空字典。既然輸出為字典,那麼自然可以用屬性名獲取屬性值,見

print(soup.li.a.attrs[href])

的輸出結果。其實,我們還有一種更為簡單的獲取屬性值的方式,及直接在選中節點後用[]將屬性名括起來,見

print(soup.li[href])
print(soup.li[class])

對於這種方式需要注意的是當選中的節點只有一個屬性值時則返回屬性值,當節點含有鎖哥屬性值時,則返回包含所有屬性值的列表。

節點選擇器支持嵌套選擇和關聯選擇。所謂嵌套選擇就是對於具有包含關係的節點可以進行嵌套進一步選擇被包含的節點,以代碼說明

from bs4 import BeautifulSoup

html = <li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
soup = BeautifulSoup(html,lxml)
print(soup.li)
print(soup.li.a)

輸出如圖

可以看到html中<li>節點內包含<a>節點,我們在soup.li之後可以繼續選擇<li>節點包含的節點,即soup.li.a,這就進一步選擇出<a>節點,如果<a>還包含別的節點,還可以繼續依次選擇,這就是所謂的嵌套選擇。

所謂關聯選擇就是根據節點之間的相互關係進行選擇,第一篇爬蟲基礎講過節點間的相互關係有父子關係、兄弟關係和祖孫關係,下面一一介紹。

  • 子節點及子孫節點

對於某一個節點,我們可以調用contents屬性獲取其全部直接子節點。

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4>
<li><a href="qe/intro.html" class="item sister">Lec 0</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.div.contents)

輸出結果

可以看到輸出結果為列表形式,其中不僅僅包含直接子元素,還好吧文本信息。需要注意的是contents屬性並不會將孫節點獨立地取出來。

要獲取直接子節點我們還可以使用children屬性:

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4>
<li><a href="qe/intro.html" class="item sister">Lec 0</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.div.children)
for i,child in enumerate(soup.div.children):
print(i,child)

輸出結果為

可以明顯地發現contents與children屬性獲取的內容是一致的,指數contents屬性返回列表,children屬性返回生成器類型。

如果想要獲取所有的子孫節點,可以使用descendants屬性

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4>
<li><a href="qe/intro.html" class="item sister">Lec 0</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.div.descendants)
for i,child in enumerate(soup.div.descendants):
print(i,child)

輸出結果

可以看到返回數據類型依舊為生成器,並且獨立地給出了所有子節點、孫節點和文本信息。

  • 父節點和祖先節點

一般而言,一各節點不僅有子節點和子孫節點,還會有父節點和祖先節點。如果想要獲取一個節點的直接父節點,可以調用parent屬性:

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4>
<li><a href="qe/intro.html" class="item sister">Lec 0</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.a.parent) #首先選中了第一個<a>節點,然後獲取了其父節點<li>

輸出結果為:

可以看到僅僅輸出了其直接父節點,並沒有給出祖先節點,如果我們不僅僅要獲取父節點,還要獲取所有的祖先節點,那麼可以調用parents屬性

print(soup.a.parents)
for i,parent in enumerate(soup.a.parents):
print(i,parent)

輸出結果

可以看到調用parents屬性將以生成器的類型返回所有的祖先節點。

  • 兄弟節點

a與b說明瞭如何獲取一個節點的上下級的節點,接下來說明如何獲取同級的節點(兄弟節點)。這裡我們可以調用四個屬性:next_sibling、previous_sibling、next_siblings、previous_siblings。分別表示獲取當前節點的下一個兄弟節點、上一個兄弟節點、後面的兄弟節點、前面的兄弟節點。代碼如下:

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.li.next_sibling)
print(soup.li.previous_sibling)
print(soup.li.next_siblings)
print(soup.li.previous_siblings)

輸出結果為

可以看到next_siblings、previous_siblings均以生成器類型返回。

到此,節點選擇器就介紹完畢了,可以看到其核心就是先選擇節點得到Tag類型的數據,然後調用一系列屬性提取信息。其中生成器類型可以轉換成轉換成列表進行處理,轉換方法如下

list(enumerate(soup.a.children))

下面介紹第二種選擇節點的方式:方法選擇器

(2)方法選擇器

前面介紹的節點選擇器雖然速度很快,但是當網路結構複雜時就不夠用了,這時候使用方法選擇器就比較靈活了。

下面介紹一種爬蟲中常用的方法:find_all(name,attrs,text)

這個方法要求傳入一些節點的參數作為選擇條件,然後選出所有符合條件的元素。下面對其參數進行簡單說明

  • name顧名思義,該參數意思是可以根據節點名來查詢元素,示例如下:

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.find_all(name=li))
print(type(soup.find_all(name=li)[0]))

輸出結果

可以看到find_all()的返回數據是所有<li>節點組成的一個列表,並且列表中每一個元素均是一個Tag類型,這意味列表中每一個元素都可以使用節點選擇器中介紹的屬性提取信息,也可以使用find_all()繼續獲取其內部的節點。如

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
for a in soup.find_all(name=li):
print(a.find_all(name=a))

輸出結果:

注意這裡選出來的節點依舊是Tag類型,我們可以使用string屬性獲取信息,不再贅述。

  • attrs

attrs參數是根據節點的屬性來查找節點,常用屬性為id、class等,示例如下:

from bs4 import BeautifulSoup

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.find_all(attrs={href:qe/about_py.ipynb}))
print(soup.find_all(class_=item))

結果:

當我們使用attrs參數作為查找條件時,我們可以傳入包含屬性名和屬性值的字典查找節點,如{『name』:title},{『id』:list_1}等等,但是對於一些常用的屬性如id、class我們可以使用更為簡單的方式查詢,如:

soup.find_all(class_=item)
soup.find_all(id=list_1)

注意class為Python關鍵詞,所以需要使用class_ 。

  • text

text參數注意用於匹配文本,傳入的形式可以為字元串,也可以為正則表達式。

from bs4 import BeautifulSoup
import re

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.find_all(text=Lec 1.1))
print(soup.find_all(text=re.compile(Lec)))

結果

find_all方法就介紹到這裡。實際上,除了find_all方法外,還有find()方法,兩種使用方法一致,區別在於後者僅僅返回符合條件的第一個元素。還有其他一些方法由於不常用就不再贅述,有興趣查詢BeautifulSoup官方文檔。

(3)CSS選擇器

在第一篇中我們講過CSS選擇器的一點基本規則:#代表id,(.)代表class,還有節點名稱可以直接選擇節點。各個選擇器之間如果不加空格代表並列關係,加了空格代表嵌套關係。

在BeautifulSoup中,使用CSS選擇器只需要將CSS選擇器傳入select()方法即可,示例如下:

from bs4 import BeautifulSoup
import re

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.select(li a.item))

輸出結果

注意,選出的元素依舊是Tag類型,這意味著節點選擇器、方法選擇器、CSS選擇器可以相互嵌套使用。這裡有一個額外的知識點是獲取節點內的文本,除了可以使用string屬性以外,還可以調用get_text()方法。示例如下:

from bs4 import BeautifulSoup
import re

html =
<body>
<div>
<h4>Course Materials</h4><li><a href="qe/intro.html" class="item sister">Lec 0</a></li><li><a href="qe/about_py.ipynb">Lec 1.1</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.2</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.3</a></li>
<li><a href="qe/about_py.ipynb">Lec 1.4</a></li>
<div>
<h5>Data</h5>
</div></div>
soup = BeautifulSoup(html,lxml)
print(soup.select(li a.item)[0].get_text())

輸出:

三、總結

  • 本篇精髓就是首先選擇節點,然後再從選中的節點內提取需要的信息
  • 選擇節點的三中選擇器應當熟練掌握

本篇學起來可能會感到雜亂,建議結合下一篇的實例仔細閱讀。

如果覺得本篇文章不錯,歡迎關注我的爬蟲系列教程公眾號【痕風雨】,一起學習交流。

推薦閱讀:

相關文章