【備忘録】Python×Seleniumで取得したデータをSpreadsheetに書き込むプログラムをCloud Functionsにデプロイ

## 背景
とあるサイトをSeleniumでスクレイピングし、そのデータをスプレッドシートに蓄積するフローを定期実行したい時に、自分でサーバーを用意するのは面倒なので、Google Cloud Functionsを使いました。Qiitaを探しても途中で詰まってしまうことが多かったため、Cloud Funtionsの使い方含め、デプロイする流れや注意点をここに詳細に備忘録としてまとめようと思います。
自分がまだまだインフラ周りに弱いので、もしアドバイスなどありましたらご教示いただけると嬉しいです😊

## この記事が想定しているシステム
定期的にhttps://www.google.com にseleniumでアクセスし、そのページ全体のHTMLをスプレッドシートに毎回記録する

## 実現方法
1. Google Cloud周りの準備
    1-1. 新規プロジェクトの作成
    1-2. サービスアカウントの作成およびキーの入手
    1-3. Cloud Pub/Subの準備
2. スプレッドシート周りの準備
    2-1. Google Sheet APIとGoogle Drive APIの有効化
    2-2. スプレッドシートへの権限付与
    2-3. スプレッドシートキーの取得
3. スクレイピングプログラムの作成
    3-1. main.py
    3-2. requirements.txt
4. CloudShellでのデプロイ
    4-1. パッケージをgit clone
    4-2. 必要なファイルをアップロード
    4-3. デプロイ

## 1. Google Cloud周りの準備
### 1-1. 新規プロジェクトの作成

https://www.teijitaisya.com/gcp-new-project/

### 1-2. サービスアカウントの作成およびキーの入手
キーの作成まで進めると、jsonがローカルにダウンロードされます。次以降で使うので、安全に保管してください。

https://help.deepsecurity.trendmicro.com/aws/ja-jp/gcp-account-create.html

### 1-3. Cloud Pub/Subの準備

下記でPub/Subのトピックを作成し、トピックの名前を控えてください。

https://cloud.google.com/pubsub/docs/create-topic?hl=ja

## 2. スプレッドシートへの権限付与
### 2-1. Google Sheet APIとGoogle Drive APIの有効化
https://www.otsuka-bs.co.jp/web-creation/blog/archive/20230904-03.html

https://zenn.dev/spacegeek/articles/45270323b1d2bc

(Google Drive APIの有効化が必要かどうかは覚えてないです、、すみません！心配な方は一応有効化しておいていただけると！)

### 2-2. スプレッドシートキーの取得
スプレッドシートのURLの「d/」より後から、次のスラッシュの前までの文字列です。

## 3. スクレイピングプログラムの作成
### 3-1. main.py
ここからスクレイピングプログラムの作成に移ります。まずはコード。

