許多人喜歡在介紹正則表達(dá)式以后才來(lái)介紹本篇BeautifulSoup的用法,但是我覺(jué)得BeautifulSoup比正則表達(dá)式好用,而且容易上手,非常適合小白入門爬蟲,并且可以利用學(xué)到的這個(gè)知識(shí)立即去爬取自己想爬的網(wǎng)站,成就感滿滿的。好了話不多說(shuō),立即進(jìn)入今天的介紹吧。
“
”
翻譯過(guò)來(lái)就是:我們叫他烏龜因?yàn)樗塘宋覀儭J遣皇沁€是懵圈,沒(méi)辦法外國(guó)人起名字就是這么隨意。誰(shuí)能知道那么厲害的Java竟然是開發(fā)者在樓下覺(jué)得味道不錯(cuò)的一種咖啡的名字呢,哈哈哈哈。算了,我們不糾結(jié)這個(gè)問(wèn)題了,我們還是開始介紹它的安裝和使用吧。話不多說(shuō),走你!
BeautifulSoup的安裝
目前BeautifulSoup已經(jīng)更新到了BeautifulSoup4,在Python中你只需要以bs4模塊引入即可。小編我用的Python的版本是3.6.4,所以可以使用pip3 install bs4 來(lái)進(jìn)行安裝,當(dāng)然了你也可以去官方下載到本地然后再進(jìn)行安裝:鏈接:https://www.crummy.com/software/BeautifulSoup/,具體的安裝我這里就不介紹了,不懂的可以自行百度。
說(shuō)到這里,你可能還是不知道BeautifulSoup是干嘛的,說(shuō)白了它其實(shí)就是Python的一個(gè)HTML或者XML的解析庫(kù),但是它在解析的時(shí)候?qū)嶋H上還是依賴解析器的,下面就列舉一些BeautifulSoup支持的解析器:
解析器 | 使用方法及特點(diǎn) |
---|---|
Python標(biāo)準(zhǔn)庫(kù) | BeautifulSoup(markup, "html.parser"),速度適中,容錯(cuò)能力較弱 |
lxml HTML解析器 | BeautifulSoup(markup, "lxml"),速度快,文檔容錯(cuò)能力強(qiáng) |
lxml XML解析器 | BeautifulSoup(markup, ["lxml", "xml"])BeautifulSoup(markup, "xml"),速度快,唯一支持XM鏈的解析器 |
html5lib | BeautifulSoup(markup, "html5lib"),速度慢、不依賴外部擴(kuò)展 |
通過(guò)以上對(duì)比可以看出, lxml解析器有解析 HTML 和 XML 的功能, 而且速度快, 容錯(cuò)能力強(qiáng)所以推薦使用它。
接下來(lái)教你如何使用BeautifulSoup和lxml進(jìn)行數(shù)據(jù)的提取。在此之前,我們需要?jiǎng)?chuàng)建一個(gè)BeautifulSoup的文檔對(duì)象,依據(jù)不同需要可以傳入“字符串”或者“一個(gè)文件句柄”。
當(dāng)傳入“字符串”時(shí),
soup = BeautifulSoup(html_doc,"lxml")
當(dāng)傳入“文件句柄”并打開一個(gè)本地文件時(shí),
soup = BeautifulSoup(open("index.html"),"lxml")
接下來(lái)便是BeautifulSoup的對(duì)象種類的介紹,它有4種類型,下面我們分別進(jìn)行說(shuō)明。
BeautifulSoup的對(duì)象種類
Beautiful Soup實(shí)質(zhì)是將復(fù)雜的HTML文檔轉(zhuǎn)換成一個(gè)復(fù)雜的樹形結(jié)構(gòu)(因?yàn)镠TML本身就是DOM),然后每個(gè)節(jié)點(diǎn)都是Python對(duì)象,通過(guò)分析可以把所有對(duì)象分成4種類型:Tag、NavigableString、BeautifulSoup、Comment。
1、<Tag>
Tag其實(shí)就是html或者xml中的標(biāo)簽,BeautifulSoup會(huì)通過(guò)一定的方法自動(dòng)尋找你想要的指定標(biāo)簽??聪旅孢@個(gè)例子:
soup=BeautifulSoup('<p class="good">Excelent boy</p>')
tag = soup.p
type(tag)
>>> <class 'bs4.element.Tag'>
其實(shí)Tag標(biāo)簽也是有屬性的,name和attributes就是非常重要的兩個(gè)屬性。
Name
Name就是標(biāo)簽tag的名字,一個(gè)標(biāo)簽的名字是唯一的,我們直接調(diào)用tag.name即可簡(jiǎn)單獲取tag的名字。
tag.name
>>> 'p'
Attributes
我們知道一個(gè)標(biāo)簽下面可能會(huì)有很多屬性,比如上面那個(gè)標(biāo)簽p有class屬性,屬性值為good,那么我們?nèi)绾潍@取這個(gè)屬性值呢?我們可以仿照Python中操作字典那樣通過(guò)key來(lái)獲取value的值的方法,來(lái)獲取tag的每個(gè)屬性對(duì)應(yīng)的值:
tag['class']
>>> 'good'
當(dāng)然你也是可以通過(guò)tag.attrs來(lái)獲取所有屬性(采用tag.attrs['屬性名稱']獲取指定屬性值),比如:
tag.attrs
>>> {'class': 'good'}
tag.attrs['class']
>>> 'good'
2、<NavigableString>
NavigableString其實(shí)就是可以遍歷的字符串(標(biāo)簽內(nèi)包括的字符串),在BeautifulSoup中可以采用.string的方式來(lái)直接獲取標(biāo)簽內(nèi)的字符串。
tag.string
>>> ''Excelent boy'
是不是非常簡(jiǎn)單。不過(guò)要說(shuō)明的是,tag中包含的字符串是不能編輯的,但是可以替換:
tag.string.replace_with("Bad boy")
tag
>>><blockquote>Bad boy</blockquote>
3、<BeautifulSoup>
BeautifulSoup對(duì)象其實(shí)它表示的是一個(gè)文檔的全部?jī)?nèi)容,不過(guò)大部分情況下,我們都是把它當(dāng)作Tag對(duì)象來(lái)使用的。例如:
soup.name
>>> '[document]'
但實(shí)際上BeautifulSoup對(duì)象不是一個(gè)真正的tag,前面說(shuō)了,tag有2個(gè)重要的屬性name和attributes,它是沒(méi)有的。但是卻可以查看它的name屬性,如上面采用soup.name方式獲取的“[document]”,我們可以理解為“[document]”是BeautifulSoup對(duì)象的特殊屬性名字。
4、<Comment>
Comment就是注釋,它是一個(gè)特殊類型的NavigableString對(duì)象,為什么這么說(shuō)呢,因?yàn)槲覀兛梢灾苯硬捎妙愃朴贜avigableString對(duì)象獲取字符串的方式來(lái)獲取注釋文本,看下面的例子你就明白了:
web_data = "<p><!--hello, everybody. Welcome to the world for python--></p>"
soup = BeautifulSoup(web_data)
comment = soup.p.string
type(comment)
>>> <class 'bs4.element.Comment'>
comment
>>> ''hello, everybody. Welcome to the world for python'
是不是和NavigableString的使用非常相似,我們這里使用 p.string 對(duì)標(biāo)簽內(nèi)的字符串進(jìn)行提取。但是這里有一個(gè)疑問(wèn),就是我們通過(guò)這種方式可以得到字符串,但是如果我們獲取了字符串,我們反過(guò)來(lái)是不知道這個(gè)字符串是Comment注釋,還是正常的標(biāo)簽內(nèi)的文本。所以我們?cè)谂廊?shù)據(jù)的時(shí)候需要進(jìn)行判斷,如果是Comment對(duì)象,我們就不爬了,直接跳過(guò):
if type(soup.p.string)==bs4.element.Comment:
continue;
說(shuō)完了4種對(duì)象類型,接下來(lái)說(shuō)一下BeautifulSoup如何對(duì)文檔樹進(jìn)行遍歷,從而找到我們想要的數(shù)據(jù)。
BeautifulSoup遍歷文檔樹
為了更好的介紹這些功能,我采用官方的例子進(jìn)行說(shuō)明:這段例子引自《愛麗絲漫游記》。
web_data =
"""
<html><head><title>The Dormouse's story</title></head><body>
<p class="title"><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" id="link1">Elsie</a>,<a class="sister" id="link2">Lacie</a>and<a class="sister" id="link3">Tillie</a>;and they lived at the bottom of a well.</p><p class="story">...</p>
"""
我們以本體為起點(diǎn),先介紹子節(jié)點(diǎn),子孫節(jié)點(diǎn),再介紹父節(jié)點(diǎn),祖宗節(jié)點(diǎn),兄弟節(jié)點(diǎn)等信息。
子節(jié)點(diǎn)
子節(jié)點(diǎn)有就是當(dāng)前本體的下延,當(dāng)然就包括直接下延(子節(jié)點(diǎn))和間接下延了(子孫節(jié)點(diǎn)) ,首先介紹如何返回所有的子節(jié)點(diǎn),將介紹.contents 和 .children 的用法。
contents
contents可以將標(biāo)簽所有的子節(jié)點(diǎn)以列表形式返回。
# <head><title>The Dormouse's story</title></head>
print(soup.head.contents)
>>> [title>The Dormouse's story</title>]
是不是很簡(jiǎn)單,當(dāng)然你也可以使用soup.title同樣能實(shí)現(xiàn)這個(gè)功能,但是你想過(guò)沒(méi),當(dāng)文檔結(jié)構(gòu)復(fù)雜的時(shí)候,比方說(shuō)不止一個(gè)title的時(shí)候,你還采用soup.title這種方式是不是太慢了,你需要區(qū)分那些title的不同,還需要全部輸出,用contents直接一步完事,超級(jí)easy。如果你不相信可以采用body這個(gè)標(biāo)簽來(lái)進(jìn)行測(cè)試:
print(soup.body.contents)
>>>
['\n', <p class="title"><b>The Dormouse's story</b></p>, '\n', <p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a>
and<a class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, '\n', <p class="story">...</p>, '\n']
你會(huì)發(fā)現(xiàn).contents返回的是一個(gè)列表,而且列表中有很多“\n”,這是因?yàn)樗?strong>空格也包括進(jìn)去了,所以如果我們需要提取其中的文本內(nèi)容,我們還需要采用split()或者sub()來(lái)去掉空格,這部分后面實(shí)戰(zhàn)部分有介紹。
children
我們也可以通過(guò) .chidren 的方式獲取所有的子節(jié)點(diǎn),與之不同的是 .chidren返回的是一個(gè)生成器(generator),而不是一個(gè)列表。
print(soup.body.children)
>>> <list_iterator object at 0x00000000035B4550>
對(duì)于生成器,我們可以先采用list(),轉(zhuǎn)化為列表,再進(jìn)行遍歷:
for child in list(soup.body.children): print(child)
子孫節(jié)點(diǎn)
說(shuō)完了子節(jié)點(diǎn),下面我們說(shuō)一下子孫節(jié)點(diǎn)。子孫節(jié)點(diǎn)使用 .descendants 屬性。子節(jié)點(diǎn)可以直接獲取標(biāo)簽的直接子節(jié)點(diǎn)(沒(méi)有間接子節(jié)點(diǎn),因?yàn)槟蔷褪亲訉O節(jié)點(diǎn)了),子孫節(jié)點(diǎn)則可以獲取所有子孫節(jié)點(diǎn),看一下下面的例子:
for child in head_tag.descendants:
print(child)
>>> <title>The Dormouse's story</title>
>>> The Dormouse's stor
我們知道title是head的子節(jié)點(diǎn),而title中的字符串又是title的子節(jié)點(diǎn),所以title和title所包含的字符串都是head的子孫節(jié)點(diǎn),因此都會(huì)被查找出來(lái)。.descendants 的用法和.children 是一樣的,會(huì)返回一個(gè)生成器,所以需要先轉(zhuǎn)化為list再進(jìn)行遍歷。
父節(jié)點(diǎn)
對(duì)于父節(jié)點(diǎn),我們可以使用 .parents 得到父標(biāo)簽。
title_tag = soup.title
title_tag
>>> <title>The Dormouse's story</title>
title_tag.parents
>>> <head><title>The Dormouse's story</title></head>
title_tag.parents.name
>>> head
如果要獲得全部父節(jié)點(diǎn)則可以使用 .parents ,就能得到所有父節(jié)點(diǎn)。
link = soup.a
for parent in link.parents:
if parent is None:
print(parent)
else:
print(parent.name)
>>>
p body html [document] None
我們可以看到a節(jié)點(diǎn)的所有父標(biāo)簽都被遍歷了,包括BeautifulSoup對(duì)象本身的[document]。
兄弟節(jié)點(diǎn)
兄弟節(jié)點(diǎn)使用 .next_sibling 和 .previous_sibling 來(lái)進(jìn)行獲取,其中next_sibling 是用來(lái)獲取下一個(gè)兄弟節(jié)點(diǎn),而previous_sibling 是獲取前一個(gè)兄弟節(jié)點(diǎn)。
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
同樣,你可以可以通過(guò) .next_siblings 和 .previous.siblings獲取所有前后兄弟節(jié)點(diǎn),返回結(jié)果也是一個(gè)生成器。
說(shuō)完了節(jié)點(diǎn)的獲取,接下來(lái)說(shuō)一下如何提取已經(jīng)獲取的節(jié)點(diǎn)的內(nèi)容呢?
節(jié)點(diǎn)內(nèi)容
前面說(shuō)過(guò)對(duì)于NavigableString對(duì)象,我們可以采用 .string 來(lái)獲取文本信息。如果tag只有一個(gè)NavigableString 類型的子節(jié)點(diǎn),那么這個(gè)tag可以使用 .string 得到文本信息,就像之前提到的一樣??聪旅娴膖itle和head標(biāo)簽:
title_tag.string
>>> 'The Dormouse's story'
head_tag.contents
>>> [<title>The Dormouse's story</title>]
head_tag.string
>>> 'The Dormouse's story'
上面那種方法只適用于tag只有一個(gè)NavigableString 類型的子節(jié)點(diǎn)情況,如果這個(gè)tag里面有多個(gè)節(jié)點(diǎn),那就不行了,因?yàn)閠ag無(wú)法確定該調(diào)用哪個(gè)節(jié)點(diǎn),就會(huì)出現(xiàn)下面這種輸出None的情況:
print(soup.html.string)
>>> None
需要說(shuō)明的是,如果tag中包含多個(gè)字符串,我們可以使用 .strings 來(lái)循環(huán)獲取。如果輸出的字符串中包含了很多空格或空行,則可以使用 .stripped_strings 來(lái)去除多余的空白內(nèi)容(包括空格和空行)。
現(xiàn)在有一個(gè)問(wèn)題了,你上面介紹的都是如何遍歷各個(gè)節(jié)點(diǎn),可是有時(shí)候我不需要你進(jìn)行遍歷全部,那樣會(huì)增加運(yùn)行時(shí)間,我只需要提取我需要的那部分即可,所以我們就可以搜索文檔,直接輸出滿意的結(jié)果就行。
BeautifulSoup搜索文檔樹
搜索文檔樹有很多方法,match,find,find_all...,這里介紹比較常用的fnd_all()。fnd_all()它可以設(shè)置過(guò)濾條件,直接返回滿足條件的值。
find_all()語(yǔ)法格式:
find_all(name, attrs , recursive , text , **kwargs)
通過(guò)一個(gè)簡(jiǎn)單的例子,來(lái)感受一下它的魅力:
soup.find_all("a")
>>>
[<a class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a>,
<a class="sister" id="link3">Tillie</a>]
soup.find_all(id="link3")
>>>
<a class="sister" id="link3">Tillie</a>]
下面簡(jiǎn)單介紹一下它的幾個(gè)重要的參數(shù):name,keywords。
Name參數(shù)
name就是標(biāo)簽的名字,如在上面的例子中尋找所有的a標(biāo)簽,name參數(shù)可以是字符串、True、正則表達(dá)式、列表、甚至是具體的方法。
Keyword參數(shù)
這種形式非常類似于我們Python中對(duì)字典的操作,通過(guò)設(shè)置key這個(gè)過(guò)濾條件來(lái)獲取指定信息:
soup.find_all(id="link3")
>>>
<a class="sister" id="link3">Tillie</a>]
這里找到了id為link3的a標(biāo)簽信息。當(dāng)然也是可以采用關(guān)鍵字的方式:
soup.find_all(href=re.compile("lacie"))
>>>
[<a class="sister" id="link2">Lacie</a>]
這里找到了href屬性里含有“lacie”字樣的a標(biāo)簽的信息,我們也可以同時(shí)定義多個(gè)關(guān)鍵字來(lái)進(jìn)行更嚴(yán)格的過(guò)濾:
soup.find_all(href=re.compile("lacie"), id='link2')
>>>
[<a class="sister" id="link2">Lacie</a>]
簡(jiǎn)單再說(shuō)一下match和search的用法:你只要記住match 方法用于查找字符串的頭部(也可以指定起始位置),它是一次匹配,只要找到了一個(gè)匹配的結(jié)果就返回,而不是查找所有匹配的結(jié)果。search則是全局搜索,用于查找字符串的任何位置,它也是一次匹配,只要找到了一個(gè)匹配的結(jié)果就返回,而不是查找所有匹配的結(jié)果。關(guān)于其他方法的介紹請(qǐng)點(diǎn)擊閱讀原文進(jìn)行查看吧。
好了本篇關(guān)于用BeautifulSoup來(lái)煲美味的湯的介紹就到此為止了,感謝你的賞閱!
聯(lián)系客服