Onikenがセールになったら通知してくれるやつをつくる

この記事は[リンク情報システム](https://www.lis.co.jp/)の[2021新春アドベントカレンダー Tech Connect!](https://qiita.com/dgkz/items/eabde6515457d8acec9b)のリレー記事です。
Tech Connect! は勝手に始めるアドベントカレンダーとして、engineer.hanzomon のグループメンバによってリレーされます。

---
この間のセールで買いそびれた人に向けた記事です。

スクレイピングした結果をSlackに連携するだけのよくあるやつですが、他の記事との差別化を図るためにGoogle Cloud Platform(以下GCP), Microsoft Azure(以下Azure), Amazon Web Services(以下AWS)にそれぞれデプロイしてみます。

# つくるもの
1日1回 [My Nintendo StoreのOnikenのページ](https://store-jp.nintendo.com/list/software/70010000016680.html) を見に行ってセール中だったらSlackに連携する。

# つくる
## My Nintendo Storeのrobots.txtを確認
https://store-jp.nintendo.com/robots.txt を確認します。
「/list/software/」はDisallowされていなかったので問題なし。

## Slackの設定
### アプリの追加画面を開く
![slack01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/cdde2b83-775f-764c-255c-13f149c23741.png)

### Incoming WebHooksを追加
![slack02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/4a8751d9-5b9f-06c5-4564-083f0576f641.png)

![slack03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/7610d635-a53e-96ed-d26b-6c2666c00f3e.png)

### 通知先のチャンネルを指定
![slack04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/fab47663-1db9-e7cc-d55e-051c7a54e4a9.png)

### Webhook URLをメモ
名前もわかりやすいものに変えておく。
![slack05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/6f10c6be-33fc-9ad2-4b97-8346669dc2bb.png)

## スクリプトなどの準備
### スクレイピングとSlackへの連携用のスクリプト
とりあえず動けばいいぐらいのやつ。

```python
import json
import os

import requests
from bs4 import BeautifulSoup

res = requests.get(os.environ['TARGET_URL'])
soup = BeautifulSoup(res.text, 'html.parser')
div = soup.select_one('div.productDetail--detail__pricePrice')

if div is not None:
    item = {
        "text": div.text.strip()
    }
    json_data = json.dumps(item)
    res = requests.post(os.environ['WEBHOOK_URL'], json_data)
```

#### スクレイピング
`div = soup.select_one('div.productDetail--detail__pricePrice')` で以下の情報(値段)を取得します。
これが取得できなかった場合はセール中ではないためSlackへは連携しません。
![s01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/4e0ed164-db60-77ef-1677-f35abebbc009.png)

#### Slackへの連携
`res = requests.post(os.environ['WEBHOOK_URL'], json_data)` の部分。
Webhook URLへjsonのデータをpostするだけです。

今回は単純なメッセージですが、複雑なものを作ってみたい場合は[Block Kit Builder](https://api.slack.com/tools/block-kit-builder)で色々試してみるのがいいかと思います。

### requirements.txt
```txt
beautifulsoup4==4.9.1
requests==2.24.0
```

## GCPへデプロイ
<details><summary>開く</summary><div>

### Cloud Functions
スクレイピングしてSlackに連携する部分を担当。
#### Cloud Functionsを選択
![gcp01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/4853b0f8-5055-9e9f-0a23-2322eda50298.png)

#### 関数を作成
![gcp02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/be53e28c-65ea-6798-a3aa-7bb9cf808c21.png)

#### 各種設定
"基本"はお好きなように。
"トリガー"は画像の通りに設定して"保存"する。
**URLは後で使うのでメモしておくこと**
![gcp03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/e1a6d36c-a14f-0085-3c52-c7d8a7020e1e.png)

#### ランタイム環境変数の設定
名前 | 値
--- | ---
TARGET_URL | OnikenのページのURL
WEBHOOK_URL | 最初の方でメモしたSlackのWebhook URL

設定したら"次へ"。
![gcp04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/d10b2b27-281b-8cb0-41e8-b7e611f064e8.png)

#### Build APIの有効化
![gcp05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/5a02e056-dee8-1849-7c45-8e990dfa89e6.png)

#### スクリプトを編集
まんまコピペしているわけではないので注意。
![gcp06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/b75f5b27-187f-18fc-61fb-5deb1a4d8169.png)

#### requirements.txtを編集
コピペしたら"デプロイ"。
![gcp07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/98429b44-064e-a2c1-343c-95071ca908cc.png)

#### デプロイ完了を確認
これは少し時間がかかります。
![gcp08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/e11088d9-310c-87c4-f193-cb4ecad09b30.png)

### Cloud Scheduler
これを使って1日1回Cloud Functionsを実行します。
**無料枠で3ジョブまで作成できる**

#### Cloud Schedulerを選択
![gcp09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/fcdcb2f5-0f78-a356-816f-24b24fbcad24.png)

#### ジョブを作成
この画面のあとにリージョン選択画面がありますが、スクショを撮り忘れました。
好きな場所を選択してください。
![gcp10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/4a9767d0-ea32-cfc5-8aa8-f1c2eb42f634.png)

#### 各種設定
名前、説明などはお好きなように。

項目 | 値
--- | ---
ターゲット | HTTP
URL | 先ほど作成したCloud FunctionsのトリガーURL
HTTPメソッド | GET

設定が終わったら"作成"。

![gcp11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/103430eb-18b7-590f-9f2f-d0098a754f0d.png)

### GCPへのデプロイ終わり
GCPへのデプロイはこれで終わりです。
</div></details>

## Azureへデプロイ
<details><summary>開く</summary><div>

### 環境構築
今回の構成ではAzure Portal上でコードを書けないため、色々と準備が必要になります。
長くなりそうなので詳細は割愛します。

- Node.js
- Python(3.6, 3.7, 3.8のいずれか)
- Visual Studio Code
- Visual Studio Codeの[Python拡張](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- Visual Studio Codeの[Azure Functions拡張](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions)

### 関数アプリ(Azure Functions)
#### リソースの作成
![az01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/b7099e51-3fb3-5a72-8a95-3d51b38c0b25.png)

#### 関数アプリを選択
![az02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/d1719de1-869d-9ad1-3865-5a9754bf9490.png)

#### 各種設定
赤枠以外はお好きなように。
他はデフォルトで"確認および作成"。
![az03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/51003eee-720a-3130-83f7-be173434a729.png)

#### 作成
![az04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/92b3d750-e748-461d-b63a-59a4a4fd2b10.png)

デプロイ完了したら"リソースに移動"。
![az05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/372ff04f-4883-e3a4-4e3a-6f466cd45f09.png)

#### 環境変数の設定
以下の2つを設定して"保存"。

名前 | 値
--- | ---
TARGET_URL | OnikenのページのURL
WEBHOOK_URL | 最初の方でメモしたSlackのWebhook URL

![az06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/c0dfde4d-1872-b6be-1ac9-7902d23fd28a.png)

### 関数を作成
Visual Studio Codeでの操作になります。

#### Azureにサインイン
![az07_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/55532f80-beb3-82aa-77fc-64dc234fc529.png)

#### プロジェクト作成
今回はDesktopに作ったazure_sampleフォルダで作業を行います。
![az07_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/d4004bb4-773a-ee06-3269-671a6d86cf5c.png)

#### 言語を選択
![az08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/ce789995-263b-6033-4847-e37c0678af1b.png)
![az09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/d361c023-fa1c-b074-d185-af4fa7345abd.png)

#### 関数のトリガーを選択
"Timer trigger"を選択します。
![az10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/9610c238-30da-779b-f010-1774fb88f1be.png)

#### 名前を付ける
![az--.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/7c65c6d8-5bbe-fb5e-aa0e-84d0cdf199f8.png)

#### スケジュールを指定
毎日01:00に実行するように指定しています。
![az11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/e1fa5449-a23c-7059-bfb6-4e2c486b0f2f.png)

#### スクリプトを編集
しばらく待つと`__init__.py`というファイルが開かれるので、事前に用意していた内容に書き換えます。
![az12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/def122c9-9e75-a144-3102-c6837eb53259.png)

#### requirements.txtを編集
プロジェクトフォルダ(今回はazure_sample)にrequirements.txtがあるのでそれを編集します。
![az13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/723c488c-e5cd-208f-1b73-d8ba4f10fb2a.png)

#### デプロイ
多少時間がかかります。
![az14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/2f456712-497c-5ddc-d1a9-1ef05d627859.png)

ちゃんとデプロイされています。
![az15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/9c5fbebb-a8c7-7023-9947-d632f424a631.png)

### Azureへのデプロイ終わり
Azureへのデプロイはこれで終わりです。
Timer triggerのおかげで関数アプリ単独で実現できました。
</div></details>

## AWSへデプロイ
<details><summary>開く</summary><div>

### Lambda
#### 関数作成の画面に行く
![aws01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/ab97d505-09a6-dbb9-eca4-0956d9632166.png)
![aws02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/ef0dda75-5430-a70a-e603-f561c44f48b4.png)

#### 関数の作成
関数名はお好きなように。
オプションとランタイムの選択が終わったら"関数の作成"。
![aws03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/00a78f2e-ac23-fc3b-948d-f3e03df4f337.png)

#### zipファイルをつくる
bs4～urllib3まではpythonのライブラリ。
`lambda_function.py` は事前に用意したスクリプトをlambda用にしたもの。

```
oniken.zip
 ├ bs4
 ├ certifi
 ├ chardet
 ├ idna
 ├ requests
 ├ soupsieve
 ├ urllib3
 └ lambda_function.py
```

```python:lambda_function.py
import json
import os

import requests
from bs4 import BeautifulSoup

def lambda_handler(event, context):
	res = requests.get(os.environ['TARGET_URL'])
	soup = BeautifulSoup(res.text, 'html.parser')
	div = soup.select_one('div.productDetail--detail__pricePrice')

	if div is not None:
	    item = {
	        "text": div.text.strip()
	    }
	    json_data = json.dumps(item)
	    res = requests.post(os.environ['WEBHOOK_URL'], json_data)
	    
	    return "success"
```

#### zipファイルのアップロード
![aws04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/0bff1161-a9cc-f802-1df1-a50632e2c383.png)
![aws05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/2ed4b88e-cc33-827d-2642-859ce9162acf.png)

#### 環境変数の設定
![aws06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/eae7e7b2-ada4-981e-cf5c-bb2e85ba6d11.png)

以下の2つを設定して"保存"。

名前 | 値
--- | ---
TARGET_URL | OnikenのページのURL
WEBHOOK_URL | 最初の方でメモしたSlackのWebhook URL

![aws07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/8768c0c7-6dfe-8527-a02f-c0062355bada.png)

### トリガー
#### トリガーを追加
![aws08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/43f5ceff-aa28-dd14-1791-22d74395e96a.png)

#### EventBridgeの設定
毎日02:00に実行するように指定しています。
![aws09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/0d5a312c-61fc-db62-ff3d-e426f8d6857d.png)

### AWSへのデプロイ終わり
AWSへのデプロイはこれで終わりです。
ライブラリも自分でアップロードしないといけないので面倒でした。(GCP, Azureはrequirements.txt書くだけ)
</div></details>

# 動作確認
00:00(GCP)、01:00(Azure)、02:00(AWS)にそれぞれ通知が来る予定でしたが、AzureとAWSが9時間ほどずれています。
GCPはCloud Schedulerでタイムゾーンを設定しましたが、そういえばAzureとAWSではそのような設定が無かったような気がします。
![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532463/80a447f3-fc44-9f86-af58-27443793d3cd.png)

## Azure
ログを見ると01:03(UTC)に動いていました。
3分のずれは一体？

## AWS
ログを見るとこちらも02:00(UTC)に動いていました。

## とはいえ
動きはしたのでこれは宿題とします。

# まとめ
環境構築が出来ていればAzureが圧倒的に楽でした。
Timer triggerは偉大。

種類 | かんたん | ふつう | むずかしい
--- | --- | --- | ---
環境構築していない | GCP | AWS | Azure
環境構築済み | Azure | GCP | AWS

# 参考
- [Visual Studio Code と Python を使用して Azure に関数を作成する](https://docs.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-python)
- [Azure での関数アプリの設定の構成 | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-how-to-use-azure-function-app-settings)
- [Python の AWS Lambda デプロイパッケージ - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html)


<details><summary></summary><div>
Microsoft StoreのOnikenのページがどうしてあんなことになっているのか解明してくれる人を探しています。

**あんなこととは？**
[Microsoft StoreのOnikenのページ](https://www.microsoft.com/ja-jp/p/oniken-unstoppable-edition/9n7nsln5tbd8?activetab=pivot:overviewtab)より引用。

> 不吉な企業によって制御された目次録後の未来において、8ビットのアクション/プラットフォームの過酷なゲームプレイに触発されて、鬼剣は敵、罠、そして挑戦的なボスの籠手に対してあなたの敏捷性と刀剣を打ちつけます。

少し読んでもらえば分かる通り、日本語がおかしなことになっています。

**翻訳元を探る**
[Microsoft StoreのOnikenのページ(英語)](https://www.microsoft.com/en-us/p/oniken-unstoppable-edition/9n7nsln5tbd8?source=lp&activetab=pivot:overviewtab)より引用。
パブリッシャーの製品ページも同じ文章。

> In a post-apocalyptic future controlled by a sinister corporation, ninja mercenary Zaku is the last hope for humanity. Inspired by the demanding gameplay of 8-bit action/platformers, Oniken pits your agility and swordplay against a gauntlet of enemies, traps, and challenging bosses.

「目次録」とは「黙示録」のことでした。

**翻訳してもらう**
原文(日本語ページ)を見る限り何らかの翻訳をかませてそうなので、英語ページの文章をいくつかのサイトで翻訳してもらいます。

- [Google翻訳](https://translate.google.co.jp/?hl=ja)

> 不吉な企業によって支配された終末後の未来において、忍者傭兵ザクは人類への最後の希望です。 8ビットアクション/プラットフォーマーの要求の厳しいゲームプレイに触発されたOnikenは、敵、罠、挑戦的なボスのガントレットに対して敏捷性と剣術を打ち負かします。

- [DeepL翻訳](https://www.deepl.com/ja/translator)

> 邪悪な企業に支配されたポスト黙示録的な未来では、忍者の傭兵ザクが人類の最後の希望となっていた。8ビットアクション/プラットフォーマーの厳しいゲーム性にインスパイアされた「鬼剣」は、敏捷性と剣術を駆使して、敵、トラップ、ボスなどの難関を突破していく。

- [Bing Microsoft Translator](https://www.bing.com/translator)

> 不吉な企業が支配するポスト黙示録的な未来の中で、忍者傭兵ザクは人類の最後の希望です。8ビットアクション/プラットホームの厳しいゲームプレイに触発され、Onikenは敵、トラップ、そして挑戦的なボスのガントレットに対してあなたの敏捷性と剣遊びをピット。

- [IBM Watson Language Tranlator Demo](https://www.ibm.com/demos/live/watson-language-translator/self-service/home)

> 不吉な企業によって支配された後の後進法において、忍者の傭兵のザクは人類にとって最後の希望である。 8 ビットアクション/小皿の厳しいゲームに触発され、鬼剣は敵、罠、そして挑戦的なボスに対して、機敏に剣術を注ぎ、その腹部に対して剣術を行う。

**Google翻訳の結果が原文に近い**
DeepL翻訳を除いた3つはほぼ似た結果になっており、中でもGoogle翻訳は忍者傭兵ザクの部分を除くとかなり原文に近い印象です。
原文とGoogle翻訳の結果を並べて違いを見てみます。

- 原文

> 不吉な企業によって**制御**された**目次録後**の未来において、8ビット**の**アクション/**プラットフォーム**の**過酷な**ゲームプレイに触発**されて、鬼剣は**敵、罠、**そして**挑戦的なボスの**籠手**に対して**あなたの**敏捷性と**刀剣**を**打ちつけます**。

- Google翻訳(から忍者傭兵ザクの部分を除外)

> 不吉な企業によって**支配**された**終末後**の未来において、8ビットアクション/**プラットフォーマー**の**要求の厳しい**ゲームプレイに触発**されたOnikenは、**敵、罠、挑戦的なボスの**ガントレット**に対して敏捷性と**剣術**を**打ち負かします**。

Google翻訳のアップデートで翻訳結果が変わってしまったようなレベルの違いしかないように見えます。

**深まる謎**
日本語ページの文章はおそらくGoogle翻訳の結果をベースにしていると思われる、というところまでは来ましたが理解できない謎が残っています。

- 目次録 
何をどうやったら黙示録が目次録になってしまうのか検討もつきません。
英語 → (翻訳) → 日本語 → (音声読み上げ) → (Speech to Text) → 日本語 とすれば、Speech to Textの箇所で聞き間違いが発生する可能性もなくはないですが、わざわざこんなことする意味もないでしょう。

- 消えてしまった忍者傭兵ザク 
ザクはOnikenの主人公です。
翻訳の際に

**おわりに**
答えに近づけた方がいましたらぜひ教えて下さい。

</div></details>