おうちだいすき

自宅警備員からデータサイエンティストになった人の雑記的ななにか。

Scrapyでスクレイピングやクローリングをしてみたので大事そうなとこをまとめた

「あのサイトのこのデータ、欲しいなぁ」

「データはこんだけあるがもう少し情報があれば分析できるのになぁ。あのサイトには情報乗っかってるけど」

「ああ、私にスクレイピングの技術があれば…」

みたいな事ってありませんか?ありますよね?(迫真)

 

なんでもいいです。

例えば価格.c○mで価格推移を拾ってきたいとか、DMM.c○mで動画の情報を拾ってきたいとかなどなど…。

 今回はそんな時のために備えて、僕がスクレイピング改め、クローラを作った時になかなかネットを調べても出てこなかった内容を中心にまとめていければと思います。


取り扱うこと

  • スクレイピングやクローリングについて
  • どのライブラリ・機能を使うべきか
  • Scrapy(CrawlSpider)で大事そうなこと  

取り扱わない事

  • ライブラリのインストール方法
  • HTML要素の抽出方法

などの基礎事項。ソースコードは少なめです。

目次は以下の通りです。

それでは始めます

スクレイピングやクローリングはデータ収集の最終兵器である!!

余程データ収集に困ってない限りは使わないことが推奨です。

そもそもグレーゾーンです。

先方のサーバーに大きな負担かける危険性があります。

サーバーに負担がかかって事件になった事例もあります。

何かしらの形式でデータが配布されていたり、APIが公開されている場合はそちらを使いましょう。

「データが公開されてない……。くっ、、、ならばこやつを使うしか、、、」

とっておきの場面で使いましょう。すぐにスクレピングやクローリングに走るのは愚策です。*1

 

そもそもスクレイピングとクローリングって何?

最初のうちは混同する方も大勢いらっしゃいます。

というか私がそうでした。

ここではとある家電製品の情報を収集したいと仮定します。
すると、スクレイピングとクローリングは以下のように説明ができます。

  • スクレイピング

    • 商品ページから価格やスペックなどの商品情報を抽出する事。(そんまんま)
  • クローリング

    • 商品ページからさらに踏み込んで情報を取得する場合や、検索結果などから複数の商品について情報を抽出したい場合。つまりリンクをたどる行為が発生したスクレイピングを行う事。

スクレイピングか、クローリングかによって使用するライブラリが変わってきます。

 

どのライブラリを使うべきか

  • スクレイピングで十分な時

    • BeautifulSoup
  • クローリングが必要な時

    • Selenium … HTMLがJSで動的に書き換えられている(URLに変化がない)
    • Scrapy … HTMLが動的に書き換えられていない(URLに変化がある)

そして本記事では特にScrapy*2についてまとめたいと思います。
インストールやプロジェクトの立ち上げ方など こちらについては公式ドキュメントにチュートリアルがありますのでそちらを読んで実践してもらう方が素早いです。

Scrapy チュートリアル — Scrapy 1.2.2 ドキュメント

ただし、setting.pyにてDOWNLOAD_DELAYとHTTPCASHE_ENABLEDは最低限設定しましょう*3
最悪、事件になります。

こちらに設定方法やScrapyについても詳しく書かれてますので是非ご参考くださいませ。*4

shinyorke.hatenablog.com

スパイダーの引数について

新しい単語を急に出してしまいました。
あまり深く考え込まず、クローラとほぼほぼ同義だと思ってもらって問題ないと思います。

個人的な解釈としては

  • クローラ … Scrapyプロジェクトのディレクトリ全体
  • スパイダー*5 … 巡回ルールを定義するクラス

という意識でいます。(異論は認める)

以降、スパイダーと述べた際はソースコードの中身的なお話をするのだろうと思ってもらうと幸いです。

 

チュートリアルではスパイダーを

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)

こんな感じのクラスで書かれてて、

class QuotesSpider(scrapy.Spider):

この部分がスパイダーの引数を設定している箇所です。
この引数の部分が使用用途によって大きく変化します。

そもそもスクレイピングとクローリングって何?の項ではクローリングを 

商品ページからさらに踏み込んで情報を取得する場合や、検索結果などから複数の商品について情報を抽出したい場合。つまりリンクをたどる行為が発生したスクレイピングを行う事。

と定義しました。

この、辿るリンクを一意に特定できるのか、出来ないのかによって引数が変わり、スパイダーを定義するファイルの書き方も大きく変わってきます。 

  • URLが一意に特定できる場合

    • class QuotesSpider(scrapy.Spider):
  • URLが一意に特定できない場合

    • class QuotesSpider(scrapy.CrawlSpider):

URLが一意に特定できる場合はstart_urlsの中身を増やせばいいですし、量が膨大ならリスト内包表記などなんなりと用いれば良さそうです。

問題は特定できない場合です。
どこかのページを起点として、そのページから複数のリンク先を辿る場合、たくさんの商品ページのデータを収集したいのにURLが特定できず、商品検索結果からリンクを辿るしかない場合などなど。

こんな時にCrawlSpiderを用いると便利です。
そして、ここからはCrawlSpiderについてまとめます。

