2019

5

2

Pythonでスクレイピングフレームワークのscrapy入門してやんよ!!!

タグ:

Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

「クローラーで一気にデータ集めといて!」なんて気軽に言われてwebクローラーのための
スクレイピングツール開発と一言に言っても実際は結構なロジックとモジュールなどを組み合わせて作り上げる必要があって、意外とゼロから構築するには結構な工数がかかってしまいます。

  • requestsモジュールでサイトにhttpでアクセス&ページのダウンロード
  • seleniumなどUIテストツールで自動的にwebページ内を操作・遷移
  • BeautifulSoupなどのライブラリでソースコードを取得して必要な情報をパース・整形
  • MySQLなどのデータベースまたはファイルに取得したデータを書き込んで保存
  • cronでコードの自動実行日時スケジュールを登録

Scrapy」はこの辺のWEBスクレイピング周りのアーキテクチャをいっぺんにまとめてくれたwebスクレイピングに特化したアプリケーションフレームワークです。こりゃ使うしかない!

①Scrapyのインストール

# pipでインストールする場合
$ pip install Scrapy

ローカルマシン内にインストールする場合は既にインストールされているPythonシステムパッケージ(システムツールやスクリプトの中には壊れる可能性がある)と衝突しないようにvirtualenvの仮想環境内にインストールが推奨されています。 その辺は各自で導入してください。

Dockerで構築する場合

Docker Desktopなどが利用できる場合はこちらの方が気楽にいじれるので、コンテナ内にpythonとscrapyをインストール済みの環境を作成します。

# Dockerfile
FROM python:3.9
USER root

RUN apt-get update && \
    apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 && \
    mkdir crawl

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9

# 開発ディレクトリ設定
ENV WORKDIR /usr/src/
WORKDIR ${WORKDIR}

# python package
RUN pip install --upgrade pip && \
    pip install scrapy

# コンテナにアクセスできるようにサーバー起動
CMD ["python3" "-m" "http.server" "8001"]

②Scrapyで新規プロジェクトを作成

scrapyをインストールできたら以下のコマンド実行するとディレクトリ構造が自動生成されます。

# 新規プロジェクトを作成
$ scrapy startproject crawl_app

生成ファイル一覧

「crawl_app」
  ┣━ 「scrapy.cfg」   # デプロイ用の構成ファイル
  ┗━ 「crawl_app」    # プロジェクトソースディレクトリ
     ┗━ 「__init__.py」
     ┗━ 「items.py」.         # プロジェクト項目の定義ファイルです
     ┗━ 「middlewares.py」.   # プロジェクトのミドルウェア構成ファイル
     ┗━ 「pipelines.py」.   # プロジェクトのパイプライン構成ファイル
     ┗━ 「settings.py」    # プロジェクトの設定ファイル
     ┗━ 「spiders」
              ┗━ 「__init__.py」

基本的にはこの構成でファイルが生成されます。

設定ファイルの基本設定

最初に色々いじる場合は設定ファイル「settings.py」は最低限の部分以外はコメントされて機能していませんが、内容をある程度把握しておきましょう

プロパティ 概要
CONCURRENT_REQUESTS Scrapy ダウンローダーによって実行される同時 (同時) リクエストの最大数。
CONCURRENT_REQUESTS_PER_DOMAIN
単一のドメインに対して実行される同時 (同時) 要求の最大数
CONCURRENT_REQUESTS_PER_IP
単一の IP に対して実行される同時 (同時) 要求の最大数。
こちらを機能させるとCONCURRENT_REQUESTS_PER_DOMAINは無視されます。
DOWNLOAD_DELAY 同じ Webサイトから連続したページをダウンロードする前に、ダウンローダーが待機する時間 (秒単位)。
COOKIES_ENABLED
Cookie ミドルウェア有効の有無
DEFAULT_REQUEST_HEADERS
Scrapy HTTP リクエストに使用されるデフォルトのヘッダー設定
SPIDER_MIDDLEWARES
プロジェクト内で有効にするミドルウェア(取得処理中にはさむ例外処理や自作モジュールなど)設定
DOWNLOADER_MIDDLEWARES
プロジェクト内で有効にするダウンローダ (ヘッドレスブラウザツールなど)設定
EXTENSIONS
メール通知機能などの独自プラグインの読み込み設定
ITEM_PIPELINES
取得タグやパースなどのデータ整形やDB保存のためのパイプラインの設定
HTTPCACHE_ENABLED
HTTPキャッシュの有効設定
HTTPCACHE_EXPIRATION_SECS
キャッシュの有効期限
HTTPCACHE_DIR
キャッシュデータの保管ディレクトリ

