跳到主要內容

DE Day 17 爬蟲的禮儀與工程:速率、重試與 robots.txt

DE Day 17 爬蟲的禮儀與工程:速率、重試與 robots.txt

執行需求:CPU 可跑。今天承接 Day 15 的靜態爬蟲與 Day 16 的動態瀏覽器自動化,把焦點拉回「工程化」與「禮儀」這兩件資料工程必談的事。我們會把昨天寫的 Playwright 範例擴充成「會讀 robots.txt、會節流、會重試、會留下稽核紀錄」的版本,並用真實公開網站(中華民國政府資料開放平臺 data.gov.tw 與 example.com 的 robots.txt)做端到端示範。版本基準沿用系列:Python 3.13、httpx 0.28、Playwright 1.50,皆為 2025 年 11 月的主流世代。

引言

寫爬蟲最常被問到的不是「怎麼寫」,而是「這樣做合法嗎」、「會不會被 ban」、「怎麼不踩到對方擋」。這些問題本質上都不是技術問題,而是工程倫理與法律邊界。資料工程師每天都在跟這條線打交道:抓太多是濫用、抓太少效率差、抓錯資料更麻煩。今天我們把這條線畫清楚,並且把它寫成可重複、可交接、可被審查的程式碼。

這篇會做六件事:第一,介紹 robots.txt 規範與它在台灣 / 國際的法律定位;第二,說明禮貌爬蟲的四個核心原則(節流、識別、重試、稽核);第三,用 urllib 寫一個小型的 robots.txt 解析器,把昨天的 Playwright 範例加上這個能力;第四,設計「節流 + 重試」共用模組,用指數退避(exponential backoff)與抖動(jitter)避免雪崩;第五,把整個流程包成一支 pipelines/day17_etiquette.py;第六,整理爬蟲相關的台灣法規(個資法、著作權法、資料庫保護)與灰色地帶的實務判斷。

這套禮儀原則不是寫給「倫理學家」看的,而是寫給「每天要交付管線的工程師」看的。一支好爬蟲的標準是「對方伺服器管理員不會想擋你」。換句話說,禮貌不是道德選擇,而是工程品質的延伸。下面我們就把這個品質做出來。

禮貌爬蟲的核心原則

先建立四個原則,再談實作。第一是「節流(throttling)」:不要用滿對方的頻寬。一般建議是每秒 1 到 5 個請求(QPS = 1 到 5);對於公開資料平台,可以再降到每秒 0.5 個。第二是「識別(identification)」:在 User-Agent 裡放上自己的名字與聯絡方式(網站或 email)。當對方覺得你濫用時,找得到你才能溝通,這是最基本的禮貌。第三是「重試(retry)要聰明」:不要瘋狂重試,要用指數退避(每次失敗等更久)並加上隨機抖動(jitter)避免「雪崩重試」讓伺服器瞬間爆量。第四是「稽核(audit)」:把每一次抓取的時間、URL、回應狀態、位元組數都記下來。出事時才查得到,平常也能用來調整節流策略。

接著談 robots.txt。這個檔案最早由 Martijn Koster 在 1994 年提出,是給網路爬蟲看的「君子協定」。它的本質是「網站管理員告訴爬蟲哪些路徑不要來」。語法簡單:User-agent: * 表示對所有爬蟲生效;Disallow: /private/ 表示禁止抓取 /private/ 路徑;Crawl-delay: 5 建議每次抓取間隔 5 秒;Sitemap: https://example.com/sitemap.xml 列出網站地圖。它不是法律強制,但「忽略 robots.txt 又被檢舉」會讓你在民事或刑事上站不住腳。台灣目前沒有專法直接規範爬蟲行為,但實務上會用《個人資料保護法》、《著作權法》、《刑法》妨害秘密罪、或《公平交易法》來處理糾紛。

台灣《個人資料保護法》對資料工程師特別重要。簡單說,個資法保護「自然人之個人資料」,包括姓名、身分證字號、地址、電話、Email、病歷、醫療、基因、性生活、健康檢查、犯罪前科、肖像等。公開資料如果經過「去識別化」處理,原則上不受個資法約束;但如果資料可「還原」到特定個人,仍受規範。實務上,看到「會員編號 + 生日 + 居住縣市」這種組合就要警覺,極可能屬於「得以間接識別特定個人」。Day 18 處理政府開放資料時,會再用一節展開資料授權與個資的界線。

完整實作:禮貌爬蟲的標準配備

