データベース(postgreSQL)を利用した簡単なlinebotアプリを作る(データベース移行編・とりあえず完成!)

baku1101.hatenablog.com
これの続きです.
前回は,データベースのファイルをローカル(heroku上)で保管するような仕様にしていたら,heroku上にファイルの保存はできないことが判明してどうしようかというところでした.

データベースどうする?

とりあえずheroku flask データベースとかで調べると,SQLAlchemyってやつを使う記事が大量にでてきます.

SQLAlchemyとは?

これはこの記事を見たほうが早いですね.
端的に言うと,ORMというSQLをオブジェクトとして扱う概念があり,これのpythonのモジュールがSQLAlchemyってやつです(多分).sqlを生で書かないので記述が楽,sqlインジェクションなどのセキュリティを気にする必要がない,同一の記述で様々な種類のRDBMSに対応できるなどの利点がある一方,生成されるsqlコードを結局気にする必要がある,細かい指示ができないなどの欠点もあります.
個人的にはもうsql文で書いてしまったので,これを再利用したい気持ちがあるのでできればこれは使いたくないです..

本当にSQLiteは使えない?

heroku上のデータベースでなくとも,外部のデータベースを利用すればいい話なので,本当にsqlite3で書くことはできないのか?と思いました.が,ローカルのファイルにしか対応していないみたいです..まあしょうがない(参考: SQLite - PycharmのデータベースツールでリモートホストのSQLiteに接続出来ない|teratailなど)

herokuのpostgreSQLを使う

生のsql文を使いたい,sqliteは使えないとなったらこれが一番楽そうだなと思いました.postgreSQLならリモートのデータベースにも接続できます.
pythonpostgreSQLに接続するためにはpsycopg2というモジュールをインストールする必要がありますが,ここでちょっとエラーがでたのでメモ.

$ pip3 install psycopg2
Collecting psycopg2
Downloading https://files.pythonhosted.org/packages/23/7e/93c325482c328619870b6cd09370f6dbe1148283daca65115cd63642e60f/psycopg2-2.8.2.tar.gz (368kB)
    100% |████████████████████████████████| 368kB 484kB/s 
    Complete output from command python setup.py egg_info:
    running egg_info
    creating pip-egg-info/psycopg2.egg-info
    writing pip-egg-info/psycopg2.egg-info/PKG-INFO
    writing dependency_links to pip-egg-info/psycopg2.egg-info/dependency_links.txt
    writing top-level names to pip-egg-info/psycopg2.egg-info/top_level.txt
    writing manifest file 'pip-egg-info/psycopg2.egg-info/SOURCES.txt'
    Error: b'You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.\n'

postgresql-server-devをインストールしろと言われてるのでそのとおりにします.

$ sudo apt install postgresql-server-dev-10

もう一度pipをやってインストールできればok.

postgreSQLをつかって書き直す

ということでsql文を書き直します.
psycopg2を使う上で参考になったサイト:
psycopg2 でよくやる操作まとめ - Qiita
PythonでPostgreSQLに接続してみた - 薮蛇なエンジニアの開発備忘録
Pythonのmodule「psycopg2」を使ってPostgreSQLへ接続する - ITの隊長のブログ
PostgreSQLをPythonからpsycopg2を使っていじる — そこはかとなく書くよん。

幸運なことにsqlite3とほとんど方言が変わりませんでした.変わったところをかいつまんで行きます.

import datetime
import time
import psycopg2
import os
import sys

url = os.getenv('DATABASE_URL', None)
if url is None:
    print('must set DATABASE_URL')
    sys.exit(1)

データベースが参照できるURLはherokuの環境変数のDATABASE_URLとして登録されています.ローカルでテストする時は自分でexportしていました.セットされていなかった場合は標準出力にprintする(これはherokuのlogに出てくる).
ローカルのpostgreSQLのデータベース作成の参考にしたサイト:CentOS で PostgreSQL を使ってみよう!(2) | Let's Postgres

続き

con = psycopg2.connect(url)
cur = con.cursor()

def CreateTable(usrname):
    cur.execute("CREATE TABLE IF NOT EXISTS {} (year int, month int, start timestamp, finish timestamp, id serial primary key);".format(usrname))
# primary keyの型はint でなく serial じゃないとauto incrementが適用されないので注意

def DropTable(usrname):
    cur.execute("DROP TABLE IF EXISTS {}".format(usrname))
    con.commit()

def Start(usrname):
    CreateTable(usrname)
    if isExistTable(usrname + '_tmp'):
        return False
    cur.execute("CREATE TABLE {}_tmp(start timestamp)".format(usrname))
    start = datetime.datetime.now()
    cur.execute("INSERT INTO {}_tmp(start) values (%s)".format(usrname), (start,))
    con.commit()
    return True

primary keyの型がinteger から serialになっていたり,valuesの中身が ? から %sになっていたりと,方言による細かい変更はありますがほとんどそのままでできました.良かった.

