【Python】Selenium + Docker + Cloud Runでサンリオピューロランドのグリーティング予定を通知する

# 前置き

サンリオピューロランドというサンリオのテーマパークがあります。

こちらではキャラクターグリーティングを行っていますが、全キャラクターが毎日登場するわけではありません。1日におおむね8キャラクター程度が登場するようになっており、その予定は公式サイト上で確認することができます。

https://www.puroland.jp/greeting/charaguri_residence/

しかし、登場するキャラクターはある程度固定化されており、定番以外のメンバーに会える日程はとても少ないです。具体的に言えばハンギョドンに会いたいけど全然いない。最後に登場したのが2ヶ月前とからしい。

(なお、キャラクターの誕生日などであれば登場する確率が高くて狙いやすいのですが、混みやすいのでそれ以外の日を狙いたいです。)

公式サイト上では3日後の予定までしか載っていないので、webサイトで細かく確認する必要が生じます。

今回はそれを省力化するため、webサイトでの確認をスクレイピングで実現しようと思います。

# 概要

以下のような構成を作成します。

- (WSL環境で開発)
- Dockerで実行
- Python + Seleniumによるスクレイピングの実装
- メールによる通知
- Google Cloud Run Jobs + Google Cloud Schedulerによる定期実行

なお、本記事内ではDockerやgcloudコマンドなどの初期設定に関しては省きます。

# 注意事項

スクレイピングを行う際は以下の事項に十分に気を付ける必要があります。

- webサイトの利用規約違反にならないか
- サーバーに負荷をかけないか

今回は以下から問題ないと判断して開発を行いました。

- サンリオピューロランドのwebサイト、サンリオのwebサイトどちらを確認したところ、自動化に関する記載はなく、また「個人利用の範囲」であればデータ収集に問題がなさそうなこと
    - https://www.puroland.jp/sitepolicy/
    - https://corporate.sanrio.co.jp/legal-notices/
- 開発過程においても多くて1分に1度程度の常識的なアクセスしかせず、最終的に1日1回のアクセスしかしないこと

本記事を参考にする際はこういった問題が生じないか慎重に確認をお願いします。

データの利用が**個人利用の範囲**に制限されているため、「**自分から自分にメールを送信する**」ことのみを通知の手段として、それ以外の外部には情報を出さない方針とします。

# Docker環境の作成

cloud run jobで動かすことが目標なので、Docker環境で作成します。

なお、ローカルで動かすだけならローカルマシンにChromeDriverと各種ライブラリを入れるのみで大丈夫です。(その説明は省略します)

以下のようなdockerfileを作成しました。

```dockerfile
# ベースイメージとしてPythonを使用
FROM python:3.10-slim

# 作業ディレクトリを設定
WORKDIR /app

# 必要なパッケージをインストール
RUN pip install --no-cache-dir selenium webdriver_manager beautifulsoup4

# アプリケーションのソースコードをコピー
COPY src/ src/

# ChromeとChromeDriverをインストール
RUN apt-get update && apt-get install -y wget unzip \
    && wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
    && dpkg -i google-chrome-stable_current_amd64.deb || apt-get -fy install \
    && wget https://storage.googleapis.com/chrome-for-testing-public/127.0.6533.99/linux64/chrome-linux64.zip \
    && unzip chrome-linux64.zip \
    && mv chrome-linux64 /usr/bin/chromedriver \
    && chmod +x /usr/bin/chromedriver

# コンテナ起動時に実行するコマンド
CMD ["python", "src/getNames.py"]
```

なお、chromeDriverのインストールに関してはダウンロードサイトに新しいものが公開されていなかったようで、以下を参考にしました。

https://voicetechno-jp.secure-web.jp/ChromeDriverV115orNewer.html

# コード(Python)

## キャラクター一覧の取得

https://www.puroland.jp/greeting/charaguri_residence/

上記サイトから頑張ってcssセレクタを調べて、以下のコードを書きました。



```py:src/getName.py(前半)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
from selenium.webdriver.common.by import By
import time
import json
import uuid

# ヘッドレスモードでChromeを起動
options = Options()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
driver = webdriver.Chrome(options=options)

# 指定したURLにアクセス
url = 'https://www.puroland.jp/greeting/charaguri_residence/'
driver.get(url)

# JavaScriptのレンダリングを待つ
time.sleep(3)

# select要素の値を変更
select_element = driver.find_element(By.CSS_SELECTOR, '#app-greeting > div.c-select.u-mx-pc-auto.u-mx-sp-auto.u-mb-pc-50.u-mb-sp-30 > select')
all_options = select_element.find_elements(By.TAG_NAME, "option")
# 今日、明日、2日後、3日後のオプションがあるので
all_options[3].click()  # 4番目のオプション(3日後)を選択

# JavaScriptのレンダリングを待つ
time.sleep(3)

# ページのHTMLを取得
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')

# 指定した要素のテキストを取得
value_list = []
i = 3
while True:
    element = soup.select_one(f'#app-greeting > div:nth-child({i}) > div.p-greeting-residence__char > p')
    if element is None:
        break
    value_list.append(element.text)
    i += 1
print(value_list)

# ブラウザを閉じる
driver.quit()
```

## メールの送信

以下を参考にメールの送信部分を実装しました。
https://qiita.com/taidong5588/items/731c7f226e80c5b908d5

アプリパスワードの取得を以下の方法から行う必要があります。

https://support.google.com/mail/answer/185833?hl=ja

毎日通知されても面倒なだけなので、該当のキャラクターがいる時にだけメッセージを送るように実装します。

