Scrapyの入門記事の多くは、スパイダーの起点を start_requests() で書いています。このメソッドは2026年5月の2.16.0で削除され、呼ばれなくなりました。例外も警告も出ないため、start_requests() だけを定義したスパイダーは0リクエストで「正常終了」します。本記事は2026年9月10日公開の2.19.0を基準に、インストールからパイプライン、Scrapy Cloudへのデプロイまでを、Docker上のScrapy 2.19.0で動かしたコードで説明します。
まとめ
- 現行版は2.19.0(2026年9月10日)。対応PythonはPython 3.10〜3.15です。
- スパイダーの起点は
async def start()。旧start_requests()は2.16.0で削除され、定義しても無視されます。 - パイプラインは
process_item(self, item)と書きます。spider引数付きの書き方は2.14.0から非推奨の警告が出ます。 - 2.19.0では、実行中のクロールにHTTPでコードを送れる
RemoteControl拡張が既定で有効です。待ち受けは127.0.0.1だけですが、不要ならREMOTE_CONTROL_ENABLED=Falseで止めます。 - Scrapy Cloudの最新スタックは
scrapy:2.18-20260824(Scrapy 2.18.0・Python 3.14.5)で、2.19の機能はまだ使えません。デプロイする場合は、ローカルの版もスタックに合わせます。 - Scrapy Cloudの無料枠は同時実行1ジョブ・1ジョブ最大1時間・データ保持最大7日で、管理画面の定期実行は利用できません。有料は1ユニット月9ドルからです。
Scrapyの担当範囲とライブラリの選び方
Scrapyは、Python製のクローリング・スクレイピングフレームワークです。リンクをたどってページを集める「クローリング」と、HTMLから値を抜き出す「スクレイピング」を1つの枠組みで扱います。リクエストのスケジューリング、同時実行数の制御、リトライ、重複URLの除外、ファイル出力までが組み込まれている点が、HTMLパーサ単体との違いです。両者の線引きやAPI利用との使い分け、法務面の判断はスクレイピングとは?クローリング・APIとの違いと実装・法務の判断を解説で扱っています。
| 選択肢 | 向く規模 | JS描画 | 待機・リトライ | 導入の重さ |
|---|---|---|---|---|
| Scrapy | 数百ページ以上 | 不可(別手段と連携) | 組み込み | プロジェクト構成が必要 |
| requests+BeautifulSoup | 数十ページ | 不可 | 自作 | スクリプト1本 |
| Selenium/Playwright | 数十〜数百ページ | 可 | 自作 | ブラウザ実体が必要 |
ページ数よりも、待機間隔やリトライを自分で書きたいかどうかで決めます。書きたくないならScrapy、単発の取得ならパーサ単体で足ります。パーサ側はBeautifulSoupとは?pip installと使い方(find・select)・文字化け対策を解説【bs4 4.15】、JavaScriptで描画されるページはPlaywright MCPとは?設定・使い方からDify連携まで実務ガイド【2026年版】が参考になります。Scrapyはブラウザを内蔵しないため、描画後のDOMが必要なサイトでは別の手段と組み合わせます。
動作環境と2.13〜2.19の変更点
版ごとの対応Pythonと主な変更
| 版 | 公開日 | Python | 主な変更 |
|---|---|---|---|
| 2.13.0 | 2025-05-08 | 3.9〜 | start()導入/asyncioリアクタ既定化 |
| 2.14.0 | 2026-01-05 | 3.10〜 | 3.9とPyPy 3.10を切り捨て |
| 2.16.0 | 2026-05-19 | 3.10〜3.14 | start_requests()を削除 |
| 2.17.0 | 2026-07-07 | 3.10〜3.14 | s3://を既定でHTTPS化 |
| 2.18.0 | 2026-08-20 | 3.10〜3.14 | brotli・zstd必須化 |
| 2.19.0 | 2026-09-10 | 3.10〜3.15 | RemoteControl拡張 |
2.19.0のパッケージ定義は requires_python >=3.10 です。Python 3.9の環境で pip install scrapy を実行すると2.13系までしか解決しません。古い解説どおりに進めてインストールが通らないときは、まず手元のPythonのマイナーバージョンを確認してください。版ごとの変更の全文はScrapy公式のリリースノートにあります。
2.18・2.19で変わった既定の挙動
2.18.0では、BrotliとZstandardの展開が必須の依存になりました。リクエストの Accept-Encoding に br と zstd が常に入るため、以前と違う圧縮形式のレスポンスが返ってくるサイトがあります。同じ版で、スパイダー属性の download_delay が非推奨になりました。待機は DOWNLOAD_DELAY 設定か、ドメイン別の DOWNLOAD_SLOTS で指定します。start() の中で例外が出たときに、終了理由が finished ではなく start_error になったのも2.18.0からです。
2.19.0では RANDOMIZE_DOWNLOAD_DELAY が非推奨になり、揺らぎの幅を数値で指定する DOWNLOAD_DELAY_JITTER(既定0.5=±50%)に置き換わりました。旧設定を書くと ScrapyDeprecationWarning が出ることを2.19.0で確認しています。FEEDS 設定では、format を省くと出力先の拡張子から形式が推定されるようになりました。
RemoteControl拡張と公式MCPサーバー
2.19.0で運用時に確認したい変更の一つが、既定で有効な RemoteControl 拡張です。クロール中のプロセスにHTTPで接続し、統計の確認やコードの実行ができます。2.19.0のソースでは、待ち受けは 127.0.0.1 のランダムなポートで、Bearerトークンによる認証が必要です。トークンとポートはジョブファイルに書き出されます。実行すると、ログに次の1行が出ます。
[scrapy.extensions.remote_control] INFO: Remote control HTTP server listening on port 42297 (job 19-57dc5568f92f4a75ab2ee2d6eacc0f38)
外部から直接は届きませんが、同じホストの別ユーザーがジョブファイルを読めれば、クロール中のプロセスでコードを実行できます。共用サーバーや、複数の利用者が同じ環境で動かすコンテナでは、REMOTE_CONTROL_ENABLED = False を settings.py に書いて止めておくのが安全です。ジョブファイルの置き場所は REMOTE_CONTROL_JOBS_DIR で変えられ、既定は platformdirs のユーザー状態ディレクトリ配下です。
この拡張の使い道は、コーディングエージェントからの調査です。公式は2.19.0で、エージェント向けのプラグイン(scrapy/scrapy-agent-plugin)と、実行中のクロールにつなぐScrapy MCPサーバーを案内しています。統計の推移を見て「リクエストは出ているのにアイテムが出ない」原因を探す、といった用途です。MCPそのものの仕組みはMCPとは?AIと外部ツールをつなぐ標準規格とMCPサーバーの仕組み・作り方を解説を参照してください。
2.17未満に残る既知の脆弱性
2.14.2(2026年3月12日)で修正された GHSA-cwxj-rr6w-m6w7 は深刻度「高」で、影響範囲は1.4.0〜2.14.1です。RefererMiddleware がレスポンスの Referrer-Policy ヘッダを呼び出し可能オブジェクトのパスとして解釈し、クロール先のサーバーが任意のモジュールを読み込ませることができました。もう1件の GHSA-76g3-c3x4-crvx も深刻度「高」で、2.16以下の S3DownloadHandler が署名済みのS3リクエストを平文HTTPで送っていました。どちらもScrapyのセキュリティアドバイザリに掲載されています。版を固定するなら2.17.0以上にしてください。
インストールからスパイダー実行までの手順
インストールとプロジェクト生成
pip install scrapy
scrapy startproject quotes_crawler
cd quotes_crawler
startproject は scrapy.cfg、settings.py、items.py、pipelines.py、spiders ディレクトリを作ります。このとき作られる settings.py はフレームワークの既定値より抑制的な値を持つため(後述)、練習でもプロジェクトを作ってから始めるほうが安全です。
genspiderでのスパイダー雛形作成
scrapy genspider quotes quotes.toscrape.com
2.19.0で生成されるのは、name、allowed_domains、start_urls、空の parse だけの最小構成です。start_urls を書けば、Spider.start の既定実装がそこからリクエストを作るため、起点のコードは不要です。allowed_domains に無いドメインへのリクエストは Filtered offsite request というログとともに捨てられ、統計の offsite/filtered に件数が出ます。練習用には公式チュートリアルと同じ quotes.toscrape.com を使います。練習用サイトにもリクエスト処理の負荷は生じるため、待機間隔と同時実行数を抑えて利用します。2.19.0からは -t にテンプレートファイルのパスも渡せます。
parseメソッドの実装とページ送り
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ["https://quotes.toscrape.com/page/1/"]
def parse(self, response):
for card in response.css("div.quote"):
yield {
"text": card.css("span.text::text").get(),
"author": card.css("small.author::text").get(),
"tags": card.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
parse で辞書を yield すればアイテムになり、items.py の定義は必須ではありません。response.follow は相対URLを解決するので、urljoin は要りません。値が取れないと .get() は None を返してそのまま進むため、セレクタは後述のシェルで先に確かめます。
crawlの実行とファイル出力
scrapy crawl quotes -O quotes.json
scrapy crawl quotes -o quotes.jsonl
-O(大文字)は上書き、-o(小文字)は追記です。JSON形式で -o を繰り返すと、配列の後ろに次の配列が続き、1つのJSONとして読めないファイルになります。結果を貯めるなら1行1件の jsonl、毎回作り直すなら -O を使います。試しに -s CLOSESPIDER_PAGECOUNT=3 を付けて走らせると、2.19.0で上の quotes を最後まで走らせると、10ページから100件が出力され、robots.txtを含めて11リクエストでした。
scrapy shellでのセレクタ検証
scrapy shell "https://quotes.toscrape.com/page/1/"
>>> response.css("span.text::text").get()
>>> response.css("li.next a::attr(href)").get()
シェルはレスポンスを保持したまま対話的にセレクタを試せるので、スパイダーの書き換えと再実行の往復がなくなります。ブラウザの開発者ツールに見えるのはJavaScript実行後のDOMで、Scrapyが受け取るのはHTMLそのものです。開発者ツールからコピーしたセレクタが効かないときは、response.text に目的の要素があるかを先に見ます。
Item Pipelineとスパイダー引数の書き方
process_itemでの検証と重複除去
取れた値の整形・検証・保存はパイプラインに分けます。pipelines.py に書き、settings.py の ITEM_PIPELINES で有効にします。数字が小さいほど先に実行されます。
# pipelines.py
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
class CleanQuotesPipeline:
def open_spider(self):
self.seen = set()
def process_item(self, item):
adapter = ItemAdapter(item)
text = (adapter.get("text") or "").strip("“”")
if not text:
raise DropItem("text が空")
if text in self.seen:
raise DropItem(f"重複: {text[:20]}")
self.seen.add(text)
adapter["text"] = text
return item
# settings.py
ITEM_PIPELINES = {
"quotes_crawler.pipelines.CleanQuotesPipeline": 300,
}
既存の解説の多くは process_item(self, item, spider) と書いています。2.19.0でこの形のまま動かすと、open_spider() と process_item() のそれぞれに「spider引数は将来渡されなくなる」という ScrapyDeprecationWarning が出ました。この警告は2.14.0から出ており、2.17.0では startproject の雛形からも spider 引数が消えています。スパイダーの情報が必要なら、from_crawler() で受け取った crawler を保持し、その spider 属性から読みます。
-aオプションによる起点URLの切り替え
起点をコマンドラインから変えたいときは、スパイダー引数(-a)を受け取り、start() でリクエストを作ります。
import scrapy
class ByTagSpider(scrapy.Spider):
name = "bytag"
allowed_domains = ["quotes.toscrape.com"]
def __init__(self, tag="love", *args, **kwargs):
super().__init__(*args, **kwargs)
self.tag = tag
async def start(self):
yield scrapy.Request(f"https://quotes.toscrape.com/tag/{self.tag}/")
def parse(self, response):
for card in response.css("div.quote"):
yield {
"text": card.css("span.text::text").get(),
"author": card.css("small.author::text").get(),
"tags": card.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
scrapy crawl bytag -a tag=humor -O humor.jsonl
2.19.0でこのコマンドを実行すると、humorタグの12件が出力されました。-a の値は文字列で渡るので、数値として使うなら int() で変換します。
start()への移行と2.16での削除
Spider.start は2.13で追加された非同期ジェネレータで、既定の実装は start_urls を順に読んでリクエストを作ります。start_urls だけを書くスパイダーは何も変えずに動きます。書き換えが要るのは、起点でPOSTを投げる、ヘッダやクッキーを付ける、DBから対象URLを読むなど、start_requests() を自分で定義していた場合です。
# 2.16.0で削除された書き方
def start_requests(self):
yield scrapy.Request("https://quotes.toscrape.com/page/1/")
# 2.13以降の書き方
async def start(self):
yield scrapy.Request("https://quotes.toscrape.com/page/1/")
上のようにリクエストを直接yieldする例では、def を async def に変え、メソッド名を差し替えれば移行できます。問題は削除後の挙動です。start_requests() だけを定義したスパイダーを2.19.0で実行すると、WARNINGは1行も出ず、ダウンロードは0件、終了理由は finished でした。2.13より前の版でも動かす必要があるなら、start() と start_requests() を両方定義します。2.16以降では後者が無視されるだけです。
2.13では TWISTED_REACTOR の既定が AsyncioSelectorReactor になり、asyncio前提のライブラリを設定なしで await できるようになりました。一方、サードパーティのスパイダーミドルウェアのうち同期版 process_spider_output() しか持たないものは2.16.0で動かなくなっています。外部プラグインを使っている場合は、対応状況を確かめてから版を上げてください。
settings.pyの既定値と雛形の差
| 設定キー | フレームワークの値 | startproject雛形の値 |
|---|---|---|
| ROBOTSTXT_OBEY | False | True |
| DOWNLOAD_DELAY | 0 | 1 |
| CONCURRENT_REQUESTS_PER_DOMAIN | 8 | 1 |
| FEED_EXPORT_ENCODING | None | utf-8 |
| DOWNLOAD_DELAY_JITTER | 0.5 | (指定なし) |
| REMOTE_CONTROL_ENABLED | True | (指定なし) |
公式ドキュメントは「startproject が設定する値をDefault、scrapy.settings.default_settings の値をfallback」として記載しています。ドキュメントの「Default」は、プロジェクト外で動かしたときの値と一致しないことがあります。プロジェクトの外で、個別の上書きをしない1ファイルのスパイダーを scrapy runspider で動かすと、ROBOTSTXT_OBEY はFalse、DOWNLOAD_DELAY は0、FEED_EXPORT_ENCODING は None でした。robots.txtを読まず、待機なしで連続してリクエストを送る状態です。robots.txtの書き方と効く範囲はrobots.txtとは?クロール制御の書き方と効く場面・不要な場面【2026年時点】で整理しています。
FEED_EXPORT_ENCODING が None のままだと、JSON系の出力で日本語が \u65e5\u672c\u8a9e のような数値エスケープになります。2.19.0で確かめたところ、.json だけでなく .jsonl でも同じでした。プロジェクト外で動かすときは -s FEED_EXPORT_ENCODING=utf-8 を付けます。なお RETRY_TIMES の既定2は「初回+再試行2回=計3リクエスト」の意味です。
Scrapy Cloudへのデプロイ手順と料金
shubでのログインとデプロイ
pip install shub
shub login
shub deploy
shub はScrapy Cloudのコマンドラインクライアントで、2026年10月時点の最新は2.18.1(2026年7月8日公開・Python 3.10以上)です。shub login はAPIキーを ~/.scrapinghub.yml に保存します。CIでは対話入力を避けて SHUB_APIKEY 環境変数を使います。初回の shub deploy ではウィザードが起動し、プロジェクトIDを scrapy.cfg と同じ階層の scrapinghub.yml に保存します。
# scrapinghub.yml
projects:
default: 12345
stack: scrapy:2.18-20260824
requirements:
file: requirements.txt
実行環境は stack で固定します。2026年10月26日時点で公開されている最新のスタックは scrapy:2.18-20260824 で、中身はScrapy 2.18.0とPython 3.14.5です。2.19.0のスタックはまだありません。ローカルを2.19.0で開発して DOWNLOAD_DELAY_JITTER を書いても、クラウド側の2.18.0では使えません。デプロイ前提のプロジェクトは、ローカルも pip install "scrapy==2.18.0" で合わせておくと、手元とクラウドで結果が食い違いません。追加のパッケージは requirements.txt に書き、requirements の file で指定します(shubの設定ドキュメント)。
ジョブの実行とログ・アイテムの取得
shub schedule quotes
shub log -f 2/34
shub items -f 2/34
shub schedule にスパイダー名を渡すと、クラウドでジョブが走ります。ジョブIDは「プロジェクトID/スパイダーID/ジョブID」の形式で、scrapinghub.yml に既定のプロジェクトがあれば 2/34 のように省略できます。-f を付けると、実行中のログとアイテムを追い続けます。ローカルの scrapy crawl と出力ファイルの確認が、そのまま shub schedule と shub items に置き換わります。Zyteのサポート記事によれば、管理画面の定期実行(Periodic Jobs)は2017年10月10日から有料プラン限定です。
無料プランと有料プランの分かれ目
| 項目 | 無料 | 有料(1ユニット月9ドル) |
|---|---|---|
| ジョブ実行時間 | 最大1時間 | 制限なし |
| データ保持 | 7日 | 最大120日 |
| リソース | 通常ユニットの半分 | RAM 1GB/ディスク2.5GB |
| 同時実行 | 1本 | 保有ユニット数まで |
| 定期実行 | 不可 | 可 |
| カスタムDockerイメージ | 不可 | 可 |
境目はリクエスト数ではなく、1ジョブの時間と定期実行の可否です。Zyteの料金ドキュメントでは、無料枠に付くのは通常ユニットの半分のリソースを持つlow-resource unitで、有料は1ユニット月9ドルです。無料枠は1時間でジョブが打ち切られるので、ページ数と DOWNLOAD_DELAY から所要時間を先に見積もります。1秒間隔で1時間送り続けても単純計算で3,600リクエストで、応答時間・再試行・robots.txtの取得を差し引くと、完了するページ数はそれより少なくなります。
Scrapy Cloud・Scrapyd・cronの選び分け
| 運用先 | 定期実行 | ログ・結果の閲覧 | 費用 |
|---|---|---|---|
| Scrapy Cloud | 有料プランで管理画面から | Web画面・shub | 月9ドル/ユニット |
| Scrapyd(自前サーバー) | なし(cronからAPIを叩く) | HTTP API・簡易画面 | サーバー代のみ |
| cron+scrapy crawl | cronで設定 | ファイルとログを自前で管理 | サーバー代のみ |
Scrapy Cloudを選ぶ理由は、ジョブ履歴・ログ・アイテムの保存と閲覧を作らずに済む点に尽きます。スパイダーが1〜2本で、結果を毎回DBやS3に書き出すなら、cronから scrapy crawl を叩くだけで足ります。スパイダーが増えて「どれがいつ失敗したか」を追うのがつらくなったら、Scrapy Cloudに移す時期です。Scrapydは中間の選択肢で、最新の1.6.0(2025年7月22日)は既定で 127.0.0.1:6800 にHTTP APIを開き、schedule.json でジョブを登録します。ただし定期実行の機能は持たないため、cronから curl http://localhost:6800/schedule.json -d project=myproject -d spider=quotes を叩く形になります。
Scrapy Cloudに向かないのは、クロール対象が社内ネットワークにある場合と、取得データを社外のクラウドに置けない場合です。どちらもデータの置き場所の問題なので、自前のサーバーで動かします。収集の仕組みを業務システムに組み込む場合は、フルスクラッチ開発で収集・保存・画面までまとめて設計する方法もあります。
つまずきやすいエラーの切り分け
- アイテムが0件で、ログにも統計にもダウンロードが無い:
start_requests()だけで起点を書いていないかを見ます。2.16以降は無視されます。 - アイテムが0件で、ログに
Forbidden by robots.txt:がある:雛形のROBOTSTXT_OBEY=Trueによるスキップで、リクエスト自体が送られていません。 - 統計に
offsite/filteredが出ている:たどったリンクがallowed_domainsの外です。サブドメインを含めるなら親ドメインを書きます。 - 要素が
Noneになる:JavaScript描画が原因か、セレクタの誤りかをscrapy shellのresponse.textで切り分けます。 - 2.18.0に上げたら
crawler.statsを読む行でRuntimeError:クロール開始前にstatsなどを読むと、Noneではなく例外になりました。if crawler.stats:のような判定は書き換えます。 - POSTの後でフォームの値が消える:2.14.2以降はPOSTへの301リダイレクトがGETに変わります。
scrapy.FormRequestは2.16.0で非推奨になりましたが、2.17.0でクラス自体の非推奨は撤回されました。引き続き非推奨なのはfrom_response()で、form2requestへの移行が案内されています。
ログレベルの既定はDEBUGで、雛形も上書きしません。どこで止まっているか分からないときはまずログを読み、多すぎれば scrapy crawl quotes -L INFO で絞ります。
よくある質問
Scrapy Cloudの無料プランだけで定期クロールを回せますか?
管理画面の定期実行は有料プラン限定なので、無料のままでは設定できません。外部のcronやCIから、SHUB_APIKEY を渡して shub schedule を実行すればジョブは起動できます。ただし1ジョブ1時間・データ保持7日の制限は残るため、日次で少数のページを巡回し、その都度結果を取り出す用途に限られます。
Scrapy CloudでScrapy 2.19を使えますか?
2026年10月26日時点の最新スタックは scrapy:2.18-20260824 で、2.19.0のスタックは公開されていません。RemoteControl 拡張や DOWNLOAD_DELAY_JITTER などの2.19の機能は、2.18スタックでは使えません。有料プランならカスタムDockerイメージをデプロイできるので、2.19の実行環境を自分で用意する方法はあります。スタックの一覧は更新されるので、デプロイ前に公開されているタグを確認してください。
Python 3.9でScrapyを動かせますか?
2.14.0でPython 3.9のサポートが切れたため、3.9では pip install "scrapy<2.14" で2.13系に固定するしかありません。shub も2.16.0以下に揃えます。ただし2.13系は、上で挙げた深刻度「高」の脆弱性2件の両方に該当します。Pythonを3.10以上に上げるのが先です。
ScrapyとBeautifulSoupは併用できますか?
parse の中で BeautifulSoup(response.text, "html.parser") を呼べば併用できます。ただしScrapyには response.css と response.xpath があるため、抽出の書き方が2系統になるだけです。既存のBeautifulSoupのコードを流用する移行期を除き、Scrapyのセレクタに寄せるほうが保守は楽です。
robots.txtを無視する設定にしても問題ありませんか?
ROBOTSTXT_OBEY=False はいつでも設定できますが、技術的にできることと許されることは別です。サイトの利用規約、アクセス頻度による業務妨害、取得データの利用目的は、それぞれ別の論点です。判断の枠組みはスクレイピングとは?クローリング・APIとの違いと実装・法務の判断を解説で整理しています。Python以外で書く場合はGo言語のCollyの運用設定|robots.txt・レート制限・文字化けを実測で解説も比較材料になります。