LoginSignup
5
3
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

FastAPIとsqlite3による簡単なウェブサイトを実装する

Last updated at Posted at 2024-01-05

最近FastAPIというPythonのウェブフレームワークのことを聞いて興味を持って自分も使ってみたくなって勉強し始めて、実際に使えるウェブサイトの実装までできました。

この記事ではFastAPIの基本的な使い方を説明しながらデータベースを扱う小さなウェブサイトを実装するコードを解説して、そして実行の結果の画像も載せます。

はじめに

私は元々responderを使っていたのですが、今responderを使っている人は殆どいないらしいです。このFastAPIとresponderは色々似ていますが、responderと比べてFastAPIの方がどの方面から見ても上なので、今更responderを使う理由はもうないとも言えますね。

パフォーマンスについてこの記事に書いてあります。

私は以前responderを勉強した時にこんな記事を書きました。

今回はFastAPIであの時の記事と似ているウェブサイトを作ることになります。前回の記事も一緒に読んだらresponderとFastAPIの違いを比較することもできます。

FastAPIという名前だけど、作れるのAPIだけでなく、jinja2テンプレートを使うことで普通のhtmlウェブサイトも簡単に作れます。

準備

作るものの纏め

今回作るのはこういうウェブサイトです。

  • 店などの客の「名前」と「年齢」を登録して閲覧したり編集したり削除したりできるサイト
  • サイトを操るのはたった一つのpythonファイル
  • データベースに収めるのはたった2列しかないテーブル
  • 各ページはjinja2のテンプレートによって成される
  • 構成するページは3つ
    • 全部のデータを閲覧したり追加したりダウンロードしたりできるインデックスページ
    • データを編集できるページ
    • エラーが出た時に出現するページ
  • スタイルシートも簡単なcssファイルだけど一応準備しておく
  • formsとinputでデータを入力と編集する
  • javascriptの出番はない

モジュールのインストール

まずは今回で必要なモジュールをインストールするのです。必要なのは

  • fastapi
  • uvicorn
  • jinja2

もしただAPIを作りたいならfastapiuvicornだけで十分かもしれませんが、今回htmlテンプレートも使うのでjinja2もインストールする必要があります

どれも簡単にpipでインストールできます。

pip install fastapi uvicorn jinja2

その他にsqlite3も使いますが、これはpythonの組み込みモジュールなので特にインストールする必要ありません。

ファイル

サイトは全部ただこの5つのファイルから成されることになります。.pyと.cssそれぞれ一つで、テンプレートの.htmlは3つ。

├── web.py
├── tem
│   ├── index.html
│   ├── kyaku.html
|   └── error.html
├── stt
|   └── css.css

データベース

SQLでこんな簡単なデータベースを作ります。

内容 列名 データ型
名前 namae text
年齢 nenrei integer

今回の目標はただちゃんとデータベースに接続してやり取りする例を挙げたいだけなので、簡単のために2列しかないテーブルにします。

このテーブルはSQLコードにするとこんな感じになります。

create table kyaku (namae text,nenrei integer, primary key (namae));

又、今回は直接sqlite3モジュールを使っていますが、sqlalchemyを使う場合も多いです。
sqlalchemyによる実装の例はこれらの記事を参考に:

環境

OSとPythonとモジュールのバージョンが違ってもあまり問題ないと思いますが一応今回で試した時の環境を書いておきます。

  • windows 11
  • python 3.11.5
  • conda 23.9.0
  • fastapi 0.108.0
  • jinja2 3.1.2
  • starlette 0.32.0.post1

コード

次は各ファイルの中のコードの説明となります。

html(jinja2)

各ページを構成するjinja2テンプレートのhtmlファイル。

index.html

まずはルート(root)にあるインデックスページ。

index.html
<head>
    <meta charset="utf-8">
    <title>とあるfastAPIのウェブサイト</title>
    <link rel="stylesheet" href="/stt/css.css" type="text/css" media="all">
</head>

<body style="background-color: #cfe8f8;">
    <h3>登録した客</h3>
    <ul style="border: solid #914 2px;">
        {% for k in kyaku %}
        <li>
            <a href="/kyaku/{{ k[0] }}">{{ k[0] }}</a> {{ k[1] }}歳
        </li>
        {% endfor %}
        {% if not kyaku %}
            <br>まだ何も登録されていない<br><br>
        {% endif %}
    </ul>
    <h3>新しい客を登録する</h3>
    <form action="/touroku" method="post">
        <div>名前 <input type="text" name="namae"></div>
        <div>年齢 <input type="text" name="nenrei"><br></div>
        <div><input type="submit" value="登録"></div>
    </form>
    <br>
    <a href="/csv">データを.csvにしてダウンロードする</a>
