Try   HackMD

Python爬蟲使用Beautiful Soup

tags: python crawler beautifulsoup

介紹

Beautiful Soup是Python中用來解析HTML、XML標籤文件的模組,並能修復含有未閉合標籤等錯誤的文件(此種文件常被稱為tag soup);解析後會為這個頁面建立一個BeautifulSoup物件,這個物件中包含了整個頁面的結構樹,透過這個BeautifulSoup物件的結構樹,就可以輕鬆的提取頁面內任何有興趣的資料了。

找出資料在網頁哪個元素的三種方式

  1. 土法煉鋼:在瀏覽器點右鍵,檢視原始碼後直接用網頁內容來搜尋資料被什麼元素包住。
  2. 先排版:有些網站原始碼較混亂,可以使用網路上的HTML排版工具先排版後再進行資料定位。
  3. 使用Chrome的開發工具,在想要定位的元素上點滑鼠右鍵->檢查(Inspect)

BeautifulSoup支援的網頁元素定位方式

  1. 基本find系列方法
  2. CSS Selector(選擇器)
  3. XPath

安裝Beautiful Soup模組

Python 3.x版本可以使用 pip3 安裝:

pip3 install bs4

如果是Python 2.x版本,可以這樣安裝:

pip install bs4

參考資料:https://pypi.org/project/beautifulsoup4/

解析HTML文件

建立BeautifulSoup物件

下面範例為建立一個解析HTML文件的BeautifulSoup物件,解析完成後會產生一個soup物件,這個就是該HTML的結構樹物件,之後所有資料的搜尋、提取等操作都是透過這個物件來進行。

# 引入Beautiful Soup模組 from bs4 import BeautifulSoup # 原始HTML原始碼 html_doc = """ <html> <head> <title>這是HTML文件標題</title> </head> <body> <h1 id="article" class="banner">網頁標題</h1> <p data-author='aaron'>文章段落</p> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> <a href="https://www.aaronlife.com/ref2">參考資料連結2</a> <p>這是一份<b class="boldtext">HTML文件</b>。</p> <h2 id="article">網頁標題2</h2> </body> </html> """ # 建立BeautifulSoup物件解析HTML文件 soup = BeautifulSoup(html_doc, 'html.parser')
BeautifulSoup文件解析器有以下幾種:
解析器名稱 使用範例 說明
Python’s html.parser BeautifulSoup(markup, "html.parser") Python內建
lxml’s HTML parser BeautifulSoup(markup, "lxml") 速度較快的HTML解析器
lxml’s XML parser BeautifulSoup(markup, "lxml-xml")
BeautifulSoup(markup, "xml")
速度較快的XML解析器
html5lib BeautifulSoup(markup, "html5lib") 解析後建立HTML 5物件

官方文件:https://www.crummy.com/software/BeautifulSoup/bs4/doc/

補充

lxml解析器需要另外安裝:

pip3 install lxml
輸出排版後的HTML文件:prettify()

呼叫BeautifulSoup物件的prettify方法可以輸出排版過後的HTML文件,在開發時可以用來觀察其文件的整個輪廓:

print(soup.prettify())

會輸出:

<html> <head> <title> 這是HTML文件標題 </title> </head> <body> <h1 id="article" class="banner"> 網頁標題 </h1> <p data-author='aaron'> 文章段落 </p> <a href="https://www.aaronlife.com/ref1"> 參考資料連結1 </a> <a href="https://www.aaronlife.com/ref2"> 參考資料連結2 </a> <p> 這是一份 <b class="boldtext"> HTML文件 </b></p> </body> </html>

取得指定的標籤及內容

直接指定網頁標題標籤的名稱(例如:title),即可將該標籤的節點抓出來:

print(soup.title) # 取得第一個title標籤 print(soup.html.body.h1) # 取得指定位置的第一個h1標籤 print(soup.a) # 取得文件內第一個a標籤

會輸出包含標籤的文字:

<title>這是HTML文件標題</title>
<h1 id="article" class="banner">網頁標題</h1>
<a href="https://www.aaronlife.com/ref1">參考資料連結1</a>

透過 string 屬性,可以取得標籤內的純文字:

print(soup.title.string)

會輸出:

這是HTML文件標題

基本find系列方法

搜尋標籤 - find_all()

透過 find_all 可以在文件內找出指定的所有標籤,find_all會以list傳回所有找到的標籤:

tag_p = soup.find_all('p') for p in tag_p: print(p.getText())

會輸出:

文章段落
這是一份HTML文件。

注意:

因為第二個p標籤內含有子標籤,所以如果直接使用soup.string只會得到None,這時就必須使用getText()方法來取得文字。

取出節點屬性

get()方法

透過 get()方法,可以取得標籤內的屬性值,下面透過get()方法來取得a標籤內的href屬性:

tag_a = soup.find_all('a') for a in tag_a: print(a.get('href'))

會輸出:

https://www.aaronlife.com/ref1
https://www.aaronlife.com/ref2
attrs

