RTHK(香港電台)新聞列表使用傳統靜態 HTML 渲染,class 名稱語義清晰(如 ns2-title、ns2-created),是學習 XPath 的最佳入門網站。
| 字段 | XPath Expression | 語法重點 |
|---|---|---|
| 新聞標題 | //h4[@class="ns2-title"]/a/text() | 直接文字節點 |
| 新聞連結 | //h4[@class="ns2-title"]/a/@href | 屬性選取 |
| 發布時間 | //div[@class="ns2-created"]/font/text() | 嵌套元素 |
| 新聞類別 | //div[@class="ns2-category"]/a/text() | 類別標籤 |
| 所有條目容器 | //div[@class="ns2-inner"] | 父容器選取 |
| 第1條新聞標題 | (//h4[@class="ns2-title"]/a/text())[1] | 位置 predicate |
| 文字含「香港」的標題 | //h4[@class="ns2-title"]/a[contains(text(),"香港")]/text() | contains() 函數 |
# 安裝所需套件(在 Google Colab 中執行)
!pip install requests lxml beautifulsoup4# 導入所需套件
import requests # 發送 HTTP 請求
from lxml import etree # XPath 解析引擎
import csv # 儲存為 CSV 格式
import time # 控制請求間隔
import random # 隨機休息時間(防反爬)
from datetime import datetime # 記錄爬取時間
from google.colab import files # Google Colab 自動下載
print("✅ 所有套件導入成功")# ═══════════════════════════════════════════
# 設定參數(可根據需要修改)
# ═══════════════════════════════════════════
# 目標 URL:RTHK 即時新聞列表
BASE_URL = "https://news.rthk.hk/rthk/ch/latest-news.htm"
# 請求 Headers:模擬真實瀏覽器,避免被封鎖
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-HK,zh;q=0.9,en;q=0.8",
"Referer": "https://news.rthk.hk/", # 模擬從 RTHK 首頁跳轉
}
# 請求間隔(秒):隨機休息防止被封
MIN_SLEEP = 1.5 # 最短休息時間
MAX_SLEEP = 3.5 # 最長休息時間
# 輸出 CSV 檔案名稱
OUTPUT_FILE = "rthk_news.csv"
print(f"✅ 設定完成")
print(f" 目標網址:{BASE_URL}")
print(f" 輸出檔案:{OUTPUT_FILE}")def parse_news_list(html_content):
"""
使用 XPath 解析 RTHK 新聞列表頁面
Args:
html_content: 頁面的 HTML 字符串
Returns:
list: 包含所有新聞條目的字典列表
"""
# 將 HTML 字符串解析為 lxml 可操作的樹狀結構
# etree.HTML() 自動修復不完整的 HTML 標籤
tree = etree.HTML(html_content)
# ─── XPath 1:選取所有新聞條目的父容器 ───────────────────────
# 每條新聞都包裹在 class="ns2-inner" 的 div 中
# //div 表示在整個文檔中搜尋 div 元素
# [@class="ns2-inner"] 是 predicate,篩選 class 屬性等於 "ns2-inner" 的元素
news_items = tree.xpath('//div[@class="ns2-inner"]')
print(f" 找到 {len(news_items)} 條新聞")
results = []
for item in news_items:
# ─── XPath 2:提取新聞標題 ─────────────────────────────
# .// 表示從當前節點(item)開始搜尋,而非整個文檔
# h4[@class="ns2-title"] 選取 class="ns2-title" 的 h4 元素
# /a 選取其子元素 a 標籤
# /text() 提取文字內容(注意:返回列表,用 [0] 取第一個)
title_list = item.xpath('.//h4[@class="ns2-title"]/a/text()')
title = title_list[0].strip() if title_list else ""
# ─── XPath 3:提取新聞連結 ─────────────────────────────
# @href 表示提取 href 屬性的值(而非文字內容)
# 屬性選取用 @ 符號,這是 XPath 的重要語法
link_list = item.xpath('.//h4[@class="ns2-title"]/a/@href')
link = link_list[0] if link_list else ""
# RTHK 連結是相對路徑,需要加上域名
if link and not link.startswith("http"):
link = "https://news.rthk.hk" + link
# ─── XPath 4:提取發布時間 ─────────────────────────────
# div[@class="ns2-created"] 選取時間容器
# /font 選取其內的 font 標籤(RTHK 用舊式 HTML)
# /text() 提取時間文字
time_list = item.xpath('.//div[@class="ns2-created"]/font/text()')
pub_time = time_list[0].strip() if time_list else ""
# ─── XPath 5:提取新聞類別 ─────────────────────────────
# div[@class="ns2-category"] 選取類別容器
# /a/text() 提取類別文字(如「港聞」「財經」)
cat_list = item.xpath('.//div[@class="ns2-category"]/a/text()')
category = cat_list[0].strip() if cat_list else ""
# ─── XPath 6:提取文章 ID(從 URL 中提取)──────────────
# 使用 substring-after() 函數從 URL 中提取文章 ID
# 例如 /rthk/ch/component/k2/1847547-20260316.htm → 1847547-20260316
article_id = ""
if link:
# Python 方式提取(XPath 也可以用 substring-after())
parts = link.split("/k2/")
if len(parts) > 1:
article_id = parts[1].replace(".htm", "")
# ─── XPath 7:提取摘要/副標題(如有)──────────────────
# 有些新聞條目有副標題,用 normalize-space() 清除多餘空白
summary_list = item.xpath('normalize-space(.//div[@class="ns2-summary"])')
summary = summary_list if isinstance(summary_list, str) else ""
# 記錄爬取時間
scraped_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 只保留有標題的條目
if title:
results.append({
"article_id": article_id, # 文章 ID
"title": title, # 新聞標題
"link": link, # 新聞連結
"category": category, # 新聞類別
"publish_time": pub_time, # 發布時間
"summary": summary, # 摘要
"scraped_at": scraped_at, # 爬取時間
})
return results
print("✅ XPath 解析函數定義完成")def fetch_rthk_news():
"""
發送 HTTP 請求獲取 RTHK 新聞列表並解析
Returns:
list: 所有新聞條目的列表
"""
print(f"📡 正在請求:{BASE_URL}")
try:
# 發送 GET 請求
# timeout=15 設定超時時間為 15 秒
response = requests.get(BASE_URL, headers=HEADERS, timeout=15)
# 檢查 HTTP 狀態碼
# raise_for_status() 在狀態碼非 200 時自動拋出異常
response.raise_for_status()
# 設定正確的編碼(RTHK 使用 UTF-8)
response.encoding = "utf-8"
print(f" ✅ 請求成功,狀態碼:{response.status_code}")
print(f" 📄 頁面大小:{len(response.text):,} 字符")
# 調用 XPath 解析函數
news_list = parse_news_list(response.text)
# 防反爬:隨機休息
sleep_time = random.uniform(MIN_SLEEP, MAX_SLEEP)
print(f" 💤 休息 {sleep_time:.1f} 秒...")
time.sleep(sleep_time)
return news_list
except requests.exceptions.Timeout:
print("❌ 請求超時,請檢查網絡連接")
return []
except requests.exceptions.HTTPError as e:
print(f"❌ HTTP 錯誤:{e}")
return []
except Exception as e:
print(f"❌ 未知錯誤:{e}")
return []
# 執行爬取
print("🚀 開始爬取 RTHK 即時新聞...")
print("=" * 50)
all_news = fetch_rthk_news()
print("=" * 50)
print(f"✅ 共爬取 {len(all_news)} 條新聞")
# 預覽前 3 條
if all_news:
print("\n📋 預覽前 3 條新聞:")
for i, news in enumerate(all_news[:3], 1):
print(f" {i}. [{news['category']}] {news['title']}")
print(f" 時間:{news['publish_time']}")
print(f" 連結:{news['link'][:60]}...")def save_and_download_csv(data, filename):
"""
將數據儲存為 CSV 並在 Colab 中自動下載
Args:
data: 新聞數據列表(字典格式)
filename: 輸出檔案名稱
"""
if not data:
print("❌ 沒有數據可以儲存")
return
# CSV 欄位名稱(與字典的 key 對應)
fieldnames = [
"article_id", # 文章 ID
"title", # 新聞標題
"link", # 新聞連結
"category", # 新聞類別
"publish_time", # 發布時間
"summary", # 摘要
"scraped_at", # 爬取時間
]
# 寫入 CSV 檔案
# encoding="utf-8-sig":加入 BOM 標記,確保 Excel 正確顯示中文
# newline="" 防止 Windows 上出現多餘空行
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
# 寫入標題行
writer.writeheader()
# 逐行寫入數據
writer.writerows(data)
print(f"✅ 已儲存 {len(data)} 條新聞到 {filename}")
# ─── 在 Google Colab 中自動觸發下載 ───────────────────────
# files.download() 是 Colab 專屬 API,會彈出下載對話框
files.download(filename)
print(f"📥 正在下載 {filename}...")
# 執行儲存和下載
save_and_download_csv(all_news, OUTPUT_FILE)import pandas as pd
# 讀取剛才儲存的 CSV
df = pd.read_csv(OUTPUT_FILE, encoding="utf-8-sig")
print(f"📊 爬取結果統計")
print(f"{'='*40}")
print(f" 總新聞數:{len(df)} 條")
print(f" 欄位:{list(df.columns)}")
print()
# 各類別新聞數量
if "category" in df.columns:
print("📂 各類別新聞數量:")
print(df["category"].value_counts().to_string())
print()
# 顯示前 5 筆數據
print("📋 前 5 條新聞:")
print(df[["title", "category", "publish_time"]].head().to_string(index=False))