</body>

基本的に3つに分けられています。

  • 登録されたデータの列挙
  • 新しいデータを登録するフォーム
  • データを纏めてダウンロードするリンク

kyaku.html

次は、指定の名前の客のデータを表示したり編集したりするためのページ。

kyaku.html
<head>
    <meta charset="utf-8">
    <title>{{ namae }}のページ</title>
    <link rel="stylesheet" href="/stt/css.css" type="text/css" media="all">
</head>

<body style="background-color: #ddf4be;">
    <form action="/koushin/{{ namae }}" method="post">
        <div>名前: <input type="text" name="namae" value="{{ namae }}"></div>
        <div>年齢: <input type="text" name="nenrei" value="{{ nenrei }}"></div>
        <input type="submit" value="更新">
    </form>
    <form action="/sakujo" method="post">
        <input type="hidden" name="namae" value="{{ namae }}">
        <input type="submit" value="削除">
    </form>
    <div><a href="/">戻る</a></div>
</body>

error.html

何かの間違いが起きたら出てくる簡単なページ。

error.html
<head>
    <meta charset="utf-8">
    <title>エラーのページ</title>
</head>

<body style="background-color: #f6d7d7;">
    {{error_message}}
</body>

css

css.css

ウェブサイトを綺麗に装飾する部分。

css.css
div,li,h3 {
    padding: 2px;
    font-size: 20px;
}
input {
    border: solid #149 2px;
    font-size: 19px;
}
form {
    margin: 3;
}

python

web.py

全てのサイトの動作を操るpythonコードです。

複雑なサイトなら色々なファイルに分裂することが多いかしれませんが、今回は小さくて簡単なサイトなので、分ける必要なく一つのファイルにします。

全部のルーティングのコントローラーと、データベースに接続するコードは全てここに書いてあります。

web.py
import os,sqlite3
from fastapi import FastAPI, Request, Response
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse

dbfile = 'dbfile.db' # SQLデータを保存するファイル

# 初めて実行した時、新たにテーブルを作っておく
if(not os.path.exists(dbfile)):
    with sqlite3.connect(dbfile) as conn:
        sql_create = '''
            create table kyaku (
                namae text,
                nenrei integer,
                primary key (namae)
            )
        '''
        conn.execute(sql_create)

app = FastAPI()
app.mount(path="/stt", app=StaticFiles(directory='stt')) # cssファイルを収める場所
jintem = Jinja2Templates(directory='tem') # jinja2テンプレートのhtmlファイルを収める場所

# インデックスページ
@app.get('/')
def index(request: Request):
    with sqlite3.connect(dbfile) as conn:
        sql_select = '''
            select * from kyaku
        ''' # 全ての客のデータを取得する
        kyaku_lis = conn.execute(sql_select).fetchall()
        param = {'request': request, 'kyaku': kyaku_lis}
        return jintem.TemplateResponse('index.html',param)

# 各データ表示と編集のページ
@app.get('/kyaku/{namae}')
def kyaku(request: Request,namae: str):
    with sqlite3.connect(dbfile) as conn:
        sql_select = '''
            select * from kyaku where namae==?
        ''' # その名前を持つキャラのデータを取る
        kyaku = conn.execute(sql_select,[namae]).fetchone()
        if(kyaku):
            param = {'request': request,
                     'namae': namae,
                     'nenrei': kyaku[1]
            }
            return jintem.TemplateResponse('kyaku.html',param)
        else:
            param = {'request': request,
                     'error_message': 'このページは存在しない'
            }
            return jintem.TemplateResponse('error.html',param) # 存在しない名前が入れられる場合、エラーページへ

# 新しいデータ登録する処理
@app.post('/touroku')
async def touroku(request: Request):
    try:
        with sqlite3.connect(dbfile) as conn:
            form = await request.form() # フォームに記入されたデータを取得
            namae = form['namae'] # 名前
            nenrei = int(form['nenrei']) # 年齢
            sql_insert = '''
                insert into kyaku (namae,nenrei)
                values (?,?)
            ''' # 新しいデータ追加
            conn.execute(sql_insert,(namae,nenrei))
        return RedirectResponse(url='/',status_code=303) # 完成したらインデックスページに戻る
    except Exception as err:
        param = {'request': request,
                 'error_message': f'エラー:{type(err)} {err}'
        }
        return jintem.TemplateResponse('error.html',param) # なにか間違いがある場合

