kuroの覚え書き

96の個人的覚え書き

データベースサイトの構築 flaskによるwebアプリ開発

今日もflaskから始めよう。

本日の目標
まずはhtmlを読み込む。
次にインタラクティブな動作。

htmlファイルの読み込み。
用意するもの
hello2.py

# hello2.py
from flask import Flask, render_template #render_templateクラスもインポート
app = Flask(__name__)

@app.route("/")
def index():
     return render_template('index.html', message="Hello") #ここの中身が変わっている

if __name__ == "__main__":
        app.run()

そしてhello2.pyをおいたディレクトリにtemplatesという名前のディレクトリを作成し、
index.html

<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>{{ message }}</p>
</body>
</html>

を配置。タグ内の{{ message }}の部分がhello2.pyによってHelloに置き換えられて表示されている。それ以外のhtmlは普通どおりの書式でいいようだ。

次行ってみよう。
Flaskの公式チュートリアルではflaskrというブログサイトをSQLite3をデータベースに使用して作成している。今回の最終目標ではSQLAlchemyをつかってMySQLと連携したいので
http://study-flask.readthedocs.io/ja/latest/02.html
こちらのサイトに従って作ってみることにする。こっちはSQLAlchemyを使った例になっている。
まず、ざっと必要な構成を空のまま作っていく。

$ mkdir -p tutorial/flaskr/{static,templates}
$ cd tutorial/
$ touch manage.py requirements.txt
$ touch flaskr/{__init__,views,models,config}.py
$ touch flaskr/static/style.css
$ touch flaskr/templates/{layout,show_entries}.html

requirement.txtに必要なライブラリを記述。

Flask
Flask-SQLAlchemy
$ pip3 install -r requirements.txt 
Requirement already satisfied: Flask in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from -r requirements.txt (line 1))
Collecting Flask-SQLAlchemy (from -r requirements.txt (line 2))
  Downloading Flask_SQLAlchemy-2.2-py2.py3-none-any.whl
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->-r requirements.txt (line 1))
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->-r requirements.txt (line 1))
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->-r requirements.txt (line 1))
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->-r requirements.txt (line 1))
Requirement already satisfied: SQLAlchemy>=0.8.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask-SQLAlchemy->-r requirements.txt (line 2))
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask->-r requirements.txt (line 1))
Installing collected packages: Flask-SQLAlchemy
Successfully installed Flask-SQLAlchemy-2.2

requirement.txtに従って必要なファイルをインストール
今回必要なコンポーネントはほぼインストール済みだったが、Flask-SQLAlchemyパッケージだけが追加された模様。

$ pip3 list
DEPRECATION: The default format will switch to columns in the future. You can use --format=(legacy|columns) (or define a format=(legacy|columns) in your pip.conf under the [list] section) to disable this warning.
click (6.7)
cycler (0.10.0)
Flask (0.12.2)
Flask-SQLAlchemy (2.2)      <-これ
itsdangerous (0.24)
Jinja2 (2.9.6)
MarkupSafe (1.0)
matplotlib (2.0.2)
numpy (1.13.0)
pip (9.0.1)
PyMySQL (0.7.11)
pyparsing (2.2.0)
pysam (0.11.2.2)
python-dateutil (2.6.0)
pytz (2017.2)
scipy (0.19.1)
setuptools (28.8.0)
six (1.10.0)
SQLAlchemy (1.1.12)
Werkzeug (0.12.2)

flaskrを実際に起動するためのスクリプトがmanage.py

from flaskr import app
#flaskrディレクトリに__init__.pyがあるのでflaskrの中にあるスクリプトの中のオブジェクトをこのようにインポートできる。
app.run(host='127.0.0.1', port=5000, debug=True)

ホスト、ポートもちゃんと記述。あとデバッグモードをTrueにしている。

flaskr/__init__.pyを編集。
__init__.pyはそれがあるディレクトリはpythonスクリプトがあるということを示す役割があり、その場合中身はからでもいい。また中にインポートするモジュールを書いておくこともできる。
FlaskとプラグインのSQLAlchemyを生成するらしい。hello.pyのときの前半部分に相当?

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
#Flaskクラスのインスタンス(app)を作る。