今天的實作目標是建立一個「禮貌爬蟲基礎套件 pipelines/_etiquette.py」,提供四個能力:robots.txt 解析、節流、重試、稽核記錄。然後用一支 pipelines/day17_demo.py 示範怎麼用這個套件抓 https://example.com(用它的 robots.txt 做練習)與 data.gov.tw 的目錄頁。先看完整程式碼,再分段解釋。

第一步:建立工作目錄與安裝套件,沿用系列基準:

cd de-journey
uv pip install httpx==0.28.1 tenacity==9.0.0
# tenacity 是社群常用的重試函式庫,搭配 urllib 標準函式庫即可運作

這段指令安裝 httpx 0.28(2025 年 11 月主流版本,與 Day 15、Day 16 一致)與 tenacity 9.0(重試輔助)。tenacity 不是必要選擇,自己寫一個 with_retry 也能跑;引入它是為了說明「業界怎麼處理重試」,並且對 Day 22 的失敗處理篇章先建立介面概念。

第二步:寫一個簡單的 robots.txt 解析器。Python 標準函式庫沒有 built-in 的 robots 解析(2025 年的提案仍在討論),所以我們自己寫一個支援 User-agent、Disallow、Allow、Crawl-delay 的版本:

"""pipelines/_robots.py:輕量 robots.txt 解析器。"""
from dataclasses import dataclass, field
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser  # 標準函式庫,但 API 不夠好用
import httpx

@dataclass
class Robots:
    user_agent: str
    allow: list[str] = field(default_factory=list)
    disallow: list[str] = field(default_factory=list)
    crawl_delay: float = 1.0
    sitemaps: list[str] = field(default_factory=list)

    def can_fetch(self, url: str) -> bool:
        u = urlparse(url)
        path = u.path or "/"
        for rule in self.disallow:
            if path.startswith(rule):
                # 例外:若 disallow 的子路徑被 allow 涵蓋,就放行
                for allow in self.allow:
                    if path.startswith(allow):
                        return True
                return False
        return True

def fetch_robots(base_url: str, user_agent: str, timeout: float = 10.0) -> Robots:
    parsed = urlparse(base_url)
    robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
    text = ""
    try:
        r = httpx.get(robots_url, headers={"User-Agent": user_agent}, timeout=timeout)
        if r.status_code == 200:
            text = r.text
    except httpx.HTTPError:
        pass

    bot = Robots(user_agent=user_agent)
    current_group = None
    matched = False
    for raw in text.splitlines():
        line = raw.split("#", 1)[0].strip()
        if not line:
            continue
        key, _, value = line.partition(":")
        key = key.strip().lower()
        value = value.strip()
        if key == "user-agent":
            current_group = value
            matched = (value == "*" or value.lower() == user_agent.lower())
        elif matched and key == "disallow":
            if value:
                bot.disallow.append(value)
        elif matched and key == "allow":
            if value:
                bot.allow.append(value)
        elif matched and key == "crawl-delay":
            try:
                bot.crawl_delay = float(value)
            except ValueError:
                pass
        elif key == "sitemap":
            bot.sitemaps.append(value)
    return bot

if __name__ == "__main__":
    UA = "HaoBot/1.0 (+https://blog.hao-code.com/bots)"
    for site in ["https://example.com", "https://www.wikipedia.org"]:
        rb = fetch_robots(site, UA)
        print(f"{site} 解析結果:")
        print(f"  Disallow: {rb.disallow[:3]}")
        print(f"  Crawl-delay: {rb.crawl_delay}")
        print(f"  can_fetch 首頁:{rb.can_fetch(site + '/')}")

這段程式有兩個關鍵設計:第一,Robots 用 dataclass 表達,欄位意義直覺;第二,can_fetch() 用「路徑前綴比對」判斷,這是 robots.txt 規範的核心邏輯。如果網站沒有 robots.txt(例如連線失敗、404),我們回傳預設 Robots,預設 crawl-delay 1.0 秒,這是「保守預設」:不知道對方規矩時,先慢慢抓。實際執行時,example.com 與 wikipedia.org 都會回傳各自的規則,可以看出兩個網站對爬蟲的開放程度差異很大。

第三步:寫節流與重試模組,組合 Day 16 的骨架擴充成完整版:

"""pipelines/_etiquette.py:禮貌爬取 + 自動重試 + 稽核記錄。"""
import asyncio
import json
import random
import time
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Awaitable, Callable, TypeVar

