Selenium＋Pythonでデータ取得を自動化し、BigQueryに連携した話（前編）

# はじめに

こんにちは、京セラコミュニケーションシステム　西田（@kccs_hiromi-nishida）です。

皆さんはWEBサイトのデータを収集したいけれど、API公開されていないから困ったな・・とか、WEBサイトにデータは公開されているけれどファイルでダウンロードできないから、データ分析のインプットにしにくいな・・と思ったことはありませんか？

今回はSeleniumというツールを使ってWEBサイトからのデータ収集を自動化し、収集したデータをBigQueryに連携してみたのでご紹介したいと思います！

:::note warn
本記事は2022年12月ごろに作成しております。よって、引用している文章などはこの時点での最新となります。ご了承ください。
:::

## この記事の対象者

- WEBサイトからのデータ収集を自動化してみたいと思っている方
- Pythonを使ったスクレイピングに興味のある方

## 今回の記事の範囲

Selenium+Pythonを使ってWEBサイトからデータを自動収集してCSVファイル出力するところ（下図のオレンジ枠の範囲）が今回の記事範囲です。

<img width="700" alt="構成図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/3c554a67-6631-bb52-046c-607a8a074334.png">

この記事を最後まで見て頂くと、この動画のようにPythonプログラムを実行するだけでブラウザが起動し、WEBサイト上のデータをCSV保存する自動化プログラムを作成できます。
少し長い記事になりますが、どうぞ最後までご覧ください！
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/7c28de62-8c15-cf28-45a6-6fe9249aa5aa.gif)

## 前提

Pythonの実行環境が必要になりますので、実行環境がない方は以下のサイトを参照して環境を構築してみてください。
また、pandasというデータ分析用のライブラリと、スクレイピング用のライブラリも必要になるので、Python実行環境構築後にインストールをしておいてください。（Windowsの方はコマンドプロンプト、Macの方はターミナルで実行）

```console:インストールコマンド
pip install pandas
pip install lxml html5lib beautifulsoup4
```

:::note info
参照サイト
[python Japan](https://www.python.jp/install/windows/install.html)
:::

## Seleniumとは

Selenium[^1]は、WEBブラウザの操作を自動化するためのアプリケーションです。
WEBブラウザで行うクリック操作や文字入力をプログラム言語を使って操作できるため、WEBアプリケーションのテストやスクレイピングで利用されています。

使用できるプログラム言語はJava/Python/C#/Ruby/JavaScript/Kotlinで、今回はPythonを使います。

## スクレイピングの注意点

今回、Seleniumを使ってWEBサイトのデータを収集します。これをスクレイピングと言いますが、スクレイピングを行うにあたって注意点があります。

- 情報解析目的で利用する（著作権法の順守）
- 同意なく個人情報を取得しない
- 対象のサーバーに過剰な負荷をかけない（スクレピング頻度を自然検索の範囲内にする）
- 利用規約を守る（利用規約でスクレイピングやクローリングが禁止されていないか確認）

詳しくは以下の参照サイトを確認してください。
法律の観点から詳しく解説されているサイトです。

:::note info
参照サイト
[ココナラ法律相談](https://legalmedia.coconala.com/3182)
:::

## Seleniumのインストール

Seleniumのインストールはとても簡単です。
コマンドプロンプト（Macの方はターミナル）を起動し、以下のコマンドを入力するだけでインストールが完了します。

```console:インストールコマンド
pip install selenium
```

このようにSuccessfully installed…と表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/2950b3f6-ad98-5009-e246-2a3f6834c6a4.png)

## Webdriver Managerのインストール

Seleniumを使ってブラウザ操作の自動化を行うためには、各ブラウザに対応したドライバーが必要です。ですが、ブラウザのバージョンとドライバーのバージョンを合わせる必要があるため、ブラウザのバージョンアップに伴い、自動化プログラムがエラーで動かなくなったりします。

そのたびにドライバーのバージョンアップ作業をするのは大変なので、今回はドライバーを自動で更新してくれるWebdriver Managerというライブラリを導入します。

コマンドプロンプト（Macの方はターミナル）を起動して以下を入力するだけでインストールが完了します。

```console:インストールコマンド
pip install webdriver_manager
```

このようにSuccessfully installed…と表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/334dfa27-768e-3503-6671-7f687a0b0265.png)

