初心者がAWS Lambdaを活用してログインが必要なGoogle Formに自動回答するプログラムを作ってみた

みなさんこんにちは。コロナが収まったかと思えばまた蔓延したりとまだまだ大変ですね。

昨今は部活や大学、高校などで毎日体温を測定しGoogle Formなどで○○時までに提出するよう求められることがあると思います。僕も毎日(決まりでは)提出しないといけないのですが、根がズボラなのでサボりがちです。
こんなだらしない人間がこの問題を解決するにはプログラミングしかない！中学生の頃にHSP(Hot Soup Processor)を少し触った程度の初心者だけどプログラミングのいい勉強にもなるだろう、ということで毎日決まった時間に自動送信するプログラムを作りたいと思いました。

今回は、途中でログインが必要となるタイプのフォームに対してのプログラムです。**ただし、Google Workplace for Educationに登録されている大学のアカウントを用いたログインのため、Googleログインの途中で大学アカウントのログインページに移行しています。** そのためGoogleがHeadless操作を検知してログインできない、ということは起こりませんでした。一般のGoogleアカウントを用いた操作の場合はbot判定されることもあるようなのですがそのケースは今回は考慮していません。
ローカル環境だと常時パソコンの電源をつけておく、あるいは時間指定でパソコン起動というやり方も調べるとあるようですが、デスクトップパソコンある人はまだいいとして、ノートパソコンしかない人などにとっては非現実的です。また電気代も多少かさんでしまいます。そのため、AWSなどを用いてサーバーレスで実行するのが最も利便性が高いと考えたため、その中でも比較的扱いやすい(といわれている)Lambdaを使用することにしました。(ほとんどが過去にある記事やサイトをなぞった内容になっています。)
あくまでも**初心者が書いた記事なので、専門用語などはかなり曖昧ですが何卒ご了承下さい。**