# 直近の1列削除
def DeleteRow(usrname):
    cur.execute("SELECT max(id) FROM {}".format(usrname))
    bottom = cur.fetchone()[0]
    cur.execute("DELETE FROM {} WHERE id = {}".format(usrname, bottom))
    con.commit()

ここの直近のものを削除するクエリですが,多分前の記事に書いてあるやつは間違いでした.(idの最大値 = rowの数としていた)

# nameテーブルがあるかどうかを返す(tmpテーブルがあるかの確かめ用)
def isExistTable(name):
    cur.execute("SELECT relname FROM pg_stat_user_tables")
    l = cur.fetchall()
    if ((name,) in l):
        return True
    else:
        return False

pg_stat_user_tablesというテーブルがpostgreSQL側で最初から用意されており,これのrelnameというカラムに現在のデータベースのすべてのtablenameが格納されています.参考:PostgreSQLにてテーブルやカラムの各種情報を取得するSQL (テーブル一覧, カラム一覧, プライマリーキー情報取得, テーブルのコメントを取得, カラムのコメントを取得) - いろいろ備忘録日記

そういえばpostgreSQLpythonのdatetime型に対応していて,こちらでわざわざ変換してやる必要がないんですね.これ結構便利でした.

できたもの

  • ファイル構成
├── Procfile
├── __pycache__
├── example
├── main.py
├── mydatabase.py
├── requirements.txt
└── test.py

exampleはlinebotのサンプルを入れてるだけです.詳しい中身はGitHub - baku1101/linebot_katekyo: 家庭教師やる用のlinebotから

  • できること
    • start,finishボタンでdatetime型を使って時間の計測ができる
    • それをherokuのデータベースに保管する
    • 月ごとの計測した日付・時間(何時から何時まで)がわかる
    • 月ごとの計測した時間の合計がわかる
    • 直近に追加したデータの削除ができる
  • イメージ画像

f:id:baku1101:20190419110646j:plain
ボタンを上から押してみた画像

最後に

一応最低限の機能をもたせたものができたので完成ですが,せっかくpythonでデータ持ってきてるからグラフ出力とかできたら楽しそう.
SQL初めて触ったけど便利だった(今までpickleでデータ保存しかしたことなかったから,データの保存・取り出しがめんどくさかった)
あとこれの友だち追加できるQRコードです.お試ししたかったらどうぞ
f:id:baku1101:20190419111439p:plain

データベース(SQLite3?)を利用した簡単なlinebotアプリを作る(アプリ作成編)

baku1101.hatenablog.comの続きになります.

作成アプリの要件(再掲)

  • スタート,エンドがあり,その間の時間を計る
  • (スタート,エンドの時間自体もあるといい)
  • 計った時間を(少なくとも1ヶ月は)保存する
  • 月末にその月に計った時間の合計を送る

kitchensinkを試す

linebotはいくつかのサンプルが公式のgitで用意されており,その中でもkitchensinkは使える機能を網羅しているので,これを試しながらどうやって作るか考えていきます.
kitchensinkのリポジトリはここ
line-bot-sdk-python/examples/flask-kitchensink at master · line/line-bot-sdk-python · GitHub
なお,サンプルコードは以下のように一部書き換えました(修正前はwebhookが機能しなかった,portの問題?)

いくつか試してみたところ,buttonsにスタート,エンドをつければ良さそうかなという気持ちになりました.
kitchensinkにおけるbuttonsのサンプル実装と見た目はこんな感じです.

f:id:baku1101:20190411205928j:plain

なお,PostbackActionは受け取ったdataによってlinebot側が取る行動を変えられるactionです.サンプル実装はこんな感じ(pingボタンを押すとlinebotがpongを返します)

実装を考える

  • start, endボタンを作る
    • 押された時の挙動(データベースの作成や保存)は前回作ったやつをうまく使う
  • 今データベースに格納されているデータを取り出すボタンも欲しい(show)
    • 月ごとに取り出せると嬉しい -> DatetimePickerActionなる便利なものがあったので利用する
  • 間違えて入れたデータを削除するボタンも欲しい(del)
    • データを選択して削除させる方針は難しそう (データにidがついてればできる?)
    • とりあえず直近のデータが消せれば十分かな?
  • 直接データを挿入できるボタンもあると嬉しい(insert)

とりあえずこんな感じで良さそうかな.

実装する

てことで実装しました.大きく詰まった点は以下の2つで

実際の実装はこれです.linebotのメインがmain.pyで,データベース関連の操作を行う関数群がmydatabase.pyということにしています.

なお,lineアカウントのaccess_token, secretキーはheroku の環境変数として設定しています(main.py:56,57).設定方法は
$ heroku config:set LINE_CHANNEL_SECRET="シークレットキー" --app アプリ名
こんな感じでできます.
それと,herokuでdatetime使う時はheroku側のタイムゾーンの設定をする必要があります.
$ heroku config:set TZ='Asia/Tokyo'

