Scrapy 完整教學 2026:從安裝、Selector 到 Pipeline 上線

Scrapy 是 6.5 萬星的 Python 爬蟲框架,但官方教學停在最小範例。本文用 Scrapy 2.19.0 實跑一次完整流程:建專案、寫 spider、Selector 速查、Item Pipeline 去重、ROBOTSTXT_OBEY 實測、JSON/CSV 匯出到 cron 排程,並附上五個實際踩到的坑(含 2.19 換成 dataclass 後的 TypeError)。

  • Dennis
  • 9 分鐘閱讀
Scrapy 完整教學 2026:從安裝、Selector 到 Pipeline 上線

Scrapy 是 Python 最成熟的爬蟲框架:6.5 萬顆星、BSD-3 授權、2010 年活躍至今。它把「抓取、解析、去重、節流、匯出」拆成可組合的零件,適合要抓上千頁而非三、五頁的場合。本文用 Scrapy 2.19.0 從零實跑一遍,所有指令與數字都是實際執行結果。

一句話結論:什麼時候該用 Scrapy

情境建議工具原因
抓 1~3 頁 API 或 HTMLrequests + parsel開一個框架反而多學一層
同一個站要抓數百到數萬頁Scrapy內建佇列、去重、重試、併發控制、節流、匯出格式
頁面內容靠 JavaScript 渲染Playwright/Selenium(或 scrapy-playwright)Scrapy 預設只拿到 HTML
要餵 LLM 的網頁轉 MarkdownCrawl4AI/Firecrawl直接輸出 LLM 友善格式,省一層清洗
需要登入、複雜互動、滑動載入Playwright瀏覽器自動化的主場

換句話說:Scrapy 解的是「規模」與「工程化」,不是「怎麼拿到第一個 HTML」。當你開始需要斷點續爬、去重、限速、把資料寫進資料庫、每天跑一次,Scrapy 的價值才會出現。

專案現況(2026-11-04 查證)

項目內容
最新版本Scrapy 2.19.0(本次實測安裝版本)
GitHubscrapy/scrapy,65,221 stars、12,094 forks
授權BSD-3-Clause
首個 commit2010-02-22(十多年的老專案,生態完整)
官方文件docs.scrapy.org(本文引用的頁面都回 200)

安裝:用虛擬環境,不要裝進系統 Python

python3 -m venv /tmp/scrapyenv
source /tmp/scrapyenv/bin/activate
pip install --upgrade pip
pip install scrapy

scrapy version --verbose

本次在 Python 3.12.3 上安裝,scrapy version --verbose 的實際輸出:

Scrapy       : 2.19.0
lxml         : 6.1.3
libxml2      : 2.14.6
cssselect    : 1.6.0
parsel       : 1.12.1
w3lib        : 2.5.0
Twisted      : 26.10.0
Python       : 3.12.3
pyOpenSSL    : 26.4.0
cryptography : 50.0.2

兩個實務提醒:

  1. Python 版本別亂跳。 Scrapy 的併發靠 Twisted,Twisted 對新版 Python 的支援通常落後幾季。本次測試環境是 3.12;若手上是更新的版本而安裝失敗,先降 Python 而不是硬上。
  2. scrapy version --verbose 比 scrapy version 有用。 出事時(SSL、lxml 解析、Twisted 事件迴圈)需要的是整份依賴清單,不是一個版本號。

建立專案:2.19 的模板長得跟舊教學不一樣

scrapy startproject quotebot
cd quotebot

產生的結構(find 實際輸出):

quotebot/quotebot/__init__.py
quotebot/quotebot/items.py
quotebot/quotebot/middlewares.py
quotebot/quotebot/pipelines.py
quotebot/quotebot/settings.py
quotebot/scrapy.cfg

兩個與舊文章不同的地方,值得先知道:

  • items.py 的模板已經是 @dataclass,不再是 scrapy.Item 的子類。scrapy.Item 支援 item["field"] = ... 的字典式賦值,dataclass 不支援——這一點會在下面變成一個真實的錯誤。
  • settings.py 多了 ADDONS = {}。這是 Scrapy 2.12 之後新增的 addons 機制,用來掛官方或第三方的擴充(例如 scrapy.addons.AutoThrottleAddon),寫法比直接改 EXTENSIONS / DOWNLOADER_MIDDLEWARES 乾淨。

settings.py 模板預設已經幫你打開兩個「正確預設」:

