はじめに
今日は8月19日、俳句の日ですね。私は575判定が得意で、575のセリフが載った漫画のコマの画像を切り抜いた数なら世界一を目指せると思っています。世の中にはたくさんの575が存在するのに、多くの575は無視され忘れられていく……。私はこの由々しき事態を打ち破るため、社内Slackに575スタンプを作成し、野生の575*1を毎日5件ほど救っています。しかし社員とメッセージが増えてきた今、全ての575を救えていないかも知れません。そこで575の検出を自動化に取り組みました。
目的:みんなが575メッセージに、そして575メッセージの多さに気がついてくれる
目標:Slackに575が投稿されたら、575だと気がつけるようなメッセージを送る。
手段:LambdaにPythonプログラムを設定し、CloudWatch Eventsで定期実行する。
Slackのチャンネルに、appを設定する
今回はメッセージの投稿(返信)なので、conversation.historyとchat:writeのscopeを与えたappを作成し、Slackに接続します。
OAuth Tokens for Your Workspaceからtokenが手に入ります。これはPythonからSlackへ投稿するときに必要です。
AWS Lambdaで、Slackのメッセージを取得・形態素解析し、575だった場合に返信する
aws_575_projectディレクトリをプロジェクトのディレクトリとして作成し、
|-aws_575_project |---Dockerfile(layer作成のために作成) |---kanehosi_tukeai.py(処理本体。Lambdaにのせる) |---layer(Lambdaのレイヤーを作るためのディレクトリ) |-----layer.zip(下のディレクトリを圧縮したもの) |-----python(MeCabを使うために必要なパッケージをインストールするディレクトリ)
のような構成で開発しました。
以下のプログラム(kanehosi_tukeai.py)をLambdaに乗せます。プログラムは直近60秒のSlackメッセージを取得し、各メッセージに対してMeCabを使って読みを取得し、“音”(モーラ)を数えて575判定を行います。
チャンネルIDと、さっき手に入れたtokenを記入します。今回は自分しか入っていないSlack ワークスペースに入れるだけなのでAWS Secrets Managerなどは使っていません*2。
import ipadic import jaconv import MeCab import re import requests import time m = MeCab.Tagger(ipadic.MECAB_ARGS) def post_message(thread_ts: str, header: dict[str, str], channel_id: str) -> None: data = {"channel" : channel_id, "text" : "それにつけても金の欲しさよ", "thread_ts": thread_ts} requests.post("https://slack.com/api/chat.postMessage", headers=header, data=data) def extract_hiragana(text: str) -> str: return "".join(re.findall(r'[ぁ-ゔー]', text)) def get_pronunciation_list(text: str) -> list[str]: m_result = m.parse(text).splitlines()[:-1] #mecabの解析結果の取得 pro: list[str] = [] #いい感じに切った読みの全体を格納する変数 for v in m_result: if '\t' not in v or v.split('\t')[1].split(",")[0] in ["記号"] or len(v.split('\t')[1].split(",")): continue p = v.split('\t')[1].split(",")[7] #読みを取得, 8は発音 if v.split('\t')[1].split(",")[0] in ["助詞", "助動詞"] and pro: pro[-1] += p else: pro.append(p) pro = [extract_hiragana(jaconv.kata2hira(p)) for p in pro if p] #ひらがなをカタカナに変換し余計な記号を削除 return pro def is_575(text: str) -> bool: words = get_pronunciation_list(text) length = 0 reversed_expected_length_list = [5, 7, 5] for word in words: if not reversed_expected_length_list: # 575が終わったのにまだ続く return False length += len(re.sub(r'[ぁぃぅぇぉゃゅょ]', '', word)) # 1音を構成しない文字 if length == reversed_expected_length_list[-1]: length = 0 reversed_expected_length_list.pop() if reversed_expected_length_list: # 575と切り取れなかった return False return True def post_77_to_575(event, context) -> None: token = "xoxb-xxxxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxx..."# tokenを入れてください channel_id = "C0123456789",# Conversation IDを入れてください header={"Authorization": "Bearer {}".format(token)} payload = {"channel" : channel_id, "limit": 20} res = requests.get("https://slack.com/api/conversations.history", headers=header, params=payload) json_channel_history = res.json() for message in json_channel_history["messages"]: if is_575(message["text"]) and time.time() - float(message["ts"]) <= 60: post_message(message["ts"], header, channel_id) return
必要なパッケージ類(ipadic, jaconv, MeCab, requests)をLambda ランタイムと同じ環境(Amazon Linux 2)でインストールしたものを、AWS Lambda レイヤーに乗せる。
Python による Lambda 関数の構築 - AWS Lambda によれば、LambdaはAmazon Linux 2上で動いているようです。そこで、レイヤーを作成するためのDockerをamazon linux 2(latest) を元にして作ります。
Dockerfileを
FROM amazonlinux:latest RUN yum install -y gcc openssl-devel bzip2-devel libffi-devel wget zip tar gzip make && \ cd /opt && \ wget https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz && \ tar xzf Python-3.9.6.tgz && \ /opt/Python-3.9.6/configure --enable-optimizations && \ make altinstall && \ rm -f /opt/Python-3.9.6.tgz && \ python3.9 -m pip install --upgrade pip
とし、ターミナルで
mkdir layer # docker build -t イメージ名 Dockerfileのあるディレクトリ docker build -t docker_575_awslinux2_image . docker run --name docker_575_awxlinux2_container -it docker_575_awslinux2_image /bin/bash docker run -i -t --rm --name docker_575_awxlinux2_container -v `pwd`/layer:/var/app/tmp:z docker_575_awslinux2_image
を実行してdocker containerの中に入る。
cd var/app/tmp mkdir python pip install -t python requests pip install -t python jaconv pip install -t python mecab-python3 pip install -t python ipadic pip install -t python regex zip -r9 layer.zip python
で必要なパッケージがインストールされたlayer.zipが手に入ります(layerディレクトリの中にあります)。これをLambdaのレイヤーに設定します。
Amazon CloudWatch Eventsでcron式「cron( * * * ? )」を設定し、AWS Lambdaのトリガーに設定する
毎分なので全てワイルドカードで設定します。正確に0秒に実行されることを保証しないので取り逃すかも知れませんが、設定したらほぼ60秒ごとに実行されたので今回はこれでやっていきます。
完成!
おめでとうございます。これから毎分、575に対して「それにつけても金の欲しさよ」とリプライが受け取れます。
「松島やあ あ 松島や 松島や」と区切ってしまっているので、完璧ではありませんが……この問題はとても難しいため人工知能に仕事を取られなかったと安心することにします。他の形態素解析器を使ってみると解決するかも知れません。
おわりに
社員が増えても575を検出できるようになりました。社員が増えても大丈夫になった現在、estieではエンジニアを積極採用中です。詳しくは採用ページをご覧ください。
スムーズに開発したようにみえる記事を書きましたが、AWSをあまり触ってこなかった人(著者)が試行錯誤したからこそ書ける、とても詳しい「おくのほそ道編」を11月に予定しています。社員がドン引きするくらい大量の試行錯誤とその解決譚を載せる予定で、AWS Lambdaで形態素解析をする世界で一番優しい記事を目指します。「なんでこうしたら動くのか分からん!」「応用しようとしたらエラーが出た!」という方は特にお楽しみに!
*1:業界用語で、n575といいます。
*2:https://aws.amazon.com/jp/kms/pricing/ によれば1KMS キーあたり月1米ドルなので、遊ぶには高く本番には安いと感じてます。