20 min readPython
【コード付!?】Cloud Functions x Seleniumを徹底解説
Pythonを使ってSeleniumを動かせるが、自動実行したい!そんな悩みを抱えている方も多いのではないでしょうか?今回はCloud Functions x Seleniumを用いてAmazonの商品価格を通知する実例を説明いたします。
執筆者 - おすすめの記事3選
用意するもの
- GCPプロジェクトの作成が済んでいる
- Lineを利用していること、Line利用開始時に設定した「Email address, Password」の設定を覚えていること
- 通知したいLineグループが存在する場合、Lineアプリからグループを作成しておくこと、またLine NotifyをLineグループ内に友達追加しておくこと
アーキテクチャ
実行手順
上記のアーキテクチャを見ただけではわからない部分も多々あるかと思います。以下の手順を踏まえて、実装してみましょう。
Line Notifyの設定を行う
以下の作業は、Line通知するために必要な「トークン」を発行するために必要な工程になります。トークンを発行する理由としては、「様々なグループ、ユーザー間に対して、通知が送られないように必要な鍵を発行している」のだと考えてください。
- こちらのページに訪問する。
- 右上に存在する「Log in」 テキストリンクをクリックする。
- ラインのログイン時に必要な「Email address, Password」を入力する。
- ログインしたら右上の自分の名前をクリックして、「My Page」のテキストリンクをクリックする。
- 下の方へ行くと、「Generate token」の文字が確認できるので、そのボタンをクリックする。
- 「Please enter a token name to be displayed before each notification.」の箇所に、通知の際にどのようなテキストを埋め込みたいのか設定します。(完成動画内では、「amazon価格通知」を通知するものですので、「amazon価格通知」を設定しております。)
- 「Select a chat to send notifications to.」の箇所では、通知したいグループ名を選択します。
- 「Generate token」をクリックします。
- 最後にトークンが表示されるので、忘れないようにメモしておいてください。
Zipファイルの準備
以下の作業では、私の方でコード作成したものを、Cloud Functions内にZipファイルにて設置するための下準備です。
- こちらのページに訪問ください。
- 緑の「Code」と書かれているボタンをクリックして、 「Download ZIP」をクリックください。
- ダウンロードしたZipファイルをドラック&ドロップを使って、デスクトップへ移動して下さい。
- Zipファイルを展開するために、デスクトップに置いた先ほどのファイルをダブルクリックして下さい。
- 展開すると、check_price_masterフォルダが生成されます。フォルダ内にchromedriver.zip, headless-chromium.zipファイルが存在するので、同様にZipファイルを展開してください。
- ターミナルを開いて、check_price_masterディレクトリへ移動します。
zip deploy *
を実行して、deploy.zipファイルを生成します。
以上でZipファイルの完成です。deploy.zipをCloud Functions内に設置します。
Cloud Functions作成
以下の作業は、Pythonが実行する関数をデプロイするために必要な工程になります。
お好みのGCPプロジェクトへアクセスする。そして、上の検索覧へ「Cloud Functions」と入力する。すると、Cloud Functionsを操作する画面が表示される。続けて「関数の作成」をクリックする。
以下のように関数の詳細を設定していく。
- 関数名(関数名称) : check_price_master
- リージョン(どの地域で関数を実行するのか?) : asia-northeast1
- トリガーのタイプ(何をきっかけとし、関数を実行するのか?) : Cloud Pub/Sub
- Cloud Pub/Subトピックを選択してください(もう一度アーキテクチャを確認するとわかるのですが、Cloud Schedulerを始めとしてCloud Pub/Subの特定のトピックが呼び出されます。そしてトピックに紐づくCloud Functionsの関数が実行されます。そのために、ここでは独自のトピックを作成してCloud Schedulerで呼び出せるように設定している。) 「トピックを作成する」をクリック → トピックIDへいい感じの名前を設定。「トピックを作成」をクリックする。
- ランタイム、ビルド、接続の設定(関数を実行する際に変数の扱いや、接続先の利用用途を設定する。) : ランタイム環境変数へ名前を「token」, 値を先ほどLine Notifyで取得したトークンを格納する。また、割り当てられるメモリを「1GiB」と設定する。
「次へ」ボタンをクリックする。
次にエントリーポイント(初回実行先の関数名)、ファイル等の設定を行います。
- ランタイム(実行する言語の選択) : Python 3.9
- エントリ ポイント(初回実行する関数名) : check_price
- ソースコード(どのような手段を用いてソースコードをCloud Functions上に置くのか?) : ZIP アップロード
- ZIPファイル : deploy.zip
- ステージ バケット(アップロードされたZIPファイルをどこのStorageで保管するか?) : どこでも大丈夫です。
- 「デプロイ」ボタンをクリックする。
Cloud Scheduler作成
Cloud Pub/Subのトピックを実行する、Cloud Schedulerを作成します。上の検索覧へ「Cloud Scheduler」と入力する。するとCloud Schedulerを操作する画面が表示される。続けて「ジョブを作成」をクリックする。
Cloud Schedulerの詳細設定を行います。
- 名前(タスクの名前) : check_price_master
- 説明(タスクを実行する説明文) : amazonの洗濯機の価格を確認するスクリプト
- 頻度(タスクを実行する頻度。書き方の詳細はこちら) : 00 9 * * *
- タイムゾーン(どこの国を基準として実行するのか?) : 日本標準時(JST)
- ターゲットタイプ(どこのタスクを実行するのか?) : Pub/Sub
- トピック(どのPub/Sub内で生成されたトピックを選択するのか?) : 先ほど作成したトピック
- メッセージ本文(タスクを実行する際にデータを送りたい場合に利用する) : {}
「作成」ボタンをクリックする。
最後に「今すぐ実行」を選択して、Line通知されるか確認する。
完成動画
よくわかるコード解説
以下のコードは、初回実行される関数になります。
- automate.Selenium()を実行してSeleniumを活用するためのインスタンス生成を行っています。
- インスタンスに紐づく関数「access」を実行して、Amazonの商品詳細ページへ移動する。
- ページ読み込みの時間を考慮して、stop関数を実行する。
- 商品の価格情報を取得したいので、価格を表示するHTML要素を指定する。
- HTML要素に記載される価格情報を取得して、不必要な文字を取り除く。
- 毎回通知が届くのは億劫なので、今回は商品の設定価格を決めておいて、現在の価格 <= 設定価格になった場合のみ通知する。
- quit関数を利用して、Seleniumを終了する。
を行っています。
1# エントリーポイント
2def check_price(event, context):
3 # seleniumに関するinstance生成を行う。
4 selenium = automate.Selenium()
5
6 # amazonの洗濯機詳細ページに移動
7 selenium.access(CHECK_URL)
8 # ページ読み込みのために遅延させる。
9 selenium.stop(5)
10
11 # 洗濯機の値段要素を指定
12 selenium.find_element_by_id('price_inside_buybox')
13 # 値段を取得して整形してpriceへ格納する。
14 price = getPrice(selenium.get_element())
15
16 # 通知判定、敷居値より安い値段の場合に通知する。
17 if price <= BASE_PRICE:
18 # Notify URL
19 payload = {'message': "現在の価格は" + str(price) +
20 "円です。敷居値より安くなっております。(敷居値 : " + str(BASE_PRICE) + "円)\n" + CHECK_URL}
21 headers = {'Authorization': 'Bearer ' + LINE_NOTIFY_TOKEN}
22 requests.post(LINE_NOTIFY_API, data=payload, headers=headers)
23
24 # 処理終了
25 selenium.quit()
以下のコードでは、Seleniumの初期化を行っています。
ポイントとして、
- Cloud Functionsへコードをデプロイすると、headless-chromium等のファイルはカレントディレクトリにあります。
- ただ、配下のファイルにアクセスはできません。(恐らくセキュリティの観点からこのような設計になっている。GCP側が所有するコンピュータのみがアクセスできる仕組み。)
- 代わりに/tmpフォルダを用意して、ユーザーが自由にアクセスできるように設定します。
- /tmpフォルダに設置したファイルを実行できるように、権限変更を行います。(addExecutePermission関数)
- Chrome環境でブラウザ立ち上げする際に、細かなオプションを付与する。
が考えられます。
1class Selenium:
2 driver = None
3 element = None
4
5 # ファイル実行権限を付与する関数
6 def addExecutePermission(self, path: Path, target: str = "u"):
7 # https://www.lifewithpython.com/2017/10/python-add-file-permission.html
8 # stat : https://docs.python.org/ja/3/library/stat.html
9 mode_map = {
10 "u": stat.S_IXUSR,
11 "g": stat.S_IXGRP,
12 "o": stat.S_IXOTH,
13 "a": stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH,
14 }
15
16 # 現在所有するファイル権限をmode変数へ集約する。
17 mode = path.stat().st_mode
18 # 追加したいファイル権限を格納する。
19 for t in target:
20 # http://www.tohoho-web.com/python/operators.html
21 mode |= mode_map[t]
22
23 # chmod : https://www.lifewithpython.com/2017/10/python-add-file-permission.html
24 path.chmod(mode)
25
26 def settingDriver(self, driverPath, replaceDriverPath, headlessPath, replaceHeadlessPath):
27 # copy and change permission
28 # GoogleCloudFunctionsへデプロイすると、chromedriverなどのファイルはカレントディレクトリ"/user_code"にあります。
29 # ただしこの配下のファイルにはアクセスできません。代わりに"/tmp"フォルダであればユーザーが自由にアクセスできます。
30 # https://qiita.com/NearMugi/items/8146306168dd6b41b217
31 # copyfile : https://python.keicode.com/lang/file-copy.php#1
32 shutil.copyfile(os.getcwd() + driverPath, replaceDriverPath)
33 # Path : https://note.nkmk.me/python-pathlib-usage/
34 self.addExecutePermission(Path(replaceDriverPath), "ug")
35
36 # copy and change permission
37 # GoogleCloudFunctionsへデプロイすると、chromedriverなどのファイルはカレントディレクトリ"/user_code"にあります。
38 # ただしこの配下のファイルにはアクセスできません。代わりに"/tmp"フォルダであればユーザーが自由にアクセスできます。
39 # https://qiita.com/NearMugi/items/8146306168dd6b41b217
40 shutil.copyfile(os.getcwd() + headlessPath, replaceHeadlessPath)
41 # Path : https://note.nkmk.me/python-pathlib-usage/
42 self.addExecutePermission(Path(replaceHeadlessPath), "ug")
43
44 def __init__(self):
45 driverPath = "/chromedriver"
46 headlessPath = "/headless-chromium"
47 replaceDriverPath = '/tmp' + driverPath
48 replaceHeadlessPath = '/tmp' + headlessPath
49
50 self.settingDriver(driverPath, replaceDriverPath,
51 headlessPath, replaceHeadlessPath)
52
53 # https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.chrome.options
54 options = webdriver.ChromeOptions()
55 options.add_argument("--headless")
56 # https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja
57 options.add_argument("--disable-gpu")
58 # ウィンドウ幅設定
59 options.add_argument("--window-size=1280x1696")
60 # https://daily.belltail.jp/?p=2691
61 options.add_argument("--no-sandbox")
62 # スクロールバーを表示しない
63 options.add_argument("--hide-scrollbars")
64 # ログ周り : https://qiita.com/grohiro/items/718239cdd36da42bd517#%E3%83%AD%E3%82%B0%E3%82%92%E5%87%BA%E3%81%99
65 options.add_argument("--enable-logging")
66 # ログ周り : https://qiita.com/grohiro/items/718239cdd36da42bd517#%E3%83%AD%E3%82%B0%E3%82%92%E5%87%BA%E3%81%99
67 options.add_argument("--log-level=0")
68 # http://chrome.half-moon.org/43.html#e25988df
69 options.add_argument("--single-process")
70 # ssh証明書周りのエラー回避
71 options.add_argument("--ignore-certificate-errors")
72 # https://daily.belltail.jp/?p=2691
73 options.add_argument("--disable-dev-shm-usage")
74
75 options.binary_location = replaceHeadlessPath
76
77 self.driver = webdriver.Chrome(
78 replaceDriverPath, chrome_options=options)
今日のまとめ
- Cloud Functionsを活用してSeleniumを実行できる。
- Cloud Schedulerを用いることで、自動で定期実行したい処理を記述できる。
こんなことにもチャレンジしてみよう
- その他の言語を利用して、Cloud Functionsを実行してみよう。
- Cloud Functionsへローカルからコードデプロイしてみよう。
参考文献
- Line Notifyの使い方
- zipコマンド
- デプロイとは?
- エントリーポイントとは?
- インスタンスとは?
- カレントディレクトリとは?
- Pub/Sub を使用して Cloud ファンクションをトリガーする
- cron ジョブ スケジュールの構成
- Cloud Functions対応可能言語一覧
- Cloud Functionsコードデプロイ
- GoogleCloudFunctionsでSeleniumを使う