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

##できたもの
お題をあげると「募ってはいるが、募集はしていない」っぽい返事をしてくれるLINEBot  
**実物**

<b>[GitHub](https://github.com/soku-madori/ape.Bot)</b>
<BLOCKQUOTE CLASS="twitter-tweet">
<a https://twitter.com/MlSoku/status/1241367537125736450?s=20><b>Twetter記事</b></a>
<p lang="ja" dir="ltr">とある天才記事に感銘を受けて有名人っぽい人と会話できる気になれるLINEBotを作ってみました<br>酔っぱらった状態でやって有名っぽい人を怒らせたら負、みたいなマイルールを作って楽しめます<br>ちょっとだけ語彙が増えるかもしれません<br>元ネタ：<a href="https://t.co/lkzxtXvP97">https://t.co/lkzxtXvP97</a></p>&mdash; soku-madori.ml (@MlSoku) <a href="https://twitter.com/MlSoku/status/1241367537125736450?ref_src=twsrc%5Etfw">March 21, 2020</a></BLOCKQUOTE> 

**コードうちわけ**

|使用用途|　　　　　使ったもの　　　　　|
|:--:|:--:|
|ラインボット本体|[LINE Developers](https://developers.line.biz/ja/)|
|ラインエンドポイント|[Cloud Functions](https://cloud.google.com/functions?hl=ja)|
|コーディング言語|[Python 3.7.0](https://www.python.org/)|

**ディレクトリ構成**
/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フォルダだけで良いはず

**完成コード**
<details><summary>コード(クリックしてください)</summary><div>

```python: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))]
        )


```
```python: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))
```
```text:requirements.txt
line-bot-sdk
nagisa
```
</div></details>

##やりたかったこと
[@omiita](https://qiita.com/omiita)さんの[「募ってはいるが、募集はしていない」 人たちへ](https://qiita.com/omiita/items/0f811f15e569bf2539b8)　をみて面白すぎてどうにかしたいなと思った挙句、『LINEBotにすれば面白いじゃん！』ってなって即[@omiita](https://qiita.com/omiita)さんに確認。
すぐに快諾頂いて、さっそく着手することに。
LINEBotはいくつか作ったことがあったし、[@omiita](https://qiita.com/omiita)さんの記事にソースコードも全部載せて頂いてます。
コピペで作れば設定入れて２時間もあれば余裕でしょ、最悪ハマっても半日使えば形にはなるよね♪ととてもお気楽に取り掛かりました。
この記事を書くまでに１０日以上かかるなんて、当時は夢にも思っていませんでした。。。 
 
##前提条件
完全趣味の道楽なので製作に際して以下の条件を決めました

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

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


##COTOHA API部分をすべて置き換える
**[COTOHA API](https://api.ce-cotoha.com/contents/index.html)が使えなかった！！**:joy:

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

|使用用途|　　使用予定（置き換え先）　　|
|:--:|:--:|
|形態素解析|[Janome](https://mocobeta.github.io/janome/)|
|類語列挙|[日本語 WordNet](http://compling.hss.ntu.edu.sg/wnja/)|
|類度計測|[word2vec 0.10.2](https://pypi.org/project/word2vec/)|
|連用タ変換|[IPA辞書](http://taku910.github.io/mecab/#download)|  
色々調べて
既に１時間が経過。。。

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

<details><summary><b>コードはこちら</b>　(クリックしてください)</summary><div>

```python
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))
```


</div></details>

>**total作業時間　１時間**



##ばらして漬物にする
**[WordNet](http://compling.hss.ntu.edu.sg/wnja/)が使えなかった！！**:joy:

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

##NLTKのWordNetをcloudfunctionsで使用する
**類語列挙ができなかった！！**:joy:

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

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

# synkeys.pickle　の中身
syns_dic = {"00044353-a":["食べる","飲む",,,],,,}
```


1. まずは単語キー辞書でsynkeyリストを見つける
2. そこから先頭のsynkeyで該当する単語を列挙

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

>作業時間　 約１時間
 **total　　　　４時間**

試しに作ってみたやつ
<details><summary><b>コードはこちら</b>　(クリックしてください)</summary><div>

```python
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))

```
</div></details>


##[Google Cloud Functions](https://cloud.google.com/functions?hl=ja)で[NLTK](https://www.nltk.org/install.html)から[WordNet](http://compling.hss.ntu.edu.sg/wnja/)を使う
**やっぱり類語列挙ができなかった！！**:joy:

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

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

何とかしたい。あ！最初っからバンドルすればいいじゃん。
[Cloud Functions](https://cloud.google.com/functions?hl=ja)はコード以外にファイルも一緒にデプロイ出来て、そのままコードから参照も出来ます。モジュールすらもバンドルしてデプロイできちゃう（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](http://www.nltk.org/data.html)

>作業時間 　約１２時間
 **total　　　　１９時間**


##Word2Vecを捨てる
**Word2Vecがでかすぎた！！**:joy:

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

<b>exceed deploy limit</b>

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

>作業時間　 1時間
 **total　　１7時間**

##[nagisa](https://pypi.org/project/nagisa/)を使う
**[Janome](https://mocobeta.github.io/janome/)が遅い！！**:joy:

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

<b>invalied replay token</b>

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

|使用用途|　　使ったもの　　|
|:--:|:--:|
|形態素解析|[nagisa 0.2.5](https://pypi.org/project/nagisa/)|
|類語列挙|[nltk 3.4.5](https://pypi.org/project/nltk/)|
|類度計測|[random()](https://docs.python.org/ja/3/library/random.html)|
|連用タ変換|[IPA辞書](http://taku910.github.io/mecab/#download)|


>作業時間　 ２時間
その他　　３時間
 **total　　１９時間以上**

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


##結論

- [COTOHA API](https://api.ce-cotoha.com/contents/index.html)って優秀
- 走り出す前にコースとゴールをちゃんと確認しよう
- 走ってる最中にゴールを忘れない

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

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

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