上一篇分享了正則表達式的使用,相信大家對正則也已經有了一定的了解。它可以針對任意字符串做任何的匹配並提取所需信息。
但是我們爬蟲基本上解析的都是html或者xml結構的內容,而非任意字符串。正則表達式雖然很強大靈活,但是對於html這樣結構複雜的來說,寫pattern的工作量會大大增加,並且有任意一處出錯都得不到匹配結果,比較麻煩。
本篇將介紹一款針對html和xml結構,操作簡單並容易上手的解析利器—BeautifulSoup。
第一次使用BeautifulSoup的時候就在想:這個名字有什麼含義嗎?美味的湯?於是好信也在網上查了一下。來看,官方文檔是這麼解釋的:BeautifulSoup: We called him Tortoise because he taught us意思是我們叫他烏龜因為他教了我們,當然這裡Tortoise是Taught us的諧音。BeautifulSoup這個詞來自於《愛麗絲漫遊仙境》,意思是「甲魚湯」。上面那個官方配圖也是來自於《愛麗絲漫遊仙境》,看來是沒跑了,估計是作者可能很喜歡這部小說吧,因而由此起了這個名字。
好,讓我們看看真正的BeautifulSoup是什麼?
BeautifulSoup是Python語言中的模塊,專門用於解析html/xml,非常適合像爬蟲這樣的項目。它有如下幾個使其強大的特點:
目前BeautifulSoup的最新髮型版本是BeautifulSoup4,在Python中以bs4模塊引入。
博主使用的Python3.x,可以使用 pip3 install bs4 來進行安裝,也可以通過官方網站下載來安裝,連結:https://www.crummy.com/software/BeautifulSoup/,具體安裝步驟不在此敘述了。
以為安裝完了嗎?還沒有呢。
上面介紹BeautifulSoup的特點時說到了,BeautifulSoup支持Python標準庫的解析器html5lib,純Python實現的。除此之外,BeautifulSoup還支持lxml解析器,為了能達到更好的解析效果,建議將這兩個解析器也一併安裝上。
根據作業系統不同,可以選擇下列方法來安裝lxml:
$ apt-get install Python-lxml
$ easy_install lxml
$ pip install lxml
另一個可供選擇的解析器是純Python實現的 html5lib , html5lib的解析方式與瀏覽器相同,可以選擇下列方法來安裝html5lib:
$ apt-get install Python-html5lib
$ easy_install html5lib
$ pip install html5lib
下面列出上面提到解析器的使用方法。
解析器
使用方法
Python標準庫
BeautifulSoup(markup, "html.parser")
lxml HTML解析器
BeautifulSoup(markup, "lxml")
lxml HTML解析器
BeautifulSoup(markup, ["lxml", "xml"])
BeautifulSoup(markup, "xml")
html5lib
BeautifulSoup(markup, "html5lib")
推薦使用lxml作為解析器,lxml是用C語言庫來實現的,因此效率更高。在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必須安裝lxml或html5lib, 因為那些Python版本的標準庫中內置的HTML解析方法不夠穩定。
首先引入bs4庫,也就是BeautifulSoup在Python中的模塊。
from bs4 import BeautifulSoup
好了,我們來看一下官方提供的例子,這段例子引自《愛麗絲漫遊記》。
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p><b>The Dormouse's story</b></p>
<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a>
and<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p>...</p>
"""
假設以上html_doc就是我們已經下載的網頁,我們需要從中解析並獲取感興趣的內容。
首先的首先,我們需要創建一個BeautifulSoup的文檔對象,依據不同需要可以傳入「字符串」或者「一個文件句柄」。
傳入「字符串」
soup = BeautifulSoup(html_doc)
傳入「文件句柄」,打開一個本地文件
soup = BeautifulSoup(open("index.html"))
文檔首先被轉換為Unicode,如果是解析html文檔,直接創建對象就可以了(像上面操作那樣),這時候BeautifulSoup會選擇一個最合適的解析器對文檔進行解析。
但同時,BeautifulSoup也支持手動選擇解析器,根據指定解析器進行解析(也就是我們安裝上面html5lib和lxml的原因)。
手動指定解析器如下:
soup = BeautifulSoup(html_doc, "lxml")
如果僅是想要解析HTML文檔,只要用文檔創建 BeautifulSoup 對象就可以了。Beautiful Soup會自動選擇一個解析器來解析文檔。但是還可以通過參數指定使用那種解析器來解析當前文檔。
BeautifulSoup 第一個參數應該是要被解析的文檔字符串或是文件句柄,第二個參數用來標識怎樣解析文檔。如果第二個參數為空,那麼Beautiful Soup根據當前系統安裝的庫自動選擇解析器,解析器的優先數序: lxml, html5lib, Python標準庫。在下面兩種條件下解析器優先順序會變化:
要解析的文檔是什麼類型: 目前支持, 「html」, 「xml」, 和 「html5」
指定使用哪種解析器: 目前支持, 「lxml」, 「html5lib」, 和 「html.parser」
Beautiful Soup將複雜HTML文檔轉換成一個複雜的樹形結構,每個節點都是Python對象,所有對象可以歸納為4種:
Tag
NavigableString
BeautifulSoup
Comment
<Tag>
Tag就是html或者xml中的標籤,BeautifulSoup會通過一定的方法自動尋找你想要的指定標籤。查找標籤這部分會在後面「遍歷查找樹」和「搜索查找樹」中介紹,這裡僅介紹對象。
soup = BeautifulSoup('<b>Extremely bold</b>')
tag = soup.b
type(tag)
>>> <class 'bs4.element.Tag'>
Tag標籤下也有對象,有兩個重要的屬性對象:name和attributes。
Name
Name就是標籤tag的名字,以下簡單操作便可獲取。
Attributes
我們都知道一個標籤下可能有很多屬性,比如上面那個標籤b有class屬性,屬性值為boldest,那麼我們如何獲取這個屬性值呢?
其實標籤的屬性操作和Python中的字典操作一樣的,如下:
tag['class']
>>> u'boldest'
也可以通過「點」來獲取,比如:
tag.attrs
>>> {u'class': u'boldest'}
<NavigableString>
NavigableString是可遍歷字符串的意思,其實就是標籤內包括的字符串,在爬蟲裡也是我們主要爬取的對象之一。
在BeautifulSoup中可以非常簡單的獲取標籤內這個字符串。
tag.string
>>> u'Extremely bold'
就這麼簡單的完成了信息的提取,簡單吧。要說明一點,tag中包含的字符串是不能編輯的,但是可以替換。
tag.string.replace_with("No longer bold")
tag
>>><blockquote>No longer bold</blockquote>
<BeautifulSoup>
BeautifulSoup對象表示的是一個文檔的全部內容。大部分時候,可以把它當作Tag對象。
soup.name
>>> u'[document]'
BeautifulSoup對象不是一個真正的tag,沒有name和attributes,但是卻可以查看它的name屬性。如上所示,「[document]」為BeautifulSoup文檔對象的特殊屬性名字。
<Comment>
還有一些對象也是我們需要特殊注意的,就是注釋。其實comment對象是一個特殊類型的NavigableString對象,請看下面。
markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
>>> <class 'bs4.element.Comment'>
comment
>>> u'Hey, buddy. Want to buy a used parser'
這和NavigableString的使用是一樣,同樣使用 .string 對標籤內字符串進行提取。但是,請看上面comment這個例子,裡面字符串是一個comment,有<!--comment-->這樣的格式,一樣使用了 .string 對其進行提取,得到的結果是去掉了comment標誌的裡面的字符串。這樣的話,當我們並不知道它是否是comment,如果得到以上的結果很有可能不知道它是個comment。
因此,這可能會讓我們得到我們不想要的comment,擾亂我們的解析結果。
為了避免這種問題的發生,可以在使用之前首先通過以下代碼進行一個簡單的判斷,然後再進行其它操作。
if type(soup.b.string)==bs4.element.Comment:
print(soup.b.string)
仍然用最開始的《愛麗絲》中的一段話作為例子。
子節點
子節點有 .contents 和 .children 兩種用法。
contents
content屬性可以將標籤所有子節點以列表形式返回。
# <head><title>The Dormouse's story</title></head>
print(soup.head.contents)
>>> [title>The Dormouse's story</title>]
這樣就可以返回一個子節點標籤了。當然你也可以通過soup.title來實現,但是當文檔結構複雜的時候,比如有不止一個title的話,那這樣就不如contents使用來的快了。
head下只有一個標籤title,那麼如果我們查看一下body下的子標籤。
print(soup.body.contents)
>>>
['\n', <p><b>The Dormouse's story</b></p>, '\n', <p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a>
and<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, '\n', <p>...</p>, '\n']
你會發現這些子節點列表中有很多「\n」,這是因為它把空格包括進去了,所以這裡需要注意一下。
children
也可以通過 .chidren 得到相同的結果,只不過返回的children是一個生成器(generator),而不是一個列表。
print(soup.body.children)
>>> <list_iterator object at 0x00000000035B4550>
看到這是一個生成器,因此我們可以for..in..進行遍歷,當然也可以得到以上同樣的結果。
for child in soup.body.children:
print(child)
子孫節點
子孫節點使用 .descendants 屬性。如果子節點可以直接獲取標籤的直接子節點,那么子孫節點則可以獲取所有子孫節點,注意說的是所有,也就是說孫子的孫子都得給我找出來,下用面開一個例子。
for child in head_tag.descendants: print(child)
>>> <title>The Dormouse's story</title>
>>> The Dormouse's stor
title是head的子節點,而title中的字符串是title的子節點,title和title所包含的字符串都是head的子孫節點,因此被循環遞歸的查找出來。.descendants 的用法和 .children 是一樣的,會返回一個生成器,需要for..in..進行遍歷。
父節點
父節點使用 .parents 屬性實現,可以得到父輩的標籤。
title_tag = soup.title
title_tag
>>> <title>The Dormouse's story</title>
title_tag.parent
>>> <head><title>The Dormouse's story</title></head>
title_tag.parent.name
>>> head
獲得全部父節點則使用 .parents 屬性實現,可以循環得到所有的父輩的節點。
link = soup.a
for parent in link.parents: if parent is None: print(parent) else: print(parent.name)
>>>
p
body
html
[document]
None
可以看到a節點的所有父輩標籤都被遍歷了,包括BeautifulSoup對象本身的[document]。
兄弟節點
兄弟節點使用 .next_sibling 和 .previous_sibling 屬性。
兄弟嘛,不難理解自然就是同等地位的節點了,其中next_sibling 獲取下一個兄弟節點,而previous_sibling 獲取前一個兄弟節點。
a_tag = soup.find("a", id="link1")
a_tag.next_sibling
>>> ,
a_tag.previous_element
>>>
Once upon a time there were three little sisters; and their names were
兄弟節點可以通過 .next_siblings 和 .previous.sibling 獲取所有前後兄弟節點,同樣需要遍歷獲取每個元素。
回退和前進
當然還有一些其它用法,如回退和前進 .next_element 和 .previous_element,它是針對所有節點的回退和前進,不分輩分。
a_tag = soup.find("a", id="link1")
a_tag
>>>
<a href="http://example.com/elsie" id="link1">Elsie</a>,
a_tag.next_element
>>> Elsie
a_tag.previous_element
>>>
Once upon a time there were three little sisters; and their names were
因為使用了回退,將會尋找下一個節點對象而不分輩分,那麼這個<a>標籤的下一個節點就是它的子節點Elsie,而上一個節點就是上一個標籤的字符串對象。find用法會在後續搜索文檔樹裡面詳細介紹。
回退和前進也可以尋找所有的前後節點,使用 .next_elements 和 .previous_elements。
for elem in last_a_tag.next_elements:
if elem.name is None:
continue
print(elem.name)
>>>
a
a
p
返回對象同樣是生成器,需要遍歷獲得元素。其中使用了if判斷去掉了不需要的None。
節點內容
前面提到過NavigableString對象的 .string 用法,這裡在文檔遍歷再次體會一下其用法。
如果tag只有一個NavigableString 類型子節點,那麼這個tag可以使用 .string 得到子節點,就像之前提到的一樣。而如果一個tag裡面僅有一個子節點(比如tag裡tag的字符串節點),那麼這個tag也可以使用 .string 方法,輸出結果與當前唯一子節點的 .string 結果相同(如上所示)。
title_tag.string
>>> u'The Dormouse's story'
head_tag.contents
>>> [<title>The Dormouse's story</title>]
head_tag.string
>>> u'The Dormouse's story'
但是如果這個tag裡面有多個節點,那就不靈了。因為tag無法確定該調用哪個節點,如下面這種。
print(soup.html.string)
>>> None
如果tag中包含多個字符串,可以使用 .strings 來循環獲取,輸出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多餘空白內容。
上面提介紹的都是如何遍歷各個節點,下面我們看看如何搜索我們我們真正想獲取的內容,如標籤屬性等。
搜索文檔樹有很多種用法,但使用方法都基本一致。這裡只選擇介紹一種 .find_all。
find_all()
find_all(name, attrs , recursive , text , **kwargs)
find_all() 方法可以搜索當前標籤下的子節點,並會經過過濾條件判斷是否符合標準,先隨便看個例子。
soup.find_all("a")
>>>
[<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a>,
<a href="http://example.com/tillie" id="link3">Tillie</a>]
soup.find_all(id="link2")
>>>
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
通過以上例子,可以發現,我們只要設定好我們的過濾條件,便可輕鬆的解析我們想要的內容。這些條件如何設定呢?
就是通過find_all()的這些參數來設置的,讓我們來看看。
Name參數
name參數就是標籤的名字,如上面的例子尋找所有標籤<a>,name參數可以是字符串、True、正則表達式、列表、甚至具體方法。
下面舉個正則表達式的例子。
import re
soup = BeautifulSoup(html_doc, 'lxml')
for tag insoup.find_all(re.compile("^t")):
print(tag.name)
>>> title
可以看到正則表達式的意思是匹配任何以「t」開頭的標籤名稱,就只有title一個。
使用「True」會匹配任何值,使用「列表」會匹配列表中所有的標籤項,如果沒有合適的過濾條件,還可以自定義一個「方法」。
Keyword參數
就如同Python中的關鍵字參數一樣,我們可以搜索指定的標籤屬性來定位標籤。
soup.find_all(id='link2')
>>>
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
找到了id屬性為link2的標籤<a>。
soup.find_all(href=re.compile("elsie"))
>>>
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
找到了href屬性裡含有「elsie」字樣的標籤<a>。
也可以同時定義多個關鍵字條件來過濾匹配結果。
soup.find_all(href=re.compile("elsie"), id='link1')
>>>
[<a href="http://example.com/elsie" id="link1">three</a>]
text參數
通過text參數可以搜索匹配的字符串內容,與name的用法相似,也可以使用字符串、True、正則表達式、列表、或者具體方法。
soup.find_all(text="Elsie")>>> [u'Elsie']
soup.find_all(text=re.compile("Dormouse"))>>>
[u"The Dormouse's story", u"The Dormouse's story"]
limit參數
limit參數可以限制返回匹配結果的數量,看下面這個例子。
soup.find_all("a", limit=2)
>>>
[<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a>]
文檔中本來有三個<a>標籤,但是通過限制只得到了兩個。
recursive參數
find_all()會尋找符合匹配條件的所有子孫節點,如果我們只想找直接的子節點,就可以設置recursive參數來進行限制,recursive=False。
soup.html.find_all("title")
>>> [<title>The Dormouse's story</title>]
soup.html.find_all("title", recursive=False)
>>> [ ]
上面是兩種使用recursive和沒有使用recursive的情況,可以發現它的作用。
以上就是find_all()所有參數的介紹,其它方法如find(),find_parents()等更多方法與find_all()基本一致,可以舉一反三。
以上就是BeautifulSoup的使用方法介紹,主要記住三個部分內容:
BeautifulSoup對象種類
BeautifulSoup的遍歷文檔樹
BeautifulSoup的搜索文檔樹
更多內容請參考官網文檔:https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/
~掃描二維碼關注我,發送「學習資料」獲取經典書籍電子書~
~歡迎大家轉載分享~