自動化

Scrapyの使い方とScrapy Cloudへのデプロイ手順【2.19対応】

Scrapyの使い方とScrapy Cloudへのデプロイ手順【2.19対応】

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・レート制限・文字化けを実測で解説も比較材料になります。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  2. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  3. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  4. 2026.11.02 テックブログ サントリーの不正アクセスで約10万人分漏えい|M365のファイル大量ダウンロードを監査ログで調べる手順
  5. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告

RELATED POSTS 関連記事

目次