python & heroku & ClearDBで作る　国の首都が分かるLINEBOT

# 首都を教えてくれるLINEBOT
形態素解析を使ってLINEBOTを試したいなと思って作ってみました。
せっかくなのでSQLも使いました。

## とりあえず作ってみる
SQLを使う前に辞書で用意してみました。
``国名``を受け取ると、辞書に設定した``首都``を返してくれます。

```python:main.py
import os
import errno
import tempfile
from flask import Flask, request, abort
from janome.tokenizer import Tokenizer
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, ImageMessage, TextSendMessage, FollowEvent
)

from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)

CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
CHANNEL_SECRET = os.environ["CHANNEL_SECRET"]

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

@app.route("/callback", methods=['POST'])
def callback():
    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

    # 国名と首都の辞書
    country_dict = {
        "アメリカ" :["ワシントン"],
        "イタリア" :["ローマ"],
        "カナダ" :["オタワ"],
        "コートジボワール" :["ヤムスクロ"],
        "中国": ["北京"],
        "日本": ["東京"],
        "バチカン": ["ありません"],
        "モナコ" :["モナコ"],
        "モンゴル" :["ウランバートル"]
    }

    tokenizer = Tokenizer()
    for token in tokenizer.tokenize(text):
        # 分かち書きで国を受け取った場合
        if token.part_of_speech.split(",")[3] =="国":
            # 辞書にある国は首都を返す、辞書にない国は知りません！
            if token.surface not in country_dict:
                rep_text = f"私は{token.surface}を知りません"
                line_bot_api.reply_message(
                event.reply_token, TextSendMessage(text=rep_text))
                break
            else:
                rep_text = f"{token.surface}の首都は{country_dict[token.surface][0]}です"
                line_bot_api.reply_message(
                event.reply_token, TextSendMessage(text=rep_text))
                break

if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8000))
    app.run(host ='0.0.0.0',port = port)
```

### Herokuでアプリ作成
```ruby:コマンドプロンプト
# .envを導入
$ pip install python-dotenv
Successfully installed python-dotenv-0.19.2

# Herokuでアプリを作成
$ git init
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
ほげほげ
Logging in... done

$heroku git:remote -a アプリ名
set git remote heroku to アプリURL

# gitでpush
$ git add .
$ git commit -am "first-commit"
$ git push heroku master
ほげほげ
remote: Verifying deploy... done.
To アプリURL
 * [new branch]      master -> master
```
```python:.env
CHANNEL_ACCESS_TOKEN: "CHANNEL_ACCESS_TOKEN"
CHANNEL_SECRET: "CHANNEL_SECRET"
```
```python:.gitignore
.env
```
```python:requirements.txt
Flask==0.12.2
Janome==0.4.2
line-bot-sdk==1.19.0
python-dotenv==0.19.2
```

.envはpushしないので、環境変数はHerokuアプリ上で設定
設定できているかはコマンドプロンプトで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/772ba099-7dc8-4840-f842-8e0005925d09.png)
```ruby:コマンドプロンプト
$ heroku config -a アプリ名
=== アプリ名 Config Vars
CHANNEL_ACCESS_TOKEN: "CHANNEL_ACCESS_TOKEN"
CHANNEL_SECRET: "CHANNEL_SECRET"
```

### 首都を教えてもらう
日本の首都を教えてくれますが、辞書にないドイツとフランスは知りません。
![IMG_0012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/91fe0f97-c3d2-af5a-c0c1-63d16ea6ff7b.png)

