用Beautiful Soup整理地方活動網頁——負責任的HTML擷取
先用自建教學頁學會解析,再把robots、條款、頻率、個資與著作權放進真正的擷取流程。
完成本篇後,你能使用Beautiful Soup解析HTML,透過標籤、屬性與CSS選擇器擷取文字和連結,處理缺漏節點與相對網址,建立結構驗證及離線測試,並在擷取前檢查robots.txt、使用條款、授權與網站負載。
一、API、HTML與瀏覽器畫面不同
若網站提供正式API或資料下載,應優先使用;HTML主要為人閱讀,版面一改,選擇器就可能失效。有些頁面內容由JavaScript執行後才出現,requests取得的初始HTML未必包含瀏覽器畫面上的全部內容。
| 來源 | 優點 | 限制 |
|---|---|---|
| 官方API/下載檔 | 欄位較明確、適合程式 | 仍可能改版或限制頻率 |
| 靜態HTML | Beautiful Soup容易解析 | 版面變動會影響選擇器 |
| JavaScript動態內容 | 互動豐富 | 初始HTML可能沒有資料 |
二、安裝Beautiful Soup
python -m pip install beautifulsoup4 requestsBeautiful Soup套件名稱是 beautifulsoup4,程式匯入名稱則是 bs4。本篇使用Python內建的 html.parser,減少額外環境需求。
三、先用自建HTML練習
from bs4 import BeautifulSoup
html_text = """
<section id="events">
<article class="event-card" data-id="E001">
<h2 class="title">水井三寶導覽體驗</h2>
<time datetime="2026-10-13">2026年10月13日</time>
<p class="place">教學示範場域</p>
<a class="detail" href="/events/E001">活動資訊</a>
</article>
</section>
"""
soup = BeautifulSoup(html_text, "html.parser")
print(soup.title)
print(soup.find("h2").get_text(strip=True))這是自建教學資料,不代表真實活動公告。先在固定HTML上開發與測試,避免學習階段反覆請求外部網站。
四、find與find_all
first_card = soup.find("article", class_="event-card")
all_cards = soup.find_all("article", class_="event-card")
print(first_card.get("data-id"))
print("活動數:", len(all_cards))Python的 class 是保留字,所以依CSS class搜尋時使用 class_。屬性不存在時,tag.get("name") 回傳 None,比直接索引更適合可缺欄資料。
五、CSS選擇器
Beautiful Soup官方文件說明,可使用CSS選擇器搜尋解析樹。select_one() 取第一個結果,select() 取全部。
card = soup.select_one("#events .event-card")
title = card.select_one(".title")
date_tag = card.select_one("time[datetime]")
detail = card.select_one("a.detail[href]")
print(title.get_text(" ", strip=True))
print(date_tag.get("datetime"))
print(detail.get("href"))選擇器應鎖定有語意且相對穩定的id、class或 data-* 屬性;避免依賴「第3個div裡第2個span」等脆弱位置。
六、把卡片轉成字典
def required_text(card, selector, field_name):
tag = card.select_one(selector)
if tag is None:
raise ValueError(f"缺少欄位:{field_name}")
text = tag.get_text(" ", strip=True)
if not text:
raise ValueError(f"欄位空白:{field_name}")
return text
def parse_event_card(card):
event_id = (card.get("data-id") or "").strip()
date_tag = card.select_one("time[datetime]")
link_tag = card.select_one("a.detail[href]")
if not event_id:
raise ValueError("缺少活動id")
if date_tag is None or link_tag is None:
raise ValueError("缺少日期或詳情連結")
return {
"id": event_id,
"title": required_text(card, ".title", "title"),
"date": date_tag.get("datetime", "").strip(),
"place": required_text(card, ".place", "place"),
"href": link_tag.get("href", "").strip(),
}七、把相對網址轉成完整網址
from urllib.parse import urljoin, urlparse
def normalize_link(base_url, href):
absolute = urljoin(base_url, href)
parsed = urlparse(absolute)
if parsed.scheme != "https":
raise ValueError("只接受HTTPS連結")
if parsed.hostname != "example.edu":
raise ValueError("連結超出允許網域")
return absolute
print(normalize_link(
"https://example.edu/activities/",
"/events/E001",
))範例網域只供教學。若擷取結果含連結,應限制允許的協定與網域;不要直接把不可信連結交給後續下載程式。
八、批次解析與錯誤隔離
def parse_event_page(html_text, base_url):
soup = BeautifulSoup(html_text, "html.parser")
records = []
errors = []
used_ids = set()
for index, card in enumerate(
soup.select("#events article.event-card"),
start=1,
):
try:
record = parse_event_card(card)
if record["id"] in used_ids:
raise ValueError(f"重複id:{record['id']}")
record["url"] = normalize_link(
base_url, record.pop("href")
)
used_ids.add(record["id"])
records.append(record)
except ValueError as error:
errors.append({"序號": index, "原因": str(error)})
return records, errors九、先檢查robots.txt
Python標準函式庫 urllib.robotparser 可讀取robots.txt並詢問特定User-Agent是否可擷取網址。
from urllib.parse import urljoin
from urllib.robotparser import RobotFileParser
def robots_allows(url, user_agent):
robots_url = urljoin(url, "/robots.txt")
parser = RobotFileParser()
parser.set_url(robots_url)
parser.read()
return parser.can_fetch(user_agent, url)
# 真實使用前,另確認網站條款與資料授權
# allowed = robots_allows(
# "https://example.edu/activities/",
# "NFU-Python-USR-Education/1.0",
# )robots.txt的錯誤回應與快取有標準化處理細節;正式爬蟲應依RFC 9309實作或使用成熟工具。即使允許擷取,也不代表取得內容再出版、商用或訓練AI的權利。
十、負責任地下載一個頁面
import requests
def fetch_html(url):
headers = {
"User-Agent": "NFU-Python-USR-Education/1.0"
}
response = requests.get(
url,
headers=headers,
timeout=(3.05, 15),
)
response.raise_for_status()
content_type = response.headers.get(
"Content-Type", ""
).lower()
if "text/html" not in content_type:
raise ValueError("回應不是HTML")
if len(response.content) > 2_000_000:
raise ValueError("頁面超過教學程式允許大小")
return response.text十一、先保存快照,再離線解析
from datetime import datetime, timezone
from pathlib import Path
def save_html_snapshot(folder, html_text):
timestamp = datetime.now(timezone.utc).strftime(
"%Y%m%dT%H%M%SZ"
)
path = Path(folder) / f"activity_{timestamp}.html"
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("x", encoding="utf-8") as file:
file.write(html_text)
return path保存快照可減少重複請求,也能重現當時的解析結果。快照若含個資或受著作權保護內容,必須限制保存範圍、期限與存取權限。
十二、HTML改版要被測試發現
def validate_page_structure(html_text):
soup = BeautifulSoup(html_text, "html.parser")
container = soup.select_one("#events")
cards = soup.select("#events article.event-card")
if container is None:
raise ValueError("找不到活動容器,網頁可能改版")
if not cards:
raise ValueError("找不到活動卡片,請人工檢查")
return len(cards)不要把「擷取到0筆」自動解讀成「今天沒有活動」。這也可能是網站改版、權限變化或動態內容造成。
十三、動態網頁怎麼辦?
先使用瀏覽器開發工具確認資料是否來自官方JSON API;若有,依規範使用API。若網站明確禁止自動化或需登入,不要繞過。只有在取得許可且確有必要時,才考慮瀏覽器自動化;不要破解驗證碼、存取控制或反爬蟲機制。
十四、不要大量重製原文
擷取活動標題、日期、地點與來源連結,通常比複製整篇文章更符合資料最小化。摘要與再利用仍須依網站授權及著作權判斷;人物照片、電話、報名名單與訪談內容更需要個資及同意檢查。
十五、常見錯誤檢查表
| 現象 | 原因 | 修正 |
|---|---|---|
| select_one回傳None | 選擇器錯誤或網頁改版 | 先檢查節點,不直接.get_text() |
| 瀏覽器看得到,requests卻沒有 | 內容由JavaScript載入 | 尋找官方API或取得許可後調整方案 |
| 相對連結無法開啟 | 未與基底網址合併 | 使用urljoin並驗證網域 |
| 網站回傳429 | 請求過於頻繁 | 停止、尊重Retry-After並降低頻率 |
| 擷取0筆就說沒有活動 | 忽略結構變動 | 結構驗證失敗時人工檢查 |
十六、USR實作挑戰
自行建立含三張活動卡的HTML,其中一張缺日期、一張重複ID,輸出有效清單與錯誤報告。
為預計使用的公開網站記錄API、robots.txt、條款、授權、個資、頻率、快取、停止條件與聯絡方式。
修改教學HTML的class名稱,確認測試會明確失敗,而不是輸出空資料假裝成功。
十七、與生成式AI協作
請擔任Beautiful Soup與負責任網頁擷取助教。
請檢查我的地方活動擷取程式:
1. 有官方API時優先使用API;
2. 擷取前檢查robots.txt、條款與授權;
3. 使用可識別User-Agent、timeout、快取及低頻率;
4. 驗證Content-Type、大小與HTML結構;
5. 節點缺漏不可直接當成沒有活動;
6. 相對連結須urljoin並限制HTTPS允許網域;
7. 不蒐集不必要個資,不大量重製原文;
8. 不繞過登入、驗證碼、存取控制或反爬機制。
請先列風險,再提供離線測試HTML與測試案例。Beautiful Soup能把HTML轉成可搜尋的樹,但負責任的擷取還包括來源選擇、網站規則、頻率、資料最小化、授權與改版偵測。技術上抓得到,不等於應該抓、可以公開。
沒有留言:
張貼留言