ROBOTSTXT_OBEY = True
CONCURRENT_REQUESTS_PER_DOMAIN = 1
DOWNLOAD_DELAY = 1
FEED_EXPORT_ENCODING = "utf-8"

這代表預設就是「每網域每秒 1 個請求、遵守 robots.txt」。想加速要自己改,這個預設方向是對的。

第一支 spider:抓分頁、產生 item

以 Scrapy 官方教學用的 quotes.toscrape.com 為目標(刻意設計給爬蟲練習的站),spiders/quotes.py:

import scrapy
from quotebot.items import QuoteItem


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    allowed_domains = ["quotes.toscrape.com"]
    start_urls = ["https://quotes.toscrape.com/"]

    custom_settings = {
        "DOWNLOAD_DELAY": 0.5,
        "AUTOTHROTTLE_ENABLED": True,
        "AUTOTHROTTLE_START_DELAY": 0.5,
        "AUTOTHROTTLE_MAX_DELAY": 5.0,
        "AUTOTHROTTLE_TARGET_CONCURRENCY": 2.0,
        "CONCURRENT_REQUESTS_PER_DOMAIN": 2,
    }

    def parse(self, response):
        for box in response.css("div.quote"):
            item = QuoteItem(
                text=" ".join(box.css("span.text::text").get("").split()),
                author=box.css("small.author::text").get(""),
                tags=box.css("div.tags a.tag::text").getall(),
                url=response.url,
            )
            item.word_count = len(item.text.split())
            yield item

        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

對應的 items.py:

from dataclasses import dataclass, field


@dataclass
class QuoteItem:
    text: str = ""
    author: str = ""
    tags: list[str] = field(default_factory=list)
    url: str = ""
    word_count: int = 0

執行的實際結果(scrapy crawl quotes -O quotes.json):

{'downloader/response_count': 11,
 'downloader/response_status_count/200': 10,
 'downloader/response_status_count/404': 1,
 'elapsed_time_seconds': 5.992566949920729,
 'item_scraped_count': 100,
 'items_per_minute': 1200.0,
 'memusage/max': 80510976,
 'request_depth_max': 9,
 'robotstxt/response_status_count/404': 1,
 'finish_reason': 'finished'}

換成人話:

  • 10 頁、100 則引言、5.99 秒跑完,request_depth_max: 9 代表分頁是「一層一層往下」的鏈狀結構,不是一頁列出所有連結。
  • 那 1 個 404 是 Scrapy 自己去抓 robots.txt 的結果——這個站沒有 robots.txt。404 不等於被禁止:依 robots 規範,沒有 robots.txt 就視為「全部允許」,所以抓取照常進行(robotstxt/response_status_count/404 就是這條紀錄)。
  • 記憶體峰值約 80.5 MB(memusage/max: 80510976),對長時間排程很重要——這是「一個 Scrapy 行程的常駐成本」,不是每頁成本。

Selector 速查:這六個寫法等於八成的爬蟲

Scrapy 的 Selector 底層是 parsel,同時支援 CSS 與 XPath。實測 scrapy shell "https://quotes.toscrape.com/" -c "print(len(response.css('div.quote')), response.css('small.author::text').getall()[:3])" 回傳:

10 ['Albert Einstein', 'J.K. Rowling', 'Albert Einstein']

也就是這一頁 10 則、作者抓得到。常用寫法:

目的寫法備註
取文字response.css("h1::text").get()只取第一個
取全部文字response.css("a.tag::text").getall()回傳 list
取屬性response.css("a::attr(href)").get()圖片改 ::attr(src)
XPath 取上層response.xpath("//div[@class='quote']/span/text()").get()結構複雜時比 CSS 好用
用父節點反查response.xpath("//small[@class='author']/parent::*/text()")常見於「標籤在隔壁」的版面
正規表達式response.css("span.text::text").re(r"\d+")從文字裡撈數字最快

三個實務建議:

  1. 先開 scrapy shell 再寫 selector。 在 shell 裡打 view(response) 可以看 Scrapy 實際收到的 HTML(不是瀏覽器看到的),能立刻判斷「抓不到」是選擇器錯還是 JavaScript 沒渲染。
  2. 不要用絕對路徑。 div.quote > span.text 這種相對寫法比 body > div:nth-child(3) > ... 耐用一個量級。
  3. get() 沒命中回 None,要給預設值。 上面程式碼用 .get("") 就是避免後面 len(None.split()) 直接爆掉。

Item Pipeline:去重、正規化、收尾統計