import httpx
from tenacity import AsyncRetrying, stop_after_attempt, wait_exponential_jitter

T = TypeVar("T")
AUDIT_LOG = Path("logs/day17_audit.jsonl")
AUDIT_LOG.parent.mkdir(exist_ok=True)

@dataclass
class AuditRecord:
    ts: str
    url: str
    status: int
    bytes: int
    latency_ms: float
    user_agent: str

def write_audit(rec: AuditRecord) -> None:
    with AUDIT_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(asdict(rec), ensure_ascii=False) + "\n")

class PoliteSession:
    """整合節流、重試、稽核的禮貌爬取 session。"""

    def __init__(self, user_agent: str, qps: float = 1.0, max_attempts: int = 4) -> None:
        self.user_agent = user_agent
        self.min_interval = 1.0 / qps
        self.max_attempts = max_attempts
        self._last_ts = 0.0
        self._lock = asyncio.Lock()

    async def _pace(self) -> None:
        async with self._lock:
            now = time.monotonic()
            wait = self.min_interval - (now - self._last_ts)
            if wait > 0:
                await asyncio.sleep(wait)
            self._last_ts = time.monotonic()

    async def get(self, client: httpx.AsyncClient, url: str) -> httpx.Response:
        await self._pace()
        retryer = AsyncRetrying(
            stop=stop_after_attempt(self.max_attempts),
            wait=wait_exponential_jitter(initial=1, max=10),
            reraise=True,
        )
        for attempt in retryer:
            with attempt:
                t0 = time.monotonic()
                resp = await client.get(url, headers={"User-Agent": self.user_agent}, timeout=20.0)
                rec = AuditRecord(
                    ts=datetime.now(timezone.utc).isoformat(),
                    url=url,
                    status=resp.status_code,
                    bytes=len(resp.content),
                    latency_ms=(time.monotonic() - t0) * 1000,
                    user_agent=self.user_agent,
                )
                write_audit(rec)
                if resp.status_code in (429, 500, 502, 503, 504):
                    resp.raise_for_status()
                return resp

if __name__ == "__main__":
    UA = "HaoBot/1.0 (+https://blog.hao-code.com/bots)"
    async def demo() -> None:
        async with httpx.AsyncClient() as client:
            session = PoliteSession(UA, qps=2.0)
            for url in ["https://example.com/", "https://example.org/"]:
                r = await session.get(client, url)
                print(f"{url} → {r.status_code} ({len(r.content)} bytes)")
    asyncio.run(demo())

這段是今天的工程核心。PoliteSession 把四件事綁在一起:節流用 _pace() 與 asyncio.Lock 確保同 session 內每個請求至少間隔 min_interval;重試用 tenacity 的 wait_exponential_jitter,第一次失敗等 1 到 2 秒、第二次 2 到 4 秒、第三次 4 到 8 秒,最多四次;稽核把每次請求的時間、URL、狀態、位元組、延遲寫進 logs/day17_audit.jsonl。這個檔案格式是 JSON Lines(每行一筆 JSON),方便後續用 DuckDB 直接讀入分析(Day 32 會用到)。

第四步:把 robots 解析、PoliteSession 與昨天的 Playwright 範例接起來,展示「禮貌動態爬蟲」的完整樣貌:

"""pipelines/day17_demo.py:禮貌版動態爬蟲,串起 robots + 節流 + 稽核。"""
import asyncio
from playwright.async_api import async_playwright
from pipelines._etiquette import PoliteSession, write_audit, AuditRecord
from pipelines._robots import fetch_robots, Robots
import time

UA = "HaoBot/1.0 (+https://blog.hao-code.com/bots)"

async def polite_playwright_example() -> None:
    robots = fetch_robots("https://example.com", UA)
    print(f"example.com Crawl-delay: {robots.crawl_delay} 秒")
    print(f"can_fetch 首頁: {robots.can_fetch('https://example.com/')}")
    if not robots.can_fetch("https://example.com/"):
        print("robots.txt 禁止,跳過")
        return

    session = PoliteSession(UA, qps=1.0)
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        ctx = await browser.new_context(user_agent=UA)
        page = await ctx.new_page()
        t0 = time.monotonic()
        await page.goto("https://example.com/", wait_until="domcontentloaded")
        html = await page.content()
        await browser.close()
        write_audit(AuditRecord(
            ts=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            url="https://example.com/",
            status=200,
            bytes=len(html),
            latency_ms=(time.monotonic() - t0) * 1000,
            user_agent=UA,
        ))
        print(f"抓到 {len(html)} bytes,已寫入稽核記錄")