app.config.from_object('flaskr.config')
#Flaskのconfigがオブジェクト(flaskr/config.py)から設定を読み込む。
#config.pyの中で大文字で書かれた変数が読み込まれる。
#config.pyを使わずこのファイルにconfigを列記しておく場合ならfrom_object(__name__)になる。

db = SQLALchemy(app)
#SQLAlchemyクラスからインスタンスdbを生成。
#このdbがFlask-SQLAlchemyの全機能にアクセスを可能とする?

import flaskr.views

flaskr/config.py
でデータベースの設定と、セッション情報を暗号化するためのキーを設定。今回はまずsqliteでやってみて、ちゃんと動いたらMySQLに変えてみる。SECRET_KEYは実運用するならちゃんと何か設定しておかないとまずい。今回はテストなので例と同じで行く。

SQLALCHEMY_DATABASE_URI = 'sqlite:///flaskr.db'
SECRET_KEY = 'secret key'

ここまでが下準備で、ここからアプリの本体部分の作り込みになる。逆に言うとここまでの設定は似たような構成のアプリならたいてい流用できるということかな。

ブログの記事を格納するデータベースを作成するためのスクリプト
flaskr/models.py

from flaskr import db

class Entry(db.Model):
    __tablename__ = 'entries'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)

    def __repr__(self):
        return '<Entry id={id} title={title!r}>'.format(
                id=self.id, title=self.title)

def init():
    db.create_all()

id、title、textの3カラム分で1エントリーとなり、idはprimary_key=Trueなので自動的に割り振られる設定のようだ。

まずはデータベースを作ってみて、ちゃんと動作することを確認してみる。
Python3のREPLで

$ python3
>>> from flaskr.models import init
/Users/kkuro/python_test/tutorial/flaskr/__init__.py:2: ExtDeprecationWarning: Importing flask.ext.sqlalchemy is deprecated, use flask_sqlalchemy instead.
  from flask.ext.sqlalchemy import SQLAlchemy
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:839: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '

>>> init()

これでflaskr/flaskr.dbが生成されたらしい。
出来上がったデータベースにエントリーし、書き換え、削除のテストをしてみる。

>>> from flaskr.models import Entry, db
>>> Entry.query.all()
[]
#作ったばかりなのでからっぽ

>>> entry = Entry(title='title', text='text')
>>> db.session.add(entry)
>>> db.session.commit()
>>> Entry.query.all()
[<Entry id=1 title='title'>]
#titleが'title'本文が'text'というエントリーがid=1で出来上がった

>>> entry = Entry.query.get(1)
>>> entry
<Entry id=1 title='title'>
#id=1のエントリーを呼び出し

>>> entry.title = 'Hello world'
>>> db.session.add(entry)
>>> db.session.commit()
>>> Entry.query.all()
[<Entry id=1 title='Hello world'>]
#タイトルを'Hello world'に書き換え

>>> entry = Entry.query.filter(Entry.title == 'Hello world').first()
>>> entry
<Entry id=1 title='Hello world'>
#タイトルが'Hello world'のエントリーを抽出

>>> db.session.delete(entry)
>>> db.session.commit()
>>> Entry.query.all()
[]
#エントリーを全部削除

という感じでちゃんと動作しているっぽい。

とりあえずこれで動作に必要なスクリプトが出来上がったようなものだが、肝心の表示画面がまだ準備できていない。なのでmanage.pyを走らせれば動作はするがページはNot Foundになる。というわけで投稿と表示のページを作っていく。

投稿画面と一覧画面を生成する。
flaskr/views.py

from flask import request, redirect, url_for, render_template, flash
from flaskr import app, db
from flaskr.models import Entry

@app.route('/')
def show_entries():
    entries = Entry.query.order_by(Entry.id.desc()).all()
    return render_template('show_entries.html', entries=entries)

@app.route('/add', methods=['POST'])
def add_entry():
    entry = Entry(
            title=request.form['title'],
            text=request.form['text']
            )
#POSTメソッドで入力窓から入力フォームを受け付けて、データベースのtitleとtextカラムにそれぞれ受け渡す
    db.session.add(entry)
    db.session.commit()
#渡されたエントリーをデータベースに追加実行する
    flash('New entry was successfully posted')
