Pythonでスクレイピングの練習

スクレイピングに興味があったので、PythonによるWebスクレイピング 第2版を読んでいる。

GithubでJupyter notebooksが公開されていて(REMitchell/python-scraping: Code samples from the book Web Scraping with Python)、実際にコードを動かしながら読めるのでわかりやすい。3章までに読んだ内容を元に、スクレイピングの練習をしてみる。

架空のオンライン書店スクレイピング

スクレイピングの対象は以下のサイトにした。

スクレイピングの学習者向けに公開されているサイトで、架空のオンライン書店の形をしている。自由にスクレイピングしていいらしいので、とてもありがたい。

今回は、このオンライン書店内にある全ての本のタイトルと価格を抽出して表示するプログラムを作る。

ページ内のリンクを再帰的に取得する

以下のコードは、http://books.toscrape.com/ を起点として、ページ内のリンクを再帰的に辿って表示する。

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
from urllib.parse import urljoin


# すでに訪れたページのリスト
visitedPages = set()


def getLinks(pageUrl):
    global visitedPages

    html = urlopen(pageUrl)
    bs = BeautifulSoup(html, 'html.parser')

    print(pageUrl)

    for link in bs.find_all('a'):
        if 'href' in link.attrs:
            newPage = urljoin(pageUrl, link.attrs['href'])

            if newPage not in visitedPages:
                visitedPages.add(newPage)
                getLinks(newPage)


getLinks('http://books.toscrape.com/')

実行するとサイト内のすべてのページのURLが出力される。

これだけのコード量でサイト内のページをすべて取得できるのだから、再帰というものは魔法みたいだと思う。

$ python3 book-crawler.py
http://books.toscrape.com/
http://books.toscrape.com/index.html
http://books.toscrape.com/catalogue/category/books_1/index.html
http://books.toscrape.com/catalogue/category/books/travel_2/index.html
http://books.toscrape.com/catalogue/category/books/mystery_3/index.html
http://books.toscrape.com/catalogue/category/books/historical-fiction_4/index.html
http://books.toscrape.com/catalogue/category/books/sequential-art_5/index.html
http://books.toscrape.com/catalogue/category/books/classics_6/index.html
http://books.toscrape.com/catalogue/category/books/philosophy_7/index.html
http://books.toscrape.com/catalogue/category/books/romance_8/index.html
http://books.toscrape.com/catalogue/category/books/womens-fiction_9/index.html
〜〜以下省略〜〜

基本的には 『PythonによるWebスクレイピング』の第3章のコード例 とそれほど変わらないが、いくつか変えたところもある。以下で説明する。

相対URLを絶対URLに変換する

