序章

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

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

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

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

前提条件

このチュートリアルを完了するには、Python3のローカル開発環境が必要です。 Python 3のローカルプログラミング環境をインストールおよびセットアップする方法に従って、必要なものをすべて構成できます。

ステップ1—基本的なスクレーパーを作成する

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

  1. Webページを体系的に見つけてダウンロードします。
  2. あなたはそれらのウェブページを取り、それらから情報を抽出します。

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

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

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

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

Scrapyは、ほとんどのPythonパッケージと同様に、PyPI(別名 pip). PyPI(Python Package Index)は、公開されているすべてのPythonソフトウェアのコミュニティ所有のリポジトリです。

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

  1. pip install scrapy

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

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

  1. mkdir brickset-scraper

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

  1. cd brickset-scraper

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

  1. touch scraper.py

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

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

  • name —クモの名前です。
  • start_urls —クロールを開始するURLのリスト。 1つのURLから始めます。

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

scarer.py
import scrapy


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

これを1行ずつ分割してみましょう。

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

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

次に、クモに名前を付けます brickset_spider.

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

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

  1. scrapy runspider scraper.py

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

Output
2016-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) <GET http://brickset.com/sets/year-2016> (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からのデータの読み取りを処理するために必要な追加のコンポーネントと拡張機能を初期化し、ロードしました。
  • で提供したURLを使用しました start_urls Webブラウザと同じように、HTMLを一覧表示して取得します。
  • そのHTMLを parse デフォルトでは何もしないメソッド。 自分で書いたことがないので parse 方法では、スパイダーは何もしなくても終了します。

次に、ページからいくつかのデータを取得しましょう。

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

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

スクレイプしたいページを見ると、次の構造になっていることがわかります。

  • すべてのページに存在するヘッダーがあります。
  • 一致する数、検索対象、サイトのパンくずリストなど、いくつかのトップレベルの検索データがあります。
  • 次に、セット自体があり、テーブルまたは順序付きリストのように表示されます。 各セットの形式は似ています。

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

brickset.com/sets/year-2016
<body> <section class="setlist"> <article class='set'> <a href="https://images.brickset.com/sets/large/10251-1.jpg?201510121127" class="highslide plain mainimg" onclick="return hs.expand(this)"><img src="https://images.brickset.com/sets/small/10251-1.jpg?201510121127" title="10251-1: Brick Bank" onError="this.src='/assets/images/spacer.png'" /></a> <div class="highslide-caption"> <h1>Brick Bank</h1><div class='tags floatleft'><a href='/sets/10251-1/Brick- Bank'>10251-1</a> <a href='/sets/theme-Creator-Expert'>Creator Expert</a> <a class='subtheme' href='/sets/theme-Creator-Expert/subtheme-Modular- Buildings'>Modular Buildings</a> <a class='year' href='/sets/theme-Creator- Expert/year-2016'>2016</a> </div><div class='floatright'>&copy;2016 LEGO Group</div> <div class="pn"> <a href="#" onclick="return hs.previous(this)" title="Previous (left arrow key)">&#171; Previous</a> <a href="#" onclick="return hs.next(this)" title="Next (right arrow key)">Next &#187;</a> </div> </div> ... </article> </section> </body>

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

  1. まず、必要なデータが含まれているページの部分を探して、各LEGOセットを取得します。
  2. 次に、セットごとに、HTMLタグからデータを取得して、必要なデータを取得します。

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

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

scarer.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
<h1>Brick Bank</h1><div class='tags floatleft'><a href='/sets/10251-1/Brick-Bank'>10251-1</a>

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

scarer.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(),
            }

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

このコードでは、次の2つのことが起こっていることに気付くでしょう。

  • 追加します ::text 名前のセレクターに。 これは、CSS pseudo-selector であり、内のテキストをフェッチします。 a タグ自体ではなくタグ。
  • 私たちは extract_first() によって返されたオブジェクトに brickset.css(NAME_SELECTOR) セレクターに一致する最初の要素が必要だからです。 これにより、要素のリストではなく、stringが得られます。

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

  1. 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'} ...

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

特定のセットのHTMLをもう一度見てください。

brickset.com/sets/year-2016
<article class="set"> <a class="highslide plain mainimg" href="http://images.brickset.com/sets/images/10251-1.jpg?201510121127" onclick="return hs.expand(this)"> <img src="https://images.brickset.com/sets/small/10251-1.jpg?201510121127" title="10251-1: Brick Bank"></a> ... <div class="meta"> <h1><a href="/sets/10251-1/Brick-Bank"><span>10251:</span> Brick Bank</a> </h1> ... <div class="col"> <dl> <dt>Pieces</dt> <dd><a class="plain" href="/inventories/10251-1">2380</a></dd> <dt>Minifigs</dt> <dd><a class="plain" href="/minifigs/inset-10251-1">5</a></dd> ... </dl> </div> ... </div> </article>

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

  • セットの画像はに保存されます src の属性 img 内部のタグ a セットの先頭にタグを付けます。 各セットの名前を取得したときと同じように、別のCSSセレクターを使用してこの値をフェッチできます。
  • ピースの数を取得するのは少し難しいです。 あります dt テキストを含むタグ Pieces、そして dd それに続くタグには、実際の個数が含まれています。 これを取得するには、XMLをトラバースするためのクエリ言語である XPath を使用します。これは、CSSセレクターを使用して表現するには複雑すぎるためです。
  • セット内のミニフィグの数を取得することは、ピースの数を取得することに似ています。 あります dt テキストを含むタグ Minifigs、続いて dd その直後に番号をタグ付けします。

それでは、この新しい情報を取得するためにスクレーパーを変更しましょう。

scarer.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(),
            }

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

  1. scrapy runspider scraper.py

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

Output
2016-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'}

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

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

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

各ページの上部と下部に少し正しいカラットがあることに気付くでしょう(>)結果の次のページにリンクします。 そのためのHTMLは次のとおりです。

brickset.com/sets/year-2016
<ul class="pagelength"> ... <li class="next"> <a href="http://brickset.com/sets/year-2017/page-2">&#8250;</a> </li> <li class="last"> <a href="http://brickset.com/sets/year-2016/page-32">&#187;</a> </li> </ul>

ご覧のとおり、 li のクラスのタグ next、そしてそのタグの中には a 次のページへのリンクをタグ付けします。 私たちがしなければならないのは、スクレーパーにそのリンクが存在する場合はそれをたどるように指示することだけです。

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

scarer.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
            )

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

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

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

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

scarer.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. ご想像のとおり、現時点では2016年の結果のみを解析しています。 2016 一部の http://brickset.com/sets/year-2016 —他の年の結果をどのようにクロールしますか?
  2. ほとんどのセットには小売価格が含まれています。 そのセルからどのようにデータを抽出しますか? どうやってそれから生の数字を得るのですか? ヒント:データは dt ピースとミニフィグの数と同じように。
  3. ほとんどの結果には、セットまたはそのコンテキストに関するセマンティックデータを指定するタグが含まれています。 1つのセットに複数のタグがある場合、これらをどのようにクロールしますか?

それはあなたが考えて実験するのに十分なはずです。 Scrapyの詳細については、Scrapyの公式ドキュメントをご覧ください。 Webからのデータの操作の詳細については、「美しいスープとPython3でWebページをスクレイピングする方法」のチュートリアルを参照してください。