供應商的 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 預設會解碼 & 這類字元引用。儲存文字時,把原始 URL 一起保留,方便追溯提取結果。解析器只提供結構,不判斷頁面內容是否可信。
from html import unescape
raw = "Tom & 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 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。