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

前書き

多くの場合、WebクロールまたはWebスパイダーと呼ばれるWebスクレイピング、または「プログラムでWebページのコレクションを調べてデータを抽出する」は、Web上のデータを操作するための強力なツールです。

Webスクレイパーを使用すると、一連の製品に関するデータをマイニングしたり、大量のテキストや定量データを再生したり、公式APIを使用せずにサイトからデータを取得したり、個人的な好奇心を満たすことができます。

このチュートリアルでは、遊び心のあるデータセットを探索しながら、スクレイピングおよびスパイダープロセスの基本について学習します。 LEGOセットに関する情報を含むコミュニティ運営のサイトであるBrickSetを使用します。 このチュートリアルの終わりまでに、Bricksetの一連のページをウォークスルーし、各ページからLEGOセットに関するデータを抽出して、画面にデータを表示する、完全に機能するPython Webスクレイパーができます。

スクレーパーは簡単に拡張できるので、いじくり回して、Webからデータをスクレイピングする独自のプロジェクトの基盤として使用できます。

前提条件

このチュートリアルを完了するには、Python 3のローカル開発環境が必要です。 How To Install and Set Up a Local Programming Environment for Python 3に従って、必要なすべてを構成できます。

[[step-1 -—- creating-a-basic-scraper]] ==ステップ1—基本的なスクレーパーの作成

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

  1. Webページを体系的に見つけてダウンロードします。

  2. それらのWebページを取得し、そこから情報を抽出します。

これらの手順は両方とも、多くの言語でさまざまな方法で実装できます。

プログラミング言語が提供するmodulesまたはライブラリを使用してスクレイパーを最初から作成できますが、スクレイパーがより複雑になるにつれて、いくつかの潜在的な頭痛の種に対処する必要があります。 たとえば、一度に複数のページをクロールできるように、並行性を処理する必要があります。 おそらく、スクレイピングしたデータをCSV、XML、JSONなどのさまざまな形式に変換する方法を理解する必要があります。 また、特定の設定とアクセスパターンを必要とするサイトに対処しなければならない場合があります。

これらの問題を処理する既存のライブラリの上にスクレーパーを構築すると、幸運が得られます。 このチュートリアルでは、PythonとScrapyを使用してスクレーパーを作成します。

Scrapyは、最も人気があり強力なPythonスクレイピングライブラリの1つです。スクレイピングには「バッテリーを含む」アプローチが必要です。つまり、すべてのスクレイパーが必要とする多くの共通機能を処理するため、開発者は毎回車輪を作り直す必要がありません。 スクレイピングが迅速で楽しいプロセスになります!

Scrapyは、ほとんどのPythonパッケージと同様に、PyPI(pipとも呼ばれます)上にあります。 PythonパッケージインデックスであるPyPIは、公開されているすべてのPythonソフトウェアのコミュニティ所有のリポジ​​トリです。

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

pip install scrapy

インストールで問題が発生した場合、またはpipを使用せずにScrapyをインストールしたい場合は、official installation docsを確認してください。

Scrapyをインストールしたら、プロジェクト用の新しいフォルダーを作成しましょう。 ターミナルでこれを実行するには、次を実行します。

mkdir brickset-scraper

次に、作成した新しいディレクトリに移動します。

cd brickset-scraper

次に、scraper.pyという名前のスクレーパー用の新しいPythonファイルを作成します。 このチュートリアルでは、すべてのコードをこのファイルに配置します。 このファイルは、次のように、touchコマンドを使用してターミナルで作成できます。

touch scraper.py

または、テキストエディタまたはグラフィカルファイルマネージャを使用してファイルを作成できます。

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

  • name —スパイダーの名前です。

  • start_urls —クロールを開始するURLのlist。 1つのURLから始めます。

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

scraper.py

import scrapy


class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

これを行ごとに分けましょう。

まず、パッケージが提供するクラスを使用できるように、importscrapyを実行します。

次に、Scrapyによって提供されるSpiderクラスを取得し、それからBrickSetSpiderと呼ばれるsubclassを作成します。 サブクラスは、親クラスのより特殊な形式と考えてください。 Spiderサブクラスには、URLを追跡し、見つかったページからデータを抽出する方法を定義するメソッドと動作がありますが、どこを探すか、どのデータを探すかはわかりません。 サブクラス化することで、その情報を提供できます。

次に、スパイダーにbrickset_spiderという名前を付けます。

最後に、スクレーパーに開始する単一のURL(http://brickset.com/sets/year-2016)を指定します。 ブラウザでそのURLを開くと、検索結果ページが表示され、LEGOセットを含む多くのページの最初のページが表示されます。

それでは、スクレーパーをテストしてみましょう。 通常、Pythonファイルはpython path/to/file.pyなどのコマンドを実行して実行します。 ただし、Scrapyには、スクレーパーを開始するプロセスを合理化するためのits own command line interfaceが付属しています。 次のコマンドでスクレーパーを起動します。

scrapy runspider scraper.py

次のようなものが表示されます。

Output2016-09-22 23:37:45 [scrapy] INFO: Scrapy 1.1.2 started (bot: scrapybot)
2016-09-22 23:37:45 [scrapy] INFO: Overridden settings: {}
2016-09-22 23:37:45 [scrapy] INFO: Enabled extensions:
['scrapy.extensions.logstats.LogStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.corestats.CoreStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 ...
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 ...
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2016-09-22 23:37:45 [scrapy] INFO: Enabled item pipelines:
[]
2016-09-22 23:37:45 [scrapy] INFO: Spider opened
2016-09-22 23:37:45 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-22 23:37:45 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-22 23:37:47 [scrapy] DEBUG: Crawled (200)  (referer: None)
2016-09-22 23:37:47 [scrapy] INFO: Closing spider (finished)
2016-09-22 23:37:47 [scrapy] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 224,
 'downloader/request_count': 1,
 ...
 'scheduler/enqueued/memory': 1,
 'start_time': datetime.datetime(2016, 9, 23, 6, 37, 45, 995167)}
2016-09-22 23:37:47 [scrapy] INFO: Spider closed (finished)

それは大量の出力なので、それを分解しましょう。

  • スクレーパーは、URLからのデータの読み取りを処理するために必要な追加のコンポーネントと拡張機能を初期化し、ロードしました。

  • Webブラウザーと同じように、start_urlsリストで指定したURLを使用し、HTMLを取得しました。

  • そのHTMLをparseメソッドに渡しましたが、デフォルトでは何も実行されません。 独自のparseメソッドを作成したことがないため、スパイダーは何もしなくても終了します。

次に、ページからデータを取得します。

[[step-2 -—- extracting-data-from-a-page]] ==ステップ2—ページからのデータの抽出

ページをプルダウンする非常に基本的なプログラムを作成しましたが、まだスクレイピングやスパイダーは行いません。 抽出するデータを与えましょう。

the page we want to scrapeを見ると、次の構造になっていることがわかります。

  • すべてのページにヘッダーがあります。

  • 一致の数、検索対象、サイトのパンくずなど、トップレベルの検索データがいくつかあります。

  • 次に、セット自体があり、テーブルまたは順序付きリストのように表示されます。 各セットには同様の形式があります。

スクレーパーを作成するときは、HTMLファイルのソースを見て、構造に慣れることをお勧めします。 そのため、読みやすくするためにいくつかのものを削除しました。

brickset.com/sets/year-2016
  

このページのスクレイピングは、2段階のプロセスです。

  1. 最初に、必要なデータがあるページの部分を探して、各LEGOセットを取得します。

  2. 次に、各セットについて、HTMLタグからデータを引き出して、必要なデータを取得します。

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

CSSは簡単なオプションであり、ページ上のすべてのセットを見つけるのに最適なので、ここではCSSセレクターを使用します。 ページのHTMLを見ると、各セットがクラスsetで指定されていることがわかります。 クラスを探しているので、CSSセレクターに.setを使用します。 次のように、そのセレクターをresponseオブジェクトに渡すだけです。

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):
            pass

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

解析しているページのsourceをもう一度見ると、各セットの名前が各セットのh1タグ内に格納されていることがわかります。

brickset.com/sets/year-2016

Brick Bank

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

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
            }

[.note]#Noteextract_first()の後の末尾のコンマはタイプミスではありません。 間もなくこのセクションにさらに追加する予定なので、後でこのセクションに簡単に追加できるように、カンマを残しました。

このコードでは次の2つのことが行われています。

  • 名前のセレクターに::textを追加します。 これは、タグ自体ではなく、aタグのテキストinsideをフェッチするCSSpseudo-selectorです。

  • セレクターに一致する最初の要素が必要なため、brickset.css(NAME_SELECTOR)によって返されるオブジェクトに対してextract_first()を呼び出します。 これにより、要素のリストではなく、stringが得られます。

ファイルを保存し、スクレーパーを再度実行します。

scrapy runspider scraper.py

今回は、出力にセットの名前が表示されます。

Output...
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Brick Bank'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Volkswagen Beetle'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Big Ben'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Winter Holiday Train'}
...

画像、ピース、ミニチュアフィギュア、またはセットに付属するminifigsの新しいセレクターを追加して、これをさらに拡張していきましょう。

特定のセットのHTMLをもう一度見てみましょう。

brickset.com/sets/year-2016