問題

とりあえずこれで動いたのですが,ものすごく大きい問題が判明してしまい...
herokuにはファイルが保存できない(時間でリセットされる)ので,データベースファイルが保持できない!!
ということで,次でherokuのデータベース(postgreSQL)を利用するように改修する予定です.最初からそうしときゃよかったなあ...

データベース(sqlite3)を利用した簡単なlinebotアプリを作る(データベース勉強編)

baku1101.hatenablog.comの記事の続きになります.

作成アプリの要件

  • スタート,フィニッシュがあり,その間の時間を計る
  • (スタート,フィニッシュの時間自体もあるといい)
  • 計った時間を(少なくとも1ヶ月は)保存する
  • 月末にその月に計った時間の合計を送る

とりあえず必要最低限はこんなものかなあ.
正直linebotで作る必要性とかデータベースを使う必要性はわからないけど,まあ勉強したいので.
ということでデータベースを使いたいけど使ったことないので,必要になりそうな領域について勉強をします.

データベースの勉強

データベースとは?

まずデータベースと一口に言っても色々あるらしいですが,今回使うのは関係データベースというもの(というか今情報系で使われるのは大体これ?)です.これは表のような構造でデータを管理し,クエリを与えてデータの検索・変更を行うデータモデルです.このモデルを用いてデータを管理するシステムが関係データベース管理システム(RDBMS)と呼ばれるものであり,これにSQLite3とかMySQLなどが該当するみたいです.(関係データベース - Wikipediaより)

SQLとは?

SQLとは,前述したRDBMSのデータ操作を行う際に用いるドメイン言語のことです.SQL文を扱う際に用いる用語には以下のようなものがあります(自分はexcelと対応させる感じで理解してます).

  • table : excelのsheetに対応するもの
  • column : excelの表の列に対応する.データベースでは列ごとに同じデータを入れる(下の表をイメージしている)
id name age
0 taro 20
1 jiro 15
2 saburo 10
  • record : こちらは行に対応する.普通にrowって書いてるところもある
  • field : データ一つ一つのこと.
  • cursor : excelのカーソルと同じって記述もあったけど,テーブル作ったり色々できるからいまいちイメージできていない.基本的にこのカーソルを動かしてデータを取得していくイメージなのかな.

とりあえず用語はこんなところにして,実際に書いてみます.

練習用コード(datetimeを扱う)

今のところ,スタートとフィニッシュにはdatetimeを使いたいと考えています.pythonのsqlite3でdatetimeを扱う方法については沢山記事がありましたが,結局公式のリファレンス(sqlite3 --- SQLite データベースに対する DB-API 2.0 インタフェース — Python 3.7.3 ドキュメント)を参考にしました.


これを実行した時の出力は以下のとおりです.

$ python3 test.py
2019-04-02 => 2019-04-02 <class 'datetime.date'>
2019-04-02 11:12:33.914567 => 2019-04-02 11:12:33.914567 <class 'datetime.datetime'>

ちゃんとrow[0]に日付,row[1]に日付を含めた時間が入っているのがわかります.

作成したいアプリで使えそうなコードを書いてみる

結果的にはこんな感じになりました.

データベース(sqlite3)を利用した簡単なlinebotアプリを作る(環境構築編)

以前に簡単なlinebotを作ったことはあったが,作り方を忘れてしまったのでそれを繰り返さないためのメモ

作りながら書いていくので試行をそのまま残します

作りたいもの

  • 時間を計って,月末に総合の時間を送ってくれるbot
  • 's'を送ると計測開始,'f'を送ると計測終了(のつもり)

開発環境

環境作り

参考:
PythonでLine botを作ってみた - Qiita
Python+Flask+Herokuで作るLINE bot (具体的な操作: Postback, carouselなど) - Qiita

それではやっていきます.

まずはvenvで開発するアプリ専用のpython環境を作ります.

$ python3 -m venv myapp
$ cd myapp
$ source ./bin/activate
$ pip install line-bot-sdk
$ pip install flask

ここでinstallされたmoduleを確認すると

$ pip3 freeze
certifi==2019.3.9
chardet==3.0.4
Click==7.0
Flask==1.0.2
future==0.17.1
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10
line-bot-sdk==1.8.0
MarkupSafe==1.1.1
pkg-resources==0.0.0
requests==2.21.0
urllib3==1.24.1
Werkzeug==0.15.1

flaskとline-bot-sdkのインストールのために色々ついてくるみたい

herokuのための環境

一般的なものと変わりません.一応ディレクトリ構成を書いておくと

$ tree
.
├── Procfile
├── main.py
├── requirements.txt
└── runtime.txt

(ただしモジュールを分離する過程などで実行に関するファイルが増える可能性はあり)

とりあえずこんなところにして,次からアプリ開発に取り掛かろうと思います.