ウェブサイト検索

ScrapyとPython 3を使ってWebページをクロールする方法


紹介

ウェブスクレイピングは、しばしばウェブクロールやウェブスパイダリングと呼ばれるもので、プログラムを使ってウェブページのコレクションを閲覧し、データを抽出する行為であり、ウェブ上のデータを扱うための強力なツールです。

ウェブスクレイパーを使用すると、一連の商品に関するデータを収集したり、遊ぶための大規模なテキストまたは定量データを取得したり、公式APIなしでサイトからデータを取得したり、自分の好奇心を満たすことができます。

このチュートリアルでは、遊び心のあるデータセットを探求しながら、スクレイピングとスパイダリングプロセスの基本について学びます。私たちは、ウェブスパイダーのテスト用に設計されたサイトにホストされている引用のデータベース「Quotes to Scrape」を使用します。このチュートリアルの終わりまでには、引用を含む一連のページを巡回し、それらを画面に表示する完全に機能するPythonウェブスクレイパーを手に入れることができます。

スクレイパーは簡単に拡張できるので、自分のプロジェクトの基盤としてウェブからデータをスクレイピングするためにいじって使うことができます。

前提条件

このチュートリアルを完了するには、Python 3用のローカル開発環境が必要です。必要なすべてを設定するには、「Python 3のローカルプログラミング環境をインストールして設定する方法」を参考にしてください。

ステップ1 — 基本的なスクレイパーの作成

スクレイピングは二段階のプロセスです:

  1. 体系的にウェブページを見つけてダウンロードする。
  2. ダウンロードしたページから情報を抽出します。

それらのステップは、多くの言語でさまざまな方法で実装することができます。

プログラミング言語が提供するモジュールやライブラリを使って、スクレイパーをゼロから構築できますが、その場合、スクレイパーがより複雑になるにつれていくつかの頭痛の種に対処しなければならなくなります。たとえば、一度に複数のページをクローリングできるように、並行処理を扱う必要があります。また、スクレイピングしたデータをCSV、XML、またはJSONなどの異なる形式に変換する方法を考える必要があるでしょう。そして、特定の設定やアクセスパターンを必要とするサイトに対処しなければならないこともあります。

既存のライブラリの上にスクレイパーを構築すると、これらの問題を処理してくれるので、より良い結果が得られるでしょう。このチュートリアルでは、PythonとScrapyを使ってスクレイパーを構築します。

Scrapyは最も人気があり、強力なPythonスクレイピングライブラリの一つです。スクレイピングに対して「バッテリー付き」のアプローチを採用しており、これはすべてのスクレイパーが必要とする一般的な機能の多くを処理することを意味します。そのため、開発者は毎回車輪を再発明する必要がありません。

Scrapyは、ほとんどのPythonパッケージと同様に、PyPI(別名pip)に存在します。PyPI、Pythonパッケージインデックスは、すべての公開されたPythonソフトウェアのコミュニティ所有のリポジトリです。

このチュートリアルの前提条件に記載されているようなPythonのインストールがある場合、すでにあなたのマシンにはpipがインストールされているため、次のコマンドでScrapyをインストールできます:

  1. pip install scrapy

インストールに問題が発生した場合や、pipを使用せずにScrapyをインストールしたい場合は、公式のインストールドキュメントを確認してください。

Scrapyがインストールされたら、プロジェクト用の新しいフォルダーを作成します。これはターミナルで次のコマンドを実行することで行えます:

  1. mkdir quote-scraper

さて、今作成した新しいディレクトリに移動してください:

  1. cd quote-scraper

次に、scraper.pyという名前の新しいPythonファイルを作成します。このチュートリアルでは、このファイルにすべてのコードを配置します。お好きな編集ソフトウェアを使用してこのファイルを作成できます。

プロジェクトを開始するには、Scrapyを基盤とした非常に基本的なスクレイパーを作成します。そのためには、Scrapyが提供する基本的なスパイダークラスであるscrapy.Spiderをサブクラス化するPythonクラスを作成する必要があります。このクラスには2つの必須属性があります:

  • 名前 — spiderのための単なる名前です。
  • start_urls — これは、クロールを開始するURLのリストです。最初は一つのURLから始めます。

scrapy.pyファイルをテキストエディタで開き、このコードを追加して基本的なクモを作成します:

import scrapy


class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

これを行ごとに分けてみましょう:

まず、scrapyをインポートして、パッケージが提供するクラスを使用できるようにします。

次に、Scrapyが提供するSpiderクラスを使って、BrickSetSpiderというサブクラスを作成します。サブクラスは親クラスのより専門的な形と考えてください。Spiderクラスには、URLを辿り、見つけたページからデータを抽出する方法を定義するメソッドと振る舞いがありますが、どこを見ればよいのか、何のデータを探せばよいのかは分かりません。サブクラス化することで、私たちはその情報を与えることができます。