from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem


class QuotebotPipeline:
    def __init__(self):
        self.seen = set()
        self.kept = 0
        self.dropped = 0
        self.tag_counter = {}

    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        key = (adapter["author"], adapter["text"][:40])
        if key in self.seen:
            self.dropped += 1
            raise DropItem(f"duplicate: {key}")
        self.seen.add(key)

        adapter["tags"] = sorted({t.lower().strip() for t in adapter["tags"]})
        for t in adapter["tags"]:
            self.tag_counter[t] = self.tag_counter.get(t, 0) + 1
        self.kept += 1
        return item

    def close_spider(self, spider):
        top = sorted(self.tag_counter.items(), key=lambda kv: -kv[1])[:5]
        spider.logger.info("PIPELINE kept=%d dropped=%d top_tags=%s",
                           self.kept, self.dropped, top)

在 settings.py 啟用:

ITEM_PIPELINES = {
    "quotebot.pipelines.QuotebotPipeline": 300,
}

實跑的 log 尾巴:

[quotes] INFO: PIPELINE kept=100 dropped=0 top_tags=[('love', 14), ('inspirational', 13), ('life', 13), ('humor', 12), ('books', 11)]
[scrapy.extensions.feedexport] INFO: Stored json feed (100 items) in: quotes.json

Pipeline 的價值不在「清洗」這一步,而在它是唯一保證「每一筆 item 都經過」的位置:去重、欄位補齊、寫資料庫、打 API 都在這裡,比散在 spider 裡可靠。

禮貌與法規:ROBOTSTXT_OBEY 到底做什麼

很多人以為打開 ROBOTSTXT_OBEY 就「安全」。實際行為值得用一個可重現的實驗看清楚——本機起一個測試站,robots.txt 寫 Disallow: /private/,首頁同時有 /public/ok.html 與 /private/secret.html 兩個連結:

User-agent: *
Disallow: /private/

跑完之後的統計與 log:

2026-11-04 ... [rbt_demo] INFO: PARSED http://127.0.0.1:8899/ links=2
2026-11-04 ... [rbt_demo] INFO: GOT http://127.0.0.1:8899/public/ok.html title=public ok
2026-11-04 ... [scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET http://127.0.0.1:8899/private/secret.html>

{'downloader/exception_type_count/scrapy.exceptions.IgnoreRequest': 1,
 'robotstxt/forbidden': 1}

三個結論:

  1. ROBOTSTXT_OBEY 是「請求層」的過濾。被擋的那個請求根本沒發出去(記成 IgnoreRequest),不是抓回來才丟掉。
  2. 連結照樣被解析。spider 還是看到兩個連結、還是呼叫了 response.follow,只是其中一個被下載器中介層攔掉。所以「遵守 robots」不會讓你的解析邏輯變乾淨,只是不發出請求。
  3. 要驗證有沒有真的擋,看統計不要看 log 等級。 robotstxt/forbidden 這個計數器是唯一可靠的證據;把 LOG_LEVEL 設成 INFO 時那句 DEBUG 訊息根本不會出現。

與 robots 相關但常被混淆的幾件事:

機制作用常見誤解
robots.txt站方聲明的抓取範圍(自願性規範)不是法律,也不等於授權
ROBOTSTXT_OBEY = TrueScrapy 遵守上面那份聲明打開它不等於合法,也不等於對方同意
DOWNLOAD_DELAY / AutoThrottle控制請求速率設了就不會被 ban?不一定,雲端 IP 常直接封
網站 ToS合約層面的限制robots 允許 ≠ ToS 允許

倫理與法律面的完整版(含 Cloudflare Bot Management、指紋辨識、以及台灣常見的個資問題)另文處理,這裡只留一句:抓取前先確認「對方有沒有提供 API」,有 API 就別爬 HTML,這是成本最低也最不容易出事的選擇。

匯出資料:-O 與 -o 差一個字母,行為完全不同

# 覆寫(overwrite):開發時用,每次跑都清空重寫
scrapy crawl quotes -O quotes.json

# 附加(append):累積歷史時用
scrapy crawl quotes -o quotes-$(date +%F).jsonl

實測 CSV 匯出(-O quotes.csv)的檔案開頭與行數:

text,author,tags,url,word_count
"“The world as we have created it is a process of our thinking...",Albert Einstein,"change,deep-thoughts,thinking,world",https://quotes.toscrape.com/,21
"“It is our choices, Harry, that show what we truly are, far more than our abilities.”",J.K. Rowling,"abilities,choices",https://quotes.toscrape.com/,16

101 quotes.csv   # 100 筆資料 + 1 行表頭

幾個格式選擇的實務建議:

  • 要進資料庫或餵程式:用 jsonl(JSON Lines)。 一行一筆,可以串流處理;json 是整包陣列,百萬筆時記憶體會炸。
  • 要給人看:csv。 但注意 utf-8 在 Excel 會亂碼,前面加 BOM 或改用 xlsx 外掛。
  • 欄位順序由 item 的定義決定(dataclass 的宣告順序),不是由 spider 的賦值順序決定。

上線:cron 或 systemd timer

每日排程最簡單的長相(把 log 與產物分開放,失敗才有東西可查):

cd /srv/quotebot && /srv/venv/bin/scrapy crawl quotes \
  -O /srv/data/quotes-$(date +%F).jsonl \
  -s LOG_FILE=/srv/logs/quotes-$(date +%F).log
30 1 * * * cd /srv/quotebot && /srv/venv/bin/scrapy crawl quotes -O /srv/data/quotes-$(date +\%F).jsonl

四個上線才會遇到的問題:

  1. cron 裡的 % 要跳脫(\%F),否則 cron 會把它當換行。
  2. 一定要用絕對路徑:cron 的 PATH 與工作目錄跟你手動跑不同,scrapy 直接寫會找不到。
  3. 不要把 -O 指向同一個檔名又併發跑兩個行程,會互相截斷;用「日期+時段」命名。
  4. 監控「有沒有抓完」而不是「有沒有跑」。 Scrapy 有 finish_reason(finished / closespider_* / shutdown),結束碼 0 只代表「沒爆炸」,不代表抓完整。真要監控,就在 close_spider 裡把 crawler.stats.get_stats() 寫進一個 JSON,再比對 item_scraped_count 是否落在預期區間。

五個實際踩到的坑

坑症狀解法
2.19 的 dataclass item 不能用字典賦值TypeError: 'QuoteItem' object does not support item assignment,item_scraped_count 為 0用 QuoteItem(text=..., author=...) 建構,之後賦值用 item.word_count = ...。若沿用 scrapy.Item 舊寫法就會踩到
robots.txt 404 被誤認為「被封鎖」統計出現 robotstxt/response_status_count/404404 依規範視為允許,屬正常;真正被擋是 robotstxt/forbidden
-O 蓋掉昨天的資料檔案永遠只有當天那批開發用 -O,排程用 -o 加日期檔名,或改用 feed URI 帶時間變數
選擇器抓得到、view(response) 卻是空殼瀏覽器看得到內容、Scrapy 拿到空 HTML內容是 JS 渲染的,需 scrapy-playwright 或改用 Playwright/Crawl4AI
把 DOWNLOAD_DELAY 當成萬用護身符併發 16 條同時打,還是被 WAF 封DOWNLOAD_DELAY 只管「每個 request 間隔」,要一起調 CONCURRENT_REQUESTS_PER_DOMAIN 與 AUTOTHROTTLE;再加 ROTATING_PROXY/USER_AGENT 才有意義

限制:這篇沒有做的事

  • 沒有實測 scrapy-playwright。 文中提到它是 JS 頁面的解法,但本次測試站是純 HTML,沒有驗證套件版本與設定。
  • 沒有實測分散式(Scrapy-Redis/ScrapydWeb)。 單機 5.99 秒抓 10 頁的規模用不到,硬寫只是在抄文件。
  • 沒有做壓力測試。 「能抓多少頁」取決於目標站、延遲設定、網路與被封的機率,任何「每秒 N 頁」的通用數字都不可信。
  • 數字都是單次執行結果。 5.99 秒那次含 0.5 秒下載延遲與 AutoThrottle 的動態調整,不是純解析效能。
  • quotes.toscrape.com 是練習站。 真實網站會遇到登入、反爬、動態 token、圖形驗證碼,那些不在本文範圍。

站內延伸閱讀

原始來源

📬 訂閱 most.tw 電子報

每週精選 AI 工具教學與技術乾貨,直接送到你的信箱。免費、隨時可退訂。

訂閱即表示同意收到 most.tw 電子報,隨時可一鍵退訂。

💬 有問題想討論?加 LINE 聯絡我

歡迎透過 LINE 官方帳號直接留言,我會盡快回覆你的問題。

加入 LINE 好友