DE Day 16 動態頁面:Playwright 自動化瀏覽器
執行需求:CPU 可跑。今天是「資料工程實戰」系列的第十六篇,承接 Day 15 用 requests 與 BeautifulSoup 抓靜態 HTML 的作法。當網頁內容是靠 JavaScript 動態渲染(俗稱 SPA)或需要互動才能取得資料時,requests 拿到的常常是空白殼殼,這時就要派出真正的瀏覽器上場。我們會用 Playwright 1.50(2025 年 11 月的主流版本)示範四種動態場景:等待元素出現、捲動載入、按下分頁、處理登入,並把結果寫進 Day 8 已建好的 DuckDB。
引言
Day 15 我們用 requests 抓公開資料頁,幾行程式就把表格拉回來;但實務上常會碰到 requests 抓回來是空殼的情況——HTML 裡沒有資料、要等 JavaScript 跑完才會塞進去。常見原因有三個:第一,網站用 React、Vue、Angular 等前端框架,內容是執行階段才掛上的;第二,資料透過 XHR 或 fetch 二次呼叫才取得,HTML 只剩骨架;第三,需要捲動到某個位置、按下「載入更多」才會出來新資料。這三種狀況 requests 都不在行,我們需要能「真的把網頁跑起來」的工具。
Playwright 是微軟在 2020 年開源的瀏覽器自動化函式庫,跟 Selenium、Cypress、Puppeteer 同類,但有兩個特點讓它在資料工程領域特別受歡迎:第一,內建等待機制(auto-wait)寫起來直覺,不會滿地雷;第二,支援 Chromium、Firefox、WebKit 三套引擎,可以模擬不同瀏覽器。2025 年 11 月的主流版本是 1.50 世代,安裝指令仍是 pip install playwright 加上 python -m playwright install chromium,這是 Playwright 1.5x 的固定安裝法。
今天的文章會做五件事:第一,說明 Playwright 與 requests 的差別、何時該用哪一個;第二,安裝並啟動第一個無頭瀏覽器;第三,示範四種動態頁面處理——等待元素、捲動載入、按下分頁、模擬登入;第四,把抓下來的資料寫進 DuckDB 與 Parquet,與 Day 8、Day 9 的流程接軌;第五,誠實談談動態爬蟲在禮儀、法律與效能上的取捨。這篇不會把 Playwright 的所有 API 講完,但讀完應該能寫出八成資料工程會用到的動態爬蟲。
動態頁面與 Playwright 的工作原理
先建立心智模型。requests 的工作流程是「送 HTTP 請求、收 HTML 字串、剖析」;Playwright 的工作流程是「啟動一整個瀏覽器、讓 JavaScript 跑起來、等 DOM 穩定、把 DOM 內容抓回來」。前者像寄一封信要對方回信,後者像坐在對方家裡看著他把信寫完再拿走。差別在於:瀏覽器會執行 JavaScript、發出額外的 API 呼叫、把資料渲染到畫面上,這些動作 requests 完全模擬不來。
Playwright 的兩個關鍵概念是「瀏覽器(browser)」與「頁面(page)」。瀏覽器是一個 process,負責管理所有頁面的生命週期;頁面是一個分頁(tab),對應一個獨立的瀏覽上下文。我們會建立一個 browser,再用它開新的 page,最後把 page 上的元素抓下來。這跟 Selenium 的「WebDriver」觀念類似,但 Playwright 用 WebSocket 跟瀏覽器溝通,比 Selenium 的 HTTP 協定快很多。
另一個關鍵設計是「auto-wait」。傳統 Selenium 寫爬蟲最痛苦的地方是「元素還沒出現就去找,導致 NoSuchElement 錯誤」;Playwright 在你呼叫 locator.click()、locator.text_content() 之前,會自動等元素變成 visible、enabled、stable。這讓程式碼乾淨很多,但代價是「真的要等」,如果網站慢你就會等很久。理解這點之後,你會知道何時該用 auto-wait、何時該自己用 page.wait_for_selector() 明確等待。當網站回應慢或設計不良時,明確等待比 auto-wait 更可控。
最後談「locator」。Locator 是 Playwright 用來定位元素的物件,類似 CSS 選擇器或 XPath,但更強大。每次你呼叫 page.locator("#spa") 都會回傳一個新的 locator 物件(lazy 評估),實際尋找元素的動作會在 click()、inner_text() 等終端操作時才發生。這設計的好處是同一個 locator 可以重複使用,而且會隨著 DOM 變化自動重新尋找。對資料工程來說,locator 把「找元素」與「操作元素」分離,寫起來比 Selenium 的 find_element_by_* 直覺很多。
完整實作:四種動態場景與 DuckDB 落地
假設工作目錄沿用 Day 1 的 de-journey/,今天的任務是把四種常見的動態頁面處理方式收斂成一支腳本 pipelines/day16_playwright.py。為了不依賴任何實際網站(避免哪天網站改版範例就壞掉),我們用 Python 標準函式庫寫一個迷你伺服器模擬「動態頁面」,再用 Playwright 去抓它。這個寫法的另一個好處是「完全離線也能跑」,你只要裝好 Playwright 就能重現結果;同時也方便 Day 17 之後在這個骨架上加入禮儀與重試。
第一步:安裝 Playwright 與瀏覽器。整段指令可以一次貼上執行:
cd de-journey
uv pip install playwright==1.50.0 httpx==0.28.1 fastapi==0.115.0 uvicorn==0.32.0
python -m playwright install chromium
# 如果需要 Firefox / WebKit,可加:
# python -m playwright install firefox webkit
這段指令做了四件事:安裝 Python 套件(Playwright 1.50 是 2025 年 11 月主流版本,httpx 0.28 是搭配使用的 HTTP 函式庫,FastAPI 0.115 用來跑迷你伺服器)、下載 Chromium 瀏覽器(首次約 150 MB,會快取在 ~/.cache/ms-playwright/)、選擇性下載其他瀏覽器。執行 python -m playwright install 時若看到「Failed to install browsers」,通常是 Linux 缺少系統套件,請依錯誤訊息安裝 libnss3、libatk-bridge2.0-0 等相依套件;macOS 與 Windows 通常可以直接裝好。若用 Docker,請把這行寫進 Dockerfile 的 RUN 段落。
第二步:用 FastAPI 寫一個迷你動態網站,模擬 SPA、捲動載入、按下分頁、登入四種場景。把這個檔案存成 pipelines/_mock_server.py,稍後我們的 Playwright 腳本會連過來:
"""pipelines/_mock_server.py:模擬四種動態頁面,給 Playwright 範例用。"""
import asyncio
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse
import uvicorn
app = FastAPI()
USERS = {"demo": "demo123"}
HTML = """
<!doctype html>
<html lang="zh-Hant">
<head><meta charset="utf-8"><title>DE Day 16 範例</title></head>
<body style="font-family: sans-serif; max-width: 720px; margin: 24px auto;">
<h1 id="title">DE Day 16 動態頁面範例</h1>
<div id="spa"></div>
<button id="load_more">載入更多</button>
<ul id="list"></ul>
<form action="/login" method="post">
<input name="username" placeholder="帳號">
<input name="password" type="password" placeholder="密碼">
<button type="submit">登入</button>
</form>
<div id="after_login" style="display:none;">已登入</div>
<script>
fetch("/api/data").then(r=>r.json()).then(d=>{
document.getElementById("spa").innerText = d.message;
});
let page = 0;
document.getElementById("load_more").onclick = async () => {
page += 1;
const r = await fetch("/api/list?page=" + page);
const items = await r.json();
const ul = document.getElementById("list");
for (const it of items) {
const li = document.createElement("li");
li.innerText = it.name;
ul.appendChild(li);
}
};
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML
@app.get("/api/data")
async def api_data():
await asyncio.sleep(0.3)
return {"message": "這是 SPA fetch 後才出現的內容"}
@app.get("/api/list")
async def api_list(page: int = 1):
await asyncio.sleep(0.2)
return [{"name": f"條目 {page}-{i}"} for i in range(1, 4)]
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
if USERS.get(username) == password:
return HTMLResponse("登入成功")
return HTMLResponse("登入失敗", status_code=401)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8765)
這段迷你伺服器刻意把四種場景塞在一頁:SPA(fetch 後才塞文字)、按下分頁(按鈕觸發新資料)、登入(form submit 後改 DOM)。請用另一個終端機啟動它:uv run python pipelines/_mock_server.py,瀏覽器打開 http://127.0.0.1:8765/ 應該能看到標題與按鈕。這個伺服器只在本機跑、不對外開放,符合後面談到的「禮儀」原則。請特別注意:我們刻意不在 HTML 裡寫死資料,而是讓 fetch 後才塞,模擬真實 SPA 行為;這樣 Playwright 才會需要等元素出現。
第三步:寫 Playwright 主腳本,一次示範四種動態處理。先看完整程式碼,再分段解釋:
"""pipelines/day16_playwright.py:用 Playwright 抓動態頁面並寫進 DuckDB。"""
import asyncio
from pathlib import Path
import duckdb
from playwright.async_api import async_playwright
BASE = "http://127.0.0.1:8765"
DB_PATH = "warehouse/de-journey.duckdb"
async def main() -> None:
Path("warehouse").mkdir(exist_ok=True)
con = duckdb.connect(DB_PATH)
con.execute("CREATE SCHEMA IF NOT EXISTS raw")
con.execute(
"CREATE TABLE IF NOT EXISTS raw.day16_list ("
"item_name VARCHAR, fetched_at TIMESTAMP)"
)
con.execute("DELETE FROM raw.day16_list") # 重新跑時清空
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
ctx = await browser.new_context()
page = await ctx.new_page()
# 場景一:SPA 等待元素
await page.goto(BASE + "/", wait_until="domcontentloaded")
spa_locator = page.locator("#spa")
await spa_locator.wait_for(timeout=5000)
spa_text = await spa_locator.inner_text()
print(f"SPA 內容:{spa_text}")
# 場景二:按下「載入更多」做分頁
rows = []
for click in range(3):
await page.locator("#load_more").click()
await page.wait_for_function(
"document.querySelectorAll('#list li').length > " + str(click * 3),
timeout=5000,
)
items = await page.locator("#list li").all_inner_texts()
rows.extend(items)
print(f"分頁筆數:{len(rows)}({rows[0]} ... {rows[-1]})")
# 場景三:表單登入
await page.locator("input[name='username']").fill("demo")
await page.locator("input[name='password']").fill("demo123")
async with page.expect_navigation():
await page.locator("button[type='submit']").click()
login_ok = "成功" in await page.content()
print(f"登入成功:{login_ok}")
# 把抓到的資料寫進 DuckDB
fetched_at = await page.evaluate("() => new Date().toISOString()")
con.executemany(
"INSERT INTO raw.day16_list VALUES (?, CAST(? AS TIMESTAMP))",
[(name, fetched_at) for name in rows],
)
n = con.execute("SELECT COUNT(*) FROM raw.day16_list").fetchone()[0]
print(f"DuckDB raw.day16_list 累計:{n} 筆")
await browser.close()
asyncio.run(main())
這段腳本結構分明:先準備 DuckDB 連線與空表,再用 async with async_playwright() 進入 Playwright 上下文、啟動 Chromium、建立 context 與 page。場景一是 SPA 等待:用 goto() 進首頁,再用 locator("#spa").wait_for() 等 fetch 完才出現的內容;最多等 5 秒。場景二是按下分頁:每次按下「載入更多」後用 wait_for_function() 等 DOM 數量增加,最後用 all_inner_texts() 一次抓所有串列條目。場景三是登入:填表單、按下提交、expect_navigation() 等跳轉完成,再檢查登入後才會顯示的元素。實際輸出會依網站不同,這裡是示範結構。請特別看 con.executemany():這是 DuckDB 0.10 之後新增的批次插入方法,比 executemany 配合迴圈快上數倍。
第四步:把 DuckDB 的結果與 Polars 串起來,做一個最簡的彙總驗證:
import duckdb
import polars as pl
con = duckdb.connect("warehouse/de-journey.duckdb")
df = con.execute("SELECT * FROM raw.day16_list").pl()
print(df.head(5))
print(f"總筆數:{df.height},唯一條目數:{df['item_name'].n_unique()}")
df.write_parquet("warehouse/day16_list.parquet")
print("已輸出 Parquet:warehouse/day16_list.parquet")
這段示範 DuckDB 與 Polars 的雙向整合(Day 12 的混用策略):DuckDB 的查詢結果用 .pl() 轉成 Polars DataFrame,再用 write_parquet() 落地成 Day 9 學過的 Parquet 檔。後續章節(Day 19、Day 20)會用這份 Parquet 練習分頁與增量載入。注意這段用 n_unique() 算唯一值數量,這是 Polars 1.3x 的命名(早期版本是 unique().count())。
第五步:把 Playwright 用來抓「真的需要瀏覽器」的網站時,要做幾個禮貌設定——設定 User-Agent、降低並行、限制下載:
import asyncio
from playwright.async_api import async_playwright
UA = "Mozilla/5.0 (compatible; HaoBot/1.0; +https://blog.hao-code.com/bots)"
async def polite_fetch(url: str) -> str:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
ctx = await browser.new_context(user_agent=UA, viewport={"width": 1280, "height": 800})
await ctx.route("**/*", lambda route: (
route.abort() if route.request.resource_type in {"image", "font", "stylesheet", "media"}
else route.continue_()
))
page = await ctx.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=20000)
await page.wait_for_load_state("networkidle", timeout=10000)
return await page.content()
finally:
await browser.close()
if __name__ == "__main__":
snippet = asyncio.run(polite_fetch("https://example.com"))
print(snippet[:200]) # 輸出:<!doctype html><html ...(實際會略有不同)
這個寫法有三個重點:第一,自訂 User-Agent 並放上聯絡網址,這是 Day 17 會展開的「robots.txt 與禮儀」原則;第二,用 ctx.route() 阻擋圖片、CSS、字型,把單頁流量從 1 MB 壓到 50 KB;第三,設定 20 秒 timeout 避免被慢站拖死。實際抓 example.com 通常會成功,回傳的 HTML 開頭是 doctype。route.abort() 與 route.continue_() 是 Playwright 1.5x 推薦的寫法,比舊版的 request.abort() 直覺。
第六步:把「禮儀 + 重試」做成共用函式,這是 Day 17 會展開的完整版,今天先給一個骨架:
"""pipelines/_polite_session.py:禮貌爬取 + 自動重試(Day 17 完整版)。"""
import asyncio
import random
from typing import Awaitable, Callable, TypeVar
from playwright.async_api import async_playwright, Browser
T = TypeVar("T")
async def with_retry(fn: Callable[[], Awaitable[T]], attempts: int = 3) -> T:
"""指數退避重試:失敗後等 1, 2, 4 秒,最多三次。"""
delay = 1.0
for i in range(attempts):
try:
return await fn()
except Exception as e:
if i == attempts - 1:
raise
wait = delay + random.uniform(0, 0.5)
print(f"重試 {i + 1}/{attempts - 1}:{e}(等 {wait:.1f} 秒)")
await asyncio.sleep(wait)
delay *= 2
async def fetch_with_throttle(browser: Browser, url: str, qps: float = 1.0) -> str:
"""節流抓取:每次抓取間隔至少 1/qps 秒。"""
async def _do() -> str:
page = await browser.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=15000)
return await page.content()
finally:
await page.close()
result = await with_retry(_do)
await asyncio.sleep(1.0 / qps)
return result
if __name__ == "__main__":
async def demo() -> None:
async with async_playwright() as p:
b = await p.chromium.launch(headless=True)
try:
html = await fetch_with_throttle(b, "https://example.com", qps=2.0)
print(f"抓到 {len(html)} 字元")
finally:
await b.close()
asyncio.run(demo())
這個共用函式把「重試」與「節流」抽成兩個獨立函式,fetch_with_throttle 同時包進 with_retry,等於把「禮儀」與「工程」綁在一起。Day 17 會把這套擴充成完整的速率控制模組,包含 robots.txt 解析與動態 QPS 調整。這裡展示的是骨架:用型別提示 TypeVar 讓 with_retry 能包任何回傳型別、用 asyncio.sleep 做節流、用 random.uniform 加一點抖動避免「雪崩重試」。
第七步:用 DuckDB 做抓取結果的「去重複」與「時間標記」,這是 Day 20 增量載入的暖身:
"""pipelines/day16_dedupe.py:抓回來後馬上去重,避免後續章節資料膨脹。"""
import duckdb
con = duckdb.connect("warehouse/de-journey.duckdb")
con.execute("""
CREATE OR REPLACE TABLE raw.day16_list_dedup AS
SELECT item_name, MIN(fetched_at) AS first_seen, COUNT(*) AS times_seen
FROM raw.day16_list
GROUP BY item_name
""")
total = con.execute("SELECT COUNT(*) FROM raw.day16_list_dedup").fetchone()[0]
print(f"去重後剩餘:{total} 個唯一條目")
con.execute("""
CREATE OR REPLACE VIEW raw.day16_list_latest AS
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY item_name ORDER BY fetched_at DESC) AS rn
FROM raw.day16_list
) WHERE rn = 1
""")
latest_n = con.execute("SELECT COUNT(*) FROM raw.day16_list_latest").fetchone()[0]
print(f"最新一筆快照:{latest_n} 個條目")
這段用 DuckDB 的 GROUP BY 與 ROW_NUMBER 視窗函式,把同一個 item_name 的重複抓取合併,並保留最早出現時間。Day 4 與 Day 5 學過的視窗函式這裡派上用場:ROW_NUMBER() OVER (PARTITION BY item_name ORDER BY fetched_at DESC) 給每個 item_name 內的最新一筆編號 1,舊的編 2 以上,WHERE rn = 1 過濾掉舊的。這套語法在 Day 20 增量載入會完整展開,這裡先建立「抓回來要去重」這個習慣。實際去重筆數會依前幾步的抓取結果而定,示範情境約 9 個唯一條目。
常見錯誤與踩雷
第一個雷是「裝了 Playwright 但忘了裝瀏覽器」。如果你只跑 pip install playwright,第一次執行會看到「Executable doesn't exist at ...chromium-...」的錯誤。對應排查方向:執行 python -m playwright install chromium 補裝瀏覽器;在 CI 或 Docker 環境要把這個指令寫進 Dockerfile 的 RUN 段落;如果想縮小映像檔體積,可以改用 playwright install --with-deps chromium 把系統套件也一起裝。
第二個雷是「以為 goto() 結束後資料就緒」。即使你設了 wait_until="networkidle",有些 SPA 會持續背景輪詢(polling)、永遠不會 idle。對應排查方向:對關鍵元素用 locator(...).wait_for() 或 page.wait_for_selector() 明確等待;不要依賴 sleep() 來等,這會讓程式變慢又不可靠。auto-wait 是好幫手,但要記得給合理的 timeout(建議 5 到 15 秒)。另一個隱性問題是「頁面跳轉兩次」:按下登入按鈕後 redirect 一次、JavaScript 又再 fetch 一次,這時 expect_navigation() 只涵蓋第一次跳轉,要用 wait_for_url() 等最終 URL。
第三個雷是「在 headless 模式看不到瀏覽器」。Linux 伺服器通常沒有 X server,跑 chromium.launch(headless=False) 會直接崩潰。對應排查方向:本機除錯時用 headless=False 並打 slow_mo=200(每步延遲 200 毫秒)方便觀察;正式部署一律 headless=True;若需要遠端除錯,可用 browser.new_context().new_page() 加上 page.video.record() 錄影,事後回放。
第四個雷是「一個 page 跑太多任務,瀏覽器卡住」。每個 page 是獨立分頁,但同一個 browser 的所有 page 共享記憶體。對應排查方向:批次任務用「每個分頁一個任務、總並行數控制在 2 到 4」;大量資料改用多個 browser process;不確定的話,先在 headless=False 觀察記憶體,確認沒問題再上線。實務上一個 Chromium 處理 4 個並行 page、約 100 MB 記憶體,是 CPU 可跑 的甜蜜點。
第五個雷是「登入後 session 沒保留」。如果你用 page.goto("/login") 登入後,又開新的 browser.new_page(),新分頁不會帶 cookie。對應排查方向:把登入的 page 與後續 page 用同一個 context 開;或把 cookie 與儲存狀態序列化到磁碟,下次載入;需要多使用者隔離時,每個使用者一個 context(Playwright 的 browser.new_context() 設計就是為此)。這個設計比 Selenium 的 driver 物件更乾淨,但要記得「context 等於一個獨立的小瀏覽器」。
效能與實務提醒
Playwright 的效能瓶頸通常在「瀏覽器啟動時間」。一個 Chromium 啟動約 0.5 到 1 秒,這對抓 10 頁網站沒差,但抓 1000 頁就會累積成 15 分鐘。對應做法:把整批抓取放在同一個 async_playwright() 與 browser 內,反覆開關瀏覽器最耗時。如果真的要大量抓,改用 Playwright 的「browser context pool」:建立 N 個 context,每個處理一批頁面,搭配 asyncio.gather() 並行。同時也要記得「每個 context 獨立 cookie」的特性,避免狀態污染。
另一個常見取捨是「無頭 vs 有頭」。無頭(headless=True)跑得快、耗記憶體少;有頭(headless=False)可以即時除錯、看截圖、觀察網站行為。本機開發一律建議先在有頭模式確認邏輯,再切到無頭模式上線。Playwright 1.5x 也提供 headless="new" 模式,這是 2024 年開始推的新版 headless,行為更貼近真實瀏覽器,能避開一些網站的反爬機制(例如偵測 headless 行為)。
禮儀與法律邊界是資料工程必談的議題。動態爬蟲比靜態爬蟲更有「真的像使用者」的疑慮,因此更需要節制。第一,永遠先讀 robots.txt,Day 17 會展開;第二,設定 User-Agent 並放聯絡資訊,被檢舉時對方找得到你;第三,避免在尖峰時段抓(通常是上班時間 9:00 到 18:00);第四,只抓你真的會用的資料,不要「整站打包」;第五,不要繞過登入或付費牆,這通常違反著作權與服務條款;第六,涉及個資時要特別小心,台灣《個人資料保護法》對蒐集、處理、利用有嚴格規範,即使是不含個資的公開資料仍可能有營業秘密或資料庫保護的議題。第七,公開政府資料(Day 18 會展開)依「政府資料開放授權條款第 1 版」可商用、可改作,但仍須標示來源。
最後,Playwright 也能做「測試」與「監控」。我們可以在排程腳本裡加一段 Playwright 檢查「我們的儀表板能不能正常開啟」、「API 文件頁能不能 render 完整」,把失敗截圖存起來當證據。Day 32 與 Day 38 會把這套做法接到品質監控。整套工具鏈的核心精神是「自動化一切可重複的動作」,瀏覽器自動化是其中最強大、但也最重的一塊,能用 requests 解就先用 requests,真的不行再上 Playwright。
小結
今天把 Day 15 的靜態爬蟲延伸到動態頁面:用 Playwright 1.50 啟動 Chromium、處理四種常見的動態場景(S SPA、按下分頁、表單登入、捲動載入),並把抓下來的資料寫進 DuckDB 與 Parquet。我們用一個 FastAPI 迷你伺服器離線模擬這四種場景,避開了「網站改版就壞掉」的問題,同時也建立「可以重現的測試環境」。效能上,我們學到 Chromium 啟動成本、無頭 vs 有頭的取捨、禮儀與法律的底線;工程上,我們學到 auto-wait 的設計、locator 的選擇、以及 context pool 的延伸用法。
把今天的關鍵詞整理進筆記本:Playwright 1.5x、async_playwright、browser context、auto-wait、locator、route 阻擋、禮儀 User-Agent。這些詞會在 Day 17(爬蟲禮儀)、Day 19(API 分頁)、Day 21(排程)反覆出現。請把 pipelines/_mock_server.py 與 pipelines/day16_playwright.py 都存起來,Day 17 會在這個基礎上加入速率、重試與 robots.txt 檢查。
結語
今天我們解決了「requests 抓不到的東西」。但這個能力伴隨著責任:能跑 JavaScript 不代表「應該對所有網站這麼做」。明天 Day 17 我們會把爬蟲的禮儀與工程化一次講清楚:robots.txt 的正確讀法、速率限制的計算、重試與退避(exponential backoff)的設計、以及如何在程式碼裡表達「我尊重這個網站」。這套做法會變成後續所有採集章節(Day 18 政府開放資料、Day 19 API 分頁、Day 20 增量載入)的標準配備。
明天,我們會把今天的 Playwright 腳本加上速率控制、重試、與 robots.txt 檢查,讓一支爬蟲從「能跑」變成「能上線」。
延伸資源
- Playwright 官方文件(1.50,2025):
https://playwright.dev/python/docs/intro,async API、locators、auto-wait 的標準說明。 - Playwright Python API 參考(2025):
https://playwright.dev/python/docs/api/class-playwright,async_playwright與BrowserContext的完整介面。 - Chromium 專案(2025):
https://www.chromium.org/,Playwright 預設使用的瀏覽器核心,headless 與有頭模式的差異可從這裡開始查。 - 政府資料開放平臺(2025):
https://data.gov.tw/,Day 18 會展開的真實採集標的,依「政府資料開放授權條款第 1 版」可商用、可改作。 - 台灣個人資料保護法(2025):
https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021,資料工程師在採集涉及個資時必讀的基本法。
留言
張貼留言