if __name__ == "__main__":
    asyncio.run(polite_playwright_example())

這段把昨天的「裸抓」變成「有禮貌」的版本:先用 robots.txt 檢查能不能抓(不能就跳過),再用節流 session 控制節奏,最後把結果寫進稽核檔。實際跑 example.com 會印出 Crawl-delay 與 can_fetch 結果,並完成一次抓取。example.com 的 robots.txt 沒有禁止任何路徑(它是示範站),所以一定會跑下去;如果你換成其他網站,可能會看到「can_fetch: False」然後跳過,這就是禮貌爬蟲該有的行為。

第五步:用 DuckDB 把稽核 log 讀進來,做簡單的「抓取品質」儀表板:

"""pipelines/day17_audit_report.py:把稽核 log 讀進 DuckDB 做摘要。"""
import duckdb

con = duckdb.connect("warehouse/de-journey.duckdb")
con.execute("""
    CREATE OR REPLACE TABLE raw.day17_audit AS
    SELECT
        CAST(ts AS TIMESTAMP) AS ts,
        url, status, bytes,
        latency_ms, user_agent
    FROM read_json_auto("logs/day17_audit.jsonl", format="newline_delimited")
""")
total = con.execute("SELECT COUNT(*) FROM raw.day17_audit").fetchone()[0]
print(f"累計抓取次數:{total}")

# 失敗率與平均延遲
fail_rate = con.execute("""
    SELECT AVG(CASE WHEN status >= 400 THEN 1.0 ELSE 0.0 END) AS fail_rate,
           AVG(latency_ms) AS avg_latency_ms
    FROM raw.day17_audit
""").fetchone()
print(f"失敗率:{fail_rate[0]:.2%},平均延遲:{fail_rate[1]:.1f} ms")

# 各 host 的請求分佈
host_dist = con.execute("""
    SELECT
        REGEXP_EXTRACT(url, '^https?://([^/]+)', 1) AS host,
        COUNT(*) AS n,
        AVG(latency_ms) AS avg_ms
    FROM raw.day17_audit
    GROUP BY host ORDER BY n DESC
""").fetchall()
for host, n, ms in host_dist:
    print(f"  {host}:{n} 次,平均 {ms:.1f} ms")

這段用 DuckDB 的 read_json_auto 直接讀 JSON Lines,不需要先轉成 CSV 或 Parquet。REGEXP_EXTRACT 從 URL 抽出 host,再用 GROUP BY 統計每個 host 的請求數與平均延遲。Day 32 與 Day 38 的監控章節會把這套延伸成「資料品質儀表板」。

第六步:寫一個「禮貌合規檢查」小工具,把上面的原則變成可被 lint 的規則,這對 CI 環境特別有用:

"""pipelines/_etiquette_check.py:對現有爬蟲做禮貌合規檢查。"""
import ast
import re
import sys
from pathlib import Path

SUSPECT_UA = re.compile(r"(Mozilla|Chrome|Safari|Edge)/", re.IGNORECASE)
MISSING_UA_HINT = "未設定 User-Agent(看起來不像在標明身份)"
MISSING_DELAY_HINT = "未發現 sleep 或 throttle 設定,可能節流不足"

def scan_file(path: Path) -> list[str]:
    src = path.read_text(encoding="utf-8")
    tree = ast.parse(src)
    issues = []
    has_ua = False
    has_sleep = False
    for node in ast.walk(tree):
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            if SUSPECT_UA.search(node.value):
                has_ua = False  # 偽裝成瀏覽器,反而算沒設 UA
        if isinstance(node, ast.Call):
            func = ast.unparse(node.func) if hasattr(ast, "unparse") else ""
            if "User-Agent" in ast.unparse(node) or "user_agent" in ast.unparse(node):
                has_ua = True
            if func.endswith("sleep"):
                has_sleep = True
    if not has_ua:
        issues.append(MISSING_UA_HINT)
    if not has_sleep:
        issues.append(MISSING_DELAY_HINT)
    return issues

if __name__ == "__main__":
    target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("pipelines")
    files = list(target.rglob("*.py"))
    bad = 0
    for f in files:
        issues = scan_file(f)
        if issues:
            print(f"[!] {f}")
            for issue in issues:
                print(f"    - {issue}")
            bad += 1
    print(f"完成檢查:{len(files)} 個檔案,{bad} 個有禮貌問題")
    sys.exit(1 if bad else 0)

