LoginSignup
4
0

More than 3 years have passed since last update.

2時間で作るつもりのLINEBotに20時間以上かけた話

Posted at

できたもの

お題をあげると「募ってはいるが、募集はしていない」っぽい返事をしてくれるLINEBot

実物

GitHub

コードうちわけ

使用用途      使ったもの     
ラインボット本体 LINE Developers
ラインエンドポイント Cloud Functions
コーディング言語 Python 3.7.0

ディレクトリ構成
/ape
 │
 ├ main.py      #ラインボット制御
 ├ ape.py      #返答作成 
 ├ requestments.txt      #依存関係DLリスト(pip)
 ├ verb_utf_8.csv      #ID辞書
 ├ nltk      #NLTKモジュール(ダウンロードパスを追加したもの)
 └ nltk_data      #nltkデータ
    └ corpora     
       ├ omw      
       └ wordnet
omwについてGitHub上は間違って全部上げちゃいましたが、jpnフォルダだけで良いはず

完成コード

コード(クリックしてください)
main.py
from flask import Flask, request, abort
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent,
    TextMessage, TextSendMessage,
)

from ape import *


app = Flask(__name__)

CHANNEL_SECRET = 'YOUR CHANNEL_SECRET'
CHANNEL_ACCESS_TOKEN = 'YOUR CHANNEL_ACCESS_TOKEN'

line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)


def RunApp(request):
    signature = request.headers['X-Line-Signature']
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    text = event.message.text
    if text:
        print('token',event.reply_token)
        line_bot_api.reply_message(
            event.reply_token,
            [TextSendMessage(text=ape(text))]
        )


ape.py
import csv
import random
import re

from flask import request
from nltk.corpus import wordnet
import nagisa



hangup = 'その言葉を知ってはいますが知識はないわけであります'
angry = '主語・述語が無い、これはルール違反であり、断固とした処置をとってゆく'


def ape(sentence):
    noun,post,verb = picSPV(sentence)
    if 'None' in  (noun,post,verb):
        if random.choice(range(10)) == 1: return '知らない'
        return angry
    words = findWords(verb)
    foundwords = [w[:w.find('+')] for w in words]
    if not foundwords: return hangup
    foundword = random.choice(foundwords)
    verb = convertWord(verb)
    if not verb: return hangup
    return f'{noun}{post}{verb}てはいますが{foundword}はしていないわけであります'

def picSPV(sentence):
    morp = nagisa.tagging(sentence)
    words = morp.words
    tags = morp.postags
    s = 'None'
    p = 'None'
    v = 'None'
    for w,t in zip(words,tags):
        if '名詞'==t or '代名詞'==t: s = w
        if '助詞'==t: p = w
        if '動詞'==t: v = w
    if not p: p = 'を'
    return(s,p,v)

def findWords(verb):
    words = [i for s in wordnet.synsets(verb,lang='jpn') for i in s.lemma_names('jpn')]
    return [i for i in words if re.search(r'\+',i)]


"""
以下のコードはすべてこちらから転載、一部改変しています
Qiita: @omiita
https://qiita.com/omiita/items/0f811f15e569bf2539b8
"""
def convertWord(word):
    file_name = "verb_utf_8.csv"
    with open(file_name,"r",encoding='utf-8') as f:
        handler = csv.reader(f)
        for row in handler:
            if word == row[10]: #品詞発見
                if "連用タ接続" in row[9]: #活用発見
                    return row[0]
    return None



if __name__ == '__main__':
    sentence = 'お酒を飲む'
    print(ape(sentence))
requirements.txt
line-bot-sdk
nagisa

やりたかったこと

@omiitaさんの「募ってはいるが、募集はしていない」 人たちへ をみて面白すぎてどうにかしたいなと思った挙句、『LINEBotにすれば面白いじゃん!』ってなって即@omiitaさんに確認。
すぐに快諾頂いて、さっそく着手することに。
LINEBotはいくつか作ったことがあったし、@omiitaさんの記事にソースコードも全部載せて頂いてます。
コピペで作れば設定入れて2時間もあれば余裕でしょ、最悪ハマっても半日使えば形にはなるよね♪ととてもお気楽に取り掛かりました。
この記事を書くまでに10日以上かかるなんて、当時は夢にも思っていませんでした。。。

前提条件

完全趣味の道楽なので製作に際して以下の条件を決めました

  • 開発コスト(自分の工数)以外は1円もかけないでやりたい
  • 開発コストもなるべく抑えたい(学習コストもできるだけ抑えて、極力シンプルに)
  • でも、なるべくたくさんの人に触れてもらいたい

上記を鑑みてAPIエンドポイントをGoogle先生のCloud Functionsに置くことにしました。
何度か使った事あるし、簡単だし、Webに情報もいっぱいあるし、pythonだけで完結できるし、何より無料枠が広い!私ごときの作るアプリでその枠を突破するのは不可能に近いでしょう。
最悪、StorageやらSQLやらPubSubやらで拡張できるし。

