タグ:scrapy
「クローラーで一気にデータ集めといて!」なんて気軽に言われてwebクローラーのための
スクレイピングツール開発と一言に言っても実際は結構なロジックとモジュールなどを組み合わせて作り上げる必要があって、意外とゼロから構築するには結構な工数がかかってしまいます。
「Scrapy
」はこの辺のWEBスクレイピング周りのアーキテクチャをいっぺんにまとめてくれたwebスクレイピングに特化したアプリケーションフレームワークです。こりゃ使うしかない!
# pipでインストールする場合
$ pip install Scrapy
ローカルマシン内にインストールする場合は既にインストールされているPythonシステムパッケージ(システムツールやスクリプトの中には壊れる可能性がある)と衝突しないようにvirtualenvの仮想環境内にインストールが推奨されています。 その辺は各自で導入してください。
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 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_DELAY
やHTTPCACHE_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' ) |
作成されたディレクトリの「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を順次取得処理を行なっていきます
# 「qiitas_spider.py」を実行
$ scrapy crawl qiitas
これをプロジェクトのトップレベルディレクトリに移動してターミナルで上記のコマンドを実行すると、作成したコードの処理が走り、「new_app」ディレクトリ内に指定したURLの2ページ分のhtmlファイルが生成されます。
scrapy機能を実行できる各ツールコマンドがあります。使用可能な組み込みコマンドのリストとその説明、および使用例が含まれています。次のコマンドを実行すると、各コマンドに関する詳細情報をいつでも入手できます。
※「spider」ディレクトリで実行してください。
# 基本コマンド構文
$ scrapy <コマンド> [オプション] [引数]
# 利用可能なすべてのコマンドを表示
$ scrapy -h
# 各コマンドに関する詳細情報を表示
$ scrapy <command> -h
# 基本コマンド構文
$ scrapy startproject <プロジェクト名> [ディレクトリ]
# 「scrapy」フォルダ直下にプロジェクト名「yanyo」を新規作成
$ scrapy startproject yanyo scrapy
# 基本コマンド構文
$ 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
# 基本コマンド構文
$ scrapy crawl <スパイダー名>
# スパイダー名「yanyo」(yanyo.py)でクロールを実行
$ scrapy crawl yanyo
# 基本コマンド構文
$ scrapy check [-l] <スパイダー名>
# スパイダー名「yanyo」チェックを実行します。
$ scrapy check yanyo
# 基本コマンド構文
$ 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
--no-redirect
オプションを付ければリダイレクトに従わないレスポンスの取得も可能--spider=<スパイダー名>
オプションでURL先に指定したスパイダーロジックを強制的に実行が可能
# 基本コマンド構文
$ scrapy parse <URL> [オプション]
# ブラウザを開いて「http://tokidoki-web.com」にアクセスする
$ scrapy parse http://tokidoki-web.com --cbkwargs
--spider=<SPIDER>
オプションで特定のスパイダーを強制的に使用--a NAME=VALUE
オプションでスパイダー引数を設定--callback
オプションで応答を解析するためのコールバックとして使用--meta
オプションでリクエストに渡すJSON形式のリクエストを追加。例)–meta='{“foo” : “bar”}'
--cbkwargs
オプションでリクエストに渡すJSON形式の追加のキーワード引数を追加。例)–cbkwargs='{“foo” : “bar”}'
--pipelines
オプションでパイプラインを介してアイテムを処理--rules
オプションでスパイダー内のルール検証--noitems
オプションでスクレイピングされたアイテムはログ非表示--nolinks
オプションで抽出されたリンクをログ非表示--depth
オプションで再帰的に追跡する深さレベル を設定--verbose
オプションで各深度レベルの情報を表示--output
オプションでスクレイピングされたアイテムをファイルにダンプ(※下記にて詳細)シェルコマンドにて簡易的にscrapyを実行できるコマンド(※下記にて解説)
list | spiderディレクトリ内のプロジェクト一覧を表示します。 |
---|---|
edit | 指定したスパイダーファイルを設定されているエディタを使って編集します。 |
view | ブラウザを開いて指定URLにアクセスを実行します。 |
settings | プロジェクト内のディレクトリで実行されると、プロジェクト設定値が表示されます。 |
runspider | 個別のPythonファイルに自己完結型のspiderを実行します。 |
version | Scrapyバージョン情報を表示します。 |
bench | 簡単なベンチマークテストを実行します。 |
実行ファイルにスクレイピングコードを書き込まずとも、scrapyではそのままコマンドラインからScrapyシェルを実行してデータを抽出することができます。
# qiitaのトップページのソースコードを取得
$ scrapy shell "https://qiita.com/"
まずはshell
コマンドライン上でqiitaにアクセスします。
このコマンドを実行すると指定URLのページ構成を取得してresponseオブジェクトに格納されます。
抽出したい各データはこのresponseオブジェクトから操作を行います。
# 取得先の実際のHTMLソースをブラウザに表示
>>> view(response)
HTMLソースが直接表示されますが、ログが長くなるので確認しなくても大丈夫です。
# ページ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モジュールもそのまま使用できるので、抽出タグにチェーンで正規表現をかけるなどの記述も可能です
# ページ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で取得ソースの抽出などを用いて実装する工程が、かなり短いコードで簡単に再現できました。しゅ・・・しゅごい
だいたい掴めてきたので実践的なコードを作ってみます。
キータの「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」を実行しつつ、取得データをフィードエクスポート機能を使って「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ですが、スクレイピング自体が常に著作権問題がつきまとうデリケートな手段なのであくまで自己責任でお願いします。