CrawlSpider

まずはCrawlSpiderを定義する箇所を見てみましょう。

公式ドキュメントでは以下のように記述されています。(少し手を加えてあります。)

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
    name = 'example.com' # 走らせるスパイダーの名前を定義(実行コマンドの引数になる)
    allowed_domains = ['example.com'] 
    start_urls = ['http://www.example.com'] # 起点となるURLの定義

    # スパイダーが走るリンク先を定義
    rules = (
        Rule(LinkExtractor(
            allow=('category\.php', ),
            deny=('subsection\.php', )
        )), # 巡回先の指定
        Rule(LinkExtractor(
            allow=('item\.php', )
        ), callback='parse_item'), # 抽出先の指定
    )

    # 拾ってくる要素を定義
    def parse_item(self, response):
        self.logger.info('Hi, this is an item page! %s', response.url)
        item = scrapy.Item()
        item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
        item['name'] = response.xpath('//td[@id="item_name"]/text()').extract()
        item['description'] = response.xpath('//td[@id="item_description"]/text()').extract()
        return item

で、私個人が一番つまづいたのがrulesの部分でした。

なぜつまづいたかというと、rules内にデコードされたURLを指定するとスパイダーは走るものの、迷走しだしてしまうのが原因でした。
そしてrulesの上で設定しているstart_urlsにはデコードされたURLはしっかり走ってくれた事が余計な混乱を誘っていました。

日本語のURLをrules内で指定する際はデコードされたURLを使用した方が良さそうです。

CrawlSpiderの利用例

例えば'hoge.com'の商品詳細ページが'hoge.com/product/{商品名}/{商品ID}'だったとします。
商品名までだとURLを一意に特定する事が可能だったはずが、IDまで含んでいるので特定を困難にさせています。
検索ページだと'/search/?query={検索に打ち込んだ名前}'になってるので特定できそう。
つまり検索ページを起点として、商品詳細ページに踏み込んでいく必要がありそうです。

そんな時に作るソースコードの例が以下の通りとなります。

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

from product.spiders import PRODUCT_LIST # __init__.pyなどで調べたいリストを定義する
from product.items import ProductItem # items.pyで保存したい情報の名前を定義する

class ProductSpider(CrawlSpider):
    name = 'product'
    allowed_domains = ['hoge.com']
    start_urls = ['https://hoge.com/search/?query="{0}"'.format(i) for i in PRODUCT_LIST] # URLのリスト
    
    allow_list = [r'/product/.+/[\d]+'] # 'hoge.com/product'以下を正規表現でマッチング
    restrict_list = ['//h3[@title="{0}"]'.format(i) for i in PRODUCT_LIST] # スパイダーがスナイポするHTML要素
    
    rules = (
        Rule(LinkExtractor(
                allow=allow_list,
                restrict_xpaths=restrict_list,
                unique=True,
        ), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        item = ProductItem()
        item['name'] = response.xpath('//table/tbody/tr/th[text()="商品名"]/following-sibling::td/ul/li/a/text()').extract()
        item['price'] = response.xpath('//table/tbody/tr/th[text()="価格"]/following-sibling::td/ul/li/a/text()').extract()
        item['release_day'] = response.xpath('//table/tbody/tr/th[text()="発売日"]/following-sibling::td/ul/li/a/text()').extract()
        return item

ここで新しく出た表現がいくつかあります。

'restrict_list'ですね。これがまぁ便利でした。
何も設定しないとスパイダーは全てのリンクを探そうとします。
'deny_list'を設定したいけども拒否する項目があまりにも多いと厳しそうです。
そんな時に'restrict_list'を設定しておくとスパイダーはその対象のHTML要素のみリンクを探してくれます。
URLの誤検出を大幅に減らしてくれますので是非使ってあげてください。

もう1つ'unique=True'もチュートリアルにはなかった表現です。
これは重複したURLを検出しない設定です。
デフォルトではFalseになっていて、重複URLを検出してしまいますので、用途に応じて設定してあげてください。

言いたかった事

  • Scrapyは「すくらぴー」と読むそうです。
  • リンクをたどる時はCrawlSpiderが便利
  • 'restrict_list'はCrawlSpiderが辿るリンク先をスナイポしてくれる便利機能
  • 'start_urls'は日本語デコードされたURLを用いても大丈夫だけど、'rules'内は大丈夫じゃない。エンコード推奨。
  • スクレイピングやクローリングは最終手段。どうしてもという時に使いましょう。

特にURLに関するお話や、'restrict_list'についての記述が調べてもなかなか出てこなかったので記事として残しておこうと思った次第です。

是非是非、CrawlSpiderも活用して、サイト側に迷惑のかからない健全なクローリングライフを送ってください!

*1:そもそもデータが公開されてたらソースコード書く時間も省かれる

*2:「すくらぴー」と読むそうです。

*3:特に急ぎでない限り、20〜30、ほんとは60くらいでも良いくらい。単位はsecond

*4:ここに詳しく書かれてるなら私のこのエントリの存在意義…。

*5:JIS規格では「スパイダ」と記述するのが正なのでしょうが、なんか長音符あった方がカッコいいのでこの表現で