## SQLを使おう
辞書で国と首都を紐づけたけど、**国をすべて辞書にすると行数が大変なこと**になるんじゃない！？（[日本が承認した国は令和3年3月12日時点で195か国、日本も合わせると196か国](https://www.mofa.go.jp/mofaj/area/world.html)）
ということで**辞書をSQLに変更**します。

### テーブル定義
今回は首都だけだけど、国旗を表示したり豆知識を教えてくれたりするのもいいかなと思い、とりあえずカラムは用意した。
NAME: 国名
CAPITAL: 首都
FLAG: 国旗画像のファイル名格納用
INFO: 豆知識

```ruby:コマンドプロンプト
mysql> show columns from country_info;
+---------+----------------------+------+-----+---------+-------+
| Field   | Type                 | Null | Key | Default | Extra |
+---------+----------------------+------+-----+---------+-------+
| ID      | smallint(5) unsigned | NO   | PRI | NULL    |       |
| NAME    | varchar(50)          | YES  |     | NULL    |       |
| CAPITAL | varchar(30)          | YES  |     | NULL    |       |
| FLAG    | varchar(15)          | YES  |     | NULL    |       |
| INFO    | varchar(250)         | YES  |     | NULL    |       |
+---------+----------------------+------+-----+---------+-------+
5 rows in set (0.23 sec)
```

日本語を使う場合は以下のエラーに注意
MySQLdb.connectにUnicodeを指定すると回避できる。
```ruby:コマンドプロンプト
2022-03-07T11:43:06.971358+00:00 app[web.1]: UnicodeEncodeError: 'latin-1' codec can't encode characters in position 52-53: ordinal not in range(256)
conn = MySQLdb.connect(user=DB_USER, passwd=DB_PASS, host=DB_HOST, db=DB_NAME, use_unicode=True, charset="utf8")
```

```python:main.py
import os
import errno
import tempfile

# pymysqlとMySQLdbを使用
import pymysql
pymysql.install_as_MySQLdb()
import MySQLdb

from flask import Flask, request, abort
from janome.tokenizer import Tokenizer
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, ImageMessage, TextSendMessage, FollowEvent
)

from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)

CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
CHANNEL_SECRET = os.environ["CHANNEL_SECRET"]
# MySQL用の環境変数
DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"]
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]
DB_TABLE = os.environ["DB_TABLE"]

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

@app.route("/callback", methods=['POST'])
def callback():
    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
    tokenizer = Tokenizer()
    for token in tokenizer.tokenize(text):
        # 国を受け取ったらデータベースに接続して情報を確認
        if token.part_of_speech.split(",")[3] =="国":
            conn = MySQLdb.connect(user=DB_USER, passwd=DB_PASS, host=DB_HOST, db=DB_NAME, use_unicode=True, charset="utf8")
            ccur = conn.cursor()
            sql = "SELECT `CAPITAL` FROM`"+ DB_TABLE +"` WHERE `NAME` = '"+ token.surface +"';"
            ccur.execute(sql)
            ret = ccur.fetchone()
            # データベースにない国は知りません！
            if ret is None:
                rep_text = f"私は{token.surface}を知りません"
                break
            # データベースにある国は首都を返す
            elif len(ret) == 1:
                cap = ret[0]
                rep_text = f"{token.surface}の首都は{cap}です"
                line_bot_api.reply_message(event.reply_token, TextSendMessage(text=rep_text))
                break
            conn.close()
            ccur.close()

    line_bot_api.reply_message(
    event.reply_token, TextSendMessage(text=rep_text))

if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8000))
    app.run(host ='0.0.0.0',port = port)
```
```python:requirements.txt
Flask==0.12.2
Janome==0.4.2
line-bot-sdk==1.19.0
python-dotenv==0.19.2
# MySQL用のライブラリを追加
mysqlclient==2.1.0
PyMySQL==0.8.0
```

### ClearDBを使う
HerokuでMySQLをつかうには、ClearDBかJawsDBがいいらしい。今回はClearDBにした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/e0dec650-9b78-cec4-e9a9-d0a3faad48f7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/ba72394d-bee1-09a3-91e9-6349f3f9b920.png)

ClearDBにローカルから接続
```ruby:コマンドプロンプト
$ heroku config
=== アプリ名 Config Vars
CLEARDB_DATABASE_URL: mysql://username:password@hostname/database_name?reconnect=true

$ mysql -u username -h hostname database_name -p
Enter password: password
Welcome to the MySQL monitor.  Commands end with ; or \g.

mysql> show databases;
+------------------------+
| Database               |
+------------------------+
| information_schema     |
| heroku_ほげほげ         |
+------------------------+
2 rows in set (9.84 sec)

mysql> use heroku_ほげほげ; 
Database changed
mysql> show tables;
Empty set (0.20 sec)

mysql> create table COUNTRY_INFO (ID smallint unsigned not null primary key, NAME varchar(50), CAPITAL varchar(30), FLAG varchar(15), INFO varchar(250));
Query OK, 0 rows affected (1.65 sec)

mysql> show tables;
+----------------------------------+
| Tables_in_heroku_3bf16eec8b13dfa |
+----------------------------------+
| country_info                     |
+----------------------------------+
1 row in set (0.26 sec)

mysql> INSERT INTO country_info (ID, NAME, CAPITAL) values (1, '日本', '東京');
Query OK, 1 row affected (0.54 sec)
+-----+-------+----------+------+-------+
| ID  | NAME  | CAPITAL  | FLAG | INFO  |
+-----+-------+----------+------+-------+
|   1 | 日本  | 東京      | NULL | NULL  |
+-----+-------+----------+------+-------+
```

