# BeautifulSoup4 套件
[](https://hackmd.io/RoQ206rOQJOmviuHHou-og)

BeautifulSoup 4 (簡稱 BS4) 是一個專門用於解析 HTML 及 XML 文件的 Python 套件。它本身不負責下載網頁(通常搭配 `requests` 套件),而是專注於解析下載回來的原始碼。
BS4 的核心理念是**提供一個統一且友善的介面**,讓你能夠輕鬆地從複雜的 HTML 結構中提取所需資料。它**依賴底層的解析器 (Parser)** 來將 HTML 文本轉換為一個可操作的物件樹,並能自動修復格式不佳的 HTML,大幅提升爬蟲的穩定性。
BS4 支援多種解析器,你可以根據需求靈活選擇:
- **`html.parser`**:Python 內建,無需額外安裝,速度適中,容錯性不錯。
- **`lxml`**:第三方套件,基於 C 語言,**速度極快**,容錯能力強,是**最推薦**的選擇。
- **`html5lib`**:第三方套件,最符合瀏覽器標準,**容錯能力最強**,能處理最棘手的 HTML,但速度較慢。
> BS4 扮演類似 PHP 中 PDO 的角色,使用統一的介面存取不同的資料庫。
:::info
**相關工具**
`PyQuery` 是另一個優秀的網頁解析套件,它模仿了前端 jQuery 的語法,特別適合熟悉 CSS 選擇器和鏈式操作的開發者,但它固定使用 `lxml` 解析器。
:::
## 一、環境安裝
在開始之前,建議為您的專案建立一個獨立的虛擬環境。
```bash
# 1. 建立虛擬環境
py -m venv env # Windows
python3 -m venv env # macOS / Linux
# 2. 啟動虛擬環境
env\Scripts\activate # Windows
source env/bin/activate # macOS / Linux
# 3. 安裝核心套件 (BS4 + 推薦的解析器 + 網頁請求工具)
(env) pip install beautifulsoup4 lxml requests
# (可選) 安裝其他解析器
(env) pip install html5lib
# 4. 檢查安裝結果
(env) pip list
# 5. 匯出環境依賴
(env) pip freeze > requirements.txt
# 6. 離開虛擬環境
(env) deactivate
```
:::info
`soupsieve` 是 BS4 的一個依賴套件,它為 BS4 提供了強大的 CSS 選擇器功能。安裝 `beautifulsoup4` 時會自動安裝。
:::
## 二、基本使用:建立 BeautifulSoup 物件
要使用 BS4,首先需要將 HTML 原始碼和指定的解析器傳入 `BeautifulSoup` 類別,建立一個「湯」(soup) 物件。

以下範例展示了如何從不同來源(網路、本地檔案、字串)載入 HTML,並使用不同解析器。
```python
import requests
from bs4 import BeautifulSoup
# --- 來源 1: 從網路請求 ---
# 使用 lxml 解析器 (推薦:速度快、容錯性強)
try:
response = requests.get("https://example.com/")
response.raise_for_status() # 確保請求成功
soup_lxml = BeautifulSoup(response.text, "lxml")
print("--- 使用 lxml 解析成功 ---")
# print(soup_lxml.prettify()) # prettify() 可美化輸出,方便除錯
except requests.RequestException as e:
print(f"網路請求失敗: {e}")
# --- 來源 2: 從本地 HTML 檔案 ---
# 假設我們有一個名為 index.html 的檔案
html_content_file = "<h1>這是一個本地檔案</h1>"
with open("index.html", "w", encoding="utf-8") as f:
f.write(html_content_file)
with open("index.html", "r", encoding="utf-8") as file:
# 使用 Python 內建的 html.parser
soup_parser = BeautifulSoup(file, "html.parser")
print("\n--- 使用 html.parser 解析成功 ---")
if soup_parser.h1:
print(soup_parser.h1.text)
# --- 來源 3: 從 HTML 字串 ---
# 這段 HTML 標籤未閉合,結構不完整
html_string_broken = """
<html><head><title>美味的湯</title></head>
<body>
<p>這是一個不完整的段落
</body>
"""
# 使用 html5lib 解析器 (容錯能力最強,會自動補全標籤)
soup_html5lib = BeautifulSoup(html_string_broken, "html5lib")
print("\n--- 使用 html5lib 修復並解析 ---")
print(soup_html5lib.prettify())
```
:::success
**選擇建議**:在絕大多數情況下,`lxml` 都是最佳選擇,它在性能和容錯性之間取得了完美的平衡。
:::
## 三、BS4 的四大物件類型
BS4 會將複雜的 HTML 文件解析成一個由四種物件組成的樹狀結構,理解它們是操作 BS4 的基礎。
| 物件類型 | 說明 | 範例 |
| :--- | :--- | :--- |
| **`BeautifulSoup`** | 代表整個 HTML 文件,是樹狀結構的根。 | `soup = BeautifulSoup(html, 'lxml')` |
| **`Tag`** | HTML 中的一個標籤,如 `<div>`, `<a>`。**這是你最常操作的物件**。 | `soup.div` 或 `soup.find('div')` |
| **`NavigableString`** | 標籤內的文字內容。 | `soup.title.string` |
| **`Comment`** | HTML 中的註解 `<!-- ... -->`,是 `NavigableString` 的一種特殊子類。 | 少用,可透過特定方法找到。 |
```python=
from bs4 import BeautifulSoup, Comment
# 解析 HTML 文件
html = """
<html>
<head>
<title>範例網頁</title>
</head>
<body>
<div class="content">
<p>這是一段內容。</p>
<p><!-- 這是一個註解 --></p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "lxml")
# 1. BeautifulSoup 物件:代表整個 HTML 文件
print(type(soup)) # <class 'bs4.BeautifulSoup'>
print(soup.title) # <title>範例網頁</title>
# 2. Tag 物件:存取 HTML 標籤
div_tag = soup.div
if div_tag:
print(type(div_tag)) # <class 'bs4.element.Tag'>
print(div_tag["class"]) # ['content'] # 取得 class 屬性
print(div_tag.get("class")) # 使用 .get() 更安全,即使 'class' 屬性不存在也不會報錯
# 3. NavigableString 物件:取得標籤內的文字
if soup.title:
title_string = soup.title.string
print(type(title_string)) # <class 'bs4.element.NavigableString'>
print(title_string) # 範例網頁
title_text = soup.title.text
print(type(title_text)) # 'string'
print(title_text) # 範例網頁
# 4. Comment 物件:存取 HTML 註解
comment = soup.find(string=lambda text: isinstance(text, Comment))
print(type(comment)) # <class 'bs4.element.Comment'>
print(comment) # 這是一個註解
```
### Tag 物件的重要屬性與方法
每個 HTML 標籤會被轉換為 Tag 物件,我們可以透過 "物件.屬性" 的方式來存取標籤的名稱、屬性、內容等資訊,常見的屬性如下表
| 屬性/方法 | 描述 | 範例與說明 |
| :--- | :--- | :--- |
| **`.name`** | 獲取標籤名稱(字串)。 | `soup.h1.name` → `'h1'` |
| **`.attrs`** | 獲取標籤所有屬性(字典)。 | `soup.div.attrs` → `{'class': ['content'], 'id': 'main'}` |
| `tag['attr']` | 獲取指定屬性的值。 | `soup.div['id']` → `'main'` |
| **`.get('attr')`** | **更安全**的獲取屬性值,若屬性不存在返回 `None`。 | `soup.div.get('id')` → `'main'`, `soup.div.get('style')` → `None` |
| **`.text`** | 獲取標籤**及其所有子孫標籤**的文字內容,並拼接成單一字串。 | `soup.body.text` → `'這是一段內容。'` (已去除註解) |
| **`.string`** | 僅當標籤**只有一個直接的 `NavigableString` 子節點**時,返回該文字,否則返回 `None`。 | `soup.p.string` → `'這是一段內容。'`, `soup.div.string` → `None` (因為 div 內還有 p 標籤) |
| **`.get_text()`** | 功能類似 `.text`,但提供更多選項,如 `strip=True` 去除空白,`separator=' '` 指定分隔符。 | `tag.get_text(strip=True)` 是最常用的純文字提取方式。 |
| `.contents` | 以**列表**形式返回標籤的所有**直接**子節點(包含 `Tag` 和 `NavigableString`)。 | |
| `.children` | 功能同 `.contents`,但返回的是一個**產生器 (iterator)**,更省記憶體。 | `for child in tag.children:` |
| `.parent` | 返回父標籤。 | |
**範例1:**
取得標籤文字的屬性與方法
```python=
from bs4 import BeautifulSoup
html = """
<div>
<p> Hello, <b>world </b>!</p>
</div>
"""
soup = BeautifulSoup(html, 'lxml')
# print(str(soup)) # 顯示未排版的網頁原始碼
# soup.p 是 soup.find('p') 的語法糖,但在正式腳本中建議使用 find() 語意更清楚
p_tag = soup.p
print(p_tag.string) # None,因為 <p> 標籤內有 <b> 標籤
print(p_tag.text) # Hello, world !
print(p_tag.get_text(strip=True)) # Hello, world!
# get_text() 與 text 屬性都會提取子標籤的文字,但 get_text() 提供了更多的選項來控制輸出,例如分隔符和去除多餘的空白。
```
:::success
**靜態型別檢查提示**
如果在 VSCode 中這三行出現了紅色的波浪符,代表是 VSCode 中的靜態型別檢查 (Static Type Checking) 通常是 Pylance 發出的告警。

Pylance 在分析 p_tag = soup.p 這一行時,得出的結論是:p_tag 這個變數的型別可能是 Tag,也可能是 None。如果它真的是 None,你的程式在執行到這裡時會立刻崩潰,並拋出 AttributeError: 'NoneType' object has no attribute 'string' 的錯誤。Pylance 必須提醒你這個潛在的風險!
此時你有二種做法
1. 在改行程式碼後加上 `# type: ignore`,代表身為程式設計師的你保證 p_tag 絕對不會是 None。
2. 使用條件判斷 (最佳實踐)
```python=
if p_tag:
# 只要程式能進入這個 if 區塊,Pylance 就知道 p_tag 絕對不是 None
# 因此,這裡面的所有程式碼都不會再有警告
print(p_tag.string)
print(p_tag.text)
print(p_tag.get_text(strip=True))
else:
print("在 HTML 中沒有找到 <p> 標籤。")
```
:::
**範例2:**
使用了更多的標籤物件屬性,請耐心的逐一慢慢理解
```python=
from bs4 import BeautifulSoup
from bs4.element import NavigableString, Tag
# 使用 html 字串作為 HTML 來源
html = """
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<div id="content" class="main">
<h1 class="first">Hello, Beautiful Soup</h1>
<div>
<p class="brief">paragraph 1</p>
<p class="brief">paragraph 2<a href="https://www.google.com.tw">Google</a></p>
</div>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "lxml")
# 先用 find() 明確地找到我們要操作的目標
first_div = soup.find("div")
# 使用 isinstance() 進行精確的型別檢查。
# 這一步會向靜態分析器(Pylance)和Python直譯器斷言 first_div 是一個 Tag 物件,
# 從而安全地存取其屬性,並完全消除所有相關的警告。
if isinstance(first_div, Tag):
# --- 進入此區塊後,Pylance 100% 確定 first_div 是 Tag 物件 ---
print("--- 1. 獲取標籤名稱與屬性 ---")
print(f"標籤名稱 (.name): {first_div.name}")
print(f"所有屬性 (.attrs): {first_div.attrs}")
print(f"獲取 id 屬性 (['id']): {first_div['id']}")
print(f"獲取 class 屬性 (.get()): {first_div.get('class')}")
print(f"獲取不存在的 style 屬性 (.get()): {first_div.get('style', 'N/A')}")
print("\n--- 2. 獲取標籤內容 ---")
h1_tag = first_div.find("h1")
# 對於 find() 的每一次結果,都進行檢查是一種好習慣
if isinstance(h1_tag, Tag):
# .string vs .text 的經典對比
print(f"h1 的 .string: {h1_tag.string}")
print(f"h1 的 .text: {h1_tag.text}")
# 找到包含子標籤的 p,用它來展示 .string 和 .text 的區別
p_with_a = first_div.find("p", class_="brief", string=lambda s: "Google" in s) # type: ignore
if isinstance(p_with_a, Tag):
print(f"複雜 p 的 .string (因包含<a>標籤而為 None): {p_with_a.string}")
print(f"複雜 p 的 .text (會合併所有文字): {p_with_a.text}")
print("\n--- 3. 遍歷 HTML 樹 ---")
# .contents:清晰地展示它是一個包含不同類型元素的列表
print("div 的直接子節點 (.contents):")
for item in first_div.contents:
if isinstance(item, NavigableString) and item.strip() == "":
print(" - 類型: NavigableString (空白換行)")
elif isinstance(item, Tag):
print(f" - 類型: Tag, 名稱: <{item.name}>")
# .children:演示它作為一個產生器的用法
print("\n使用 .children 產生器遍歷:")
for child in first_div.children:
if isinstance(child, Tag):
print(f" - 找到子標籤: <{child.name}>")
# .parent:檢查父節點是否也是一個 Tag
if isinstance(first_div.parent, Tag):
print(f"\ndiv 的父節點 (.parent) 名稱: <{first_div.parent.name}>")
else:
# 如果連第一個 div 都沒找到,給出提示
print("在 HTML 文件中未找到任何 <div> 標籤。")
```
## 四、核心定位方法:查找元素
下圖請以 div 為中心來看

### 1. 尋找單一元素:找到返回 Tag,找不到則返回 None
1. find(tag, attributes, recursive, string, kwargs):找到第一個滿足條件的標籤,返回 Tag 或 None。
* string 參數支援正規表達式,可以使用正規表達式來匹配文字。
* limit 參數可以限制返回的結果數量,提高效能。
2. select_one(selector):使用 CSS 選擇器找到第一個匹配的元素,之後可再使用以下的 [CSS3 偽類選擇器](https://www.webdesigns.com.tw/CSS3-nth-child.asp)。
* :first-child:可選擇第一個子元素
* :last-child:可選擇最後一個子元素
* :nth-child(n):可選擇第 n 個子元素
* n:從 1 開始
* odd:選取奇數
* even:選取偶數
3. find_next(tag, attributes, string, **kwargs): 找到符合條件的第一個後續節點。
4. find_previous(tag, attributes, string, **kwargs): 找到符合條件的第一個前一節點。
5. find_next_sibling():找到下一個滿足條件的兄弟元素。
6. find_previous_sibling():找到上一個滿足條件的兄弟元素。
7. find_parent():找到父元素
8. get() 方法:從 Tag 物件中取得屬性值
get() 方法用於從 Tag 物件中取得屬性值,當屬性不存在時不會報錯,而是返回 None,這樣的特性可以避免程式發生錯誤。
9. get_text(separator, strip):返回標籤含子節點內的所有文字內容,可指定分隔符號與去除空白。此方法若不加上參數跟直接取用 tag.text 是一樣的。
* separator(分隔符號): 用於區隔相鄰文字的分隔符號,預設為換行符號。
* strip(去除空白): 一個布林值,用於指定是否去除文字內容的前後空白。預設為 False。
> 上述 3-7 的方法適合在 HTML 結構固定但缺乏 class/id 標識時,從一個已知點出發去尋找周邊元素。

**範例:**
```python=
from bs4 import BeautifulSoup
# HTML 字串
html = """
<html>
<head>
<title>BeautifulSoup範例</title>
</head>
<body>
<div id="content">
<h1 class="title">你好,BeautifulSoup!</h1>
<div class="container">
<p class="inner-paragraph">內部段落 1</p>
<p class="inner-paragraph">內部段落 2</p>
<p class="inner-paragraph">內部段落 3</p>
</div>
<h1 class="title">歡迎!</h1>
<p class="paragraph">這是一個範例段落。</p>
<p class="paragraph">另一個段落在這裡。</p>
</div>
</body>
</html>
"""
# 使用BeautifulSoup解析HTML
soup = BeautifulSoup(html, 'html.parser')
# 1. find(tag, attributes, recursive, string, kwargs)
first_paragraph = soup.find('p', class_='paragraph')
print("1. 尋找 P 標籤 class='paragraph': ", first_paragraph, sep='\n')
# 回應:<p class="paragraph">這是一個範例段落。</p>
first_paragraph = soup.find('p', string=lambda s: s and '另一個段落' in s) # type: ignore
print("1.1 尋找 P 標籤內文字內容為:另一個段落: ", first_paragraph, sep='\n')
# 回應:<p class="paragraph">另一個段落在這裡。</p>
# 2. select_one(selector)
title_element = soup.select_one('.title')
print("2. 使用 CSS 選擇器尋找標籤 class='title': ", title_element, sep='\n')
# 回應:<h1 class="title">你好,BeautifulSoup!</h1>
# 2-1. select_one 搭配 :first-child 偽類選擇器可選擇第一個子元素
first_child_paragraph = soup.select_one('div.container :first-child')
print("2-1. div.container 第一個子元素: ", first_child_paragraph, sep='\n')
# 回應:<p class="inner-paragraph">內部段落 1</p>
# 2-2. select_one 搭配 :last-child 偽類選擇器可選擇最後一個子元素
last_child_paragraph = soup.select_one('div.container :last-child')
print("2-2. div.container 最後一個子元素: ", last_child_paragraph, sep='\n')
# 回應:<p class="inner-paragraph">內部段落 3</p>
# 2-3. select_one 搭配 :nth-child 偽類選擇器可選擇第 N 個子元素
second_child_paragraph = soup.select_one('div.container :nth-child(3)')
print("2-3. div.container 的第3個子元素: ", second_child_paragraph, sep='\n')
# 回應:<p class="inner-paragraph">內部段落 3</p>
# 2-4. select 搭配 :nth-child(odd) 偽類選擇器選擇所有奇數位置的子元素
odd_paragraphs = soup.select('div.container :nth-child(odd)')
print("2-4. div.container 下所有奇數位置的子元素: ", odd_paragraphs, sep='\n')
# 回應:[<p class="inner-paragraph">內部段落 1</p>, <p class="inner-paragraph">內部段落 3</p>]
for tag in odd_paragraphs:
print(tag)
# 回應:
# <p class="inner-paragraph">內部段落 1</p>
# <p class="inner-paragraph">內部段落 3</p>
# 3. find_next(tag, attributes, string, **kwargs)
next_node = soup.find('div', class_='container').find_next() # type: ignore
print("3. div.container 的下一個節點: ", next_node, sep='\n')
# 回應:<p class="inner-paragraph">內部段落 1</p>
# 4. find_previous(tag, attributes, string, **kwargs)
previous_node = soup.find('div', class_='container').find_previous() # type: ignore
print("4. div.container 的上一個節點: ", previous_node, sep='\n')
# 回應:<h1 class="title">你好,BeautifulSoup!</h1>
# 5. find_next_sibling()
second_paragraph = soup.find('div', class_='container').find_next_sibling() # type: ignore
print("5. div.container 的下一個兄弟元素: ", second_paragraph, sep='\n')
# 回應:<h1 class="title">歡迎!</h1>
# 6. find_previous_sibling()
previous_paragraph = soup.find('div', class_='container').find_previous_sibling() # type: ignore
print("6. div.container 的上一個兄弟元素: ", previous_paragraph, sep='\n')
# 回應:<h1 class="title">你好,BeautifulSoup!</h1>
# 7. find_parent(tag, attributes, string, **kwargs)
parent_of_second_paragraph = soup.find('p', class_='inner-paragraph').find_parent() # type: ignore
print("7. 內部段落的父節點: ", parent_of_second_paragraph, sep='\n')
# 回應:
# <div class="container">
# <p class="inner-paragraph">內部段落 1</p>
# <p class="inner-paragraph">內部段落 2</p>
# <p class="inner-paragraph">內部段落 3</p>
# </div>
# 8. get(key, default=None)
tags_attribute = parent_of_second_paragraph.get('class') # type: ignore # 先決定要取得哪一個標籤的屬性,再取得該標籤的屬性值
print("8. 取得 div 標籤的 class 屬性: ", tags_attribute, sep='\n')
# 9. get_text(separator, strip)
container_div = soup.select_one('div.container')
all_text = container_div.get_text(separator=' ', strip=True) # type: ignore
print("9.1 以 tag.get_text() 取得 p 標籤文字: ", all_text, sep='\n') # 回應:內部段落 1 內部段落 2 內部段落 3
all_text = container_div.text.strip() # type: ignore
print("9.2 以 tag.text 取得 p 標籤文字: ", all_text, sep='\n')
# 回應:
# 內部段落 1
# 內部段落 2
# 內部段落 3
```
### 2. 尋找多個元素:返回 ResultSet(包含多個 Tag 的集合)或列表
1. find_all(tag, attributes, recursive, string, limit, kwargs):找到所有滿足條件的標籤。
2. select(selector):使用 CSS 選擇器找到標籤。
3. find_next_siblings():找到下一個滿足條件的所有兄弟元素。
4. find_previous_siblings():找到上一個滿足條件的所有兄弟元素。
**原始碼範例:**
```python=
from bs4 import BeautifulSoup
# HTML範例
html = """
<html>
<head>
<title>BeautifulSoup 範例</title>
</head>
<body>
<div id="content">
<h1 class="title">你好,BeautifulSoup!</h1>
<p class="paragraph">這是一個範例段落。</p>
<p class="paragraph">這是另一個段落。</p>
<div class="container">
<p class="inner-paragraph">內部段落 1</p>
<p class="inner-paragraph">內部段落 2</p>
<p class="inner-paragraph">內部段落 3</p>
</div>
</div>
</body>
</html>
"""
# 使用BeautifulSoup解析HTML
soup = BeautifulSoup(html, 'html.parser')
# 1. find_all(tag, attributes, recursive, string, limit, kwargs)
paragraphs = soup.find_all('p', class_='paragraph')
print("1. 尋找所有標籤為 p 且 class 為 'paragraph' 的元素: ", paragraphs, sep='\n')
# 回應:[<p class="paragraph">這是一個範例段落。</p>, <p class="paragraph">這是另一個段落。</p>]
# 2. select(selector)
inner_paragraphs = soup.select('.container .inner-paragraph')
print("2. 使用 CSS 選擇器尋找 class 為 container 且 class 為 inner-paragraph 的標籤:", inner_paragraphs, sep='\n')
# 回應:[<p class="inner-paragraph">內部段落 1</p>, <p class="inner-paragraph">內部段落 2</p>, <p class="inner-paragraph">內部段落 3</p>]
# 3. find_next_siblings()
first_inner_paragraph = soup.find('p', class_='inner-paragraph')
following_paragraphs = first_inner_paragraph.find_next_siblings('p') # type: ignore
print("3. 尋找第一個內部段落後的所有兄弟元素: ", following_paragraphs, sep='\n')
# 回應:[<p class="inner-paragraph">內部段落 2</p>, <p class="inner-paragraph">內部段落 3</p>]
# 4. find_previous_siblings()
preceding_paragraphs = inner_paragraphs[2].find_previous_siblings('p')
print("4. 尋找第三個內部段落前的所有兄弟元素: ", preceding_paragraphs, sep='\n')
# 回應:[<p class="inner-paragraph">內部段落 2</p>, <p class="inner-paragraph">內部段落 1</p>]
```
### 3. 其它方法
1. `.replace_with()`: 用新的內容替換掉一個 Tag 或 NavigableString。
1. `.prettify()`: 美化輸出整個 soup 物件,方便除錯時觀察 HTML 結構。
**原始碼範例:**
```python=
from bs4 import BeautifulSoup
# HTML範例
html = """
<html>
<head><title>BeautifulSoup 範例</title></head>
<body>
<div id="content"><h1 class="title"> 你好,BeautifulSoup! </h1>
<div id="container">
<p class="paragraph">這是一個範例段落。</p>
<p class="paragraph">另一個段落在這裡。</p>
</div>
</div>
</body>
</html>
"""
# 使用BeautifulSoup解析HTML
soup = BeautifulSoup(html, 'html.parser')
# 1. 使用 replace() 替換文字
title_tag = soup.find('title')
print("1.1 原始 title 內容:", title_tag.get_text()) # type: ignore # 輸出:BeautifulSoup 範例
# 替換 title 內容
title_tag.string.replace_with('新的標題') # type: ignore
print("1.2 修改後的 title 內容:", title_tag.get_text()) # type: ignore # 輸出:新的標題
# 2. prettify()
pretty_html = soup.prettify()
print("\n2. 以漂亮的格式列印整個檔的內容: ", pretty_html, sep='\n')
```
## 六、編解碼 (進階)
### 1. 呼叫 UnicodeDammit 解碼
在現代 Python 環境中,`requests` 和 BS4 通常能很好地自動處理編碼。你只需要注意兩個關鍵點:
1. **讀寫檔案時**:始終明確指定 `encoding="utf-8"`,這是最穩妥的做法。
```python
with open("data.txt", "w", encoding="utf-8") as f:
f.write(some_text)
```
2. **儲存為 JSON 時**:如果內容包含中文,使用 `ensure_ascii=False` 來確保中文能被正確寫入,而不是被轉換成 `\uXXXX` 格式。
```python
import json
data = {"title": "中文標題"}
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
```
在極少數情況下,如果 `requests` 無法正確解碼網頁,BS4 內部提供了一個名為 `UnicodeDammit` 的工具可以輔助偵測編碼。
UnicodeDammit 是 BeautifulSoup 庫中的一個工具,用於處理和轉換不同編碼的文字,使其成為 Unicode 格式。這對於解析來自不同來源的網頁內容時特別重要,因為網頁可能使用多種不同的字元編碼。
當你從網頁獲取內容並需要解析時,如果不確定該網頁使用的字元編碼,或者該網頁的字元編碼可能會變化,這時就應該使用 UnicodeDammit。它會嘗試使用多種編碼來解碼文字,直到找到一個合適的編碼。
**範例:**
以下是一個簡單的範例,展示如何使用 UnicodeDammit 和 BeautifulSoup 來解析網頁內容,並輸出網頁的 prettified 內容。
```python=
import requests
from bs4 import BeautifulSoup, UnicodeDammit
# 假設 response 是從網頁獲取的內容
url = "https://www.ncut.edu.tw"
response = requests.get(url)
# 使用 UnicodeDammit 嘗試多種編碼來解碼內容
dammit = UnicodeDammit(response.content, ["utf-8", "latin-1", "iso-8859-1", "windows-1251"])
# 使用 BeautifulSoup 解析解碼後的內容
soup = BeautifulSoup(dammit.unicode_markup, 'lxml') # type: ignore # 使用 BeautifulSoup 解析網頁內容
print(soup.prettify()) # 顯示美化後的 HTML 結構,便於閱讀
```
**說明:**
1. 導入必要的套件:包括 requests、BeautifulSoup 和 UnicodeDammit。
2. 發送 HTTP 請求:使用 requests.get 發送請求,獲取網頁內容。
3. 使用 UnicodeDammit 處理編碼:將網頁內容轉換為 Unicode 格式,並嘗試使用多種編碼來解碼內容。
4. 使用 BeautifulSoup 解析內容:將解碼後的內容傳遞給 BeautifulSoup 進行解析。
5. 輸出網頁內容:使用 soup.prettify() 方法輸出解析後的網頁內容。
這個範例展示了如何使用 UnicodeDammit 來處理不同編碼的網頁內容,確保能夠正確解析並輸出網頁的內容。
### 2. BeautifulSoup 的輸出編碼處理
在處理並輸出解析後的 HTML 或文字時,可能需要轉換為不同的編碼格式,特別是當目標環境(如終端機或檔案系統)不支援 Unicode 時,這時候就要使用 encode() 方法了。
encode() 方法可將 BeautifulSoup 解析後的內容轉換為特定的編碼格式,並將無法轉換的字元處理為替代字元或 HTML 實體。
```python=
from bs4 import BeautifulSoup
html = "<html><head><title>範例</title></head><body><p>這是段落。</p></body></html>"
soup = BeautifulSoup(html, 'lxml')
# 1. 標準輸出,不做任何格式化,寫入時使用 UTF-8 編碼
with open('output_pretty_unicode.html', 'w', encoding='utf-8') as file:
file.write(soup.prettify())
# 2. 使用 prettify() 格式化輸出並轉換為 Big5 編碼
pretty_html_big5 = soup.prettify().encode('big5', errors='xmlcharrefreplace')
# 轉換後輸出到終端機(需解碼以正確顯示)
print(pretty_html_big5.decode('big5'))
# 將格式化的內容寫入檔案
with open('output_pretty_big5.html', 'wb') as file:
file.write(pretty_html_big5)
# 3. 輸出格式化的 HTML 並轉換為 UTF-8 編碼
pretty_html_utf8 = soup.prettify().encode('utf-8')
# 轉換後輸出到終端機(需解碼以正確顯示)
print(pretty_html_utf8.decode('utf-8'))
# 將格式化的內容寫入檔案
with open('output_pretty_utf8.html', 'wb') as file:
file.write(pretty_html_utf8)
```
說明:
1. prettify() 格式化輸出:將 HTML 結構化,方便閱讀。
2. encode() 轉換編碼:可指定不同的編碼(如 Big5 或 UTF-8)來處理輸出。
3. 處理無法轉換的字元:errors='xmlcharrefreplace' 將無法轉換的字元替換為 HTML 實體,以避免丟失重要資訊。
## 七、爬蟲實戰範例

### 範例 1:爬取原價屋文章標題
以 CSS 選擇器,配合 select_one() 與 select() 方法抓取所有文章標題,最後存到 JSON 檔
```python=
import json
import requests
from bs4 import BeautifulSoup
URL = "http://www.coolpc.com.tw/phpBB2/portal.php"
try:
response = requests.get(URL)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'lxml')
if soup.title:
print(f"頁面標題: {soup.title.string.strip()}\n") if soup.title.string else print("找不到頁面標題")
# 使用 select 方法抓取所有符合條件的項目
article_links = soup.select(".ultimate-layouts-title-link")
titles = [link.get_text(strip=True) for link in article_links]
print("--- 所有文章標題 ---")
for i, title in enumerate(titles, 1):
print(f"{i}. {title}")
# 將結果存入 JSON 檔案
with open("coolpc_titles.json", "w", encoding="utf-8") as f:
json.dump(titles, f, ensure_ascii=False, indent=2)
print("\n資料已儲存至 coolpc_titles.json")
except requests.RequestException as e:
print(f"請求失敗: {e}")
```
### 範例 2:爬取博客來新書榜
這個範例結合了 `find_all`、`select_one` 和正規表達式來提取更複雜的資訊。
```python=
import json
import re
import requests
from bs4 import BeautifulSoup
from bs4.element import Tag # 明確導入 Tag 類型,以便進行檢查
URL = "https://www.books.com.tw/web/sys_newtopb/books/"
try:
response = requests.get(URL)
response.raise_for_status()
soup = BeautifulSoup(response.content, "lxml")
# 存取屬性前,先檢查 Tag 是否存在
if soup.title:
print(f"頁面標題:{soup.title.text.strip()}\n")
else:
print("頁面標題:未找到標題\n")
# 1. 先獲取整個列表,而不是直接存取索引
book_list_uls = soup.find_all("ul", {"class": "clearfix"})
# 2. 檢查列表長度是否足夠,這一步就防止了 IndexError
if len(book_list_uls) > 2:
book_list_ul = book_list_uls[2]
# 3. 進行類型檢查 (這一步現在主要是為了 Pylance,因為我們幾乎可以確定它是 Tag)
if isinstance(book_list_ul, Tag):
books = []
# 遍歷每個 <li> item (現在這裡已無任何警告)
for item in book_list_ul.select(".item"):
rank_tag = item.select_one(".stitle .no")
title_tag = item.select_one(".type02_bd-a h4 a")
author_tag = item.select_one(".type02_bd-a .msg li a")
price_tag = item.select_one(".type02_bd-a .msg .price_a")
if not (rank_tag and title_tag and price_tag):
continue
rank = rank_tag.text.strip()
title = title_tag.text.strip()
author = author_tag.text.strip() if author_tag else "N/A"
price_match = re.search(r"(\d+)元", price_tag.text)
price = price_match.group(1) if price_match else "N/A"
books.append(
{"rank": rank, "title": title, "author": author, "price": price}
)
# 格式化輸出和存檔...
for book in books:
print(
f"TOP {book['rank']:<3} | 書名: {book['title']:<40} | 作者: {book['author']:<15} | 價格: {book['price']}元"
)
with open("books_ranking.json", "w", encoding="utf-8") as f:
json.dump(books, f, ensure_ascii=False, indent=2)
print("\n資料已儲存至 books_ranking.json")
else:
# 如果列表長度不夠,給出清晰的提示
print("頁面結構可能已變更,找不到指定的書籍列表。")
except requests.RequestException as e:
print(f"請求失敗: {e}")
```
## 參考資料
* [給初學者的 Python 網頁爬蟲與資料分析 (3) 解構並擷取網頁資料](https://blog.castman.net/%E6%95%99%E5%AD%B8/2016/12/22/python-data-science-tutorial-3.html)
- [Beautiful Soup 官方文件 (英文)](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)