# 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系列方法](#基本find系列方法)
2. [CSS Selector(選擇器)](#CSS選擇器)
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的結構樹物件,之後所有資料的搜尋、提取等操作都是透過這個物件來進行。
```python=
# 引入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")`<br/>`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文件,在開發時可以用來觀察其文件的整個輪廓:
```python=
print(soup.prettify())
```
會輸出:
```html=
<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`),即可將該標籤的節點抓出來:
```python=
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` 屬性,可以取得標籤內的純文字:
```python=
print(soup.title.string)
```
會輸出:
```
這是HTML文件標題
```
## 基本find系列方法
### 搜尋標籤 - `find_all()`
透過 `find_all` 可以在文件內找出指定的所有標籤,`find_all`會以`list`傳回所有找到的標籤:
```python=
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`屬性:
```python=
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`,例:
```python
print(tag.attrs['data-table'])
```
會得到`data-table`的屬性值
### 同時搜尋多種標籤
如果要同時搜尋多種HTML標籤,可以使用 list 來指定所有要搜尋的標籤名稱:
```python=
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()`方法即可:
```python=
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')
```
會輸出:
```python=
print(tags)
```
上面程式碼,加上`recursive=False`後,表示只會搜尋在`html`標籤下第一層的標籤:
```python=
tags = soup.html.find_all('h1', recursive=False)
print(tags)
```
會輸出:
```
[]
```
> 因為`h1`標籤是在`html`標籤下的`body`標籤內,也就是底下兩層,所以不會被搜尋到。
### 以屬性名稱搜尋
透過指定標籤內的屬性名稱,可以將所有符合屬性值的所有標籤找出來,例如搜尋 `id` 屬性為 `article` 的節點:
```python=
tags = soup.find_all(id='article')
print(tags)
```
會輸出:
```
[<h1 id="article" class="banner">網頁標題</h1>, <h2 id="article">網頁標題2</h2>]
```
可以透過結合標籤名稱名稱與屬性名稱進行更精確的搜尋,例如搜尋 `id` 屬性為 `article` 的 `h2` 節點:
```python=
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的方法中的話會出現錯誤:
```python=
tag = soup.find(data-attr='aaron')
```
會出現下面錯誤:
```
SyntaxError: expression cannot contain assignment
```
解決方法為,將屬性名稱與值放進一個字典(dict)中,再將此字典設定給 `attrs` 參數即可,例如:
```python=
condition = {'data-author': 'aaron'}
tag = soup.find_all(attrs=condition)
```
以正規表示法來搜尋:
```python=
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`的標籤:
```python=
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')`
搜尋多個屬性值:
```python
tags = soup.find_all(class_=['banner', 'primary'])
```
搜尋多個屬性值需要使用list將所有要搜尋的值放在一起。
### 以文字內容搜尋
使用 `find_all` 配合 `string` 參數可以根據文字進行來搜尋特定的標籤內容,例如:
```python=
contents = soup.find_all(string='網頁標題')
print(contents)
```
會輸出:
```
['網頁標題']
```
> 注意:
>
> 輸出結果只會有內容,而不是完整的標籤。
### 搜尋上層標籤
`find_all`和`find`方法都是從外層向下搜尋子標籤,但我們也可以由下往上來搜尋上層標籤(或稱父標籤),方法`find_parents`(如同`find_all`網上搜尋全部符合條件的標籤)和`find_parent`(如同`find`指搜尋第一個符合的標籤)例如:
```python=
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()`(第一個符合的):往後搜尋。
例如:
```python=
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()`
```python=
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` 處理:
```python=
from bs4 import BeautifulSoup
# 從檔案讀取HTML原始碼來進行解析
with open("index.html") as html_file:
soup = BeautifulSoup(html_doc)
print(html_doc)
```
會輸出:
```html=
<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`模組:
```python=
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物件
```