AWS Lambda で形態素解析〜句を詠めば鐘が鳴るなりスッコココ〜

はじめに

今日は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ではエンジニアを積極採用中です。詳しくは採用ページをご覧ください。

www.estie.jp

スムーズに開発したようにみえる記事を書きましたが、AWSをあまり触ってこなかった人(著者)が試行錯誤したからこそ書ける、とても詳しい「おくのほそ道編」を11月に予定しています。社員がドン引きするくらい大量の試行錯誤とその解決譚を載せる予定で、AWS Lambdaで形態素解析をする世界で一番優しい記事を目指します。「なんでこうしたら動くのか分からん!」「応用しようとしたらエラーが出た!」という方は特にお楽しみに!

*1:業界用語で、n575といいます。

*2:https://aws.amazon.com/jp/kms/pricing/ によれば1KMS キーあたり月1米ドルなので、遊ぶには高く本番には安いと感じてます。

© 2019- estie, inc.