Scrapyチュートリアル

このチュートリアルでは、Scrapyがシステムに既にインストールされていると仮定します。 そうでない場合は、 インストール ガイド を参照してください。

ここでは quotes.toscrape.com という、有名な著者からの引用をリストするウェブサイトをスクレイピングします。

このチュートリアルでは以下の作業について説明します。

  1. 新しいScrapyプロジェクトの作成

  2. スパイダー(spider) を作成してサイトをクロールし、データを抽出します。

  3. コマンドラインを使用してスクレイピングされたデータをエクスポートする。

  4. 再帰的にリンクをたどるようにスパイダーを変更する。

  5. スパイダー引数の使用

Scrapyは Python で書かれています。 この言語を初めて使用する場合は、Scrapyを最大限に活用するために、この言語がどのようなものかを理解することから始めてください。

すでに他の言語に精通しており、Pythonをすばやく学習したい場合は、 Python Tutorial (訳注:日本語版 https://docs.python.org/ja/3/tutorial/)が優れた文書です。

プログラミングが初めてで、Pythonを使い始めたい場合は、以下の書籍が役立ちます:

また、 this list of Python resources for non-programmers (この非プログラマ向けのPythonリソースのリスト)と、 suggested resources in the learnpython-subreddit (learnpython-subredditの推奨リソース)を参照することもできます。

プロジェクトの作成

スクレイピングを開始する前に、新しいScrapyプロジェクトをセットアップする必要があります。 あなたのコードを保存して実行するディレクトリを入力してください:

scrapy startproject tutorial

これにより、以下の内容の tutorial ディレクトリが作成されます:

tutorial/
    scrapy.cfg            # deploy configuration file

    tutorial/             # project's Python module, you'll import your code from here
        __init__.py

        items.py          # project items definition file

        middlewares.py    # project middlewares file

        pipelines.py      # project pipelines file

        settings.py       # project settings file

        spiders/          # a directory where you'll later put your spiders
            __init__.py

私たちの最初のスパイダー

スパイダーはユーザが定義するクラスであり、ScrapyはWebサイト(またはWebサイトのグループ)から情報をスクレイピングするために使用します。 Spider をサブクラス化して最初のリクエストを作成し、オプションで、ページ内のリンクをたどる方法やダウンロードしたページ内容をパースしてデータを抽出する方法を定義する必要があります。

以下は、最初のスパイダーのコードです。 プロジェクトの tutorial/spiders ディレクトリの下の 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 = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log(f'Saved file {filename}')

ご覧のとおり、スパイダーは scrapy.Spider をサブクラス化し、いくつかの属性とメソッドを定義しています:

  • name は、スパイダーを識別します。 プロジェクト内で一意である必要があります。つまり、異なるスパイダーに同じ名前を設定することはできません。

  • start_requests() は、スパイダーがクロールを開始するリクエストの反復可能オブジェクト(iterable)を返す必要があります(リクエストのリストを返すか、ジェネレーター関数を作成できます)。これらの初期リクエストから後続のリクエストが連続して生成されます(訳注:iterableの意味はPythonドキュメント/用語集 https://docs.python.org/ja/3/glossary.html 参照)。

  • parse() は、行われたリクエストごとにダウンロードされたレスポンスを処理するために呼び出されるメソッドです。 リクエスト・パラメーターは TextResponse のインスタンスで、ページ内容を保持し、さらにそれを処理するための便利なメソッドを持っています。

    parse() メソッドは通常、レスポンスをパースし、スクレイプされたデータを辞書として抽出し、追跡する新しいURLを見つけて、それらから新しいリクエスト(Request)を作成します。

私たちのスパイダーの実行方法

スパイダーを動作させるには、プロジェクトの最上位ディレクトリに移動して、以下を実行します:

scrapy crawl quotes

このコマンドは、追加したばかりの quotes という名前のスパイダーを実行し、 quotes.toscrape.com ドメインへのリクエストを送信します。以下のような出力が得られます:

... (omitted for brevity)
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Spider opened
2016-12-16 21:24:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-12-16 21:24:05 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-1.html
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-2.html
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Closing spider (finished)
...

次に、現在のディレクトリのファイルを確認します。 parse メソッドが指示するように、それぞれのURLのコンテンツを持つ2つの新しいファイル quotes-1.htmlquotes-2.html が作成されていることに気付くはずです。

注釈

なぜまだHTMLをパースしていないのかって?順番に説明するからもうちょい待ってくれ。

一体全体どういう仕組みなのか?

Scrapyは、スパイダーの start_requests メソッドによって返される scrapy.Request オブジェクトをスケジュールします。 それぞれのレスポンスを受信すると、 Response オブジェクトをインスタンス化し、リクエストに関連付けられたコールバック・メソッド(この場合は parse メソッド)を呼び出して、レスポンスを引数として渡します。

start_requestsメソッドへのショートカット

URLから start_requests() オブジェクトを生成する start_requests() メソッドを実装する代わりに、単にURLのリストで start_urls を定義できます。 このリストはそれから start_requests() のデフォルト実装で使用され、あなたのスパイダーの初期リクエストを作成します:

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 = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)

parse() メソッドは、Scrapyに明示的に指示していない場合でも、これらのURLの各リクエストを処理するために呼び出されます。 これは、 parse() がScrapyのデフォルトのコールバック・メソッドであり、明示的にコールバックが割り当てられていないリクエストに対して呼び出されるためです。

データの抽出

Scrapyでデータを抽出する方法を学ぶ最良の方法は、 Scrapyシェル を使用してセレクターを試すことです。 以下のように実行します:

scrapy shell 'http://quotes.toscrape.com/page/1/'

注釈

コマンドラインからScrapyシェルを実行するときは、常にURLをクォーテーションで囲むことを忘れないでください。そうしないと、引数(つまり、 & キャラクタ)を含むURLは機能しません。

Windowsでは代わりにダブルクォーテーションを使って下さい:

scrapy shell "http://quotes.toscrape.com/page/1/"

あなたは以下のようなものを見る事になるでしょう:

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s]   item       {}
[s]   request    <GET http://quotes.toscrape.com/page/1/>
[s]   response   <200 http://quotes.toscrape.com/page/1/>
[s]   settings   <scrapy.settings.Settings object at 0x7fa91d888c10>
[s]   spider     <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local objects
[s]   view(response)    View response in a browser