也可以透過attrs屬性可以拿到標籤內全部的屬性,該屬性為一個list,例:

print(tag.attrs['data-table'])

會得到data-table的屬性值

同時搜尋多種標籤

如果要同時搜尋多種HTML標籤,可以使用 list 來指定所有要搜尋的標籤名稱:

tags = soup.find_all(['a', 'p']) for tag in tags: print(tag)

會輸出:

<p data-author='aaron'>文章段落</p>
<a href="https://www.aaronlife.com/ref1">參考資料連結1</a>
<a href="https://www.aaronlife.com/ref2">參考資料連結2</a>
<p>這是一份<b class="boldtext">HTML文件</b>。</p>

限制搜尋的標籤數量

find_all 預設會搜尋所有符合條件的節點,如果遇到文件較大或是標籤數量很多的時候,就會執行比較多的時間,如果在應用上不需要用到全部標籤,可以用 limit 參數來限制搜尋的標籤上限值,這樣一來,就只會找出前幾個符合條件的標籤:

tags = soup.find_all(['a', 'p'], limit=2)

for tag in tags:
    print(tag)

會輸出:

<p data-author='aaron'>文章段落</p>
<a href="https://www.aaronlife.com/ref1">參考資料連結1</a>

如果只需要抓出第一個符合條件的節點,可以直接使用 find()方法即可:

tag = soup.find('a') # 搜尋第一個a標籤 print("第一個搜尋結果:", tag) tag = soup.find(['a', 'p']) # 在多個標籤名稱下搜尋第一個符合條件的標籤 print("第二個搜尋結果:", tag)

會輸出:

第一個搜尋結果: <a href="https://www.aaronlife.com/ref1">參考資料連結1</a>
第二個搜尋結果: <p data-author='aaron'>文章段落</p>

遞迴搜尋

在預設的狀況下,find_all()方法會以遞迴的方式 往下尋找所有的子標籤,例如搜尋html標籤下所有h1標籤,包含子標籤下的標籤:

tags = soup.html.find_all('h1')

會輸出:

print(tags)

上面程式碼,加上recursive=False後,表示只會搜尋在html標籤下第一層的標籤:

tags = soup.html.find_all('h1', recursive=False) print(tags)

會輸出:

[]

因為h1標籤是在html標籤下的body標籤內,也就是底下兩層,所以不會被搜尋到。

以屬性名稱搜尋

透過指定標籤內的屬性名稱,可以將所有符合屬性值的所有標籤找出來,例如搜尋 id 屬性為 article 的節點:

tags = soup.find_all(id='article') print(tags)

會輸出:

[<h1 id="article" class="banner">網頁標題</h1>, <h2 id="article">網頁標題2</h2>]

可以透過結合標籤名稱名稱與屬性名稱進行更精確的搜尋,例如搜尋 id 屬性為 articleh2 節點:

tag = soup.find_all('h2', id='article') print(tag)

會輸出:

[<h2 id="article">網頁標題2</h2>]

補充:

以屬性做為條件來搜尋時,也可以一次給多個屬性條件來篩選,例如:

`tags = soup.find_all(id='article', name='title')

在HTML5中有些屬性名稱中含有符號,例如 data-開頭的屬性名稱如果直接寫在Python的方法中的話會出現錯誤:

tag = soup.find(data-attr='aaron')

會出現下面錯誤:

SyntaxError: expression cannot contain assignment

解決方法為,將屬性名稱與值放進一個字典(dict)中,再將此字典設定給 attrs 參數即可,例如:

condition = {'data-author': 'aaron'} tag = soup.find_all(attrs=condition)

以正規表示法來搜尋:

import re links = soup.find_all(href=re.compile('^https://www.aaronlife.com')) print(links)

說明:

只要href屬性開頭為https://www.aaronlife.com都會被搜尋出來。

以CSS屬性搜尋

因為class 是Python程式語言的保留字,所以在Beautiful Soup內改用 class_這個名稱來代表HTML標籤內的class屬性,例如要搜尋class為banner的標籤:

tags = soup.find_all(class_='banner') print(tags)

會輸出:

[<h1 class="banner" id="article">網頁標題</h1>]

補充:

  1. 一個class屬性可以有多個屬性值,在做class_屬性執比對時,只要有一個符合就算成功。
  2. 也可以直接拿完整的class屬性值字串比對,但如果順序不對就會失敗,例如:"banner primary""primary banner"會是不一樣的屬性值,這個情況下會建議用CSS選擇器來進行比對,例如:soup.select('h1.banner.primary')soup.select('*.banner.primary')

搜尋多個屬性值:

tags = soup.find_all(class_=['banner', 'primary'])

搜尋多個屬性值需要使用list將所有要搜尋的值放在一起。

以文字內容搜尋

使用 find_all 配合 string 參數可以根據文字進行來搜尋特定的標籤內容,例如:

contents = soup.find_all(string='網頁標題') print(contents)

會輸出:

['網頁標題']

注意:

輸出結果只會有內容,而不是完整的標籤。

搜尋上層標籤

find_allfind方法都是從外層向下搜尋子標籤,但我們也可以由下往上來搜尋上層標籤(或稱父標籤),方法find_parents(如同find_all網上搜尋全部符合條件的標籤)和find_parent(如同find指搜尋第一個符合的標籤)例如:

tag_b = soup.find('b') parent_p = tag_a.find_parent('p') print(parent_p)

會輸出:

<p>這是一份<b class="boldtext">HTML文件</b>。</p>

往前、往後搜尋標籤

如果想要搜尋的標籤在同一層,則可以使用:

  • find_previous_siblings()(全部符合的)和 find_previous_sibling()(第一個符合的):往前搜尋。
  • find_next_siblings()(全部符合的)和 find_next_sibling()(第一個符合的):往後搜尋。

例如:

tag.find_previous_siblings('a') # 往前搜尋同一層的全部a標籤 tag.find_previous_sibling('a') # 搜尋前一個a標籤 tag.find_next_siblings('a') # 往後搜尋全部的a標籤 tag.find_next_sibling('a') # 搜尋後面第一個a標籤

CSS選擇器

方法名稱 說明
select() 以CSS選擇器的方式來找出所有符合條件的元素
select_one() 以CSS選擇器的方式來定位第一筆符合條件的元素

select()

from bs4 import BeautifulSoup # 原始HTML原始碼 html_doc = ''' <html> <head> <title>這是HTML文件標題</title> </head> <body> <h1 id="article" class="banner">網頁標題1</h1> <p data-author='aaron' class="reqular text-normal">文章段落1</p> <a class="link no btn" href="https://www.aaronlife.com/ref1">參考<b>資料</b>連結1</a> <a class="link btn" href="http://www.aaronlife.org/ref2">參考<b>資料</b>連結2</a> <a class="link btn" href="http://www.aaronlife.edu/ref2">參考資料連結3</a> <a class="link btn" href="https://www.aaronlife.com/ref2">參考資料連結4</a> <p>這是一份<b class="boldtext">HTML文件</b>。</p> <h2 id="article1" class="banner">網頁標題2</h2> <p data-author='andy' class="reqular text-normal">文章段落2</p> <h2 id="article2" class="title normal">網頁標題3</h2> <p data-author='william' class="reqular text-normal">文章段落3</p> </body> </html> ''' # 建立BeautifulSoup物件解析HTML文件 soup = BeautifulSoup(html_doc, 'html.parser') # 透過標籤來定位元素 result = soup.select('a') print('select a:', result) result = soup.select('body p b') print('select p b:', result) # 透過id來定位元素 result = soup.select('#article') print('select #article:', result) # 多重選擇(只要一個符合即可) result = soup.select('#article, p, b') print('select #article, p, b:', result) # 全部class都要符合才會被定位到 result = soup.select('.no.link.btn') print('select .no.link.btn:', result) # 透過class來定位元素 result = soup.select('.banner') print('select .banner:', result) # 透過是否存在某個屬性來定位元素 result = soup.select('a[href]') print('select a[href]:', result) # 透過指定的屬性值來定位元素 result = soup.select('a[href="http://wwww.aaronlife.com"]') print('select a[href=http://wwww.aaronlife.com]:', result) # 透過指定的屬性值「開頭字串」來定位元素 result = soup.select('a[href^="http"]') print('select a[href^=http]:', result) # 透過指定的屬性值「結束字串」來定位元素 result = soup.select('a[href$="com"]') print('select a[href$=com]:', result) # 透過指定的屬性值「有包含的字串」來定位元素 result = soup.select('a[href*="com"]') print('select a[href*=com]:', result)

select_one()

使用方式與select()一樣,直接回傳符合條件的元素,而非list。

從其它的HTML文件來源建立BeautifulSoup物件

從檔案建立

如果我們想要用 Beautiful Soup 解析已經下載的 HTML 檔案,可以直接將開啟的檔案交給 BeautifulSoup 處理:

from bs4 import BeautifulSoup # 從檔案讀取HTML原始碼來進行解析 with open("index.html") as html_file: soup = BeautifulSoup(html_doc) print(html_doc)

會輸出:

<html> <head> <title>這是HTML文件標題</title> </head> <body> <h1 id="article" class="banner">網頁標題</h1> <p data-author='aaron'>文章段落</p> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> <a href="https://www.aaronlife.com/ref2">參考資料連結2</a> <p>這是一份<b class="boldtext">HTML文件</b></p> </body> </html>

備註:
須先將最上面的HTML文件範例存成index.html檔案,並和這個範例的程式碼檔案放在同一層目錄下。

從網路建立(URL)

要直接從網路抓取html文件,可以使用Python內建的request模組:

import requests from bs4 import BeautifulSoup url = "https://www.ptt.cc/bbs/Python/index.html" # PTT Python看板 response = requests.get(url) # 使用requests的get方法把網頁抓下來 html_doc = response.text # text屬性就是html文件原始碼 soup = BeautifulSoup(response.text, "lxml") # 指定lxml作為解析器來建立Beautiful物件