上記の基本的な設定の他にもたくさんの設定が可能ですが、最初のうちはミドルウェアやパイプラインの設定などは意味不明なので
開発・検証段階の間はDOWNLOAD_DELAYHTTPCACHE_ENABLED関連のコメントを復帰させてアクセス先のサーバーに負荷をかけない設定は最初に必ずしておきましょう

将来的に考慮しておいた方が良い設定

ファイルに記述はありませんが、裏で機能している設定もあり、追記することでさらに細かい設定も可能なので、ある程度本格的な運用が始まる前には設定の有無も考慮しておきましょう。

プロパティ 概要(デフォルト値)
DOWNLOAD_TIMEOUT ダウンローダーがタイムアウトするまでに待機する合計時間(180秒)
DOWNLOAD_MAXSIZE ダウンローダがダウンロードする応答の最大サイズ(1024MB)
DOWNLOAD_WARNSIZE ダウンローダーが警告する応答のサイズを定義(32MB)
DUPEFILETER_CLASS 重複するリクエストの検出とフィルタリングに使用されるクラス( 'scrapy.dupefilters.RFPDupeFilter'
LOG_ENABLED
ロギングを有効にするかどうかを定義(True
LOG_ENCODING ロギングに使用するエンコーディングのタイプを定義( 'utf-8'
LOG_FILE ロギングの出力に使用されるファイル名(なし)
LOG_FORMAT ログメッセージのフォーマットに使用できる文字列( '%(asctime)s [%(name)s] %(levelname)s: %(message)s'
LOG_DATEFORMAT ログの日付/時刻をフォーマットできる文字列( '%Y-%m-%d %H:%M:%S'
LOG_LEVEL 最小ログ レベルを定義( `DEBUG`
AUTOTHROTTLE_ENABLED サイトからリクエストをダウンロードする間、Scrapy が自動的にクロール速度を調整する機能( 'True'

③Scrapyでスクレイピングを実行

作成されたディレクトリの「spiders」フォルダの中にqiitas_spider.py」というファイル名を新規作成して
試しにQiitaの「mysql」タグの一覧ページを2ページ分取得、
それぞれのソースコードを「qiitas-mysql_◯.html」名義でファイル保存するコードを作成

スクレイピング実行ファイルを作成

import scrapy

class QiitasSpider(scrapy.Spider):
    name = "qiitas"

    # 巡回URLを指定
    start_urls = [
        'https://qiita.com/tags/mysql/items?page=1',
        'https://qiita.com/tags/mysql/items?page=2',
    ]

    # 取得ソースをページ毎に「qiitas-mysql_◯.html」名義でファイル保存
    def parse(self, response):
        # URLからカテゴリタグ名を分離
        tag = response.url.split("/")[-2]
        # URLからページ数を分離
        page = response.url.split("/items?page=")[1]
        filename = 'qiitas-{0}_{1}.html'.format(tag, page)
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('「%s」を新規作成しました' % filename)

start_urlsにアクセスするURLを設定し、parse関数でURLを順次取得処理を行なっていきます

Scrapyをクロールを実行

# 「qiitas_spider.py」を実行
$ scrapy crawl qiitas

これをプロジェクトのトップレベルディレクトリに移動してターミナルで上記のコマンドを実行すると、作成したコードの処理が走り、「new_app」ディレクトリ内に指定したURLの2ページ分のhtmlファイルが生成されます。

Scrapyツールコマンドの種類

scrapy機能を実行できる各ツールコマンドがあります。使用可能な組み込みコマンドのリストとその説明、および使用例が含まれています。次のコマンドを実行すると、各コマンドに関する詳細情報をいつでも入手できます。
※「spider」ディレクトリで実行してください。

# 基本コマンド構文
$ scrapy <コマンド> [オプション] [引数]

# 利用可能なすべてのコマンドを表示
$ scrapy -h

# 各コマンドに関する詳細情報を表示
$ scrapy <command> -h

scrapyの利用可能なコマンド一覧

startproject(プロジェクト作成)

# 基本コマンド構文
$ scrapy startproject <プロジェクト名> [ディレクトリ]

# 「scrapy」フォルダ直下にプロジェクト名「yanyo」を新規作成
$ scrapy startproject yanyo scrapy
  • [ディレクトリ]の直下に<プロジェクト名>の新規scrapyプロジェクトを作成します。
  • [ディレクトリ]指定が無い場合は<プロジェクト名>と同じ名前のディレクトリが新規作成されます

genspider(スパイダー作成)

# 基本コマンド構文
$ scrapy genspider [-t テンプレート] <スパイダー名> <アクセス先ドメイン/URL>

# 「tokidoki-web.com」対象の基本スパイダーテンプレート「yanyo」を作成
$ scrapy genspider -t basic yanyo tokidoki-web.com

# 「tokidoki-web.com」対象のクロール用スパイダーテンプレート「yanyo」を作成
$ scrapy genspider -t crawl yanyo tokidoki-web.com

# 「tokidoki-web.com」対象のCSVフィード用のスパイダーテンプレート「yanyo」を作成
$ scrapy genspider -t csvfeed yanyo tokidoki-web.com

# 「tokidoki-web.com」対象のXMLフィード用のスパイダーテンプレート「yanyo」を作成
$ scrapy genspider -t xmlfeed yanyo tokidoki-web.com
  • ページを取得するメイン処理ファイルとなるスパイダーを作成します
  • スパイダーファイルの基本テンプレート「basic」、「crawl」、「csvfeed」、「xmlfeed」を指定する事で定義済みのテンプレートに基づいてスパイダーファイルを作成するための便利なショートカットコマンドです。
  • 「basic」以外のテンプレートを指定すると必要とされるモジュールインポートも記述されたファイルが生成されます

crawl(クロール処理の実行)

# 基本コマンド構文
$ scrapy crawl <スパイダー名>

# スパイダー名「yanyo」(yanyo.py)でクロールを実行
$ scrapy crawl yanyo
  • 作成した<スパイダー名>使ってクロール処理を実行します
  • 開発したクローラーを本番環境で実行するために主に使うであろうコマンド

check( スパイダー実行チェック)

# 基本コマンド構文
$ scrapy check [-l] <スパイダー名>

# スパイダー名「yanyo」チェックを実行します。
$ scrapy check yanyo
  • 作成したスパイダーのコード検証を行なってくれます
  • ファイル内に構文ミスなどがある場合はエラーを吐き出してくれます

fetch(ページ情報の読み込み)

# 基本コマンド構文
$ scrapy fetch <URL>

# 「tokidoki-web.com」のペースソースを取得
$ scrapy fetch --nolog http://tokidoki-web.com

# 「tokidoki-web.com」のレスポンスHTTPヘッダ情報を表示
$ scrapy fetch --nolog --headers http://tokidoki-web.com
  • アクセス先のレスポンス情報(htmlソースやヘッダー情報)などを確認できる
  • --no-redirectオプションを付ければリダイレクトに従わないレスポンスの取得も可能
  • --spider=<スパイダー名>オプションでURL先に指定したスパイダーロジックを強制的に実行が可能

parse(ページを解析)

# 基本コマンド構文
$ scrapy parse <URL> [オプション]

# ブラウザを開いて「http://tokidoki-web.com」にアクセスする
$ scrapy parse http://tokidoki-web.com --cbkwargs
  • fetchよりもよりシステム的な検証ができるコマンド
  • --spider=<SPIDER>オプションで特定のスパイダーを強制的に使用
  • --a NAME=VALUEオプションでスパイダー引数を設定
  • --callbackオプションで応答を解析するためのコールバックとして使用
  • --metaオプションでリクエストに渡すJSON形式のリクエストを追加。例)–meta='{“foo” : “bar”}'
  • --cbkwargsオプションでリクエストに渡すJSON形式の追加のキーワード引数を追加。例)–cbkwargs='{“foo” : “bar”}'
  • --pipelinesオプションでパイプラインを介してアイテムを処理
  • --rulesオプションでスパイダー内のルール検証
  • --noitemsオプションでスクレイピングされたアイテムはログ非表示
  • --nolinksオプションで抽出されたリンクをログ非表示
  • --depthオプションで再帰的に追跡する深さレベル を設定
  • --verboseオプションで各深度レベルの情報を表示
  • --outputオプションでスクレイピングされたアイテムをファイルにダンプ(※下記にて詳細)

shell

シェルコマンドにて簡易的にscrapyを実行できるコマンド(※下記にて解説)

その他のコマンド一覧

listspiderディレクトリ内のプロジェクト一覧を表示します。
edit指定したスパイダーファイルを設定されているエディタを使って編集します。
viewブラウザを開いて指定URLにアクセスを実行します。
settingsプロジェクト内のディレクトリで実行されると、プロジェクト設定値が表示されます。
runspider個別のPythonファイルに自己完結型のspiderを実行します。
versionScrapyバージョン情報を表示します。
bench簡単なベンチマークテストを実行します。

Scrapyシェルの実行

実行ファイルにスクレイピングコードを書き込まずとも、scrapyではそのままコマンドラインからScrapyシェルを実行してデータを抽出することができます。

# qiitaのトップページのソースコードを取得
$ scrapy shell "https://qiita.com/"

まずはshellコマンドライン上でqiitaにアクセスします。
このコマンドを実行すると指定URLのページ構成を取得してresponseオブジェクトに格納されます。
抽出したい各データはこのresponseオブジェクトから操作を行います。

レスポンスオブジェクトを直接操作

# 取得先の実際のHTMLソースをブラウザに表示
>>> view(response)

HTMLソースが直接表示されますが、ログが長くなるので確認しなくても大丈夫です。

レスポンスオブジェクトをCSSセレクターで抽出・操作

# ページtitleのセレクターを取得
>>> response.css('title')
--> [<Selector xpath='descendant-or-self::title' data='<title>Qiita</title>'>]

# ページtitleのセレクターのテキストを抽出
>>> response.css('title::text').get()
--> 'Qiita'

# ページtitleのセレクターのテキストから正規表現で"i"以降の文字列を抽出
>>> response.css('title::text').re(r'i\w+')
--> 'iita'

responseにHTMLソースが入っている状態なので、CSSセレクターにタグを指定することで、jQueryに慣れてる人は容易にデータを抽出できます。 pythonモジュールもそのまま使用できるので、抽出タグにチェーンで正規表現をかけるなどの記述も可能です

レスポンスオブジェクトをXPathセレクターで抽出・操作

# ページtitleのセレクターを取得
>>> response.xpath('//title')
--> [<Selector xpath='//title' data='<title>Qiita</title>'>]

# ページtitleのセレクターからさらに内部のテキストを抽出
>>> response.xpath('//title/text()').get()
--> 'Qiita'

CSSセレクターと同じような記述ですが、こちらはXMLファイルでも抽出が可能な方法なのでXML構造の操作に慣れてる方はこちらの方が扱いやすいかもしれません。

ニュース一覧のタイトルを取得

ScrapyシェルコマンドのみでLivedoorのトピック一覧のタイトルを取得してみます

# Livedoorのサイト構成を取得
$ scrapy shell 'http://www.livedoor.com/'

# トピックスの要素タグを取得
>>> topics = response.css("div#newstopicsbox")[0]

# 各ニュースのliタグ内のaタグのテキストをすべて取得
>>> title = topics.css("ol > li > a::text").getall()

# タイトルをリスト出力
>>> title
--> ['裁判官1人で判決「手続き違法」', '「家族殺す」元交際相手連れ回す', '事故の女 衝突音で車に気づいた', '園児事故 相手が止まらずと説明', '男が火のついたタバコをポストに', 'クレカ流出 日本人の情報は高値', '雅子さま1人にせず 陛下の気遣い', '水ダウ SNSでタバコめぐり議論に', '五輪チケット VISAしか使えぬ訳', '木嶋死刑囚と結婚 夫が思い語る']

これだけでも通常のrequestsモジュールでhttpアクセスやBeautifulSoupで取得ソースの抽出などを用いて実装する工程が、かなり短いコードで簡単に再現できました。しゅ・・・しゅごい

クロールした取得データをJSONファイルに保存

だいたい掴めてきたので実践的なコードを作ってみます。
キータの「scrapy」タグページを対象としてエントリーの一覧情報から「タイトル」「筆者名」「記事の登録タグ」「ページURL」を取得するコードを作成します。


※「scrapy」タグ自体はエントリー数がかなり少ないですが、メジャーなタグ一覧ページを指定してしまうと終わらない戦いになるのでやめましょう

スクレイピング実行ファイルを作成

#spiders/json_spider.py
import scrapy

class JsonSpider(scrapy.Spider):
    name = "json"

    # qiita の「scrapy」タグ一覧ページURLを指定
    start_urls = ['https://qiita.com/tags/scrapy/items?page=1']

    def parse(self, response):
        # エントリー一覧から「タイトル」「筆者名」「記事の登録タグ」「ページURL」を取得
        for quote in response.css('article.tsf-Article'):
            yield {
                'title': quote.css('div.tsf-ArticleBody > a::text').get(),
                'author': quote.css('div.tsf-ArticleBody > div > span > a::text').get(),
                'tag': quote.css('div.tsf-ArticleBody > div > div > a::text').getall(),
                'link': quote.css('div.tsf-ArticleBody > a::attr(href)').get(),
            }

        # ページネーションから次のページへのリンクを取得
        for a in response.css('li.st-Pager_next > a'):
            yield response.follow(a, callback=self.parse)

記事一覧ページから「投稿タイトル」「投稿者」「タグ」「投稿URL」を取得し
ページネーションにあるリンクを再起的に巡回していくスパイダーとなります。

コマンド実行して取得データをJSONファイルに保存する

「json」を実行しつつ、取得データをフィードエクスポート機能を使って「scrapy.json」という名前でJSONファイルに保存することができます。
この機能を使うと「JSON」「JSONライン」「CSV」「XML」形式にエクスポートしてくれます!超便利!

# スパイダー名「json」を実行して取得データを「scrapy.json」に保存
$ scrapy crawl json -o scrapy.json

実行コマンドで出力ファイル名を動的に設定する場合

将来的にクローラーを開発する場合、やはり毎日勝手に動いてくれるのが理想なので、クロールの実行コマンドに動的な値を設定して、その値を出力ファイル名にできたら楽です

# ファイル名の先頭に当日の日付を設定して実行 → 「20190502_data.json」
$ scrapy crawl mysite -o `date +\%Y\%m\%d`_data.json --nolog

自動で実行させる場合は、ターミナルに無駄なログを出力させる必要もないので--nologオプションも付けときます

まとめ

scrapyについておおまかな解説をしました。

webスクレイピングに特化したFWですが、スクレイピング自体が常に著作権問題がつきまとうデリケートな手段なのであくまで自己責任でお願いします。

Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

トップへ