這個靜態檢查腳本用 Python 的 ast 模組把 Python 檔案 parse 成語法樹,再掃過所有 Call 與 Constant 節點,判斷有沒有設 User-Agent、有沒有呼叫 sleep。這不是完美的檢查(會漏掉用 httpx.Client(headers={...}) 傳 dict 的寫法),但對於「一眼看出哪些檔案沒做禮貌設定」已經夠用。可以把它接到 pre-commit hook 或 CI,讓每次提交都先過一輪。實際輸出會依你的 pipelines/ 內容而定,新寫的爬蟲通常都會被抓出「沒設 UA」這條。

第七步:把整個禮貌套件用一份 README 收尾,方便日後交接:

"""pipelines/_etiquette_init.py:禮貌套件初始化與版本說明。"""
__all__ = ["Robots", "fetch_robots", "PoliteSession", "AuditRecord"]

ETIQUETTE_VERSION = "1.0.0"
RULES = {
    "version": ETIQUETTE_VERSION,
    "原則": [
        "節流:QPS 預設 1.0,可依對方回應調整",
        "識別:User-Agent 含爬蟲名稱與聯絡網址",
        "重試:指數退避加抖動,最多 4 次",
        "稽核:每次請求寫入 logs/day17_audit.jsonl",
    ],
    "預設值": {
        "qps": 1.0,
        "max_attempts": 4,
        "crawl_delay": 1.0,
        "user_agent": "HaoBot/1.0 (+https://blog.hao-code.com/bots)",
    },
    "覆寫優先序": "PoliteSession() 參數 > 環境變數 > 預設值",
}

if __name__ == "__main__":
    import json
    print(json.dumps(RULES, ensure_ascii=False, indent=2))

這份「禮貌套件清單」把所有預設值寫在一起,新進工程師只要看這份就能知道整套禮貌規範。這是 Day 40「文件化與交接」的前置:把規則寫成程式碼可以讀的形式,遠比寫在 Word 文件可靠。執行這個檔案會印出 JSON 格式的規則清單,方便對照與覆審。系列後續每天都會引用 _etiquette.py 的 PoliteSession,這份清單就是它的「契約」。

常見錯誤與踩雷

第一個雷是「沒看 robots.txt 就開始抓」。即使你只抓公開資料,也要先看 robots.txt,這是基本禮貌。對應排查方向:在 session 初始化時呼叫 fetch_robots() 並把結果記下來;如果對方明示禁止(Disallow),就停下來評估要不要繞過(通常不要)。如果對方根本沒 robots.txt,視同「沒特別禁止」,但仍要節流。

第二個雷是「用錯重試策略」。常見錯誤有兩個:一是不加退避(每次失敗立刻重試,等於 DoS 攻擊);二是退避但沒加抖動(所有客戶端同時重試,雪崩效應)。對應排查方向:用 wait_exponential_jitter 或自己寫 delay = base * 2 ** n + random.uniform(0, 1)。第三個錯誤是「無限重試」,一定要設 stop_after_attempt 上限,建議 3 到 5 次。

第三個雷是「只重試 5xx,不重試 429」。HTTP 429(Too Many Requests)是「你抓太快」的明確訊號,這時一定要節流再重試。對應排查方向:把 429 加入重試清單,並在重試前讀 Retry-After header,這個 header 會告訴你對方要你等多久。httpx 的 response.headers.get("Retry-After") 可以讀到,單位是秒(HTTP/1.1)或 HTTP 日期(HTTP/2)。

第四個雷是「稽核 log 沒寫就上線」。很多爬蟲出包是因為「我以為我有抓,但其實沒抓到」、「以為抓到 100 筆但其實是 50 筆」。對應排查方向:把每次抓取的請求與回應都寫進日誌,定期跑摘要(如上面第五步)。如果哪天對方說「你昨天抓太多了」,你能拿出稽核 log 證明實際狀況,這比「我覺得沒有」有說服力一百倍。

第五個雷是「User-Agent 偽裝成瀏覽器」。有些爬蟲會把 User-Agent 改成 Chrome 或 Safari,企圖繞過反爬機制。這在禮貌上屬於灰色地帶(不算違法,但不算誠實)。對應做法:誠實標明自己的爬蟲身份與聯絡方式;如果對方明示拒絕非瀏覽器 UA,就不要硬抓。如果對方只接受瀏覽器 UA,請先確認這是不是「強烈拒絕爬蟲」的訊號——多數情況下,這代表「請你不要來」。

