PythonでTUIアプリを作ろう 第二部入魂編 ( その3 スクレイピングによるデータ収集 )

今回は、前回説明したListDBに使用するデータを作成していきます。



スクレイピングによるデータ収集

いろいろ考えてみたのですが、このBlogに関係のあるデータを使ってみるのが一番ですので、みんな大好き(かどうかは知りませんが、)ブックオフおよびハードオフの店舗情報を使ってみることにします。
どちらもWebサイトに店舗情報がありますから、有効性は疑わしいですが、あくまでデータの一例ということで了承ください。

さて、これらのデータ収集については、スクレイピングという手法を使ってみることにしました。

ウェブスクレイピング(英: Web scraping)とは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。通常このようなソフトウェアプログラムは低レベルのHTTPを実装することで、もしくはウェブブラウザを埋め込むことによって、WWWのコンテンツを取得する。ウェブスクレイピングはユーザーが手動で行なうこともできるが、一般的にはボットやクローラ(英: Web crawler)を利用した自動化プロセスを指す。

とありますが、要するに「プログラムからWebサイトにアクセス、返ってきたHTMLを解析して必要な情報を抜き出す」という技術です。
本来、ブラウザからのアクセスを想定しているサイトに、PCからコードでアクセスするわけですから、下手をするとDDoS攻撃になりかねません。したがって、使用する際には過度なアクセスを避け、適当なウェイトを入れるなどの配慮が必要でしょう。
また、サイトに表示されている情報からデータを抜き出しているので、デザインの変更などによって動作しなくなる場合も少なくありません。今回紹介するスクリプトも現在(2021年10月初旬)は動いていますが、今後の保証は何もありません。


Pythonによるスクレイピング手法

これについては、書籍やWebサイトで様々な情報が提供されているので、そちらを参考にしてください。ここでは、簡単に概要を説明するにとどめておきます。

(1) 使用するモジュール

Pythonでスクレイピングを行う場合、下記の3つのモジュールを使用するのが一般的なようです。

1) requests

WebサイトにHTTPでアクセスする機能を提供します。Python標準ライブラリのurlibでもできますが、requestsの利用を強くお薦めします。わたしはurlibでスクリプトをいくつか書いた後にrequestsの存在を知り、「これがあったのか」と嘆息した経験があります。

2) BeautifulSoup

Webから送られてきたHTMLを、BeautifulSoupで解析します。XML Parserみたいなものですね。APIはSAXに近いかな。

3) selenium

単にHTMLを返すだけのサイトであれば、上記2つのモジュールで処理できますが、JavaScriptを使用するサイトではうまく行きません。動的にHTMLが書き換えられますので、実際にブラウザを起動させ、生成されたHTMLを都度受け取る必要があるからです。
このようなサイトでは、まずseleniumでブラウザを起動します。次に起動したブラウザからHTMLを受け取り、BeautifulSoupで解析していく、という手順を踏まないといけません。


モジュールとドライバーのインストール

それでは、必要なモジュールをインストールしていきましょう。

(1) Pythonモジュールのインストール

先ほど説明した、3つのモジュールをpipでインストールします。以下の説明は、すべてPython3環境を想定しています。

pip3 install requets  
pip3 install beautifulsoup4  
pip3 install selenium  

(2) WebDriverのインストール

次に、seleinumが起動するブラウザに対応するWebDriverが必要になります。
ここではChromeを使っているものと仮定します。使用しているバージョンに対応するDriverをダウンロードします。例えば、Chromeのバージョンが94であれば、ChromeDriver 94.0.4606.61を選択します。また、このDriverは実行時に起動されるので、Pathを通しておく必要があります。


スクレイピングの実行

それでは、実際にスクリプトを書いていきましょう。
まずは、ブックオフの東京店舗一覧にアクセスして、HTMLを取得してみます。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# s13_01.py  
#  
from selenium import webdriver  
from bs4 import BeautifulSoup  
  
driver = webdriver.Chrome()  
driver.get("https://www.bookoff.co.jp/shop/search-result.html#opt_prefecture=13_%E6%9D%B1%E4%BA%AC%E9%83%BD/opt_and_or=and")  
  
bs = BeautifulSoup(driver.page_source, "html.parser")  
print(bs.prettify())  
driver.quit()  

このコードを実行すると、画面上には下図のようにChromeブラウザが自動起動され、その後、取得されたHTMLがTerminal上に表示されるはずです(下記のようにファイルにリダイレクトして、エディタで見るのが得策です)。

python3 s13_01.py >s13_01.html  

Chrome

