Scrapyではスクレイピングの主な処理をSpiderクラスに実装します。
今回の記事ではSpiderクラスの実装方法とScrapyの実行方法を説明します。
今までの記事
まずは結論から
scrapy crawl
コマンドをターミナルから実行してスクレイピングを開始する- ScrapyがWebページをダウンロードすると
scrapy.Spider
クラスのparse
メソッドが呼ばれる parse
メソッドの引数に渡されるscrapy.http.HtmlResponse
オブジェクトから目的の情報を抽出するscrapy.Request
オブジェクトをyield
すると別のWebページをダウンロードできる- 相対パスを簡単に処理するために
HtmlResponse.follow
メソッドを使用する
開発環境
- Ubuntu 18.04.5 LTS (Bionic Beaver)
- bash
- Python 3.8.1
- PyCharm 2020.3.2 (Professional Edition)
Scrapyプロジェクトのディレクトリ構成
まだScrapyプロジェクトを作成していない場合は、こちらの手順で作成しましょう。
この記事では以下のようなディレクトリ構成を想定しています。
$ cd <プロジェクトルートディレクトリ>
$ tree -L 3
.
├── scrapy_cats
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
├── scrapy.cfg
└── venv
(略)
また、以降の作業はすべて仮想環境内で行いますのでアクティベートしておきましょう。
$ source venv/bin/activate
(venv) $
最低限行うべきScrapyの設定
Scrapyを実行する前に、最低限でもダウンロード間隔の設定はしておきましょう。
デフォルトのままでは同じWebサイトに最大で同時16アクセスしてしまう設定になっており、アクセス先に負荷をかけすぎてしまう可能性があります。
settings.pyに以下の行を追加します。
# 同じWebサイトから連続してページをダウンロードする際の待機時間(秒)
DOWNLOAD_DELAY = 1
写真一覧ページのダウンロード
まずはスクレイピングの起点となる写真一覧ページ(https://photohito.com/dictionary/猫/)をダウンロードする処理を実装して実行します。
重要なポイントは以下のとおりです。
scrapy.Spider
クラスを継承してSpiderを実装するSpider.name
プロパティにスパイダー名を指定するSpider.start_urls
プロパティに最初にダウンロードするページを指定する- ページがダウンロードされたらコールバックとして
parse
メソッドが呼ばれる parse
メソッドの引数にscrapy.http.HtmlResponse
オブジェクトが渡されるのでそれを処理するscrapy crawl
コマンドにスパイダー名を指定して実行すればスクレイピングが開始される
from scrapy import Spider
from scrapy.http import HtmlResponse
class CatsSpider(Spider):
name = 'cats' # スパイダー名。クロールコマンド実行時に指定する
start_urls = ['https://photohito.com/dictionary/猫/'] # 最初にダウンロードするページのURL。
# parseメソッドは取得したWebページを処理するためにScrapyから呼ばれるコールバック関数
def parse(self, response: HtmlResponse):
print('ダウンロードしたURLは', response.url)
以下のコマンドでスクレイピングを実行します。
(venv) $ scrapy crawl cats
デバッグ用のログがたくさん表示される中に、さきほど実装した出力が確認できると思います。
2021-03-07 00:01:56 [scrapy.utils.log] INFO: Scrapy 2.4.1 started (bot: scrapy_cats)
...
2021-03-07 00:01:57 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://photohito.com/dictionary/%E7%8C%AB/> (referer: None)
ダウンロードしたURLは https://photohito.com/dictionary/%E7%8C%AB/
2021-03-07 00:01:58 [scrapy.core.engine] INFO: Closing spider (finished)
...
写真詳細ページのURLを抽出してダウンロード
では写真一覧ページにある各写真の詳細ページをダウンロードしてみましょう。
ページ内から必要な情報を抽出する方法は前回の記事で検討済みです。
parse
メソッドを修正し、parse_photo
メソッドを追加します。
ここでの重要なポイントはparse
メソッド最終行のyield
文です。
scrapy.Request
オブジェクトをyield
するとWebページをダウンロードできるRequest
にはURLとレスポンス処理用コールバックメソッドを指定する
from scrapy import Spider, Request
from scrapy.http import HtmlResponse
class CatsSpider(Spider):
name = 'cats'
start_urls = ['https://photohito.com/dictionary/猫/']
def parse(self, response: HtmlResponse):
"""写真一覧ページから各写真の詳細ページへのリンクを見つけてたどる"""
# 写真詳細ページのURL一覧
urls = [response.urljoin(href) for href
in response.css('.imgholder a::attr(href)').getall()]
for url in urls[:2]: # TODO: 最初からすべてたどらず、2つだけ試す
# urlをダウンロードしたレスポンスをparse_photoメソッドに渡すようにリクエストする
yield Request(url=url, callback=self.parse_photo)
def parse_photo(self, response: HtmlResponse):
"""写真詳細ページを処理する"""
print(response.url)
実行すると、以下のように2つの写真詳細ページがダウンロードされ、そのURLが表示されるのが確認できると思います。
...
2021-03-07 01:21:20 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://photohito.com/photo/10089292/> (referer: https://photohito.com/dictionary/%E7%8
C%AB/)
https://photohito.com/photo/10089292/
2021-03-07 01:21:21 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://photohito.com/photo/10128468/> (referer: https://photohito.com/dictionary/%E7%8
C%AB/)
https://photohito.com/photo/10128468/
...
上のソースコードではダウンロードするURLを絶対パスとして扱いましたが、相対パスを簡単に処理することもできます。
response
オブジェクトのfollow
メソッドを使えば、parse
メソッドを以下のように簡潔に書くことができます。
def parse(self, response: HtmlResponse):
# urlは相対パス
for url in response.css('.imgholder a::attr(href)').getall():
yield response.follow(url, self.parse_photo)
写真詳細ページで目的の情報を抽出
目的の情報を抽出するためにparse_photo
メソッドを実装していきましょう。
今回の記事では情報を抽出するところまでしか行いません。
画像ダウンロード処理、データベース登録処理の実装は次回以降の記事で行います。
前回の記事でScrapy Shellで行った検討結果をほぼそのまま使っているだけですので、特に重要なポイントはありません。
from scrapy import Spider
from scrapy.http import HtmlResponse
from pprint import pprint
class CatsSpider(Spider):
name = 'cats'
start_urls = ['https://photohito.com/dictionary/猫/']
def parse(self, response: HtmlResponse):
"""写真一覧ページから各写真の詳細ページへのリンクを見つけてたどる"""
for url in response.css('.imgholder a::attr(href)').getall()[:2]: # TODO: 最初からすべてたどらず、2つだけ試す
yield response.follow(url, self.parse_photo)
def parse_photo(self, response: HtmlResponse):
"""写真詳細ページを処理する"""
# 写真画像のダウンロード
photo_url = response.css('#photo_view img::attr(src)').re_first(r'https://.*')
print('この写真画像をダウンロード:', photo_url) # TODO: ダウンロード処理は次回以降に実装する
# 撮影情報、EXIFデータの取得
photo_data = self.get_table_data(response, '#photo_data_area')
exif_data = self.get_table_data(response, '#exif_area')
pprint(photo_data) # TODO: データーベース登録処理は次回以降に実装する
pprint(exif_data)
def get_table_data(self, response: HtmlResponse, table_id):
"""
id=table_idであるノードの子孫のtableから、
ヘッダーをキーとしデータを値とするディクショナリを取得する
"""
keys = response.css(f'{table_id} th::text').re(r'\w+')
values = list(map(lambda x: ' '.join(x.css('::text').re(r'^[^\n].*')),
response.css(f'{table_id} td')))
return dict(zip(keys, values))
実行すると、以下のように情報が正しく抽出できているのが確認できると思います。
...
2021-03-07 02:29:22 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://photohito.com/photo/10089292/> (referer: https://photohito.com/dictionary/%E7%8
C%AB/)
この写真画像をダウンロード: https://photohito.k-img.com/...jpg
{'カメラ': 'SONY ILCE-7M3', 'レンズ': '---', 'レンズタイプ': '---', '対応マウント': '---'}
{'ISO感度': '800',
'イメージサイズ': '6000 x 4000',
'ソフトウェア': 'Adobe Photoshop Lightroom Classic 10.1 (Windows)',
'フラッシュ': 'ストロボ発光せず, 強制非発光モード',
'ホワイトバランス': 'Auto',
'撮影日時': '2020:12:20 16:17:18',
'焦点距離': '200 mm',
'絞り': 'f/2.8',
'露光補正値': '1.7 EV',
'露出時間': '0.001 (1/800) 秒'}
2021-03-07 02:29:23 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://photohito.com/photo/10128468/> (referer: https://photohito.com/dictionary/%E7%8
C%AB/)
この写真画像をダウンロード: https://photohito.k-img.com/...jpg
{'カメラ': 'CANON Canon EOS 6D',
'レンズ': 'TAMRON SP AF 90mm F/2.8 MACRO1:1 (キヤノン用)',
'レンズタイプ': 'マクロ',
'対応マウント': 'キヤノンEFマウント系'}
{'ISO感度': '100',
'イメージサイズ': '4608 x 3072',
'ソフトウェア': 'Digital Photo Professional',
'フラッシュ': 'ストロボ発光せず, 強制非発光モード',
'ホワイトバランス': 'Auto',
'撮影日時': '2021:01:17 09:53:25',
'焦点距離': '90 mm',
'絞り': 'f/2.8',
'露光補正値': '0 EV',
'露出時間': '0.003 (1/400) 秒'}
...
まとめ
今回の記事ではスクレイピング処理をSpiderクラスに実装し、それを実行してWebページをダウンロードして目的の情報の抽出を行いました。
以下、重要なポイントです。
scrapy crawl
コマンドをターミナルから実行してスクレイピングを開始する- ScrapyがWebページをダウンロードすると
scrapy.Spider
クラスのparse
メソッドが呼ばれる parse
メソッドの引数に渡されるscrapy.http.HtmlResponse
オブジェクトから目的の情報を抽出するscrapy.Request
オブジェクトをyield
すると別のWebページをダウンロードできる- 相対パスを簡単に処理するために
HtmlResponse.follow
メソッドを使用する
次回は画像ダウンロード処理とデータベース登録処理を実装します。
ご精読ありがとうございました。
書籍紹介
この書籍が非常にわかりやすく、クローリング・スクレイピングを行うのに必要な知識がひととおり学べます。
Pythonクローリング&スクレイピング[増補改訂版]
免責事項
- コンテンツや情報において、必ずしも正確性を保証するものではありません。また合法性や安全性なども保証しません。
- 掲載された内容によって生じた損害等の一切の責任を負いかねますので、ご了承ください。