#追加できたらメッセージを表示させる
    return redirect(url_for('show_entries'))
#show_entry.htmlに流し込む

表示のレイアウトを決めるhtmlファイル
幾つものファイルに同じようなヘッダをかくのが面倒なので共通した部分をまとめて書いたファイルを用意するのが主流らしい。

flaskr/template/layout.html

<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
  <h1>Flaskr</h1>
  {% for message in get_flashed_messages() %}
    <div class=flash>{{ message }}</div>
  {% endfor %}
  {% block body %}{% endblock %}
</div>

{%........%}というところがFlaskが利用しているテンプレートエンジンjinja2によるもののようだ。
次のshow_entries.htmlの最初で{% extends "layout.html" %}と書いてlayout.htmlを拡張しており
中で{%block.......%}と{%endblock}で挟まれた部分がbodyブロックとして扱われ、layout.htmlのbodyブロックのところにレンダリングされるという寸法のようだ

flaskr/templates/show_entries.html

{% extends "layout.html" %}
{% block body %}
  <form action="{{ url_for('add_entry') }}" method=post class=add-entry>
    <dl>
      <dt>Title:
      <dd><input type=text size=20 name=title>
      <dt>Text:
      <dd><textarea name=text rows=5 cols=20></textarea>
      <dd><input type=submit value=Share>
    </dl>
  </form>

  <ul class=entries>
  {% for entry in entries %}
    <li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}
  {% else %}
    <li><em>Unbelievable.  No entries here so far</em>
  {% endfor %}
  </ul>
{% endblock %}

webの見た目は全部cssに記述してあるので見た目をいじりたければこれを書き換えるだけだな。
flaskr/static/style.css

body            { font-family: sans-serif; background: #eee; }
a, h1, h2       { color: #377ba8; }
h1, h2          { font-family: 'Georgia', serif; margin: 0; }
h1              { border-bottom: 2px solid #eee; }
h2              { font-size: 1.2em; }

.page           { margin: 2em auto; width: 35em; border: 5px solid #ccc;
                  padding: 0.8em; background: white; }
.entries        { list-style: none; margin: 0; padding: 0; }
.entries li     { margin: 0.8em 1.2em; }
.entries li h2  { margin-left: -1em; }
.add-entry      { font-size: 0.9em; border-bottom: 1px solid #ccc; }
.add-entry dl   { font-weight: bold; }
.metanav        { text-align: right; font-size: 0.8em; padding: 0.3em;
                  margin-bottom: 1em; background: #fafafa; }
.flash          { background: #cee5F5; padding: 0.5em;
                  border: 1px solid #aacbe2; }
.error          { background: #f0d6d6; padding: 0.5em; }

cssフレームワークはこの際公開されているものを適当に流用してちょっと変えてつかうのがかんたんだろうな。
http://getbootstrap.com
こういうの。
http://www.isozai.com/layoutit/
こういうツールをつかえば見た目だけならかんたんに立派なものが出来上がるわけだ。

さて起動してみよう。

$ python3 manage.py 
/Users/kkuro/python_test/tutorial/flaskr/__init__.py:2: ExtDeprecationWarning: Importing flask.ext.sqlalchemy is deprecated, use flask_sqlalchemy instead.
  from flask.ext.sqlalchemy import SQLAlchemy
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:839: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
/Users/kkuro/python_test/tutorial/flaskr/__init__.py:2: ExtDeprecationWarning: Importing flask.ext.sqlalchemy is deprecated, use flask_sqlalchemy instead.
  from flask.ext.sqlalchemy import SQLAlchemy
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:839: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
 * Debugger is active!
 * Debugger PIN: 624-069-997
127.0.0.1 - - [04/Aug/2017 12:05:22] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [04/Aug/2017 12:05:22] "GET /static/style.css HTTP/1.1" 200 -
127.0.0.1 - - [04/Aug/2017 12:05:47] "POST /add HTTP/1.1" 302 -
127.0.0.1 - - [04/Aug/2017 12:05:47] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [04/Aug/2017 12:07:10] "POST /add HTTP/1.1" 302 -
127.0.0.1 - - [04/Aug/2017 12:07:10] "GET / HTTP/1.1" 200 -


ばっちりだな。
さあ、次はSQLMySQLにして試してみるよ。