```python:main.py
import os
from oauth2client.service_account import ServiceAccountCredentials
from selenium import webdriver
from selenium.webdriver.common.by import By

# API有効化のスコープ
SCOPES = ["https://www.googleapis.com/auth/drive","https://www.googleapis.com/auth/spreadsheets"]
# サービスアカウントのキー
SERVICE_ACCOUNT_FILE = "your_service_account_key.json"
# スプレッドシートキー
SPREADSHEET_KEY = "your_spread_sheet_key"
//スクレイピングを行う対象のWebページのURL
TARGET_URL = "https://www.google.co.jp/"

credentials = ServiceAccountCredentials.from_json_keyfile_name(SERVICE_ACCOOUNT_FILE,SCOPES)
gs = gspread.authorize(credentials)
wb = gs.open_by_key(SPREADSHEET_KEY)
# データを蓄積したいシート
sheet = wb.worksheet("your record sheet")

# webdriverを立ち上げる関数
def start_webdriver(driver_path, headless_path):

    chrome_options = webdriver.ChromeOptions()

    chrome_options.add_argument('--headless')                         
    chrome_options.add_argument('--disable-gpu')                      
    chrome_options.add_argument('--disable-extensions')               
    chrome_options.add_argument('--single-process')               
    chrome_options.add_argument('--proxy-server="direct://"')         
    chrome_options.add_argument('--proxy-bypass-list=*')              
    chrome_options.add_argument('--blink-settings=imagesEnabled=false')
    chrome_options.add_argument('--lang=ja')                          
    chrome_options.add_argument('--no-sandbox')
    chrome_options.add_argument('--disable-dev-shm-usage')
    chrome_options.add_argument("--log-level=3")
    chrome_options.add_argument("--disable-logging")
    chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36')
    chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
    chrome_options.add_experimental_option('useAutomationExtension', False)
    chrome_options.page_load_strategy = 'eager'
    chrome_options.binary_location = headless_path
    
    driver = webdriver.Chrome(driver_path ,options=chrome_options)
    
    return driver

# メイン
def main(event, context):

    driver_path =  os.getcwd() +"/chromedriver"
    
    headless_path = os.getcwd() + "/headless-chromium"
    
    # selenium driverを立ち上げる
    driver = start_webdriver(driver_path, headless_path)

    driver.get(TARGET_URL)
    
    # webページのhtmlを取得
    html = driver.page_source

    # スプレッドシートのA1セルにhtmlを挿入
    sheet.update_acell('A1', html)
    
    driver.quit()
```
ご自身で変えていただくところは、
- `SERVICE_ACCOUNT_FILE`
- `SPREADSHEET_KEY`
- `TARGET_URL`
- `sheet = wb.worksheet("your record sheet")`
の4つです。また、より高度なスクレイピング・ブラウザ操作がしたい場合は、`driver = start_webdriver(driver_path, headless_path)`以降をよしなに書き換えていただくことになります。

### 3-2. requirements.txt
```text:requirements.txt
gspread>=5.2.0
oauth2client>=4.1.3
google-cloud-error-reporting==0.30.0
setuptools
```
ここでseleniumを入れないのは、後続のCloudShellでの操作でseleniumパッケージをダウンロードするからです。（requirements.txtの中で最新のseleniumをダウンロードするとうまく行かない事象が発生しました。）

## 4. CloudShellでのデプロイ

https://cloud.google.com/shell?hl=ja

### 4-1. パッケージをgit clone
CloudShell上で、seleniumやheadless-chromiumが入っているパッケージをgit cloneで取得し、解凍してください。
```bash
mkdir scraping_app
git clone https://github.com/ryfeus/gcf-packs.git
cd gcf-packs/selenium_chrome/source
unzip headless-chromium.zip

cp chromedriver $HOME/scraping_app
cp headless-chromium $HOME/scraping_app
cp selenium $HOME/scraping_app -r
```
### 4-2. 必要なファイルをアップロード
CloudShellのターミナルの右上のメニューよりアップロードを選択し、開いた画面からファイルとアップロード先を設定し、下方の「アップロード」をクリックします。アップロードするファイルは
- `main.py`
- `requirements.txt`
- `your_service_account_key.json` (サービスアカウントキー)
で、`~/scraping_app`にアップロードしてください。

### 4-3. デプロイ
```bash
cd $HOME/scraping_app

gcloud functions deploy scraping_app_func --trigger-topic=scheduler --entry-point=main --runtime=python39 --region=asia-northeast1 --memory=512MB --timeout=540 --set-env-vars TZ=Asia/Tokyo
```
- `--trigger-topic`: Pub/Subで設定したトピック
- `--entry-point`: エントリーポイント。今回はメイン処理を行うmain関数

デプロイが完了すると、Cloud Functionsの画面に新規デプロイが作成されていることがわかります。

最後に、Cloud Functions画面の中段あたりにある「テスト中」をクリック、その後画面が切り替わった後に、画面左下の「関数をテストする」をクリックしテスト実行をしましょう。正常に実行が完了すればOKで、エラーが生じていればそのエラーに応じてデバッグを行い、再度デプロイを行いましょう。

OKであれば、Cloud Schedulerを用いてPub/Subのトピックに対して定期実行のスケジューリングを行えば完成です。

https://blog.g-gen.co.jp/entry/cloudscheduler-explained

## そのほか参考にした記事

https://somnia.jp/hobby/bunseki_gc_init3/#toc13

https://telecom-engineer.blog/blog/2022/03/27/python-gcp-movie