ScrapyでItemとPipelineを使う
Webのスクレイピングでお世話になっているScrapyですが、フレームワークとして非常によくできており、単体でももちろん十分に強力ですが、ここで紹介するItemとPipelineを使うと、更に便利に活用できます。
というわけで、ここでは両者のメリット・デメリットや簡単な使い方について解説します。Scrapyそのものの案内やインストールについては公式ドキュメントを参照してください。もちろんItemやPipelineの概要についても記述があります。
環境はPython 3.14.2とScrapy 2.14.1です。
ItemとPipelineとは
ItemとPipelineはいずれもScrapyでクロールを行ったWebページからレスポンスが得られると呼び出されるparse()以降で使用します。
つまりstart()でリクエストしたurlごとに、レスポンスが得られるとparse()が呼び出されるわけですが、要はそれ以降のWebページへの具体的な処理に対するヘルパーです。
このときItemに保持しておきたいデータ、Pipelineに行いたい処理を記述すると、その名の通り後は優先順位に従って数珠つなぎにPipelineが自動で実行されます。
最大のメリットはいずれも定義さえすれば、後は自動でパイプラインの呼び出しとそれに伴うデータ(アイテム)の受け渡しが行われるため、この処理にまつわるデータの状態管理や処理の遷移を書く必要がなくなることです。例えば、同様の処理をメインとなるSpiderクラスでメンバやメソッドとして追加することもできますが、その場合は恐らくselfへの参照が大幅に増加するとともに、コードは複雑になるでしょう。
また、ソースコードもItemの定義はitems.py、パイプラインの定義はpipelines.pyに分離されるため、全体の見通しもよくなります。
デメリットは、最初に説明したことの裏返しとなりますが、あくまでもparse()以降に使用される前提のため、parse()以前に使用したデータや、「次へ」のような複数のページにまたがる処理は別に対処する必要があることです。
しかしながら、多数のドメインのWebページを扱っている場合には、どうしてもJavaScriptへの対応のためにSeleniumを起動しBeautifulSoupで整形しなければならない、といったケースはよくあるかと思います。これらをSelenium起動のために処理を分岐して━━というよりは、そのためのPipelineを追加するだけの方が手軽です。
そこで以下では例として、Webからtrafilaturaで本文抽出を行い、一部のサイトではJS対応のためにSeleniumを起動する、という場合のItemとPipelineを取り上げます。ちなみに以下のコードは私が実際に使用しているものを部分的に抽出したものですので、個別のモジュールのimport文などは割愛しています。
Item作成
Itemの追加はScrapyでプロジェクトを作成すると自動生成されるitems.pyへ、参照したいデータごとにscrapy.Field()を呼び出して定義します。
// items.py url = scrapy.Field() title = scrapy.Field() body = scrapy.Field() text = scrapy.Field()
定義されたItemは辞書(ハッシュ)のようにアクセスできますので、Spiderクラスのparse()メソッドでItemの初期化を行ったら、最後にyield itemします。これで以降はPipelineに処理が移ります。逆にいうと、ItemとPipelineを使う場合、parse()はItemを初期化してyield itemするだけでよいことになります。
// spider.py def parse(self, response, title): item = SpiderNameItem() # item初期化 item['url'] = response.request.url # リダイレクト前URL item['title'] = title item['body'] = response.text # 次ページに対応する場合はここでyield response.follow()呼び出しなど yield item # Pipeline実行
なお、ここでparse()以前のデータとしてtitleを受け取っています。これはSpiderのコンストラクタで取得したデータをstart()でcb_kwargsとして個別に渡すことでItemへと引き継いでいます。
// spider.py
async def start(self):
for url in self.data.keys():
yield scrapy.Request(url=url, callback=self.parse, cb_kwargs={'title': self.data[url]['title']})
Pipeline作成
Pipelineも同様にScrapyでプロジェクトを作成すると自動生成されるpipelines.pyへ記述します。パイプラインごとのクラスを作成したらprocess_item()に処理を記述します。DBアクセスなどで前処理(DB接続など)と後処理(DB切断など)が必要な場合はopen_spider()とclose_spider()を追加できます。ちなみに古い解説ではメソッドの第3引数にspiderを指定している場合がありますが、現在では実行時に警告(WARNING)が出ますので不要です。
このとき、process_item()の最後でreturn itemが必ず必要です。これがないと次のパイプラインでitemが空となり処理が継続できません。
// pipelines.py
# Selenium起動(JSサイト)
class RunSeleniumPipeline:
def process_item(self, item):
if any(d == item['domain'] for d in self.jsDomains):
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
・・・
driver = webdriver.Chrome(options=options)
driver.get(item['url'])
body = driver.page_source
soup = BeautifulSoup(body, 'html.parser')
item['body'] = soup.get_text(separator=' ', strip=True)
driver.quit()
return item # 次のPipelineへItemを渡す
# 本文取得
class GetContentPipeline:
def process_item(self, item):
# trafilatureで本文抽出
text = trafilatura.extract(item['body'])
item['text'] = text
return item
このようにPipelineを定義したらsettings.pyに優先順位をつけてPipelineを追加します。クラス名の後の数字が小さいほど優先順位が高くなり先に実行されます。
// settings.py
ITEM_PIPELINES = {
"ss3.pipelines.RunSeleniumPipeline": 100,
"ss3.pipelines.GetContentPipeline": 300,
}
というわけで、Scrapyを使うならItemとPipelineもセットで使いましょう。私はかなり以前にScrapyを使った際、ItemとPipelineを使わず独自モジュールを追加して処理していましたが、こちらの方が明らかに見通しが良いです。また、スパイダー自身のシーケンスに対して割り込みをかけたい場合にはmiddlewares.pyが用意されています。割り込みをかける対象が異なることを除けば、ほぼ同じ要領で使えます。
