Scrapyを使用したクローラ開発概要

ツイート
2021年07月15日
2021年07月22日

要件

  • Dokcerを使用して、Scrapy環境を作ること。
  • 指定したURLから、記事(コンテンツ)を取得できること。
  • robots.txtからsitemap.xmlを取得する
  • 相手サーバの負荷の考慮や法律を守ること。
  • サーバにデプロイして、定期実行すること。
  • 収集したデータは、クレンジングし、FireStoreに保存すること。

概要

Spider

クロール対象のサイトへのリクエスト、レスポンスのパース処理を記述します どのようにサイトを辿って、ページの内容をどうパースするかのロジックが Spider に書かれます

Items

クロール対象のデータから抽出したいデータ構造を記述するモデルのようなものです 目的に応じて自由な構造を定義できます Items は Spider で生成され Pipeline に渡されます

Pipeline

Spider より渡された Items に対する処理を記述します DB への保存、ファイル出力など目的に応じて自由に処理を記述できます

ログ確認

import logging logging.error("Some error")

最初にやったほうが良いこと

scrapy startproject tutorial
  • 新しいScrapyプロジェクトの作成
  • スパイダー(spider) を作成してサイトをクロールし、データを抽出します。
  • コマンドラインを使用してスクレイピングされたデータをエクスポートする。
  • 再帰的にリンクをたどるようにスパイダーを変更する。
  • スパイダー引数の使用

処理の順番

  • 1.ドメインのrobots.txtを見つける
  • 2.robots.txtからsitemap.xmlを見つける
  • 3.sitemap.xmlから各コンテンツページに行く
  • 4.コンテンツページのmetatagを取得する
  • 5.metatagをクレンジングし、 itemsに変換する
  • 6.itemsをpipelineに渡し、FireStoreに保存する(Websiteがない場合は、Websiteも登録する)

Spiderの実行

Pythonファイルの実行または、CLIの実行どちらからでも実行できる。

sh
scrapy crawl {SPIDER_NAME} # 実行 scrapy crawl {SPIDER_NAME} -o results/example.csv # 実行 & csv出力

Scrapyの中でBeautifulSoupを使う

python
from bs4 import BeautifulSoup class WebsiteSpider(scrapy.Spider): def parse(self, response): # BeautifulSoupを使う soup = BeautifulSoup(response.body, "lxml") ...

Metaタグの取得

python
print(response.xpath("//meta").extract())
python
def parse(self, response): soup = BeautifulSoup(response.body, "lxml") # MetaTagの取得 # Normal description = soup.find("meta", { "name": "description" }) title = soup.find("title").string # タグの中身を取る url = response.url print("description: %s" % (description['content'])) print("title: %s" % (title)) print("url: %s" % (url)) # OGP og_description = soup.find("meta", { "property": "og:description" }) og_title = soup.find("meta", { "property": "og:title" }) og_url = soup.find("meta", { "property": "og:url" }) og_image_url = soup.find('meta', { 'property': 'og:image' }) print("og_description: %s" % (og_description['content'])) print("og_title: %s" % (og_title['content'])) print("og_url: %s" % (og_url['content'])) print("og_image_url: %s" % (og_image_url['content']))

エラー

ERROR: Spider error processing

どこかnullだったりで落ちている。

Itemsの出力

spider.py
article = ArticleItem() article['description'] = description article['title'] = title article['url'] = url article['image_url'] = og_image_url print("article: %s" % (article)) yield article
pipeline.py
class ArticlePipeline(object): file = None def process_item(self, item, spider): print(item) return item

NOTE1: 定義したfield以外を代入すると、エラーになります。(当たり前)

NOTE2: ちなみに、クロールが長かった場合、途中で終了しても、出力設定をしていた場合、出力される。

FireStoreとの接続

py
import scrapy import firebase_admin from firebase_admin import credentials, firestore # import logging # Hack: 下記環境変数を設定しないとエラーになる import os os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = './serviceAccount.json' # 値のバリデーションチェック class ValidationPipeline(object): print('ValidationPipeline: Activate') def process_item(self, item: scrapy.Item, spider: scrapy.Spider): if item['title'] is None or item['title'] == '': raise scrapy.exceptions.DropItem('Missing value: title') if item['url'] is None or item['url'] == '': raise scrapy.exceptions.DropItem('Missing value: url') return item class ArticlePipeline(object): file = None def __init__(self): cred = credentials.Certificate('./serviceAccount.json') firebase_admin.initialize_app(cred) def open_spider(self, spider: scrapy.Spider): print('ArticlePipeline.open_spider: Activate') def close_spider(self, spider: scrapy.Spider): print('ArticlePipeline.close_spider: Activate') def process_item(self, item: scrapy.Item, spider: scrapy.Spider): print(item['title']) db = firestore.Client() # NOTE: document() 指定なしだとランダムにIDが生成される doc_ref = db.collection(u'articles').document() doc_ref.set({ u'description': item['description'], u'image_url': item['image_url'], u'title': item['title'], u'url': item['url'], }) return item

ValidationPipeline

  • None: dictのkeyがない場合、終了
  • '': 空文字の場合、終了

Pythonで、Noneは、nullと同じ使われた方をする

pipelines.py
class ValidationPipeline(object): def process_item(self, item: scrapy.Item, spider: scrapy.Spider): if item['description'] is None: raise scrapy.exceptions.DropItem('Missing value: description') if item['title'] is None or item['title'] == '': raise scrapy.exceptions.DropItem('Missing value: title') if item['url'] is None or item['url'] == '': raise scrapy.exceptions.DropItem('Missing value: url') if item['image_url'] is None: raise scrapy.exceptions.DropItem('Missing value: image_url') return item
settings.py
ITEM_PIPELINES = { 'crawler.pipelines.ValidationPipeline': 100, 'crawler.pipelines.ArticlePipeline': 300, }

設定する数字で、実行順番の制御を行っているよう。

参考