シェルを使用して、あなたはレスポンス・オブジェクトで CSS を使用して要素の選択を試す事ができます:

>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

response.css('title') を実行した結果は、 SelectorList というリストのようなオブジェクトになり、これはXML/HTML要素をラップし、さらにクエリを実行して選択範囲を細かくしたり、データを抽出したりできるオブジェクトである Selector のリストになっています。

上記のタイトルからテキストを抽出するには、以下のようにします:

>>> response.css('title::text').getall()
['Quotes to Scrape']

ここで注意すべき点が2つあります。1つは、CSSクエリに ::text を追加したことです。これは、 <title> 要素内のテキスト要素のみを直接選択することを意味します。 ::text を指定しない場合、そのタグを含む完全なタイトル要素を取得します:

>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']

もう1つは、 .getall() を呼び出した結果がリストであるということです。セレクターが複数の結果を返す可能性があり、そしてそれらの全てを抽出します。この場合のように、最初の結果だけが必要であることがわかったら、次の操作を実行できます:

>>> response.css('title::text').get()
'Quotes to Scrape'

代わりに以下のように書くこともできます:

>>> response.css('title::text')[0].get()
'Quotes to Scrape'

けれども、 SelectorList インスタンスで .get() を直接使用すると、 IndexError を回避し、セレクターに一致する要素が見つからない場合 None を返します。

ここに教訓があります。ほとんどのスクレイピングコードでは、ページ上で見つからないものに起因するエラーに対して回復力を持たせ、一部のスクレイピングに失敗した場合でも、少なくとも いくつかの データを取得できるようにします。