COTOHA API部分をすべて置き換える

COTOHA APIが使えなかった!!:joy:

早速開始。まずはCOTOHA APIって何なのか軽く調べておこう。
一応パーミッションはと。 ん?、あれ、個人使用禁止?
COTOHA APIさんは使用に登録が必要で、そもそもプライベートの使用目的で解放されていませんでした。
実は言語解析触るのは初めてでCOTOHAって単語もこの時初めて知りました。
クラウド関係のサービスは信じられないくらい有能な機能が無料で提供されまくっているので、勝手に無料で使えるだろうと思い込んでいました。
いきなり挫折。とりあえず、COTOHA API使用部分をそれぞれ置き換えて再現することにしました。

使用用途   使用予定(置き換え先)  
形態素解析 Janome
類語列挙 日本語 WordNet
類度計測 word2vec 0.10.2
連用タ変換 IPA辞書

色々調べて
既に1時間が経過。。。

上記内容で動くか一部試してみたやつ
(word2vec用に別途モデルファイルがいります)

コードはこちら (クリックしてください)
from gensim.models import word2vec
from janome.tokenizer import Tokenizer


def main(word:str):
    try:
        tag = wordClass(word)
        if not tag: return
        selections = wordList(word)
        flag = '動詞' if tag!='動詞' else '名詞'
        reword = [i for i in selections if wordclass(i)==flag]
        if reword: return reword[0]
        return 'お前誰た'
    except:
        return 'お前誰だ'

def wordClass(word:str):
    t = Tokenizer()
    res = t.tokenize(word)[0].part_of_speech
    if res: return res[:res.find(',')]

def wordList(word:str):
    model = word2vec.Word2Vec.load("word2vec.gensim.model")
    results = model.wv.most_similar(word)
    return [x for y in results for x in y if type(x)==str]


if __name__ == '__main__':
    word = '応じる'
    print(main(word))

total作業時間 1時間

ばらして漬物にする

WordNetが使えなかった!!:joy:

やっと方針が決まったのでまずはWordNetのsqlite版をダウンロードしました。
ダウンロードがなかなか終わらない。凄い手間かけて作ったんだろうなぁとかのんきに考えつつやっと終わるとなんと1.5GBもありました。
Cloud Functionsのデプロイサイズは100MBまで、実行時もMAX500MBしか使用できないので全然アウト。。。
いらないテーブルやレコード(英単語とか)を消しまくってDB作り直してみたけど全然1KB以上ある。。
結局最軽量のタブ版をさらにばらしてpickleファイルにすることで何とか合計20MB程度まで圧縮。
synkeyも有るし、何とかなるでしょとまた甘い考え。まだ余裕でした。

作業時間  約2時間
total    3時間

NLTKのWordNetをcloudfunctionsで使用する

類語列挙ができなかった!!:joy:

早速出来た辞書型リストを使って類語を抽出してみる。
前回こんな感じの辞書リストをそれぞれ作りました

.pickle
# Words.pickle の中身
words_dic = {"食べる":["00044353-a","00024323-a",,,],,,}

# synkeys.pickle の中身
syns_dic = {"00044353-a":["食べる","飲む",,,],,,}
  1. まずは単語キー辞書でsynkeyリストを見つける
  2. そこから先頭のsynkeyで該当する単語を列挙

大体1語につき5~6文字出てきます。が、欲しい単語が全然無い!
動詞で検索して類語の名詞を見つけなければならないのですが、そもそも名詞がほとんど入っていない。これじゃあ役に立たない。
じゃあ増やせばいいじゃん!と関連単語全部リストにしてみました。多い。。友達の姉ちゃんの知り合いみたいな関係性の奴がうじゃうじゃいる。。
没です。

作業時間  約1時間
total    4時間

試しに作ってみたやつ

コードはこちら (クリックしてください)
import pickle
import re

import nagisa


with open('wnjpn-synkey.pickle','rb') as f:
    syndic = pickle.load(f)

with open('wnjpn-wordkey.pickle','rb') as f:
    wrddic = pickle.load(f)

skeys = [i for i in syndic]
wkeys = [i for i in wrddic]


class ApeTalk:
    def main(self,word):
        hangup = 'その言葉を知ってはいますが知識はないわけであります'
        if not wrddic.get(word): return hangup
        pos = self.setPos(word)
        syns = wrddic[word]
        words = set([i for s in syns for i in syndic[s]])
        print('words',len(words),words)
        reword = set(words)
        print('rew',len(reword),reword)
        choice = None
        for w in words:
            c = self.setPos(w)
            if pos!=c:
                if c=='n' or c=='v':
                    choice = w
                    break
        if not word or not choice:
            return hangup
        noun = word if pos=='n' else choice
        verb = word if pos=='v' else choice
        return f'{verb}ってはいますが{noun}はしていないわけであります'

    def setPos(self,word):
        try:
            pos = nagisa.tagging(word).postags
            if not pos: return
            if '名詞' in pos: return 'n'
            if '動詞' in pos: return 'v'
        except:
            return