さて、取得したHTMLを見ていくと、下記の部分から店舗情報が始まっています。

    :  
    :  
  <!-- /shop-serch-result-info -->  
  <div class="shop-detail-lists">  
   <div class="shop-detail-item">  
    <a class="shop-detail-item__name" href="/shop/shop20396.html">  
     BOOKOFF 秋葉原駅前店  
    </a>  
    <div class="shop-detail-item__grid">  
     <dl class="shop-detail-item__data">  
      <dt class="shop-detail-item__title shop-detail-item__title--address">  
       所在地  
      </dt>  
      <dd class="shop-detail-item__detail">  
       東京都千代田区神田佐久間町1-6-4  
      </dd>  
     </dl>  
     <dl class="shop-detail-item__data">  
      <dt class="shop-detail-item__title shop-detail-item__title--phone">  
       電話番号  
      </dt>  
      <dd class="shop-detail-item__detail">  
       03-5207-6206  
      </dd>  
      <dt class="shop-detail-item__title shop-detail-item__title--open">  
       営業時間  
      </dt>  
      <dd class="shop-detail-item__detail">  
       10:00~21:00  
      </dd>  
     </dl>  
    </div>  
    :  

shop-detail-lists“が店舗一覧の始まりで、その下の”shop-detail-item“が各店舗情報だということがわかります。そのタグに店舗名、住所などがありますから、これらの情報を抜き出せば良いということになります。
こんなコードで抜き出してみました。(詳細は、Beautiful Soup Documentationなどを参照してください。)

    bs = BeautifulSoup(html, "html.parser")  
    shops = bs.find('div', class_="shop-detail-lists").find_all('div', class_="shop-detail-item")  
    for shop in shops:  
        shop_name = shop.find('a').get_text()  
        ss = shop.find_all('dd')  
        shop_address = ss[0].get_text()  
      :  
  

ListDBデータの作成

次に、抽出したデータを加工して、ListDB用のデータにしていきます。
下記に全コードを示します。Categoryは、住所から抜き出した東京23区名にしています。また、検索結果は複数ページに渡っているので、1ページ取得後、次ページに画面を遷移させないといけません。このあたりの対応も行っています。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# bookoff.py  
#  
from selenium import webdriver  
from bs4 import BeautifulSoup  
import time  
  
categories = []  
rows = []  
  
def get_category(address):  
    if address[:len('東京都')] != '東京都':  
        return  
    pos = address.find('区')  
    if pos == -1:  
        pos = address.find('市')  
        if pos == -1:  
            return  
    category = address[len('東京都'):pos+1]  
    if category not in categories:  
        categories.append(category)  
    return category  
  
def put_shop(html):  
    bs = BeautifulSoup(html, "html.parser")  
    shops = bs.find('div', class_="shop-detail-lists").find_all('div', class_="shop-detail-item")  
    for shop in shops:  
        shop_name = shop.find('a').get_text()  
        ss = shop.find_all('dd')  
        shop_address = ss[0].get_text()  
        category = get_category(shop_address)  
        del ss[0]  
        row = category+","+shop_name+","+shop_address  
        for s in ss:  
            t = s.get_text().strip()  
            if len(t) > 0:  
                row += ","+t  
        rows.append(row)  
  
driver = webdriver.Chrome()  
driver.get("https://www.bookoff.co.jp/shop/search-result.html#opt_prefecture=13_%E6%9D%B1%E4%BA%AC%E9%83%BD/opt_and_or=and")  
time.sleep(1)  
  
while True:  
    put_shop(driver.page_source)  
    try:  
        element = driver.find_element_by_link_text("次のページ")  
    except :  
        break  
    element.click()  
    time.sleep(3)  
driver.quit()  
#  
print("ブックオフ店舗情報(東京都),name,address", end="")  
for category in categories:  
    print(","+category, end="")  
print()  
for row in rows:  
    print(row)  

これを実行します。

python3 bookoff.py > bookoff_tokyo.csv  

すると、下記のようなListDB用CSVファイルが作成されます。

ブックオフ店舗情報(東京都),name,address,千代田区,港区,新宿区,文京区,台東区,墨田区,江東区,品川区,目黒区,大田区,世田谷区,渋谷区,中野区,杉並区,豊島区,北区,板橋区,練馬区,足立区,江戸川区,八王子市,立川市,武蔵野市,青梅市,府中市,昭島市,調布市,町田市,小平市,国立市,福生市,東大和市,東久留米市,多摩市,西東京市  
千代田区,BOOKOFF 秋葉原駅前店,東京都千代田区神田佐久間町1-6-4 ,03-5207-6206,10:00~21:00  
港区,BOOKOFF総合買取窓口 田町駅西口店,東京都港区芝5丁目32-3 1F,03-5439-4131,11:00~20:00  
   :  
   :  

ハードオフについても、同様な手法でListDB用CSVファイルを作成できます。詳細はソースコードを見てください。実行スクリプトは、x.shとなります。


ソースコードについて

GitHubに登録しました。今回のコードは、Section13となります。