# データ更新する処理
@app.post('/koushin/{namae}')
async def koushin(request: Request, namae: str):
    try:
        with sqlite3.connect(dbfile) as conn:
            form = await request.form()
            namae_x = form['namae'] # 新しい名前
            nenrei = int(form['nenrei'])
            sql_update = '''
                update kyaku set namae=?,nenrei=? where namae==?
            ''' # データ更新
            conn.execute(sql_update,(namae_x,nenrei,namae))
        return RedirectResponse(url=f'/kyaku/{namae_x}',status_code=303) # 完成したら新しい名前でデータ表示のページに戻る
    except Exception as err:
        param = {'request': request,
                 'error_message': f'エラー:{type(err)} {err}'
        }
        return jintem.TemplateResponse('error.html',param) # なにか間違いがある場合

# データ削除する処理
@app.post('/sakujo')
async def sakujo(request: Request):
    try:
        with sqlite3.connect(dbfile) as conn:
            form = await request.form()
            namae = form['namae']
            sql_delete = '''
                delete from kyaku where namae==?
            ''' # データ削除
            conn.execute(sql_delete,[namae])
        return RedirectResponse(url='/',status_code=303) # インデックスページに戻る
    except Exception as err:
        param = {'request': request,
                 'error_message': f'エラー:{type(err)} {err}'
        }
        return jintem.TemplateResponse('error.html',param) # なにか間違いがある場合

# データのダウンロード
@app.get('/csv')
def csv():
    with sqlite3.connect(dbfile) as conn:
        data = conn.execute('select * from kyaku').fetchall() # 全部データを読み込む
        data = '名前,年齢\n'+'\n'.join([d[0]+','+str(d[1]) for d in data]) # データをcsvに
        header = {'Content-Disposition': 'attachment; filename=data.csv'}
        return Response(content=data,headers=header,media_type='text/csv')

ルーティングは6つあります。

その中の3つはget:

名前 ルーティング 役目
index / インデックスのページ
kyaku /kyaku/{namae} 各データのページ
csv /csv データをダウンロードする処理

ここで{namae}の部分は任意の客の名前です。

後3つはpost:

名前 ルーティング 役目
touroku /touroku データを追加する処理
koushin /koushin/{namae} データを更新する処理
sakujo /sakujo データを削除する処理

これらはボタンを押したことによってアクセスするもので、SQLデータベースとやり取りした後すぐリダイレクトするのでhtmlテンプレートを準備する必要がない。

本来削除の作業はdeleteメソッドでやるべきだと思っていましたが、FastAPIでは削除もpostを使います。これについてこの記事も参考。

postではフォームの中のデータを取得するためにawaitが必要なのでdefの前にasyncが置かれて非同期処理となります。

その違いについてこの記事に解説があります。

実行と結果

コードの準備が完成したら、次はコマンドプロンプトかターミナルでこのコマンドを実行するのです。

uvicorn web:app --reload

そしてブラウザーで http://127.0.0.1:8000/ にアクセスしましょう。

インデックスページ

何かの間違いがなければこのようなページは表示されるはずです。(ここではMicrosoft Edgeで)

q01.png

まだデータが入っていないので、まずは追加してみます。

q02.png

名前と年齢を入力して「登録」ボタンをクリックすると、データが追加されます。そして
試しにもう一つ追加しますね。

q03.png

そうしたらこうなります。

q04.png

そして下にある「データを.csvにしてダウンロードする」のリンクをクリックしたら、データは.csvファイルに書かれてすぐダウンロードが始まって、こういうファイルを得られます。

data.csv
名前,年齢
山田ほげ,20
田中ふが,25

各データのページ

次は名前のリンクをクリックしてみたらこんなページに入ります。
q05.png

ここで編集して「更新」ボタンを押したら実際に更新されます。

エラーのページ

上の例ではkyaku/の次は存在する名前だから正しく表示されますが、試しに存在しない名前を入れたら、例えば http://127.0.0.1:8000/kyaku/xxx 、そうしたらこんなエラーページが出ます。

q06.png

又は、例えば「年齢」に何も入れないまま「更新」ボタンを押したらこのようなエラーページが出てきます。

q07.png

docs

FastAPIの特徴の一つはSwagger UIのドキュメントのページが自動的に生成されることです。読む方法は http://127.0.0.1:8000/docs にアクセスすること。

今回作ったサイトのdocsこんなページになります。

q08.png

このサイトにどんなページがあるか簡単に纏められていてすごい便利です。

参考

今回FastAPIの勉強のために色んな記事読みました。ここで参考になった記事を纏めておきます。

終わりに

以上FastAPIの使う簡単な例でした。かなり簡単に実装できて、使い勝手がいいです。その上パーフォーマンスのことも優秀らしいです。それだけでなく、Pythonで書くフレームワークということは、機械学習や画像処理などPythonの得意分野のモジュールと連携しやすいというのは他の言語で書いたフレームワークと比べて圧倒的なメリットでもあります。これからも人気が高まっていくでしょう。

5
3
0

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
5
3