第 37 課 · 實戰 · 18 分鐘

Python HTML 解析

讓解析器理解 HTML 結構,而不是用正規猜。

HTMLParserhandle_starttaghandle_data屬性

供應商的 HTML 裡表格套著表格,正規數不清層數。解析器按順序走標籤。哪些片段該留,哪些該丟?

HTML 是巢狀結構,不是扁平字串。Python 的 html.parser 會按順序經過開始標籤、文字和結束標籤。繼承 HTMLParser,只儲存需要的部分,解析結果既可測試,也不依賴某個固定頁面布局。 你還會用堆疊配對巢狀標籤,並把腳本內容排除在可見文字之外。

繼承解析器

建立一個繼承 HTMLParser 的類,並在添加自己的狀態前呼叫 super().__init__。重寫 handle_starttag 處理開始標籤。然後建立實例,把 HTML 字串傳給 feed。

from html.parser import HTMLParser

class Tags(HTMLParser):
    def __init__(self):
        super().__init__()
        self.names = []

    def handle_starttag(self, tag, attrs):
        self.names.append(tag)

parser = Tags()
parser.feed("<h1>Hello</h1><p>World</p>")
print(parser.names)

按順序記錄開始標籤

把屬性轉換成字典

attrs 參數是名稱和值組成的列表。需要按名稱尋找時,用 dict 轉換。還要檢查 None:布林屬性可能沒有值,缺失 href 的連結不應被當成有效位址。

from html.parser import HTMLParser

class Links(HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []

    def handle_starttag(self, tag, attrs):
        values = dict(attrs)
        if tag == "a" and values.get("href"):
            self.links.append(values["href"])

parser = Links()
parser.feed('<a href="/a">A</a><a href="/b">B</a>')
print(parser.links)

安全讀取 href

收集可見文字

handle_data 接收標籤之間的文字。去掉首尾空白並忽略空字串,輸出就不會被縮進填滿。如果只需要某個元素的文字,可以用布林標記記錄目前是否位於該元素內部。

from html.parser import HTMLParser

class Text(HTMLParser):
    def __init__(self):
        super().__init__()
        self.parts = []

    def handle_data(self, data):
        text = data.strip()
        if text:
            self.parts.append(text)

parser = Text()
parser.feed("<h1>Title</h1><p>First sentence.</p>")
print(" | ".join(parser.parts))

去掉縮進後的文字

解碼實體並保留來源

HTMLParser 預設會解碼 &amp; 這類字元引用。儲存文字時,把原始 URL 一起保留,方便追溯提取結果。解析器只提供結構,不判斷頁面內容是否可信。

from html import unescape

raw = "Tom &amp; Jerry"
print(unescape(raw))
print(raw)

解碼但保留原始值

用堆疊配對巢狀標籤

HTMLParser 分別報告開始和結束標籤,元素巢狀時,單個布林標誌容易配錯結束標籤。堆疊可以記住目前打開的 href,遇到比對的結束標籤再彈出。這個小解析器適合受控樣例;任意網頁還是完整 HTML 解析器更穩。

from html.parser import HTMLParser

class LinkCollector(HTMLParser):
    def __init__(self):
        super().__init__()
        self.stack = []
        self.links = []

    def handle_starttag(self, tag, attrs):
        if tag == "a":
            self.stack.append(dict(attrs).get("href"))

    def handle_endtag(self, tag):
        if tag == "a" and self.stack:
            href = self.stack.pop()
            if href:
                self.links.append(href)

parser = LinkCollector()
parser.feed('<a href="/one">One</a><a href="/two"><b>Two</b></a>')
print(parser.links)

開始入堆疊,結束出堆疊

把腳本和樣式擋在可見文字外

handle_data 也會收到 script 和 style 的內容。解析時統計這些元素並忽略其中的文字,否則一段 JavaScript 字串可能混進本應只含可見文字的摘要。

from html.parser import HTMLParser

class VisibleText(HTMLParser):
    def __init__(self):
        super().__init__()
        self.skip = 0
        self.parts = []

    def handle_starttag(self, tag, attrs):
        if tag in {"script", "style"}:
            self.skip += 1

    def handle_endtag(self, tag):
        if tag in {"script", "style"} and self.skip:
            self.skip -= 1

    def handle_data(self, data):
        if not self.skip and data.strip():
            self.parts.append(data.strip())

parser = VisibleText()
parser.feed("<h1>Menu</h1><script>alert('x')</script><p>Tea</p>")
print(" | ".join(parser.parts))

只要可見文字

本課要點

  • 繼承 HTMLParser,並在儲存狀態前呼叫 super().__init__。
  • handle_starttag 接收標籤名和屬性鍵值對列表。
  • handle_data 接收文位元組點,也包括空白字元。
  • 用 feed 處理 HTML 字串,解析器會呼叫你重寫的方法。
  • 把來源 URL 和提取文字一起儲存,方便核對。
  • 巢狀元素用堆疊;提取可見文字時要跳過 script 和 style。

常見錯誤

  • 忘記 super().__init__,導致解析器內部狀態缺失。
  • 以為 attrs 已經是字典,直接按名稱取值。
  • 用正規解析巢狀 HTML,而不是使用解析器。
  • 用一個布林值處理巢狀,結果把文字掛到了錯誤的結束標籤上。
輪到你

從儲存的 HTML 字串中提取所有 (文字, href) 元組,跳過沒有 href 的連結。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← HTTP 請求URL 解析 →