PythonによるWebスクレイピング』ではWikipediaの英語版をスクレイピングの対象としていて、内部リンクがすべて /wiki/ から始まる相対URLとなっているため、絶対URLをもとめるのが簡単になっていた(https://en.wikipedia.org とその相対URLを結合するだけで絶対URLになる)。

一方、http://books.toscrape.com/ では、

<a href="../../../../index.html">Home</a>

のようなダブルドット(..)を使った相対URLが使われているため、単純な文字列の結合だけでは絶対URLを得ることができない。

幸い、PythonにはURLを解析していい感じに分解したり結合したりできる urllib.parse.urljoin という標準ライブラリが用意されている (urllib.parse --- URL を解析して構成要素にする — Python 3.8.5 ドキュメント)。

使い方は簡単で、以下のように、第一引数に自分がいま見ているページの絶対URLを、第二引数にそのページからの相対URLを渡すと、相対URLを絶対URLにして返してくれる。

>>> from urllib.parse import urljoin
>>> urljoin('http://books.toscrape.com/catalogue/category/books/travel_2/index.html', '../../../../index.html')
'http://books.toscrape.com/index.html'

コードの以下の部分でページ内のリンクを絶対URLに変換するのに使った。

newPage = urljoin(pageUrl, link.attrs['href'])

本の情報を抽出する

単にURLを出力するだけではおもしろくないので、本のタイトルと価格を表示してみる。

商品ページの特徴

本の情報を抽出するためには、いま見ているページが商品のページなのか、それともトップページ(All products | Books to Scrape - Sandbox)やカテゴリごとのページ(Poetry | Books to Scrape - Sandbox)など商品の一覧のページなのか区別する必要がある。商品のページのときだけ本の情報を抽出したい。

URLに商品ページに固有の特徴を見つけられなかったので、ページの内容に特徴を探すことにした。

適当な商品のページ(A Light in the Attic | Books to Scrape - Sandbox)のソースを観察してみると、商品のページでは、

<article class="product_page">

のように <article> タグに商品ページであることを示すクラス属性 product_page が付与されていることがわかった。

このタグは以下のようにCSSセレクタを使って取得できるから、その結果から商品ページかどうか判断できる。

bs.select(".product_page")

タイトルの取得

次に商品ページから本のタイトルを取得する。

ブラウザの開発者ツールを使って本のタイトル部分の要素を選択すると、タイトルは単純に h1 タグの中にあることがわかる。

f:id:hk03ne:20210620184159p:plain
ge-title

したがって、タイトルは以下のように取得できる。

title = bs.h1.get_text()

本の価格

本の価格の場合はすこし面倒で、タイトルと同様に要素を調べると、<p class="price_color">£51.77</p> のように price_color というクラス属性が付与されているのが価格だとわかる。

f:id:hk03ne:20210620184229p:plain
get-price

ただし、本によっては他の本の情報を下の方に表示しているものがあるため(例えばThe Black Maria | Books to Scrape - Sandbox)、ページの上の方に載っているメインの商品の価格のみを取得できるようにしたい。開発者ツールを使ってメインの商品の情報の部分を選択すると、product_main というクラス属性が付与された div タグで囲まれていることがわかる。

f:id:hk03ne:20210620184249p:plain
get-product-main

したがって、本の価格は以下のように取得できる。

price = bs.select_one(".product_main>.price_color").get_text()

最終的なコード

以上を組み合わせて、最終的なコードは以下のようになった。

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
from urllib.parse import urljoin


# すでに訪れたページのリスト
visitedPages = set()


def getLinks(pageUrl):
    """
    ページ内のリンクを再帰的に辿って、タイトルと価格およびURLを表示する

    Parameters
    ----------
    pageUrl : str
        スクレイピングするページのURL

    """
    global visitedPages

    html = urlopen(pageUrl)
    bs = BeautifulSoup(html, 'html.parser')

    try:
        if bs.select(".product_page"):
            title = bs.h1.get_text()
            price = bs.select_one(".product_main>.price_color").get_text()
            print("{}, {}, {}".format(title, price, pageUrl))
    except AttributeError:
        pass

    for link in bs.find_all('a'):
        if 'href' in link.attrs:
            newPage = urljoin(pageUrl, link.attrs['href'])

            if newPage not in visitedPages:
                visitedPages.add(newPage)
                getLinks(newPage)


getLinks('http://books.toscrape.com/')

実行すると、すべての本のタイトル、価格、商品のページのURLが出力された。

$ python3 book-crawler2.py
The Long Shadow of Small Ghosts: Murder and Memory in an American City, £10.97, http://books.toscrape.com/catalogue/the-long-shadow-of-small-ghosts-murder-and-memory-in-an-american-city_848/index.html
Dark Notes, £19.19, http://books.toscrape.com/catalogue/dark-notes_800/index.html
Amid the Chaos, £36.58, http://books.toscrape.com/catalogue/amid-the-chaos_788/index.html
Chasing Heaven: What Dying Taught Me About Living, £37.80, http://books.toscrape.com/catalogue/chasing-heaven-what-dying-taught-me-about-living_797/index.html
The Activist's Tao Te Ching: Ancient Advice for a Modern Revolution, £32.24, http://books.toscrape.com/catalogue/the-activists-tao-te-ching-ancient-advice-for-a-modern-revolution_928/index.html
The Four Agreements: A Practical Guide to Personal Freedom, £17.66, http://books.toscrape.com/catalogue/the-four-agreements-a-practical-guide-to-personal-freedom_970/index.html
We Are All Completely Beside Ourselves, £24.04, http://books.toscrape.com/catalogue/we-are-all-completely-beside-ourselves_301/index.html
〜〜以下省略〜〜

まとめ

PythonによるWebスクレイピング』の第3章までで学んだ内容を元に、架空のオンライン書店から本の情報をスクレイピングする練習をしてみた。

コードを汎用的にしたり、集めたデータをDBに蓄積したりするのは、もっと後の章をよんでから。