ScrapyとPython 3を使ってウェブページをクローリングする方法
はじめに
ウェブスクレイピングは、ウェブのコレクションをプログラムで処理し、データを抽出する行為であり、データをウェブ上で取り扱うための強力なツールとされています。
ウェブスクレイパーを使えば、特定の製品についてのデータを採取したり、大量のテキストや数量データを利用したり、公式APIのないサイトからデータを抽出したり、または単に自分自身の好奇心を満たすことができます。
このチュートリアルでは、遊び心のあるデータセットを探索しながら、スクレイピングとスパイダリングの基礎について学びます。私たちは「Quotes to Scrape」というデータベースを使用します。これは、ウェブスパイダをテストするために設計されたサイトにホストされている引用の集まりです。このチュートリアルの最後までに、引用を含む一連のページを進みながら、スクリーンに表示するPythonのウェブスクレイパーを完全に動作させることができるでしょう。
以下の要点を日本語で述べると、次のようになります:
スクレーパーは簡単に拡張可能なため、それを利用してウェブからデータをスクレイピングする自分自身のプロジェクトの基盤として使用することができます。
前提条件
Python 3のために、このチュートリアルを完了するには、ローカル開発環境が必要です。必要なすべてを設定するために、「Python 3のためのローカルプログラミング環境のインストールとセットアップ」を参照してください。
Step 1 ー 基本のスクレイパーの作成
スクレイピングは、2つのステップで行われるプロセスです。
-
- ウェブページを系統的に見つけてダウンロードすること。
- ダウンロードしたページから情報を抽出すること。
それらの手順は、多言語でさまざまな方法で実装することができます。
プログラミング言語で提供されるモジュールやライブラリを使って、ゼロからスクレイパーを構築することができます。しかし、スクレイパーがより複雑になると、いくつかの問題に対処する必要が出てきます。たとえば、同時に複数のページをクロールできるように並行処理を扱う必要があります。スクレイプしたデータをCSV、XML、またはJSONといった異なる形式に変換する方法を見つける必要があるでしょう。また、特定の設定とアクセスパターンを要求するサイトと対処する必要もあります。
あなたがそれらの問題を自動で処理する既存のライブラリの上にスクレイパーを構築すれば、運よく成功するでしょう。このチュートリアルでは、PythonとScrapyを使用してスクレイパーを作成します。
Scrapyは、最も人気があり強力なPythonスクレイピングライブラリの1つであり、スクレイピングにおいて「バッテリーが付属している」というアプローチを採用しています。つまり、一般的な機能の多くを取り扱い、開発者が毎回車輪を再発明する必要がないようにしています。
スクラッピングは、他の多くのPythonパッケージと同様、PyPI(ピップとも呼ばれる)にあります。PyPIは、Pythonソフトウェアのすべての公開されたものが集まるコミュニティ所有のリポジトリであるPython Package Indexです。
もし、このチュートリアルの前提条件で説明されているようなPythonのインストールがされているなら、既にマシンにはpipがインストールされていますので、以下のコマンドでScrapyをインストールすることができます。
- pip install scrapy
もしインストールに関する問題が生じた場合や、pipを使用せずにScrapyをインストールしたい場合は、公式のインストールドキュメントを参照してください。
Scrapyをインストールした後、プロジェクト用の新しいフォルダを作成しましょう。ターミナルで以下のコマンドを実行することで、これを行うことができます。
- mkdir quote-scraper
今、作成した新しいディレクトリに移動してください。
- cd quote-scraper
その後、スクレーパー用の新しいPythonファイルscraper.pyを作成します。このチュートリアルでは、このファイルにすべてのコードを配置します。お好きな編集ソフトウェアを使用してこのファイルを作成してください。
プロジェクトを始めるにあたり、Scrapyを基にした非常に基本的なスクレイパーを作成します。それを行うためには、Scrapyが提供する基本的なスパイダークラスであるscrapy.Spiderをサブクラス化するPythonクラスを作成する必要があります。このクラスには2つの必須属性があります。
- name — just a name for the spider.
- start_urls — a list of URLs that you start to crawl from. We’ll start with one URL.
テキストエディタでscrapy.pyファイルを開き、基本的なスパイダーを作成するためにこのコードを追加してください。
import scrapy
class QuoteSpider(scrapy.Spider):
name = 'quote-spdier'
start_urls = ['https://quotes.toscrape.com']
行ごとにこれを分解しよう。「行ごとに」は単語〈line by line〉、そして「分解しよう」は単語〈break down〉を表しています。
最初に、パッケージが提供するクラスを使用するために、私たちは Scrapy をインポートします。
次に、Scrapyが提供するSpiderクラスを取り、それをBrickSetSpiderと呼ばれるサブクラスにします。サブクラスは、親クラスのより専門化された形態と考えることができます。Spiderクラスには、URLの参照方法やページからデータを抽出する方法を定義するメソッドや振る舞いがありますが、どこを探すかやどのデータを探すかはわかりません。それをサブクラス化することで、その情報を与えることができます。
最後に、クラスをquote-spiderと名付け、スクレイパーにはhttps://quotes.toscrape.comから開始する単一のURLを与えます。もしブラウザでそのURLを開くと、有名な引用の検索結果ページが表示され、複数ページの最初のページが表示されます。
今、スクレイパーをテストしてみてください。通常、Pythonファイルはpython path/to/file.pyのようなコマンドで実行されます。しかし、Scrapyには独自のコマンドラインインターフェースが付属しており、スクレイパーの起動プロセスを簡略化するためのものです。以下のコマンドでスクレイパーを起動してください。
- 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)
たくさんのアウトプットがあるので、細かく分けてみましょう。
- The scraper initialized and loaded additional components and extensions it needed to handle reading data from URLs.
- It used the URL we provided in the start_urls list and grabbed the HTML, just like your web browser would do.
- It passed that HTML to the parse method, which doesn’t do anything by default. Since we never wrote our own parse method, the spider just finishes without doing any work.
今、ページからデータを取得しましょう。 (Ima, pēji kara dēta o shutoku shimashou.)
ステップ2 — ページからデータを抽出する
私たちは非常に基本的なプログラムを作成しましたが、まだスクレイピングやスパイダリングは行っていません。データを抽出するようにしましょう。
スクレイプしたいページを見ると、次のような構造があることがわかります。
- There’s a header that’s present on every page.
- There’s a link to log in.
- Then there are the quotes themselves, displayed in what looks like a table or ordered list. Each quote has a similar format.
ウェブスクレイピングを行う際には、HTMLファイルのソースを見て、その構造について理解する必要があります。こちらには、私たちの目標に関係のないタグが削除されているため、読みやすさのために提示します。
<body> … <div class=“quote“ itemscope itemtype=“http://schema.org/CreativeWork“> <span class=“text“ itemprop=“text“>“I have not failed. I've just found 10,000 ways that won't work.”</span> <span>by <small class=“author“ itemprop=“author“>Thomas A. Edison</small> <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>
このページのスクレイピングは2つのステップで行われます。
-
- まず、私たちが必要とするデータがあるページの部分を見つけて、引用を取得します。
- そして、それぞれの引用について、HTMLタグからデータを抜き出して、私たちが必要なデータを取得します。
Scrapyは、提供されたセレクタに基づいてデータを取得します。セレクタは、ページの中の1つまたは複数の要素を見つけるために使用できるパターンです。その後、要素内のデータを処理することができます。Scrapyは、CSSセレクタまたはXPathセレクタのどちらかをサポートしています。
今のところ、すべてのセットを見つけるのにCSSセレクタを使用します。HTMLを見ると、それぞれの引用はクラスquoteで指定されていることがわかります。クラスを探しているので、CSSセレクタには.quoteを使用します。セレクタの.の部分は、要素のクラス属性を検索します。私たちのクラスにはparseという新しいメソッドを作成し、そのセレクタをレスポンスオブジェクトに渡すだけです。
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
このコードは、ページ上のすべてのセットを取得し、それらからデータを抽出するためにループ処理を行います。では、引用文からデータを抽出して表示しましょう。
ページの解析元をもう一度見ると、各引用のテキストは、テキストクラスを持つ内に保存されており、引用の著者はクラスを持つタグ内にあります。
… <span class=“text“ itemprop=“text“>“I have not failed. I've just found 10,000 ways that won't work.”</span> <span>by <small class=“author“ itemprop=“author“>Thomas A. Edison</small> …
私たちがループしている引用オブジェクトには、自身の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(),
}
Note
このコードには2つのことが起こっていることに気づくでしょう。
- We append ::text to our selectors for the quote and author. That’s a CSS pseudo-selector that fetches the text inside of the tag rather than the tag itself.
- We call extract_first() on the object returned by quote.css(TEXT_SELECTOR) because we just want the first element that matches the selector. This gives us a string, rather than a list of elements.
ファイルを保存して、スクレイパーを再実行してください。
- 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を調査することで、以下のようにわかります。
- The link to the author’s about page is stored in a link immediately following their name.
- The tags are stored as a collection of a tags, each classed tag, stored within a div element with the tags class.
では、この新しい情報を取得するためにスクレイパーを修正しましょう。
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(),
}
変更内容を保存して、スクレイパーを再実行してください。
- 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は次のとおりです。
… <nav> <ul class=“pager“> <li class=“next“> <a href=“/page/2/“>Next <span aria-hidden=“true“>→</span></a> </li> </ul> </nav> …
ソースコード内には、class=”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でウェブページをスクレイピングする方法」のチュートリアルを参照してください。