通常、scrapy crawl コマンドで Spider を実行したとき、内部で例外が発生してもコマンド自体の終了コードは0(正常終了)になってしまう。このため、ログを見ないと失敗していることに気づけない。
scrapyの設定自体にはそういう機能はなさそうだったのでアレコレした。

サンプルコード

たとえばこういう “example” Spider があり、

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import scrapy

class ExampleSpider(scrapy.Spider):
    name = "example"
    
    start_urls = [
        "http://localhost:8080",
    ]

    def parse(self, response):
        print(response.url)
        raise Exception("hogehoge")

これを

1
scrapy crawl example

で実行した場合、ログを見れば例外が発生していることがわかるが、終了コードは 0 になっている。

エラー時に終了コードを設定する方法

以下の方針でエラー時に終了コードを設定できる。

  • 直接 scrapy crawl で起動するのではなく、普通の Python スクリプトから Spider を呼び出す
  • Spider 内で問題が発生したとき、CloseSpider 例外を投げて Spider を終了する。ここで終了理由を設定する
  • scrapy.signalmanager.dispatcher を使って Spider の終了イベントをハンドルする。終了理由を確認し、異常終了していれば終了コードを設定する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import logging
import sys
import scrapy
from scrapy.exceptions import CloseSpider
from scrapy.utils.project import get_project_settings
from scrapy.spiderloader import SpiderLoader
from scrapy.crawler import CrawlerProcess
from scrapy.signalmanager import dispatcher

logging.basicConfig()
logger = logging.getLogger()

settings = get_project_settings()
loader = SpiderLoader(settings)

class ExampleSpider(scrapy.Spider):
    name = "example"
    
    start_urls = [
        "http://localhost:8080",
    ]

    def parse(self, response):
        print(response.url)
        # エラー時に CloseSpider を投げる(終了するけど…)
        raise CloseSpider("hogehoge")

# サンプルコードなので横着して一緒に書いてるけど、ここから下は main.py とかを作ってそこに書くのがいいと思う。
def run_spider(spider_name, args):
    spider = loader.load(spider_name)
    exit_code = None
    def _spider_closed(spider, reason):
        nonlocal exit_code
        # 正常終了の場合、reason は finished。
        # CloseSpider で終了した場合、reason は CloseSpider のコンストラクタに渡した値。
        if reason != "finished":
            exit_code = 1
        else:
            exit_code = 0
    dispatcher.connect(_spider_closed, signal=scrapy.signals.spider_closed)
    # CrawlerProcessの第2引数(install_root_handler)を False にしておかないと logging の設定が引き継がれない
    process = CrawlerProcess(settings, False)
    process.crawl(spider, **args)
    process.start()
    return exit_code

if __name__ == "__main__":
    exit_code = run_spider("example", {})
    sys.exit(exit_code)

こんな感じにして、スクリプトを直接実行

1
python app/spiders/ExampleSpider.py

すると、ちゃんと終了コードが 1 になる。