getall() メソッドや get() メソッドに加えて、 正規表現 (訳注: https://docs.python.org/ja/3/library/re.html ) により抽出する re() メソッドも使用できます:

>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']

使用する適切なCSSセレクターを見つけるには、 view(response) を使用してWebブラウザーのシェルからレスポンス・ページを開くと便利です。 ブラウザの開発ツールを使用してHTMLを調査し、セレクターを作成できます(Webブラウザの開発ツールを使ってスクレイピングする 参照)。

Selector Gadget という、多くのブラウザで動作する、選択された要素のCSSセレクターを視覚的にすばやく探せる素晴らしいツールもあります。

XPathの簡単な紹介

CSS に加えて、Scrapyセレクターは XPath 式の使用もサポートしています:

>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'

XPath式は非常に強力であり、Scrapyセレクターの基盤です。 実際、CSSセレクターは内部でXPathに変換されます。 シェル内のセレクター・オブジェクトのテキスト表現をよく読んでいれば、あなたはそれに気付く事ができるでしょう。

CSSセレクターほど一般的ではないかもしれませんが、XPath式は構造をナビゲートするだけでなく、内容を探すことができるため、より強力になります。 XPathを使用すると、「『Next Page』というテキストを含むリンクを選択」というような事ができます。これにより、XPathはスクレイピングのタスクに非常に適合します。CSSセレクターの構築方法を既に知っている場合でも、XPathを学ぶことをお勧めします。

ここではXPathについてはあまり取り上げませんが、 ScrapyセレクターでXPathを使用 に詳しく載っています。XPathの詳細については、 this tutorial to learn XPath through examplesthis tutorial to learn "how to think in XPath" を私たちはお勧めします。

引用と著者の抽出

選択と抽出について少し理解できたので、Webページから引用を抽出するコードを作成して、スパイダーを完成させましょう。

http://quotes.toscrape.com の各引用は、次のようなHTML要素で表されます:

<div class="quote">
    <span class="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">Albert Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
    </span>
    <div class="tags">
        Tags:
        <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>

Scrapyシェルで少しいじって、必要なデータを抽出する方法を見つけましょう:

$ scrapy shell 'http://quotes.toscrape.com'

引用HTML要素のセレクターのリストを取得します:

>>> response.css("div.quote")
[<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 ...]

上記のクエリによって返された各セレクターを使用すると、サブ要素に対してさらにクエリを実行できます。 最初のセレクターを変数に割り当てて、特定の引用でCSSセレクターを直接実行できるようにします:

>>> quote = response.css("div.quote")[0]

それでは、作成したばかりの quote オブジェクトを使用して、その引用から textauthortags を抽出しましょう:

>>> text = quote.css("span.text::text").get()
>>> text
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").get()
>>> author
'Albert Einstein'

タグは文字列のリストになっているので、 .getall() メソッドを使用してそれらすべてを取得できます:

>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']

各パーツを抽出する方法を考え出したので、すべての引用要素を反復処理して、それらをPython辞書にまとめることができます:

>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").get()
...     author = quote.css("small.author::text").get()
...     tags = quote.css("div.tags a.tag::text").getall()
...     print(dict(text=text, author=author, tags=tags))
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
...

私たちのスパイダーでデータを抽出する

私たちのスパイダーに戻ります。これまでは、特にデータを抽出せず、HTMLページ全体をローカルファイルに保存するだけでした。それでは、ここで、上記の抽出ロジックを私たちのスパイダーに組み込みましょう。

Scrapyスパイダーは通常、ページから抽出されたデータを含む多くのPython辞書を生成します。 これを行うには、以下に示すように、コールバックでPythonキーワード yield を使用します:

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):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

あなたがこのスパイダーを実行すると、抽出されたデータがlogとともに出力されます:

2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}

スクレイピングしたデータの格納

スクレイピングされたデータを保存する最も簡単な方法は、 フィード・エクスポート を以下のコマンドで使用することです:

scrapy crawl quotes -O quotes.json

それにより、スクレイピングされたすべてのアイテムを含み JSON でシリアライズされた quotes.json ファイルを生成します。

-O コマンドラインスイッチは既存のファイルを上書きします。 一方 -o を使用すると、既存のファイルに新しいコンテンツを追加します。 ただし、JSONファイルに追加をすると、無効なJSONになります。ファイルに追加するときは、 JSON Lines など別のシリアル化形式の使用を検討してください:

scrapy crawl quotes -o quotes.jl

JSON Lines フォーマットは、ストリームに似ているため便利です。新しいレコードを簡単に追加できます。 2回実行する場合、JSONと違って壊れる事はありません。また、各レコードは個別の行であるため、メモリにすべてを収めなくても大きなファイルを処理できます。コマンドラインでそれを行うのに役立つ JQ などのツールがあります。

小さなプロジェクト(このチュートリアルのようなプロジェクト)では、これで十分です。 ただし、スクレイピングされたアイテムを使用してより複雑な操作を実行する場合は、 アイテム・パイプライン を記述できます。 アイテム・パイプラインのプレースホルダーファイルは、プロジェクトの作成時に tutorial/pipelines.py に設定されています。 ただし、スクレイピングされたアイテムを保存するだけの場合は、アイテム・パイプラインを実装する必要はありません。

スパイダー引数の使用

あなたはスパイダーの実行時に -a オプションを使用して、スパイダーにコマンドライン引数を提供できます:

scrapy crawl quotes -O quotes-humor.json -a tag=humor

これらの引数はSpiderの __init__ メソッドに渡され、デフォルトでspider属性になります。

この例では、 tag 引数に指定された値は self.tag を介して利用できます。 これを使用して、スパイダーに特定のタグを持つ引用のみを読み込み(fetch)させ、引数に基づいてURLを構築できます:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

あなたがこのスパイダーに tag=humor 引数を渡すと、 http://quotes.toscrape.com/tag/humor などの humor タグのURLのみにアクセスすることに気付くでしょう。

スパイダー引数の取扱について更に学ぶ をご覧ください。

さぁてお次は?

このチュートリアルでは、Scrapyの基本のみを説明しましたが、ここには記載されていない他の多くの機能があります。 最も重要なものの簡単な概要については、 Scrapyを3行で説明シル の章の 他に何かある? 節を確認してください。

基本の概念 節では続けて、コマンドラインツール、スパイダー、セレクター、およびスクレイプデータのモデリングのようにチュートリアルで扱っていないその他のことについて詳しく知ることができます。 サンプルプロジェクトで遊びたい場合は、 節を確認してください。