# 目次
[1. AWSアカウントの作成/サインイン](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#1-aws%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90%E3%82%B5%E3%82%A4%E3%83%B3%E3%82%A4%E3%83%B3)
[2. Googleフォームで初期値付きURLを作成](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#2-google%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%A7%E5%88%9D%E6%9C%9F%E5%80%A4%E4%BB%98%E3%81%8Durl%E3%82%92%E4%BD%9C%E6%88%90)
[3. (失敗)PythonでSeleniumなどを用いてLambdaでスクレイピング](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#3-%E5%A4%B1%E6%95%97python%E3%81%A7selenium%E3%81%AA%E3%81%A9%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0
)
[4. (成功)JavaScriptでPuppeteerを用いてLambdaでスクレイピング](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#4-%E6%88%90%E5%8A%9Fjavascript%E3%81%A7puppeteer%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0
)
[5. 定期実行を設定](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#5-%E5%AE%9A%E6%9C%9F%E5%AE%9F%E8%A1%8C%E3%82%92%E8%A8%AD%E5%AE%9A
)
[6. テスト](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#6-%E3%83%86%E3%82%B9%E3%83%88)
[7. 参考文献](https://qiita.com/XinDrone/items/f96131ab3838820fdcb2#7-%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE)

## 1. AWSアカウントの作成/サインイン
まずAWS(Amazon Web Services)のアカウントを持っていない方は作成しましょう。[こちらのサイト](https://kacfg.com/aws-account-create/)に詳しく載っています。無料枠を超過した際に自動で課金されるようにクレジットカード番号を登録する必要がありますが、**Lambdaの無料枠は100万件/月**なのでよほどのことがない限りは超えないと思います。

## 2. Googleフォームで初期値付きURLを作成
[こちらのページ](https://qiita.com/kota-yata/items/9d4124ec7a7dd4e3d4f0#%E5%88%9D%E6%9C%9F%E5%80%A4%E5%85%A5%E5%8A%9B%E3%81%A4%E3%81%8Durl%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B)に詳しい説明が載っています。viewformの後の「usp=sf_link」というパラメータを「usp=pp_url」に変えて、以下回答をURLに入力していきます。
例えば選択肢の回答が「２年生」というように**数字が全角の場合**、```entry.番号=回答内容```の```回答内容```の部分も**全角で統一する必要があるので注意して下さい。**

## 3. (失敗)PythonでSeleniumなどを用いてスクレイピング
まずLambdaに"Selenium"と"ChromeDriver+Headles-Chromium"の計2つのレイヤーをアップロードしました。
**AWSはLinuxベースで動作する**ので、ChromeDriverやHeadless-chromiumはLinux版をダウンロードする必要があります。
ぼくはLinux環境を構築するのがなかなか面倒だったので、AWSのCloud9を用いてレイヤーを作成しました。
そして、レイヤーをアップロードしてから、[こちらのページ](https://qiita.com/kota-yata/items/9d4124ec7a7dd4e3d4f0#lambda%E7%94%A8%E3%81%AB%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%B0%91%E3%81%97%E5%A4%89%E3%81%88%E3%82%8B)のスクリプトを参考にPythonで以下のコードを入力しました。

```python:lambda_function.py
import json
from selenium import webdriver
import time
import random

def lambda_handler(event, context):
    body_temp = str(36 + random.randint(1,7)/10)
    url1 = 'https://www.google.com/accounts?hl=ja-JP'
    options = webdriver.ChromeOptions()
    options.binary_location = '/opt/python/bin/headless-chromium'
    options.add_argument('--headless') # サーバーレスでChromeを起動
    options.add_argument('--no-sandbox') # sandbox外でChromeを起動
    options.add_argument('--single-process') # シングルプロセスに切り替え
    options.add_argument('--disable-dev-shm-usage') # メモリファイルの出力場所を変更
    browser = webdriver.Chrome('/opt/python/bin/chromedriver', options = options)
    browser.implicitly_wait(1)
    browser.get(url1)

    G_ID = browser.find_element_by_id('identifierId')
    G_ID.send_keys('Googleアカウントのメールアドレス')
    G_Enter = browser.find_element_by_class_name('VfPpkd-vQzf8d')
    G_Enter.click()

    time.sleep(3)

    U_ID = browser.find_element_by_id('username')
    U_ID.send_keys('学内アカウントのID')
    U_PW = browser.find_element_by_id('password')
    U_PW.send_keys('学内アカウントのパスワード')
    U_Enter = browser.find_element_by_name('_eventId_proceed')
    U_Enter.click()

    time.sleep(3)
    G_Enter = browser.find_element_by_class_name('VfPpkd-vQzf8d')
    G_Enter.click()

    time.sleep(2)
    body_temp = str(36 + random.randint(1,5)/10)
    url2 = 'https://docs.google.com/forms/d/e/***********************/viewform?usp=pp_url&entry.##########=<回答内容1>&entry.##########=<回答内容2>&entry.##########='+body_temp 
    # URL最後の部分は体温入力箇所です

    browser.get(url2)

    time.sleep(2)
    try:
     F_Continue = browser.find_element_by_xpath('/html/body/div[2]/div/div[2]/div[3]/div[2]/a/span/span')
     F_Continue.click()
    except:
     pass
    time.sleep(1)
    Send_Button = browser.find_element_by_xpath('//*[@id="mG61Hd"]/div[2]/div/div[3]/div[2]/div[1]/div/span/span')
    Send_Button.click()
    
    time.sleep(2)
    browser.close
    browser.quit
    
    return {
        'statusCode': 200,
        'body': json.dumps('Form submission success!!')
    }
```
このコードを入力してDeployの後、テストしてみると以下のエラーが発生しました。
```
{
  "errorMessage": "Message: session not created: Chrome version must be >= 69.0.3497.0\n  (Driver info: chromedriver=2.43.600233 (523efee95e3d68b8719b3a1c83051aa63aa6b10d),platform=Linux 4.14.252-207.481.amzn2.x86_64 x86_64)\n",
  "errorType": "SessionNotCreatedException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 16, in lambda_handler\n    browser = webdriver.Chrome('/opt/python/bin/chromedriver', options = options)\n",
    "  File \"/opt/python/selenium/webdriver/chrome/webdriver.py\", line 73, in __init__\n    service_log_path, service, keep_alive)\n",
    "  File \"/opt/python/selenium/webdriver/chromium/webdriver.py\", line 99, in __init__\n    options=options)\n",
    "  File \"/opt/python/selenium/webdriver/remote/webdriver.py\", line 268, in __init__\n    self.start_session(capabilities, browser_profile)\n",
    "  File \"/opt/python/selenium/webdriver/remote/webdriver.py\", line 359, in start_session\n    response = self.execute(Command.NEW_SESSION, parameters)\n",
    "  File \"/opt/python/selenium/webdriver/remote/webdriver.py\", line 424, in execute\n    self.error_handler.check_response(response)\n",
    "  File \"/opt/python/selenium/webdriver/remote/errorhandler.py\", line 247, in check_response\n    raise exception_class(message, screen, stacktrace)\n"
  ]
}
```
どうやらChromeのバージョンが古いとのことですが、調べても解決策は出てこず...
ChromeDriverとHeadless-Chromiumはお互いにバージョンが合致しないと正しく動作しないのでエラーが出やすいのかもしれません。
結局埒が明かないのでこの方法は諦めることに。

## 4. (成功)JavaScriptでPuppeteerを用いてスクレイピング
ということで、ほかにいい方法はないかと検索してみると、Puppeteerというライブラリを使うとこれ単体でヘッドレス操作が可能らしいので導入することに。ただ、言語はPythonではなくJavaScript(Python用のPyppeteerもあるようですが具体例に乏しいので避けました)です。仕方ないので少し勉強して上のスクレイピングのコードをJS用に書き換えることにしました。
ところで、**AWS Lambdaにはアップデートできるレイヤーに容量制限があります。** Zip形式の場合50MBが上限です。通常のPuppeteerは250MB以上もありアップロードできないので、**[GitHub - alixaxel/chrome-aws-lambda](https://github.com/alixaxel/chrome-aws-lambda)** を用いたいと思います。
#### 4-1. Cloud9でレイヤーをダウンロード
AWSはLinuxベースで動いているので、レイヤーもLinux仕様のものをアップロードする必要があります。Windws/MacではファイリングやZip化がうまくいきません。ローカルでLinux環境を構築してレイヤーを作成してもいいのですが、少し手間がかかるので、今回はAWSのサービスの１つである[Cloud9](https://aws.amazon.com/jp/cloud9/)を用いてレイヤーを作成します。
まずアクセスします。"create environment"を押して 'Name'を適当に入力して"Next step"を押します。次の画面では何も触らずそのまま"Next step"へ。その後、"Create environmnt"を押すとCloud9が起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535800/e0e92059-0e92-787b-84d4-b92e6bfbe650.png)
上の部分に1行ずつ入力していきます。
```
git clone --depth=1 https://github.com/alixaxel/chrome-aws-lambda.git
cd chrome-aws-lambda/
npm install
make chrome_aws_lambda.zip
```
すると左上に
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535800/3fb43b95-aeb7-466d-803e-b318fd7424a8.png)
このように表示されるかと思います。上の赤丸の部分を押すと、中のファイルが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535800/150819a7-e0d1-36f0-cb8f-b12209caffa9.png)
ここで"chrome_aws_lambda"のファイルを右クリックして'Download'を押すとzipファイルをダウンロードできます。

[追記]Amazon EC2を使った際にはちゃんとインスタンスを削除しておきましょう。停止してるだけでは課金されてしまうので注意してください。(僕はこの記事のために試行錯誤した結果インスタンスを消し忘れて¥500くらい徴収されてしまいました。悔しい。)

#### 4-2. AWS Lambdaの設定
[AWS Lambda](https://aws.amazon.com/jp/lambda/)にアクセスします。

レイヤーで chrome_aws_lambda.zip を選択します。「互換性のあるランタイム」で**Node.js**を選択。入力できたら「作成」をクリックします。

AWSマネジメントコンソールで、サービス ＞ Lambda ＞「関数の作成」をクリックします。ランタイム（プログラム言語）は**Node.js 14.x**を選択して関数の作成をクリックします。
関数が作成されたら、レイヤーを追加しましょう。

「設定」→「基本設定」を開いてメモリや権限などを設定します。

デフォルトではメモリが 128 MB しか割り当たっておらず、ブラウザの動作には厳しいため、 メモリは **512~1024 MB** 程度に設定します。またタイムアウトも **1 分程度** にしましょう。

関数のコードソースから**index.js**を開きます。
下記はサンプルコードです。体温の乱数の箇所はもっとスマートな書き方があると思うのであまり当てにしない方がいいです。

```javascript:index.js
//chrome-aws-lambdaからchromiumをインポート
const chromium = require('chrome-aws-lambda');
//chrome-aws-lambdaからpuppeteerをインポート
const puppeteer = require('puppeteer-core');
//体温を36.1~36.6までの間でランダムに返すように設定
var num = 1 + Math.floor( Math.random() * 5);
const body_temp = 36 + Math.floor(num) / 10;
exports.handler = async (event, context) => {
  let result = null;
  let browser = null;
//puppeteerのヘッドレス設定
browser = await puppeteer.launch({
 args: chromium.args,
 defaultViewport: chromium.defaultViewport,
 executablePath: await chromium.executablePath,
 headless: chromium.headless,
});
  
let page = await browser.newPage();
await page.goto('https://accounts.google.com/signin/v2/identifier');
await page.waitForSelector('input[type="email"]');
//Google IDの例：example@gmail.com
await page.type('input[type="email"]', 'example@gmail.com');
await Promise.all([
 page.waitForNavigation(),
 await page.keyboard.press('Enter')
]);
await page.waitForSelector('input[type="password"]', { visible: true });
//学内アカウントIDの例：example
await page.type('input[type="text"]', 'example');
//学内アカウントのパスワードの例：abcdefg
await page.type('input[type="password"]', 'abcdefg');
await Promise.all([
 page.waitForNavigation(),
 await page.keyboard.press('Enter')
]);
await page.waitForSelector('input[type="text"]');
//回答するGoogle Formの初期値付きURL + 体温の乱数調整
await page.goto(`https://docs.google.com/forms/d/e/***********************/viewform?usp=pp_url&entry.##########=<回答内容1>&entry.##########=<回答内容2>&entry.##########=${body_temp}`);
await Promise.all([
 page.waitForNavigation(),
 await page.keyboard.press('Enter')
]);
await page.click('#mG61Hd > div.RH5hzf.RLS9Fe > div > div.ThHDze > div.DE3NNc.CekdCb > div.lRwqcd > div > span > span');
browser.close();

if (browser !== null) {
      await browser.close();
}
return context.succeed(result);
};
```
## 5. 定期実行を設定
関数の「Layers」をクリックして「トリガーを追加をクリック」し、「EventBridge (CloudWatch Events)」を選択します。
ルールタイプをスケジュール式にします。ちなみにスケジュール表記に使われるcron式は以下の形式で表します。

```
      分 時 日 月 曜日 
cron (*  *  *  *  *)  
```
今回は毎日朝7時なのでcron(0 22 ? * * *)と入力します。日の部分を?にすることで毎日実行するように設定しています。トリガーを有効にして「追加」をクリックします。上の例では22はUTC(協定世界時)の時間でトリガーされるので、日本時間朝7時=31時から9時間を引いた22を代入しています。

## 6. テスト
最後にしっかり動くかテストしましょう。関数画面の「テスト」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535800/b162aae1-0507-7c5e-e6b9-5d6d414a1cc8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535800/d34d83f5-7161-3517-b45d-1fa46c29d34e.png)
きちんと確認メールも送られていますね。

実装して5日程度経ちました。確認メールを見てみると、
![750BFEDC-FDFD-4CF1-B886-54CFD7BFE073.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2535800/578aff9f-62cd-284b-371b-791f3d145865.jpeg)
きちんと毎日送信されていることが確認できました。

## 7. 参考文献
[毎朝5時にGoogle Formに自動回答したい - Qiita](https://qiita.com/kota-yata/items/9d4124ec7a7dd4e3d4f0)
[ヘッドレスChromeをAWS Lambda上のPuppeteerから操作してみた | DevelopersIO](https://dev.classmethod.jp/articles/run-headless-chrome-puppeteer-on-aws-lambda/)
[AWS LambdaでPuppeteerを動かす - Qiita](https://qiita.com/kodai-saito/items/9051d2b30a29c7d64f7d)