PythonとSeleniumを使ってWebスクレイピングの練習

## はじめに

今回は、Pythonと現在勉強中のSeleniumを使用してWebスクレイピングを行いたいと思います。
ターゲットサイトはGoogleで、指定したキーワードで検索を行って、指定した数の項目を取得し、「タイトル、URL、概要」の３項目をデータベースに書き出す、というものです。

以下のような項目を取得します。
![sele_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262544/8a588bf6-8468-60c2-bbfa-b89a3d65b564.png)
最終目標はSQLiteのデータベースに情報を書き込みます。
![sele_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262544/d18cd516-1712-25e8-fb8b-f4698803a6d1.png)

## 開発環境

Python 3.7.1 を使用します。
開発環境はVisual Studio 2019です。
Firefox用のドライバは[geckodriver](https://github.com/mozilla/geckodriver/releases)からダウンロードしました。

## コード
ソースコードは以下の通りです。
**なお、下記のコードは「２０２０年４月２３日」の時点で動作するものですが、今後のサイトの仕様変更などにより、動作しなくなる可能性もあるのでご了承ください。**

```Python:google.py
import urllib.parse
import records
from selenium.webdriver import Firefox, FirefoxOptions
from sqlalchemy.exc import IntegrityError

# 以下のキーワードを使って検索する
keywd = ['Python','機械学習']

# 取得したデータを保存するファイルの名前
db = records.Database('sqlite:///google_search.db')

db.query('''CREATE TABLE IF NOT EXISTS items (
            url text PRIMARY KEY,
            title text,
            summary text NULL)''')

def store_data(url, title, summary):
    try:
        db.query('''INSERT INTO items (url, title, summary)
                    VALUES (:url, :title, :summary)''', 
                    url=url, title=title, summary=summary)
    except IntegrityError:
        # この項目はすでに存在するのでスキップ
        print("it's already exist.")
        return False

    return True

def visit_next_page(driver, url):
    driver.get(url)

    items = driver.find_elements_by_css_selector('#rso > div.g')

    for item in items:
        tag = item.find_element_by_css_selector('div.r > a')
        link = tag.get_attribute('href')
        title = tag.find_element_by_tag_name('h3').text.strip()

        summary = item.find_element_by_css_selector('div.s span.st').text.strip()

        if store_data(link, title, summary):
            print(title, link, sep='\n', end='\n\n')

def main():
    # ターゲットサイトと検索個数（search_unit * search_loop）
    base_url = "https://www.google.co.jp/"
    search_unit = 20 # １ページの表示件数（100以上を指定しても無理っぽい）
    search_loop = 5
    start = 0

    # キーワードを１つの文字列に結合する
    target = ' '.join(keywd)

    # URLエンコード（デフォルトのエンコードは"utf-8"）
    target = urllib.parse.quote(target)

    opt = FirefoxOptions()
    
    # 自分でブラウザの動作を観察したければ、コメントにしてください
    opt.add_argument('-headless')
    driver = Firefox(options=opt)

    # 待機時間を設定する
    driver.implicitly_wait(10)

    # １ページ分づつ読み込んでいく
    for i in range(search_loop):
        url = "{0}search?num={1}&start={2}&q={3}".format(base_url, search_unit, start, target)
        start += search_unit

        print("\npage count: {0}...".format(i + 1), end='\n\n')
        visit_next_page(driver, url)

    driver.quit()

if __name__ == '__main__':
    main()
```

## 解説

ソースにコメントを入れているのでだいたい分かると思いますが、スクレイピングの中心的な部分を解説します。

１件分のデータが入っている部分は以下のようになります。
![sele_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262544/84bd5031-8655-6884-a3f4-1d3719586920.png)
これを以下のようなコードで取得します。

```Python
items = driver.find_elements_by_css_selector('#rso > div.g')
```

タイトルとリンクのURLは以下のようになっています。
![sele_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262544/09003ac7-7c71-d629-18a2-3061b785d6be.png)
概要の部分は以下のようになっています。
![sele_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262544/41f38412-94bc-7d33-1e2a-1413afdc4bf6.png)

これを以下のようなコードで取得します。

```Python
tag = item.find_element_by_css_selector('div.r > a')
link = tag.get_attribute('href')
title = tag.find_element_by_tag_name('h3').text.strip()

summary = item.find_element_by_css_selector('div.s span.st').text.strip()
```

## 使い方

実際に使用する場合には、ソースコードの冒頭部分で検索したいキーワードと保存するファイル名を指定します。

```Python
# 以下のキーワードを使って検索する
keywd = ['Python', '機械学習']

# 取得したデータを保存するファイルの名前
db = records.Database('sqlite:///google_search.db')
```

また、同じプログラムを複数回実行しても同じURLは登録せずにスキップします。

```Python
def store_data(url, title, summary):
    try:
        db.query('''INSERT INTO items (url, title, summary)
                    VALUES (:url, :title, :summary)''', 
                    url=url, title=title, summary=summary)
    except IntegrityError:
        # この項目はすでに存在するのでスキップ
        print("it's already exist.")
        return False

    return True
```

## 補足

最初は１回のページ表示でまとめて１０００件分くらい取得しようと思ったのですが、どうもGoogleの仕様で１ページあたり100件くらいが限度のようでした。
というわけで、`search_unit`と`search_loop`の２つの変数を使用して次のページに切り替えながらスクレイピングしています。

また、なんでBeautifulSoupを使わないの？
という声が聞こえてきそうですが、Seleniumを使った練習をしてみたかったのと、最近はJavaScriptを使用したサイトが多いのでSeleniumを使う機会が増えそうなので、今回はこのような方法でスクレイピングしてみました。

## 終わりに

今回紹介したソースコードは自由に使って頂いて構いませんが、その際には自己責任でお願いします。

## 参考記事、参考書籍

[Python スクレイピングの基本と実践／Seppe vanden Broucke他](https://www.amazon.co.jp/Python%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E5%AE%9F%E8%B7%B5-%E3%83%87%E3%83%BC%E3%82%BF%E3%82%B5%E3%82%A4%E3%82%A8%E3%83%B3%E3%83%86%E3%82%A3%E3%82%B9%E3%83%88%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AEWeb%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%8E%E9%9B%86%E8%A1%93-impress-top-gear/dp/4295005282/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1587624962&sr=1-1)