```py: src/getName.py(後半)
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate

def send_gmail(from_addr, to_addr, subject, body, password):
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = from_addr
    msg['To'] = to_addr
    msg['Date'] = formatdate()

    smtpobj = smtplib.SMTP('smtp.gmail.com', 587)
    smtpobj.starttls()
    smtpobj.login(from_addr, password)
    smtpobj.sendmail(from_addr, to_addr, msg.as_string())
    smtpobj.close()

# 三日後の日付
date = time.strftime('%Y/%m/%d', time.localtime(time.time() + 60 * 60 * 24 * 3))

email = "" # 自分のメールアドレスを入れる 
mail_title = "グリーティング通知"
message = f"""3日後({date}) ハンギョドンがいます！
詳細はこちら↓
{url}"""
smtp_password = "" # アプリパスワード

if("ハンギョドン" in value_list):
    send_gmail(email, email, mail_title, message, smtp_password)
    print("メールを送信しました")

```

なお、googleのアプリパスワードを発行するのはセキュリティ的にあまり推奨できないので、できれば別の方法で通知した方がいいかもしれないです。ここは上記のコード変えるだけなので各自で通知の手法を検討してください。

## 動かしてみる

上記ファイルをsrc/getName.pyとして保存し、ビルドしてみます。

```
docker build -t sanrio-app .
docker run sanrio-app
```

以下のような実行結果が出力されます。

```
['ディアダニエル', 'ポムポムプリン', 'ウィッシュミーメル', 'まるまる', 'シナモン', 'マイメロディ', 'バッドばつ丸']
```

目的のハンギョドンは現在いないので、テストとしてシナモンがいたら通知するように変更します。(getName.pyのハンギョドン部分を全部シナモンに変更する)

再度実行したところ、以下のようなメールが送られてきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356208/992752ec-a9c2-4201-601f-16c38f69bcc5.png)
動作確認が取れました。

# Google Cloudで動かす

以下の3つのステップを踏みます。

- Artifact Registryにコンテナイメージをpushする
    - なお、Container Registryは廃止予定らしいので使いません
- Cloud Run Jobsで動かす
- Cloud Schedulerを設定する

## Artifact Registryにコンテナイメージをpushする

### レポジトリの作成

https://cloud.google.com/artifact-registry/docs/repositories/create-repos?hl=ja#create-gcloud

dockerイメージをGCP上にpushするため、レポジトリを作成します。

```
gcloud artifacts repositories create sanrio-repo --location=asia-northeast1 --repository-format=docker --project=PROJECT-ID
```



### 作成したイメージをpushする

https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling?hl=ja


まず作成したイメージの名称を変更します。

```
docker tag sanrio-app asia-northeast1-docker.pkg.dev/PROJECT-ID/sanrio-repo/sanrio-app
```

gcloudコマンドでレポジトリに対しての認証を行います。
```
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
```
作成したイメージをpushします。

```
docker push sanrio-app asia-northeast1-docker.pkg.dev/PROJECT-ID/sanrio-repo/sanrio-app
```

Artifact Registryのコンソール上から確認できればOK。

### 補足

なお、権限不足でアップロードできなかった場合は以下の点をチェックしてください。

- 利用しているサービスアカウントの権限が不足していないかIAMから確認する
- gcloud auth configure-dockerで対応するリージョンを設定し忘れていないか
- レポジトリを本当に作ったか
- 現在dockerを利用しているユーザーがdockerを利用する権限があるか
    - ここで詰まった。`sudo docker`で実行する状態ではダメで、`sudo usermod -a -G docker ${USER}`でグループに追加する必要がある。windows, linuxの場合に生じるらしい。
    - https://cloud.google.com/artifact-registry/docs/docker/store-docker-container-images?hl=ja#local-shell

## Cloud Run Jobsで動かす

Cloud Run Jobsで作成したイメージを動かします。

https://cloud.google.com/run/docs/create-jobs?hl=ja

コンソール上でも操作できますが以下のコマンドで作成できます。

```
 gcloud run jobs create sanrio-job --image=asia-northeast1-docker.pkg.dev/PROJECT-ID/sanrio-repo/sanrio-app --region=asia-northeast1 --tasks=1 --max-retries=3 --memory=512Mi --cpu=1
```

GCPのコンソールから実行して、取得したキャラクター一覧のログが出ればOK。

## Cloud Schedulerを設定する

作成したジョブを毎日実行するようにしましょう。

https://cloud.google.com/run/docs/execute/jobs-on-schedule?hl=ja#command-line

以下のコマンドからスケジューラーの作成ができます。

```
gcloud scheduler jobs create http sanrio-scheduler   --location asia-northeast1 --schedule="0 9 * * *" --time-zone=Japan --uri="https://asia-northeast1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/PROJECT-ID/jobs/sanrio-job:run" --http-method POST --oauth-service-account-email PROJECT-NUMBER-compute@developer.gserviceaccount.com
```

`--schedule="0 9 * * *" --time-zone=Japan`の部分で日本時間朝9時に動かすように指定しています。

`create`ではなく`update`で指定することで設定の上書きもできます。試しに1分後の時刻とかに設定して、Cloud Run Jobsを叩いていたら動作確認OK。

# おわりに

こうして毎朝9時に「3日後に特定のキャラクターがいるか」を判定する通知を、判定/送信してくる通知が完成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356208/5a7039b2-1352-223d-ecbd-8dd5680f6680.png)


結構簡単な構成で、かつGCPの無料枠内で利用できそうなのでよかったです。

なお、再度の注意となりますが、本記事を参考にする際はwebサイトの利用規約違反にならないか、サーバーに負荷をかけないか等の問題が生じないか慎重に確認をお願いします。