「最初は日本でしょ」とINSERTしたけど・・・これ（INSERT）を196か国分もやるのしんどいな。
ということでExcelで半自動生成しました。

### ExcelでSQL文作成
各カラムにデータを入れて、数式でSQL文を作成。
あとはSQL文を必要な分だけオートフィルでコピーするだけ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/f20abd32-ee9e-50f2-6661-ed60500a20d2.png)

SQL文の式は↓のような感じです。
```
="INSERT INTO "&$B$3&" ("& $A$5 & "," &$B$5&","&$C$5&") values (" &A6&",'"&B6&"','"&C6&"');"
```

```ruby:コマンドプロンプト
# コピペでインサート
mysql> INSERT INTO country_info (ID,NAME,CAPITAL) values (2,'アイスランド','レイキャビク');
Query OK, 1 row affected (0.54 sec)
INSERT INTO country_info (ID,NAME,CAPITAL) values (3,'アイルランド','ダブリン');
Query OK, 1 row affected (1.43 sec)
ほげほげ
INSERT INTO country_info (ID,NAME,CAPITAL) values (199,'北朝鮮','平壌');
Query OK, 1 row affected (0.72 sec)

# データ確認
mysql> select * from country_info;
+-----+--------------+--------------+------+------+
| ID  | NAME         | CAPITAL      | FLAG | INFO |
+-----+--------------+-------------+------+-------+
|   1 | 日本         | 東京         | NULL | NULL  |
|   2 | アイスランド  | レイキャビク | NULL | NULL  |
|   3 | アイルランド  | ダブリン     | NULL | NULL  |
ほげほげ
| 199 | 北朝鮮       | 平壌         | NULL | NULL  |             
+-----+-------------+--------------+------+-------+
182 rows in set (0.41 sec)
```
[外務省のページ](https://www.mofa.go.jp/mofaj/area/index.html)には日本が未承認の国もあるみたいで、全部で199か国になりました。（形態素解析で国と判定された地域を追加しています。）

## 教えてもらう
SQLに登録した国の首都は全て知っています。
![IMG_8106.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/2bca135c-0ce1-7048-c558-66eb54be6360.png)

SQLには国旗用のカラムFLAGも用意しているので、画像で国旗を教えてくれるLINEBOTもつくってみようかなと思ってます。



## 参考資料
[外務省　国・地域](https://www.mofa.go.jp/mofaj/area/index.html)
[[LINEBOT] 基本設定ファイル作成 ①](https://zenn.dev/shin_shin_01/articles/980886a2f370a2)
[python-dotenv モジュールの概要](https://maku77.github.io/python/env/dotenv.html)
[Python + HerokuでLINE BOTを作ってみた](https://qiita.com/shimajiri/items/cf7ccf69d184fdb2fb26)
[Python+Flask+Herokuで作るLINE bot (具体的な操作: Postback, carouselなど)](https://qiita.com/yuk1_37/items/ec7d2223010ad4a0dd38)
[ClearDB MySQL](https://devcenter.heroku.com/articles/cleardb)
[Heroku MySQL設定](https://learning-collection.com/heroku-mysql%E8%A8%AD%E5%AE%9A/)
[【Python, MySQL】PyMySQLの基本的な使い方](https://www.tcom242242.net/entry/python-basic/%E3%80%90python-mysql%E3%80%91pymysql%E3%81%AE%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9/)
[PythonのMySQLdbで日本語の入ったクエリを投げる際のUnicodeEncodeError回避](https://qiita.com/Yuki_Oshima/items/e99041a74ce872b945cc)
[Python Oracle SQL(select文)データ取得方法(fetchall、fetchmany、fetchone)とチューニングパラメータ](https://qiita.com/kngsym2018/items/ac9348b02589e3a93c44)
[【MySQL】Excelシートのデータから一括でSQL文を作成する方法](https://moripro.net/mysql-excel-insert/)