:::note info
公式サイト
[Seleniumライブラリのインストール](https://www.selenium.dev/ja/documentation/webdriver/getting_started/install_library/)
[ブラウザーのドライバーをインストールする](https://www.selenium.dev/ja/documentation/webdriver/getting_started/install_drivers/)
:::

## Seleniumを使ってデータ取得を自動化してみよう

今回は気象庁‐過去の気象データ検索[^2]から年ごとの東京都における気象データを取得する自動化プログラムを作成したいと思います。
空のPythonファイルを作成し（ここではsample.pyとします）、処理を追って記述していきましょう。

### Selenium関連モジュールのインポート

今回は以下4つのモジュールを使用するため、まずインポートしておいてください。

```python:sample.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
```

### Chrome Driverのインストールとオプション設定

今回はChromeブラウザをSeleniumで操作したいと思いますので、Chrome Driverを使う設定を記述します。

:::note info
コード内のheadlessオプション指定ですが、指定するとブラウザが起動せずバックグラウンドで処理が行われます。
最初はブラウザが自動で動く様子を見た方がおもしろいので、処理が完成したら指定しましょう！
:::

```python:sample.py
# 使用しているChromeブラウザのバージョンにあったDriverがインストールされます
# バージョンに相違がない場合はインストールされません
chrome_service = Service(ChromeDriverManager().install())

# Driverのオプションを指定できます
options = webdriver.ChromeOptions()
options.add_argument("--headless") #最初はコメントアウトしておいてもOKです
driver = webdriver.Chrome(service=chrome_service, options=options)
# 要素がロードされるまでの待ち時間を10秒に設定します
driver.implicitly_wait(10)
```

### WEBサイトを表示してみよう

:::note warn
使用画像について
特別な記載のない限り、画像は**気象庁-過去の気象データ検索画面**の画面をキャプチャしたものとなります
:::

では実際にPythonプログラムからWEB画面を表示してみましょう。
以下1行を記述するだけです！

```python:sample.py
# 取得したいWEBサイトのURLを指定します(今回は気象庁-過去の気象データ検索画面のURL)
driver.get("https://www.data.jma.go.jp/obd/stats/etrn/")
```

ここまでコードを記述したら、sample.pyを実行してみてください。
自動でChromeブラウザが起動し、指定したWEBサイトが表示されているはずです！

### 画面内の要素をプログラムから操作してみよう

自動操作する対象の画面を確認してみます。

今回取得したい東京都の年単位気象データを表示させるためには、まず下画像の赤枠部分をクリックし、都道府県と地点を選択する必要があります。
そしてその後、緑枠の［年ごとの値を表示］をクリックすれば、東京都の年単位気象データが表示されます。
<img width="600" alt="気象庁-過去の気象データ検索画面スクリーンショット" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/ed16b25c-63d7-f356-ca5e-094c3a26f295.png">
どう操作すればよいか分かったので、この操作をプログラムから行ってみましょう。
手順としては
　① 操作対象の要素を検索する
　② ①で見つけた要素に対して操作を実行する
となります！

#### 要素の検索

要素を検索するには、find_elementメソッド[^3]を使います。
find_elementメソッドは第1引数に、"何を使って"要素を特定するかを指定する必要があります。今回はXPath[^4]を使って要素を特定したいと思います。

XPathはChromeの開発者ツールを使えば、簡単に取得できます。
まず、対象の画面を開き、F12キーを押して開発者ツールを開きます。
この状態で、画像赤枠部分の"ページ内の要素を選択して検査"アイコンをクリックします。
<img width="500" alt="開発者ツールを開いた画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/db67cbbd-5344-6c30-98b9-fbff909686ed.png">
アイコンを押下した後、特定したい要素（ここでは"都府県・地方を選択"のリンク部分）をクリックします。
<img width="500" alt="開発者ツールで要素を選択" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/d9793422-4f55-9b01-b769-862fbf8580bf.png">
するとこの画像のようにaタグが選択された状態になるので、この状態で右クリック ⇒ コピー ⇒ XPathをコピーの順にクリックしてください。
<img width="500" alt="XPathのコピー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/6675f467-c445-fdce-17fb-99c152ed77e2.png">
あとはこのコピーしたXPathをfind_elementメソッドの第2引数に指定すればOKです。

```python:sample.py
# find_elementの第2引数にコピーしたXPathを貼り付けてください
area_link = driver.find_element(
    By.XPATH, '//*[@id="main"]/table[4]/tbody/tr/td[1]/div/table/tbody/tr[2]/td/a')
```

#### 操作の実行

要素を検索し、特定できたら次はその要素を操作します。
execute_scriptメソッドを使用して、対象の要素のclickイベントを実行します。
下のコードのように、execute_scriptの第2引数に取得した要素の変数を記載すればOKです。

```python:sample.py
driver.execute_script('arguments[0].click();', area_link)
```
ここまででsample.pyを実行すると、自動的にChromeブラウザが起動 ⇒ 気象庁‐過去の気象データ検索画面が表示 ⇒ 都府県・地方の選択画面の表示が自動的に実行されているはずです。
後は目的のデータが表示できるまで、要素の検索と操作の実行を繰り返しましょう。

#### 東京都の年単位気象データ表示

これまでのプログラムで都府県・地域の選択画面の自動表示までは完成しました。
続けて、都府県・地域の選択画面から東京都を選択 ⇒ 東京都のエリアから"東京"を選択、で地域の選択は完了 ⇒ その後、"年ごとの値を表示"を選択するという処理を記述すれば、東京都の年単位気象データが画面に表示されます。

前述した要素の検索と操作の実行を繰り返しているだけの単純なプログラムとなっています。

```python:sample.py
# 地図の中から東京都を選択
select_area_link = driver.find_element(
    By.XPATH, '//*[@id="main"]/map/area[25]')
driver.execute_script('arguments[0].click();', select_area_link)

# 東京都のエリアから東京を選択
select_tokyo = driver.find_element(
    By.XPATH, '//*[@id="ncontents2"]/map/area[9]')
driver.execute_script('arguments[0].click();', select_tokyo)

# 年ごとの値を選択
select_year = driver.find_element(
    By.XPATH, '//*[@id="main"]/table[4]/tbody/tr/td[3]/div/table/tbody/tr[1]/td[1]/table/tbody/tr[1]/td[2]/a')
driver.execute_script('arguments[0].click();', select_year)
```
ここまでの処理が記述できたら、sample.pyを実行してみてください。
自動的にブラウザが起動し、以下の画面が表示されるはずです。
<img width="500" alt="東京都の年単位気象データ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/867564b9-5f44-ee5b-a39e-5119423ad112.png">

### WEBサイト上のデータをCSVファイルに保存

画面に表示されている年ごとの気象データを、Pythonのpandasライブラリを使ってCSVファイルに保存してみたいと思います。


#### pandasを使ってHTMLの表データをDataFrameオブジェクトにする

まずはライブラリのインポートが必要です。

```python:sample.py
# sample.pyの冒頭部分に追記してください
import pandas as pd
```

次にデータ部分を取得します。
今回の場合、表示されている表全体を取得したいので、tableタグのXPathを取得します。

<img width="500" alt="取得対象の要素" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/9f71ceee-583c-cbc5-5ce8-c01ce4ae0c27.png">

```python:sample.py
# 表全体を取得
table = driver.find_element(By.XPATH, '//*[@id="tablefix1"]')
```

表の要素が取得できたら、次にpandasのread_htmlメソッド[^5]を使って、HTMLのtableタグをpandasのDataFrameオブジェクトに変換します。

```python:sample.py
# 表全体を取得
html = table.get_attribute('outerHTML')
df_temp_data = pd.read_html(html)
```

#### CSVファイルを出力する

WEB画面上の表がPythonで扱いやすいDataFrameオブジェクトに変換できたので、後はデータを整形してCSVファイルに出力するだけとなります。

今回は、最終的にBigQueryにデータをインポートするので、全項目を横に並べた項目定義を作成したいと思います。

取得したデータのヘッダー行はこの画像の通りなので、

<img width="500" alt="ヘッダ行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/0719bd4d-0093-4244-6fe0-72d50f22ee6d.png">

このヘッダー行の項目を以下のコードのように定義します。
全部横に並べるイメージです！

```python:sample.py
# CSVヘッダーの定義
header_columns = ["年", "気圧_現地_平均", "気圧_海面_平均", "降水量_合計", "降水量_最大_日", "降水量_最大_1時間",
                  "降水量_最大_10分間", "気温_平均_日平均", "気温_平均_日最高", "気温_平均_日最低", "気温_最高",
                  "気温_最低", "湿度_平均", "湿度_最小", "風向・風速_平均風速", "風向・風速_最大風速_風速",
                  "風向・風速_最大風速_風向", "風向・風速_最大瞬間風速_風速", "風向・風速_最大瞬間風速_風向",
                  "日照時間", "全天日射量平均", "雪_降雪_合計", "雪_降雪_日合計の最大", "最深積雪",
                  "雲量平均", "大気現象_雪日数", "大気現象_霧日数", "大気現象_雷日数"]
```

項目が定義できたら、後は作成した項目定義に合わせてデータを加工します。

```python:sample.py
output_data = []
# 行を1行ずつ取得
for i, data in df_temp_data[0].iterrows():
    record = []
    # 行から項目数分のデータだけ取得して保持
    for j in range(0, len(header_columns)):
        record.append(data[j])
    output_data.append(record)

# CSV出力用のDataFrameを定義
output_df = pd.DataFrame(data=output_data, columns=header_columns)
```

出力用のDataFrameを使ってCSV出力します。

```python:sample.py
file_name = "tokyo_weather_data.csv"
# output_pathはどこでも構いませんが、今回はsample.pyと同じ階層に
# outputフォルダを作成しておき、そのフォルダに出力するようにしました
output_path = "./output/" + file_name
output_df.to_csv(output_path, index=False, encoding="shift-jis")
# Chrome Driverのclose処理
driver.close()
```

ここまでのコードを記述したら、sample.pyを実行してみましょう。
自動でブラウザが起動し、指定した場所に以下のような東京都の年単位気象データファイルが出力されるはずです！
<img width="500" alt="CSVファイル" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/490024c6-cc9a-b472-5b41-bc1691e59dca.png">

## データ自動取得、その後・・

さて、Seleniumを使ってWEBサイトから自動的にデータをCSV形式で取得することができました。
ですが、それだけではもったいないので、BigQueryに取得したデータをインポートし、コネクテッドシートを使って分析できるようにしたいと思います。

次回記事では以下の内容をご紹介したいと思いますので、後編もぜひ見て頂けると嬉しいです！（下図のオレンジ枠部分となります）

+ CSVファイルをGoogle Cloud Storage APIを使ってCloud Storageにアップロード
+ Cloud StorageからBigQueryにデータをインポート
+ BigQuery上のデータをコネクテッドシートで分析

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2752159/2a075b5f-507d-f355-3ffe-fee70c544e39.png)

## おまけ

今回作成したsample.pyですが、記事上では説明の為、ソースコードを細切れで掲載しました。
ここにソースコードをまとめて掲載しておきます。

```python:sample.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import pandas as pd

# 使用しているChromeブラウザのバージョンにあったDriverがインストールされます
# バージョンに相違がない場合はインストールされません
chrome_service = Service(ChromeDriverManager().install())

# Driverのオプションを指定できます
options = webdriver.ChromeOptions()
options.add_argument("--headless")  # 最初はコメントアウトしておいてもOKです
driver = webdriver.Chrome(service=chrome_service, options=options)
# 要素がロードされるまでの待ち時間を10秒に設定します
driver.implicitly_wait(10)

# 取得したいWEBサイトのURLを指定します(今回は気象庁-過去の気象データ検索画面のURL)
driver.get("https://www.data.jma.go.jp/obd/stats/etrn/")

# find_elementの第2引数にコピーしたXPathを貼り付けてください
area_link = driver.find_element(
    By.XPATH, '//*[@id="main"]/table[4]/tbody/tr/td[1]/div/table/tbody/tr[2]/td/a')
driver.execute_script('arguments[0].click();', area_link)

# 地図の中から東京都を選択
select_area_link = driver.find_element(
    By.XPATH, '//*[@id="main"]/map/area[25]')
driver.execute_script('arguments[0].click();', select_area_link)

# 東京都のエリアから東京を選択
select_tokyo = driver.find_element(
    By.XPATH, '//*[@id="ncontents2"]/map/area[9]')
driver.execute_script('arguments[0].click();', select_tokyo)

# 年ごとの値を選択
select_year = driver.find_element(
    By.XPATH, '//*[@id="main"]/table[4]/tbody/tr/td[3]/div/table/tbody/tr[1]/td[1]/table/tbody/tr[1]/td[2]/a')
driver.execute_script('arguments[0].click();', select_year)

# 表全体を取得
table = driver.find_element(By.XPATH, '//*[@id="tablefix1"]')
html = table.get_attribute('outerHTML')
df_temp_data = pd.read_html(html)

# CSVヘッダーの定義
header_columns = ["年", "気圧_現地_平均", "気圧_海面_平均", "降水量_合計", "降水量_最大_日", "降水量_最大_1時間",
                  "降水量_最大_10分間", "気温_平均_日平均", "気温_平均_日最高", "気温_平均_日最低", "気温_最高",
                  "気温_最低", "湿度_平均", "湿度_最小", "風向・風速_平均風速", "風向・風速_最大風速_風速",
                  "風向・風速_最大風速_風向", "風向・風速_最大瞬間風速_風速", "風向・風速_最大瞬間風速_風向",
                  "日照時間", "全天日射量平均", "雪_降雪_合計", "雪_降雪_日合計の最大", "最深積雪",
                  "雲量平均", "大気現象_雪日数", "大気現象_霧日数", "大気現象_雷日数"]

output_data = []

# 行を1行ずつ取得
for i, data in df_temp_data[0].iterrows():
    record = []
    # 行から項目数分のデータだけ取得して保持
    for j in range(0, len(header_columns)):
        record.append(data[j])
    output_data.append(record)

# CSV出力用のDataFrameを定義
output_df = pd.DataFrame(data=output_data, columns=header_columns)

file_name = "tokyo_weather_data.csv"
# output_pathはどこでも構いませんが、今回はsample.pyと同じ階層に
# outputフォルダを作成しておき、そのフォルダに出力するようにしました
output_path = "./output/" + file_name
output_df.to_csv(output_path, index=False, encoding="shift-jis")
# Chrome Driverのclose処理
driver.close()
```

[^1]: [Selenium公式サイト(日本語)](https://www.selenium.dev/ja/ "Selenium公式サイト")
[^2]: 地点ごとの気象データを検索することができる　[[URL]](https://www.data.jma.go.jp/obd/stats/etrn/ "気象庁 過去の気象データ検索")
[^3]: Selenium公式ドキュメントを参照 - [WEB要素の検索](https://www.selenium.dev/ja/documentation/webdriver/elements/finders/ "WEB要素の検索")
[^4]: ツリー構造となっているXML/HTMLからの要素や属性値などを指定するための言語構文
[^5]: pandas APIリファレンスを参照 - [read_html](https://pandas.pydata.org/docs/reference/api/pandas.read_html.html "read_html")