Scrapy Tutorial

前書き

python初心者です。scrapyを使う機会を得たので公式tutorialで学習。

参考資料とバージョン

公式ドキュメント

Scrapy 1.5.1

python 3.6.5

学習記録

まずはコマンドラインから

scrapy startproject tutorial

このコマンドでproject作成。tutorialの部分が名前。これによりいくつかのディレクトリとファイルが生成される。scrapyはライブラリではなくフレームワークなので骨組みが決まっている。早速tutorial/spidersディレクトリにquotes_spider.pyというファイルを作成。これがいわゆるspider。とりあえずお手本通り作る。

    # quotes_spider.py
    
    import scrapy
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
    
        def start_requests(self):
            urls = [
                'http://quotes.toscrape.com/page/1/',
                'http://quotes.toscrape.com/page/2/',
            ]
            for url in urls:
                yield scrapy.Request(url=url, callback=self.parse)
    
        def parse(self, response):
            page = response.url.split("/")[-2]
            filename = 'quotes-%s.html' % page
            with open(filename, 'wb') as f:
                f.write(response.body)
            self.log('Saved file %s' % filename)

これを見るとpythonではたぶんクラスを定義するときに引数にクラスを渡すとそれが継承されるようになる。これで新しいQuotesSpiderを作っている。すぐ下のname定義ではプロジェクト内で一意の値を与えないといけない。下記のようにコマンドラインからこの名前で呼び出せるようになる。

    scrapy crawl quotes

これでquotesのクローリングとスクレイピングが実行される。と思いきや、エラーが返ってくる。

python3.7/site-packages/twisted/conch/manhole.py", line 154
    def write(self, data, async=False):

ググったところこのサイトを見つけた。

python3.7とscrapyの組み合わせに何か不具合があるようで仮想環境上のバージョンをpython3.6に戻すと良いらしい。そこで

brew switch python 3.6.6

Error: python does not have a version "3.6.6" in the Cellar. python installed versions: 3.6.5, 3.7.0

と返ってきたので3.6.5にした。これでsyntax errorは出なくなった。上記のコマンドも実行することができた。

返ってきたのがこれ

2018-10-15 14:53:59 [scrapy.core.engine] INFO: Spider opened
2018-10-15 14:53:59 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2018-10-15 14:53:59 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2018-10-15 14:54:00 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2018-10-15 14:54:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2018-10-15 14:54:00 [quotes] DEBUG: Saved file quotes-1.html
2018-10-15 14:54:01 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2018-10-15 14:54:01 [quotes] DEBUG: Saved file quotes-2.html
2018-10-15 14:54:01 [scrapy.core.engine] INFO: Closing spider (finished)
2018-10-15 14:54:01 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
,
,
,
2018-10-15 14:54:01 [scrapy.core.engine] INFO: Spider closed (finished)

htmlファイルを作成している。

2018-10-15 14:54:00 [quotes] DEBUG: Saved file quotes-1.html

このhtmlファイルの作成はspiderクラスで定義したparseメソッドが指示している。parseは「解析する」という意味。下記の部分がその定義。

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

ここでは、urlリクエストの結果がresponseに入って、urlのからページ数を取り出して、それを使ってファイル名をわかりやすく命名している。その後でそれをファイルとして開いている。

open(filename, mode)

ここでのmode 'wb' はwが書き込み、bはバイナリを意味している。wを指定する場合、第一引数のファイル名のファイルがあればそれを上書きし、なければ新規作成される。 そして、responseオブジェクトからbody属性を指定して、ページのhtmlを取得することができた。

    # 1
        def start_requests(self):
            urls = [
                'http://quotes.toscrape.com/page/1/',
                'http://quotes.toscrape.com/page/2/',
            ]
            for url in urls:
                yield scrapy.Request(url=url, callback=self.parse)
    
        def parse(self, response):
            ,
            ,
            ,

上記は下記のように置き換えることもできる。

    # 2
        start_urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
    
        def parse(self, response):
            ,
            ,
            ,

これらは同じ意味である。# 1のstart_requestsはscrapy.Requestを生成するためのメソッドだが、start_urlsをurlのリストで作成すると、それをもっと短い実装で実現することができる。

1ではparseをコールバック関数として明示的に指示しているが、2ではそうしていないがparseは実行される。これはscrapyにおいてはrequestに対してはparseで定義されたメソッドがデフォルトのコールバック関数として定義されているため。

ここを見ればscrapyが返すデータのオブジェクトの動きを調べることができる。

データの抽出方法

$ scrapy shell 'http://quotes.toscrape.com'
quote = response.css("div.quote")[0]
tags = quote.css("div.tags a.tag::text").extract()

scrapy shell を実行することで指定したurlのページの情報がresponseオブジェクトとして利用できるようになる。 下記がquoteとして定義している0番目のdiv.quote

    <div class="quote" itemscope itemtype="http://schema.org/CreativeWork">
            <span class="text" itemprop="text">“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”</span>
            <span>by <small class="author" itemprop="author">Albert Einstein</small>
            <a href="/author/Albert-Einstein">(about)</a>
            </span>
            <div class="tags">
                Tags:
                <meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" /    > 
                
                <a class="tag" href="/tag/change/page/1/">change</a>
                
                <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
                
                <a class="tag" href="/tag/thinking/page/1/">thinking</a>
                
                <a class="tag" href="/tag/world/page/1/">world</a>
                
            </div>
        </div>

tagsで定義している部分の要素の指定方法。

"div.tags a.tag::text"

の部分はスペースで区切られていて、親子関係を持っていることがわかる。

予め指定したurlだけでなく、ページ内のリンクを辿ってさらに別のページの情報を取得したい場合。

    next_page = response.css('li.next a::attr(href)').extract_first()
            if next_page is not None:
                next_page = response.urljoin(next_page)
                yield scrapy.Request(next_page, callback=self.parse)

上記はspiderのparseメソッド内の定義。responseオブジェクトのurljoinメソッドは相対パスで書かれているであろうurlを補足するための便利なヘルパー。

    yield scrapy.Request(next_page, callback=self.parse)

によって第一引数のurlをresponseオブジェクトとして取得した結果をさらにparseメソッドに渡している。

まとめ

これでtutorial終了。ポイントになりそうだと思ったのはrequestとresponseのそれぞれのオブジェクトとしての動きとそのデータの受け渡し、あとはparseでresponseをどう処理するかだと思う。ここからyieldしたデータをjson形式でmongoDBに入れていきたいと思っているので Item Pipelineの設定を書いてくことになりそうです。