if __name__ == '__main__':
    word = '募集'
    print(ApeTalk().main(word))

Google Cloud FunctionsNLTKからWordNetを使う

やっぱり類語列挙ができなかった!!:joy:

既に簡単にチャチャっとやるという当初目的を大幅に逸脱してますが、何も残らないのが悔しすぎるので気づかなかったことにして時間を捻出します。
何とか出来ないかWordNet絡みをネットで検索しまくりますが、とにかく情報が少ない。。
しょぼいググり力を大量に時間を使うことでカバーしつつ、何とかNLTKでWordNetを使う方法があるっぽいことを発見。簡単な類度判定も出来るらしい、それじゃん、試すしかない。
四苦八苦してどうにか日本語でシソーラス解析できるところまでこぎつけました。
しかし!またもや問題発覚!

NLTKはpipで簡単にインストールできるのですが、WordNetとOMW(日本語に変えるやつ)のデータはコード内でダウンロードしてやる必要が有ります。
果たしてCloud Functionsでやれるのか? ⇒⇒ 出来た!
そーっと試してみましたが、まさかの問題なく動作してしまいました。
神かよ、Google。しかし絶対正しい使い方じゃない気がする。。(詳しい方いらっしゃいましたらご指導ください)

何とかしたい。あ!最初っからバンドルすればいいじゃん。
Cloud Functionsはコード以外にファイルも一緒にデプロイ出来て、そのままコードから参照も出来ます。モジュールすらもバンドルしてデプロイできちゃう(pipいらない)ので、nltkにダウンロードしたWordNetとOMWデータくっつけてデプロイしたら解決じゃん!って思ったわけです。

でも、nltk.download()ってそもそも何やってるの?何を、どこから、どこにダウンロードしてるの?
情報無いので該当してるっぽい部分のコードを読みまくって何とか解決しました。
デフォルトは該当APIエンドポイントから所定のディレクトリにダウロードしており、そのパスを共有して実行時に読み込んでいるようです。
そしてパスを保持しているのがnltk.data.pyのpath変数(リスト)らしい。
なので解決方法としては

  • nltk_data\corporaを作ってWordNetデータとOMWデータを置く
  • 'nltk_data'をnldt.data.pyのパスリストに追加
  • nltkをモジュールごとバンドルしてデプロイ

何とかFunctionsは動いてくれました。これで神(Google)様にも怒られないはず。

NLTK Dataのdocumentation

作業時間  約12時間
total    19時間

Word2Vecを捨てる

Word2Vecがでかすぎた!!:joy:

やっと完成出来る。もはや何をしたかったのかも覚えていませんが、とりあえずゴール出来る。
パッケージまとめてデプロイっと ⇒ ん? なんか神(Google)様が怒っている。

exceed deploy limit

word2vec 0.10.2はモデルが必要で、白ヤギコーポレーションの方(別の神)様からありがたく使用させて頂いたのですがこちらがZIP状態で100M超えてる。。。。
たぶん反対語のモデルっぽいやつを切り離せればギリ行けそうな気もするが、WordNetでライフと時間を使い果たしてしまっている。よし、捨てよう。
捨てました。

作業時間  1時間
total  17時間

nagisaを使う

Janomeが遅い!!:joy:

もはや当初の目的は完全に消え去り、参照元の「募ってはいるが、募集はしていない」 人たちへのプログラムと比べると中身も性能も別物です。
しかし返事部分は完成したので、とにかくラインボット実装してみます。
設定終わって喜んでテスト送信するも返信が全然ない。。。なんだ?
Functionsのログを見てみるとラインから400が返ってきてます。

invalied replay token

調べてみるとどうやら30秒以内に返事をしなきゃならないらしい。
計測するとちゃんと35秒後に返信してます。ダメじゃん。。。。
形態素解析のAPIが圧倒的に遅いので、いろいろ探してJanome⇒nagisaに変更
何とか形になりました。

最終的に使った内容

使用用途   使ったもの  
形態素解析 nagisa 0.2.5
類語列挙 nltk 3.4.5
類度計測 random()
連用タ変換 IPA辞書

作業時間  2時間
その他  3時間
total  19時間以上

その他方針考えたり、隙間時間にちょこちょこググったりして2~3時間くらいは費やした気がします

結論

  • COTOHA APIって優秀
  • 走り出す前にコースとゴールをちゃんと確認しよう
  • 走ってる最中にゴールを忘れない

しっかりした計画や途中の確認変更しないとすっごい無駄が多いな、と改めて感じました。

でも、、意味のない無駄な事を一生懸命やるってなんでこんなに楽しいんでしょうか:joy:

駄文最後まで読んで頂いてありがとうございました。
初心者なので、間違いや勘違いがいっぱいあると思います。
お気づきの諸先輩方、ご指摘頂けると励みになります。
よろしくお願いいたします。

 

4
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0