最後に、クラスをquote-spiderと名付け、スクレイパーに開始するための単一のURLを与えます:https://quotes.toscrape.com。このURLをブラウザで開くと、多くの有名な引用のページの最初のページを表示する検索結果ページに移動します。

さて、スクレイパーをテストしましょう。通常、Pythonファイルはpython path/to/file.pyのようなコマンドで実行されます。しかし、Scrapyにはスクレイパーを開始するプロセスを簡素化するための独自のコマンドラインインターフェースが備わっています。次のコマンドでスクレイパーを開始してください:

  1. scrapy runspider scraper.py

コマンドはこのような出力をします:

2022-12-02 10:30:08 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.epollreactor.EPollReactor
2022-12-02 10:30:08 [scrapy.extensions.telnet] INFO: Telnet Password: b4d94e3a8d22ede1
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 ...
 'scrapy.extensions.logstats.LogStats']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 ...
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 ...
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Spider opened
2022-12-02 10:30:08 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2022-12-02 10:30:08 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2022-12-02 10:49:32 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com> (referer: None)
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Closing spider (finished)
2022-12-02 10:30:08 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 226,
 ...
 'start_time': datetime.datetime(2022, 12, 2, 18, 30, 8, 492403)}
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Spider closed (finished)

これは大量の出力ですので、分解してみましょう。

  • スクレイパーは初期化され、URLからデータを読み取るために必要な追加コンポーネントや拡張機能をロードしました。
  • それは、start_urlsリストで提供したURLを使用して、あなたのウェブブラウザが行うようにHTMLを取得しました。
  • そのHTMLはparseメソッドに渡されましたが、デフォルトでは何もしません。私たち自身のparseメソッドを作成しなかったため、スパイダーは何も作業せずに終了します。

さあ、ページからデータを取得しましょう。

ステップ2 — ページからデータを抽出する

私たちはページを取得する非常に基本的なプログラムを作成しましたが、まだデータのスクレイピングやクローリングは行っていません。抽出するデータを与えてみましょう。

スクレイピングしたいページを見ると、次のような構造になっていることがわかります:

  • すべてのページに存在するヘッダーがあります。
  • ログインするためのリンクがあります。
  • 次に引用自体があり、表や順序付きリストのように表示されます。各引用は似た形式を持っています。

スクレイパーを書くときは、HTMLファイルのソースを確認し、その構造に慣れる必要があります。そこで、可読性のために私たちの目的に関係のないタグを削除したものを以下に示します:

quotes.toscrape.com
<body> ... <div class="quote" itemscope itemtype="http://schema.org/CreativeWork"> <span class="text" itemprop="text">“I have not failed. I&#39;ve just found 10,000 ways that won&#39;t work.”</span> <span>by <small class="author" itemprop="author">Thomas A. Edison <a href="/author/Thomas-A-Edison">(about)</a> </span> <div class="tags"> Tags: <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" / > <a class="tag" href="/tag/edison/page/1/">edison</a> <a class="tag" href="/tag/failure/page/1/">failure</a> <a class="tag" href="/tag/inspirational/page/1/">inspirational</a> <a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a> </div> </div> ... </body>

このページをスクレイピングするのは二段階のプロセスです:

  1. まず、必要なデータが含まれているページの部分を見つけて、各引用を取得します。
  2. その後、各引用からHTMLタグからデータを抽出して、必要なデータを取得します。

scrapy は、指定された セレクタ に基づいてデータを取得します。セレクタは、ページ上の1つ以上の要素を見つけるために使用できるパターンであり、そこにあるデータを操作するためのものです。scrapy は、CSSセレクタまたはXPathセレクタのいずれかをサポートしています。

現時点では、CSSセレクタを使用します。CSSはページ上のすべてのセットを見つけるのに最適だからです。HTMLを見ると、各引用がクラスquoteで指定されていることがわかります。クラスを探しているので、CSSセレクタとして.quoteを使用します。セレクタの.部分は、要素のclass属性を検索します。私たちがやるべきことは、クラス内にparseという新しいメソッドを作成し、そのセレクタをresponseオブジェクトに渡すことです、こんな風に:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        
        for quote in response.css(QUOTE_SELECTOR):
            pass

このコードはページ上のすべてのセットを取得し、それらをループしてデータを抽出します。では、これらの引用からデータを抽出して表示できるようにしましょう。

解析しているページのソースをもう一度見ると、各引用のテキストは text クラスの span 内に格納されており、引用の著者は <small> タグ内の author クラスに存在しています。

quotes.toscrape.com
... <span class="text" itemprop="text">“I have not failed. I&#39;ve just found 10,000 ways that won&#39;t work.”</span> <span>by <small class="author" itemprop="author">Thomas A. Edison ...

