ScrapyとPython3を使用してWebページをクロールする方法
序章
Webスクレイピングは、WebクロールやWebスパイダリング、または「プログラムでWebページのコレクションを調べてデータを抽出する」と呼ばれることが多く、Web上のデータを操作するための強力なツールです。
Webスクレイパーを使用すると、一連の製品に関するデータをマイニングしたり、大量のテキストや定量的なデータを取得したり、公式APIを使用せずにサイトからデータを取得したり、個人的な好奇心を満たすことができます。
このチュートリアルでは、遊び心のあるデータセットを探索しながら、スクレイピングとスパイダリングのプロセスの基本について学習します。 レゴセットに関する情報が掲載されているコミュニティ運営のサイトBrickSetを使用します。 このチュートリアルを終了すると、Bricksetの一連のページをウォークスルーし、各ページからLEGOセットに関するデータを抽出して、画面にデータを表示する、完全に機能するPythonWebスクレイパーが完成します。
スクレイパーは簡単に拡張できるので、スクレイパーをいじって、Webからデータをスクレイピングする独自のプロジェクトの基盤として使用できます。
前提条件
このチュートリアルを完了するには、Python3のローカル開発環境が必要です。 Python 3のローカルプログラミング環境をインストールおよびセットアップする方法に従って、必要なものをすべて構成できます。
ステップ1—基本的なスクレーパーを作成する
スクレイピングは2段階のプロセスです。
- Webページを体系的に見つけてダウンロードします。
- あなたはそれらのウェブページを取り、それらから情報を抽出します。
これらのステップは両方とも、多くの言語でさまざまな方法で実装できます。
モジュールまたはプログラミング言語が提供するライブラリを使用してスクレーパーを最初から作成できますが、スクレーパーがより複雑になるにつれて、いくつかの潜在的な頭痛の種に対処する必要があります。 たとえば、一度に複数のページをクロールできるように、同時実行性を処理する必要があります。 おそらく、スクレイピングされたデータをCSV、XML、JSONなどのさまざまな形式に変換する方法を理解する必要があります。 また、特定の設定とアクセスパターンを必要とするサイトに対処しなければならない場合があります。
これらの問題を処理する既存のライブラリの上にスクレーパーを構築すると、運が良くなります。 このチュートリアルでは、PythonとScrapyを使用してスクレーパーを作成します。
Scrapyは、最も人気があり強力なPythonスクレイピングライブラリの1つです。 スクレイピングには「バッテリーを含む」アプローチを採用しています。つまり、すべてのスクレイパーが必要とする多くの一般的な機能を処理するため、開発者は毎回車輪の再発明を行う必要がありません。 それはスクレイピングを素早く楽しいプロセスにします!
Scrapyは、ほとんどのPythonパッケージと同様に、PyPI(pip
とも呼ばれます)上にあります。 PyPI(Python Package Index)は、公開されているすべてのPythonソフトウェアのコミュニティ所有のリポジトリです。
このチュートリアルの前提条件で概説されているようなPythonインストールがある場合は、すでにpip
がマシンにインストールされているため、次のコマンドでScrapyをインストールできます。
pip install scrapy
インストールで問題が発生した場合、またはpip
を使用せずにScrapyをインストールしたい場合は、公式インストールドキュメントを確認してください。
Scrapyをインストールしたら、プロジェクト用の新しいフォルダーを作成しましょう。 ターミナルでこれを行うには、次を実行します。
mkdir brickset-scraper
次に、作成したばかりの新しいディレクトリに移動します。
cd brickset-scraper
次に、scraper.py
という名前のスクレーパー用の新しいPythonファイルを作成します。 このチュートリアルでは、すべてのコードをこのファイルに配置します。 このファイルは、次のようにtouch
コマンドを使用してターミナルで作成できます。
touch scraper.py
または、テキストエディタまたはグラフィカルファイルマネージャを使用してファイルを作成できます。
まず、Scrapyを基盤として使用する非常に基本的なスクレーパーを作成します。 そのために、Scrapyが提供する基本的なスパイダークラスであるscrapy.Spider
をサブクラス化するPythonクラスを作成します。 このクラスには、次の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行ずつ分割してみましょう。
まず、パッケージが提供するクラスを使用できるように、 import scrapy
を実行します。
次に、Scrapyが提供するSpider
クラスを取得し、BrickSetSpider
と呼ばれるサブクラスを作成します。 サブクラスは、その親クラスのより特殊な形式と考えてください。 Spider
サブクラスには、URLをたどり、見つけたページからデータを抽出する方法を定義するメソッドと動作がありますが、どこを探すか、どのデータを探すかはわかりません。 サブクラス化することで、その情報を提供できます。
次に、スパイダーにbrickset_spider
という名前を付けます。
最後に、スクレーパーにhttp://brickset.com/sets/year-2016から開始する単一のURLを指定します。 ブラウザでそのURLを開くと、検索結果ページが表示され、LEGOセットを含む多くのページの最初のページが表示されます。
それでは、スクレーパーをテストしてみましょう。 通常、Pythonファイルは、python path/to/file.py
などのコマンドを実行して実行します。 ただし、Scrapyには、スクレーパーの起動プロセスを合理化するための独自のコマンドラインインターフェイスが付属しています。 次のコマンドでスクレーパーを起動します。
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) <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からのデータの読み取りを処理するために必要な追加のコンポーネントと拡張機能を初期化し、ロードしました。
start_urls
リストで提供したURLを使用し、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'>©2016 LEGO Group</div> <div class="pn"> <a href="#" onclick="return hs.previous(this)" title="Previous (left arrow key)">« Previous</a> <a href="#" onclick="return hs.next(this)" title="Next (right arrow key)">Next »</a> </div> </div> ... </article> </section> </body>
このページのスクレイピングは2段階のプロセスです。
- まず、必要なデータが含まれているページの部分を探して、各LEGOセットを取得します。
- 次に、セットごとに、HTMLタグからデータを取得して、必要なデータを取得します。
scrapy
は、指定したセレクターに基づいてデータを取得します。 セレクターは、ページ上の1つ以上の要素を検索するために使用できるパターンであり、要素内のデータを操作できます。 scrapy
は、CSSセレクターまたはXPathセレクターのいずれかをサポートします。
CSSはより簡単なオプションであり、ページ上のすべてのセットを見つけるのに最適であるため、ここではCSSセレクターを使用します。 このページのHTMLを見ると、各セットがクラスset
で指定されていることがわかります。 クラスを探しているので、CSSセレクターには.set
を使用します。 次のように、そのセレクターを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>
ループしている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
を追加します。 これは、タグ自体ではなく、a
タグの内のテキストをフェッチする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'} ...
画像、ピース、ミニチュアフィギュア用の新しいセレクター、またはセットに付属のミニフィグを追加して、これをさらに拡張していきましょう。
特定のセットの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="http://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>
このコードを調べると、いくつかのことがわかります。
- セットの画像は、セットの先頭にある
a
タグ内のimg
タグのsrc
属性に保存されます。 各セットの名前を取得したときと同じように、別のCSSセレクターを使用してこの値をフェッチできます。 - ピースの数を取得するのは少し難しいです。 テキスト
Pieces
を含むdt
タグと、それに続く実際の個数を含む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(), }
変更を保存して、スクレーパーを再度実行します。
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'}
それでは、このスクレーパーをリンクをたどるスパイダーに変えましょう。
ステップ3—複数のページをクロールする
その最初のページからデータを正常に抽出しましたが、残りの結果を確認するためにそれを超えて進んでいません。 スパイダーの要点は、他のページへのリンクを検出してトラバースし、それらのページからもデータを取得することです。
各ページの上部と下部には、結果の次のページにリンクする少し右のカラット(>
)があります。 そのためのHTMLは次のとおりです。
brickset.com/sets/year-2016<ul class="pagelength"> ... <li class="next"> <a href="http://brickset.com/sets/year-2017/page-2">›</a> </li> <li class="last"> <a href="http://brickset.com/sets/year-2016/page-32">»</a> </li> </ul>
ご覧のとおり、クラスがnext
のli
タグがあり、そのタグの中に、次のページへのリンクが付いた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 )
まず、「次のページ」リンクのセレクターを定義し、最初の一致を抽出して、それが存在するかどうかを確認します。 scrapy.Request
は「ねえ、このページをクロールします」と返される値であり、callback=self.parse
は「このページから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ページからデータを抽出する完全に機能するスパイダーを作成しました。 それは素晴らしいスタートですが、このスパイダーでできる楽しいことがたくさんあります。 作成したコードを拡張する方法をいくつか紹介します。 彼らはあなたにデータをこする練習をするでしょう。
http://brickset.com/sets/year-2016
の2016
の部分から推測できるように、現時点では2016年の結果のみを解析しています。他の年の結果をどのようにクロールしますか?- ほとんどのセットには小売価格が含まれています。 そのセルからどのようにデータを抽出しますか? どうやってそれから生の数字を得るのですか? ヒント:
dt
には、ピースやミニフィグの数と同じようにデータが表示されます。 - ほとんどの結果には、セットまたはそのコンテキストに関するセマンティックデータを指定するタグが含まれています。 1つのセットに複数のタグがある場合、これらをどのようにクロールしますか?
それはあなたが考えて実験するのに十分なはずです。 Scrapyの詳細については、Scrapyの公式ドキュメントをご覧ください。 Webからのデータの操作の詳細については、「美しいスープとPython3でWebページをスクレイピングする方法」のチュートリアルを参照してください。