效能與實務提醒

節流的實際數字很看場景。對「公開資料平台(data.gov.tw、政府統計)」,建議 QPS 1.0 以下;對「商業網站(電商、新聞)」,建議 QPS 0.5 以下;對「自家或合作單位的 API」,可以放到 QPS 5 到 10。原則是「先保守、再觀察、再調整」。如果對方回應都很順、沒有 429 或 5xx,可以慢慢調高;如果看到錯誤率上升,就要降回去。

另一個常見取捨是「共用 session vs 每次重開連線」。httpx 的 AsyncClient 會做 HTTP keep-alive(連線池),同一個 client 重複用會比每次重開連線快很多(節省 TCP 與 TLS handshake)。但若要為「禮貌」付出代價,可以每 N 個請求關閉 client 重來,避免長期佔用連線。實務上,公共抓取建議每 100 個請求或每 5 分鐘關閉一次。

稽核 log 的保存也是學問。長期保留會吃掉磁碟,太快刪除又失去稽核價值。建議做法:原始 log 保留 30 天(用 logrotate 或自寫排程),彙總後的指標(每小時、每天)保留一年。如果遇到法律爭議,這些彙總指標可以作為「我們的抓取行為合理」的證據。Day 38 的監控章節會再展開這個主題。

最後,禮貌爬蟲不只是一套技術,它也是一種「資料工程的職業倫理」。我們在 Day 18 處理政府開放資料時,會再談一次「授權」的議題;在 Day 39 談 LLM 輔助資料清洗時,會談「訓練資料的來源是否合法」。這條倫理線從 Day 17 開始拉,貫穿整個系列。把它寫進你的程式碼備註、寫進你的 README、寫進你的交接文件,這是「管線活得比作者久」的關鍵之一。

小結

今天把 Day 15、Day 16 的爬蟲本體擴充成「有禮貌、有工程紀律」的版本。我們建立了四個核心模組:_robots.py 解析 robots.txt、_etiquette.py 提供節流與重試、稽核 log 寫進 logs/、DuckDB 讀 log 做摘要。這套配備是後續 Day 18(政府開放資料)、Day 19(API 分頁)、Day 20(增量載入)的共用基礎。實際工作時,把它包成可重用的 pipelines/_etiquette.py 並且 commit 進版控,所有採集腳本都引用它,這樣日後維護時只要改一個地方。

把今天的關鍵詞整理進筆記本:robots.txt、User-Agent、QPS、指數退避、jitter、429 Retry-After、稽核 log、JSON Lines。這些詞在 Day 22 的失敗處理篇章會再延伸為「重試佇列」與「斷點續跑」,在那之前先把這套禮貌地基打穩。把 pipelines/_robots.py、pipelines/_etiquette.py、pipelines/day17_demo.py 都存起來,明天 Day 18 會用這個套件抓 data.gov.tw 的真實目錄。

結語

禮貌不是道德選擇,而是工程品質。今天我們把爬蟲從「會動」變成「會做事」,並且把「做事的方法」寫成可重複、可審查的程式碼。明天 Day 18 我們會把這套套件用在「中華民國政府資料開放平臺」的真實目錄上,介紹怎麼從 data.gov.tw 找資料集、讀 metadata、確認授權(政府資料開放授權條款第 1 版)、並把 CSV 與 JSON 端點拉回來。Day 18 也會展示當 robots.txt 不存在或不明時,工程師該怎麼保守行事。

明天,我們會把禮貌爬蟲套件接到政府開放平台,實際抓一份真實資料集,並討論「真實世界的 API vs 範例 API」的差異。

延伸資源

  • robots.txt 規範(Google 搜尋中心,2025):https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt,User-agent、Disallow、Allow、Crawl-delay 的語法定義。
  • tenacity 9.0 文件(2025):https://tenacity.readthedocs.io/en/latest/index.html,wait_exponential_jitter、stop_after_attempt 的標準用法。
  • httpx 0.28 文件(2025):https://www.python-httpx.org/,async client、keep-alive、HTTP/2 的 API 與設定。
  • 政府資料開放平臺(2025):https://data.gov.tw/,依「政府資料開放授權條款第 1 版」可商用、可改作,Day 18 會展開。
  • 台灣個人資料保護法(2025):https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021,資料採集涉及個資時必讀的基本法。
  • 網際網路之父的 robots.txt 起源(Koster, 1994):https://www.robotstxt.org/,君子協定的歷史與設計初衷。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...