ループしているquoteオブジェクトには独自のcssメソッドがあるため、子要素を見つけるためのセレクタを渡すことができます。セットの名前を見つけて表示するために、次のようにコードを修正してください:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        
        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
            }

注意: extract_first()の後のトレーリングカンマはタイプミスではありません。Pythonでは、dictオブジェクトにおけるトレーリングカンマは有効な構文であり、後でアイテムを追加する余地を残す良い方法です。

このコードには二つのことが行われていることに気づくでしょう:

  • 私たちは引用と著者のセレクターに::textを追加します。これはCSSの擬似セレクターで、タグ自体ではなく、タグの内部のテキストを取得します。
  • quote.css(TEXT_SELECTOR) によって返されるオブジェクトに対して extract_first() を呼び出します。理由は、セレクターに一致する最初の要素だけが欲しいからです。これにより、要素のリストではなく、文字列が得られます。

ファイルを保存して、再度スクレイパーを実行してください:

  1. scrapy runspider scraper.py

今回は出力に引用とその著者が含まれます:

...
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein'}
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen'}
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe'}
...

この作業を続け、著者に関するページへのリンクや引用のタグに新しいセレクタを追加していきましょう。各引用のHTMLを調査することで、次のようなことがわかります:

  • 著者の紹介ページへのリンクは、名前のすぐ後に保存されたリンクにあります。
  • タグは、tags クラスを持つ div 要素内に保存された、すべて tag として分類された a タグのコレクションとして保存されています。

それでは、スクレイパーを修正してこの新しい情報を取得しましょう:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

変更を保存して、スクレイパーを再実行してください。

  1. scrapy runspider scraper.py

さて、出力には新しいデータが含まれます:

2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein', 'about': 'https://quotes.toscrape.com/author/Albert-Einstein', 'tags': ['inspirational', 'life', 'live', 'miracle', 'miracles']}
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen', 'about': 'https://quotes.toscrape.com/author/Jane-Austen', 'tags': ['aliteracy', 'books', 'classic', 'humor']}
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe', 'about': 'https://quotes.toscrape.com/author/Marilyn-Monroe', 'tags': ['be-yourself', 'inspirational']}

今、このスクレイパーをリンクをたどるスパイダーに変えましょう。

ステップ3 — 複数ページのクロール

最初のページからデータを正常に抽出しましたが、その後の結果を見るために進んでいません。クモの本来の目的は、他のページへのリンクを検出し、それらのページからもデータを取得することです。

各ページの上部と下部には、次の結果ページへのリンクを持つ小さな右向きキャレット(>)があります。これがそのHTMLです:

quotes.toscrape.com
... <nav> <ul class="pager"> <li class="next"> <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a> </li> </ul> </nav> ...

ソース内には、クラスが next の li タグがあり、そのタグの内部に次のページへのリンクを含む a タグがあります。私たちがすべきことは、そのリンクが存在する場合、スクレイパーにそのリンクをたどるように指示することです。

コードを次のように修正してください:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'
        NEXT_SELECTOR = '.next a::attr("href")'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

        next_page = response.css(NEXT_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(response.urljoin(next_page))

まず、「次のページ」リンクのセレクタを定義し、最初の一致を抽出して、それが存在するかどうかを確認します。scrapy.Requestは、新しいリクエストオブジェクトで、Scrapyはそれが次を取得して解析する必要があることを認識します。

これは、次のページに移動すると、そこで次のページへのリンクを探し、そのページで次のページへのリンクを探し、というように続けていき、次のページへのリンクが見つからなくなるまで続くことを意味します。これがウェブスクレイピングの重要な部分で、リンクを見つけてたどることです。この例では非常に直線的です。一つのページには次のページへのリンクがあり、最終ページに達するまで続きますが、タグや他の検索結果、または好きな他のURLにリンクをたどることもできます。

さて、コードを保存してスパイダーを再度実行すると、最初のセットのページを反復した後に止まるだけではないことがわかります。すべての10ページにある100の引用すべてを通過し続けます。全体の計画から見ると、大きなデータの塊ではありませんが、これで自動的にスクレイピングする新しいページを見つけるプロセスを知ることができました。

こちらがこのチュートリアルの完成したコードです。

import scrapy


class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'
        NEXT_SELECTOR = '.next a::attr("href")'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

        next_page = response.css(NEXT_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
            )

結論

このチュートリアルでは、30行未満のコードでウェブページからデータを抽出する完全機能のスパイダーを構築しました。これは素晴らしいスタートですが、このスパイダーでできる楽しむことはたくさんあります。これを思考し、実験するための十分な材料になるはずです。Scrapyに関する詳細情報が必要な場合は、Scrapyの公式ドキュメントを確認してください。ウェブからデータを扱う際の詳細については、「Beautiful SoupとPython 3を使ってウェブページをスクレイピングする方法」を参照してください。

関連記事: