Pythonで「アボガドをアボカドに訂正する」を自動化する

少し前ですが、[アボガドをアボカドと訂正し続け8万回　謎のツイッター「アボガドをアボカドに訂正する委員会」](https://www.j-cast.com/2018/06/07330512.html?p=all)という記事が話題になりました。毎日100〜200件ものアボ"ガ"ドツイートにいいねをつけているそうです。なんという情熱だ...と感銘を受けたと同時に、それって自動でできるんじゃ？と思ったのでやってみました。自動でできるんですよ。**そう、Pythonならね。**(※TwitterAPIを使うだけなのでPythonじゃなくでもできます笑)

## 今回つくるもの
![アボガドじゃない](https://qiita-image-store.s3.amazonaws.com/0/163381/409f3f50-0c56-7a2e-c90f-424c8e2c7464.png)

委員会の方に倣って「アボガド」というワードが含まれるツイートにいいねをつけるPythonプログラムを作ります。そして、そのプログラムを定期実行する仕組みを作り、完全自動化します。ツイートの検索、いいねはPythonのtweepyという外部パッケージ、定期実行はAWS Lambdaを使用します。定期実行について、EC2などでサーバーを立ててcronで実行するという手もありますが、例えば1時間に1回実行するとして、1日24回数秒のプログラムを実行するためだけに24時間サーバーを動かしているのはお金もかかりますしもったないないですよね。Lambdaであれば関数が実行された時間に対してのみ課金されるのでコスパがいいですし、インフラもクラウド側が管理してくれて便利なので今回はLambdaを使っていきます。

## 用意するもの
- TwitterのConsumer Key、Consumer Secret、Access Token、Access Token Secret(開発者登録して取得します)
- AWSアカウント(加えてAdministratorAccessという管理ポリシーをアタッチしたIAMユーザー)
- Python3(v3.6.5で進めます)
- Node.js環境(Nodeはv8.11.3、npmはv5.6.0で進めます)

## ソースコード
GitHubにて公開していますので参考にしてください。
[masaki-koide/avogado-janai](https://github.com/masaki-koide/avogado-janai)

## 環境構築
まず、Pythonの仮想環境を作ります。

```bash
python3 -m venv avogado-janai
cd avogado-janai
. bin/activate #　仮想環境に入る
```

今回は外部パッケージとしてtweepyというTwitterAPIをラップしたものを使いますのでインストールします。

```bash
pip install tweepy
```

続いて、Serverless Frameworkというnpmパッケージをインストールします。Lambdaのデプロイは少々ややこしいのですが、これを使うと、コマンド一つでデプロイができます。

```bash
npm install -g serverless
sls -v # serverlessコマンド(もしくは省略でslsコマンド)が使えるようになります
```

Serverless FrameworkでPython3のLambdaのプロジェクトを作ります。

```bash
sls create --template aws-python3 --path serverless-project
cd serverless-project
npm init # 初期化しておく
ls
```

handler.pyやserverless.ymlといったファイルが生成されているのが確認できると思います。handler.pyにLambdaの関数を書き、serverless.ymlにデプロイの設定情報を書いていきます。

## handler.py
以下をコピペします。全部で40行程度です。tweepyがTwitterAPIをいい感じにラップしてくれているのでPythonを触ったことのない方でもなんとなく意味のわかるプログラムになっているのではないでしょうか。

```python:handler.py
import json
import tweepy
import os
import logging

# ①
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_TOKEN_SECRET = os.environ['ACCESS_TOKEN_SECRET']

auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)

# ②
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# ③
def favorite_avogado_tweet(event, context):
    # 最後にいいねしたツイートのIDを取得
    recent_favorited_tweet = api.favorites(api.me().screen_name)[0].id

    # 'アボガド'というワードで最新のツイート100件を検索する(リツイートは除く)
    for tweet in tweepy.Cursor(api.search, q='アボガド exclude:retweets', lang='ja').items(100):
        # 最後にいいねしたツイートに到達したら処理を終了する
        if tweet.id == recent_favorited_tweet:
            break

        # 特定のワードが入っていないツイートのみを対象とする
        exclude_words = ['アボカド', 'アボガド6', 'アボガド６']
        if not any([word in tweet.text for word in exclude_words]):
            try:
                # いいねをつける
                api.create_favorite(tweet.id)
            except Exception as e:
                logger.error(e)
                return {
                    "message": e,
                    "event": event
                }

    return {
        "message": "Success!",
        "event": event
    }
```

①ですが、TwitterのConsumer Key、Consumer Secret、Access Token、Access Token Secretを環境変数から取得します。環境変数の設定方法は後ほどserverless.ymlの箇所で説明します。続く3行でそれらの情報を使ってTwitterの認証を行っています。

②ですが、Python標準のロガーを取得しています。print関数でもいいのですが、Loggerだと時刻なども自動で出力してくれるので便利です。

③ですが、メインの関数です。Lambdaが実行されるとこの関数が呼び出されます。関数の内容ですが、改めて詳しく見てみます。

```python:handler.py
def favorite_avogado_tweet(event, context):
    # 最後にいいねしたツイートのIDを取得
    recent_favorited_tweet = api.favorites(api.me().screen_name)[0].id

    # 'アボガド'というワードで最新のツイート100件を検索する(リツイートは除く)
    for tweet in tweepy.Cursor(api.search, q='アボガド exclude:retweets', lang='ja').items(100):
        # 最後にいいねしたツイートに到達したら処理を終了する
        if tweet.id == recent_favorited_tweet:
            break

        # 特定のワードが入っていないツイートのみを対象とする
        exclude_words = ['アボカド', 'アボガド6', 'アボガド６']
        if not any([word in tweet.text for word in exclude_words]):
            try:
                # いいねをつける
                api.create_favorite(tweet.id)
            except Exception as e:
                logger.error(e)
                return {
                    "message": e,
                    "event": event
                }

    return {
        "message": "Success!",
        "event": event
    }
```

流れとしては、まず最初に自分が最後にいいねしたツイートのIDを取得します。既にいいねしたツイートに再度いいねすると例外が発生してしまうので、前回の実行で最後にいいねしたツイートに到達したらそこでプログラムを終了させたいからです。(このアカウントでいいねするツイートがアボガドツイートのみであることを前提としています)

次に、実際にアボ"ガ"ドツイートを検索します。1時間に1回実行する想定なので、100件ほど取得すれば十分でしょう。(日本でアボガドが爆発的に人気になったら調整が必要かもしれません笑)これをfor文で繰り返し処理していきます。また、リツイートは取得したくないため、検索ワードに`exclude:retweets`をつけておきます。

そして、実際にいいねをつける部分ですが、その前に関係のないツイートは除外してしまいたいです。まず、アボガドとアボカドが一つのツイートに両方入っている人はおそらくアボ"カ"ドが正しいとわかっている我軍の味方だと思われますので除外します。また、今回は初めて知ったのですがアボガド6さんという絵師の方がいるらしく、そちらも今回は関係ないので除外します。そして最後に、それらのワードがいずれも入ってないツイートに対していいねをつけて終わりです。

## serverless.yml
以下をコピペします。

```yaml:serverless.yml
service: avogado-janai

frameworkVersion: "=1.30.3"

provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1 # 東京リージョン
  # ①
  stage: ${opt:stage, self:custom.defaultStage}
  # ②
  profile: ${opt:profile, self:custom.defaultProfile}
  # ③
  environment: ${file(./conf/conf.yml)}
custom:
  defaultStage: dev
  defaultProfile: sls

# ④
plugins:
  - serverless-python-requirements

# ⑤
functions:
  favoriteAvogadoTweet:
    handler: handler.favorite_avogado_tweet
    timeout: 30
    events:
      - schedule: rate(1 hour)
```

①ですが、関数のステージを設定します。serverless.ymlの特徴として、`opt:オプション名`でデプロイコマンド実行時のオプションの値を取得できます。続いて`self:`とありますが、これで自身の設定値を参照できます。つまり、`self:custom.defaultStage`はdevを指します。この一行の意味としては「デプロイコマンド実行時にstageオプションがあればその値を設定し、なければdevを設定する」ということです。例えば、以下のようになります。

```bash
sls deploy --stage production # stageはproduction
sls deploy # stageはdev
```

②ですが、デプロイに使用するAWSのIAMユーザーを指定します。ここで指定する名前は~/.aws/credentialsファイルに書かれているものです。まだファイルが存在しない場合は新規作成してください。ちなみにデプロイにあたって、AdministratorAccessという権限が必要になりますので、事前にIAMユーザーにポリシーをアタッチしてください。今回はslsという名前をつけたIAMユーザーを指定します。

```:~/.aws/credentials
[default]
aws_access_key_id = ********************
aws_secret_access_key = ****************************************

[sls]
aws_access_key_id = ********************
aws_secret_access_key = ****************************************
```

③ですが、環境変数を設定します。そのままインラインでも書けますが、GitHub等に公開することも考え、別ファイルにまとめて出してそれを読み込むようにします。(そしてconfを.gitignoreに指定する等します).conf/conf.ymlを作成して、TwitterAPI用に以下の環境変数を設定してください。

```yaml:.conf/conf.yml
CONSUMER_KEY: YOUR_CONSUMER_KEY
CONSUMER_SECRET: YOUR_CONSUMER_SECRET
ACCESS_TOKEN: YOUR_ACCESS_TOKEN
ACCESS_TOKEN_SECRET: YOUR_ACCESS_TOKEN_SECRET
```

④ですが、プラグインを設定します。Serverless Frameworkでは様々なプラグインが公開されています。Lambdaでは本体となる関数とともに、依存している外部パッケージも同梱してパッケージングしたものをデプロイにしなければならないのですが、少々面倒です。そのあたりを、serverless-python-requirementsというプラグインで簡単にします。詳細は後ほど説明します。

⑤ですが、Lambda関数の情報を設定します。今回はfavoriteAvogadoTweetという名前で、handlerをhandler.favorite_avogado_tweetとしています。これは、handler.pyファイルのfavorite_avogado_tweetという関数をハンドラーに設定するという意味です。またタイムアウトは余裕を持って30秒に設定します。そしてeventsですが、これはLambda関数を実行するトリガーを指定します。Lambdaでは、S3(AWSのストレージサービス)にファイルがアップロードされたのをトリガーにしたり、API Gatewayと統合してAPIを作れたりします。単純に定期実行することもできて、その場合はキーをscheduleとします。値にはcron式もしくはAWS独自のrate式が使えます。今回は単純に書けるrate式を使い1時間ごとに実行するようにします。

## デプロイ
先程紹介したプラグインのserverless-python-requirementsですが、依存パッケージ情報を requirements.txtに書くことで、自動でそれらのパッケージを同梱してデプロイしてくれます。

まずプラグインをインストールします。

```bash
npm install --save serverless-python-requirements
```

`pip freeze`でインストール済みのパッケージが一覧で表示されるのでそれを requirements.txtに出力しましょう。

```bash
pip freeze > requirements.txt
```

さて、準備が整ったらいよいよデプロイします。デプロイのためのコマンドはたったこれだけで、後はツール側がよしなにしてくれます。

```bash
sls deploy
```

完了メッセージが出たら成功です！
1時間おきにアボ"ガ"ドツイートがいいねされていくのを確認してください。

これでアボカドをアボガドと間違われることのない世界が実現できますね！(自分で書いててどっちが正解か混乱してきた)