このコードを調べると、いくつかのことがわかります。

  • セットのイメージは、セットの開始時にaタグ内のimgタグのsrc属性に格納されます。 各セットの名前を取得したときと同じように、別のCSSセレクターを使用してこの値を取得できます。

  • ピースの数を取得するのは少し難しいです。 テキストPiecesを含むdtタグと、それに続く実際のピース数を含むddタグがあります。 複雑すぎてCSSセレクターを使用して表現できないため、XMLをトラバースするためのクエリ言語であるXPathを使用してこれを取得します。

  • セット内のミニフィグの数を取得することは、ピースの数を取得することに似ています。 テキストMinifigsを含むdtタグがあり、その直後に番号が付いたddタグが続きます。

したがって、スクレーパーを変更して、この新しい情報を取得しましょう。

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

変更を保存して、スクレーパーを再度実行します。

scrapy runspider scraper.py

これで、プログラムの出力に新しいデータが表示されます。

Output2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': '5', 'pieces': '2380', 'name': 'Brick Bank', 'image': 'http://images.brickset.com/sets/small/10251-1.jpg?201510121127'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '1167', 'name': 'Volkswagen Beetle', 'image': 'http://images.brickset.com/sets/small/10252-1.jpg?201606140214'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '4163', 'name': 'Big Ben', 'image': 'http://images.brickset.com/sets/small/10253-1.jpg?201605190256'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'Winter Holiday Train', 'image': 'http://images.brickset.com/sets/small/10254-1.jpg?201608110306'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'XL Creative Brick Box', 'image': '/assets/images/misc/blankbox.gif'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '583', 'name': 'Creative Building Set', 'image': 'http://images.brickset.com/sets/small/10702-1.jpg?201511230710'}

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

[[step-3 -—- crawling-multiple-pages]] ==ステップ3—複数のページをクロールする

その最初のページからデータを正常に抽出しましたが、残りの結果を確認するためにそれを過ぎて進んでいません。 スパイダーの目的は、他のページへのリンクを検出してトラバースし、それらのページからデータを取得することです。

各ページの上部と下部には、結果の次のページにリンクする少し右のカラット(>)があります。 そのためのHTMLは次のとおりです。

brickset.com/sets/year-2016

ご覧のとおり、クラスnextliタグがあり、そのタグ内に次のページへのリンクが記載されたaタグがあります。 私たちがしなければならないのは、スクレーパーにそのリンクが存在する場合はそれに従うように指示することです。

コードを次のように変更します。

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

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

まず、「次のページ」リンクのセレクターを定義し、最初の一致を抽出して、存在するかどうかを確認します。 scrapy.Requestは「ねえ、このページをクロールして」と返す値であり、callback=self.parseは「このページからHTMLを取得したら、それをこのメソッドに返して解析できるようにします。データを抽出して、次のページを見つけてください。」

つまり、次のページに移動したら、次のページへのリンクを探し、そのページで次のページへのリンクを探します。次のページへのリンク。 これは、Webスクレイピングの重要な部分です。リンクを見つけて、フォローします。 この例では、非常に直線的です。最後のページに到達するまで、あるページには次のページへのリンクがありますが、タグへのリンク、その他の検索結果、またはその他の任意のURLをたどることができます。

ここで、コードを保存してスパイダーを再度実行すると、セットの最初のページを反復処理するだけで停止しないことがわかります。 23ページで779試合すべてを続けています! 物事の壮大なスキームでは、それは巨大なデータの塊ではありませんが、スクレイプする新しいページを自動的に見つけるプロセスを知っています。

Python固有の強調表示を使用した、このチュートリアルの完成したコードは次のとおりです。

scraper.py

import scrapy


class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

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

結論

このチュートリアルでは、30行未満のコードでWebページからデータを抽出する完全に機能するスパイダーを構築しました。 これは素晴らしいスタートですが、このクモでできることはたくさんあります。 作成したコードを拡張する方法をいくつか紹介します。 データのスクレイピングの練習を行います。

  1. http://brickset.com/sets/year-20162016の部分から推測できるように、現時点では2016年の結果のみを解析しています。他の年の結果をどのようにクロールしますか?

  2. ほとんどのセットには小売価格が含まれています。 そのセルからデータをどのように抽出しますか? どのようにして生の数字を取得しますか? Hint:ピースやミニフィグの数と同じように、dtにデータが表示されます。

  3. ほとんどの結果には、セットまたはそのコンテキストに関するセマンティックデータを指定するタグがあります。 単一のセットに複数のタグがある場合、これらをどのようにクロールしますか?

これで、考えて実験することができます。 Scrapyの詳細が必要な場合は、Scrapy’s official docsを確認してください。 Webからのデータの操作の詳細については、"How To Scrape Web Pages with Beautiful Soup and Python 3”に関するチュートリアルを参照してください。