Python BeautifulSoup 網頁爬蟲基礎

當我們使用 requests 抓取到網頁原始碼後,接下來的挑戰是如何從雜亂的 HTML 標籤中精確提取出我們需要的資料(如標題、價格、鏈結)。BeautifulSoup 是 Python 中最受歡迎的 HTML/XML 解析庫,它能將網頁原始碼轉換為一個易於遍歷的樹狀結構 (DOM Tree),讓你像操作物件一樣輕鬆提取資料。

安裝與環境準備

BeautifulSoup (bs4) 需要配合一個「解析引擎」。雖然 Python 內建了 html.parser,但實務上推薦安裝 lxml,因為它的解析速度更快且對錯誤 HTML 的容忍度更高。

pip install beautifulsoup4 lxml

基礎操作:解析與定位

from bs4 import BeautifulSoup

html_doc = """
<html>
  <head><title>技術教學網</title></head>
  <body>
    <h1 id="main-title">我的購物清單</h1>
    <div class="container">
      <ul class="items">
        <li class="item" data-id="101" price="50">蘋果 (特價)</li>
        <li class="item" data-id="102" price="100">香蕉</li>
        <li class="item active" data-id="103" price="200">櫻桃</li>
      </ul>
    </div>
    <a href="https://example.com/more" id="link1">查看更多</a>
  </body>
</html>
"""

# 建立 BeautifulSoup 物件
soup = BeautifulSoup(html_doc, "lxml")

# 1. 直接訪問(僅限於簡單結構或找第一個)
print(soup.title.string)  # 技術教學網
print(soup.h1.text)       # 我的購物清單

搜尋標籤的核心方法

1. find()find_all():標籤向搜尋

這是最傳統且強大的搜尋方式。

  • find():回傳匹配的第一個標籤物件。
  • find_all():回傳包含所有匹配標籤的列表 (ResultSet)
# 搜尋 class 為 item 的所有 li 標籤
# 注意:因為 class 是 Python 關鍵字,BS 使用 class_ 作為參數名
items = soup.find_all("li", class_="item")

for item in items:
    # 取得屬性值
    pid = item.get("data-id")
    price = item["price"]
    # 取得標籤內的純文字
    name = item.get_text()
    print(f"產品 ID: {pid}, 名稱: {name}, 價格: {price}")

2. select()select_one():CSS 選擇器

如果你有前端開發經驗(CSS 或 jQuery),這絕對是你最愛的工具。它支援複雜的階層選擇。

# 透過 ID 搜尋
header = soup.select_one("#main-title")

# 透過 Class 與層級搜尋 (選取 container 下的 item)
active_item = soup.select_one(".container .item.active")

# 屬性選擇器 (選取 href 開頭為 https 的 a 標籤)
links = soup.select('a[href^="https"]')

遍歷樹狀結構 (Tree Traversal)

有時候標籤沒有明確的 ID 或 Class,我們必須根據相對位置來尋找。

ul_tag = soup.find("ul", class_="items")

# 1. 向下搜尋:子節點
children = ul_tag.contents # 取得直接子節點列表 (包含空白 text 節點)
for child in ul_tag.children: # 迭代器形式
    if child.name: # 過濾掉空白節點
        print(f"子標籤: {child.name}")

# 2. 向上搜尋:父節點
parent_div = ul_tag.parent
print(f"父容器 class: {parent_div.get('class')}")

# 3. 平級搜尋:兄弟節點
first_li = soup.find("li")
next_li = first_li.find_next_sibling("li")
print(f"下一個產品: {next_li.text}")

提取與清理資料的技巧

在真實的 HTML 中,資料周圍往往夾雜大量的換行符號、空白或是 JavaScript 代碼。

  • .strip():清理文字前後的空白與換行。
  • .get_text(separator=' ', strip=True):將標籤內的文字串接,並清理多餘空白。
  • .string vs .text.string 僅在標籤內只有單一節點時有效;.text 會遞迴取得所有子節點的文字。
container_text = soup.select_one(".container").get_text(strip=True)
# 輸出: "蘋果 (特價)香蕉櫻桃"

實戰範例:完整爬蟲工作流

結合 requestsBeautifulSoup 的專業模式:

import requests
from bs4 import BeautifulSoup

url = "https://news.ycombinator.com/"
headers = {"User-Agent": "Mozilla/5.0"}

try:
    response = requests.get(url, headers=headers)
    response.raise_for_status() # 檢查請求是否成功

    # 手動處理可能出現的亂碼 (編碼修正)
    response.encoding = response.apparent_encoding

    soup = BeautifulSoup(response.text, "lxml")

    # 尋找所有新聞標題
    # 觀察發現 Hacker News 的標題都在 class="titleline" 的 span 裡
    stories = soup.select(".titleline > a")

    for story in stories:
        title = story.get_text()
        link = story["href"]
        print(f"標題: {title}")
        print(f"連結: {link}\n" + "-"*30)

except Exception as e:
    print(f"爬取失敗: {e}")

進階應用:處理 Regex 正則表達式

有時候 Class 名稱或是文字內容是動態變化的,這時可以傳入 Regex 物件。

import re

# 搜尋所有 id 中包含 "link" 字樣的 a 標籤
links = soup.find_all("a", id=re.compile("link"))

# 搜尋文字內容中包含 "櫻桃" 的標籤
cherry = soup.find(string=re.compile("櫻桃"))

常見開發注意事項

  1. 動態網頁處理:若標籤是透過 AJAX 或 JavaScript 載入的(你在瀏覽器按右鍵選「檢視原始碼」看不到,但「檢查元素」看得到),BeautifulSoup 無法直接抓取,必須改用 SeleniumPlaywright 模擬瀏覽器行為。
  2. User-Agent:永遠要設定 Headers,否則許多網站會直接封鎖預設的 Python 請求。
  3. 解析器選擇:對品質極差(標籤沒閉合)的 HTML,可以嘗試使用 html5lib 解析器。

總結

  • BeautifulSoup 擅長 HTML 文件導航與資料提取。
  • 優先使用 CSS 選擇器 (select) 以提高程式碼的可讀性。
  • 靈活運用 Tree Traversal 處理無規律的網頁結構。
  • 爬蟲開發要遵守 robots.txt 與爬取頻率限制,做一個優雅的網路公民。