PythonのマイクロフレームワークFlaskと、標準ライブラリのsqlite3だけで、ブックマークを登録・一覧・検索できる小さなWebアプリを実際に作って動かしてみました。追加のデータベースサーバは立てず、bookmarks.dbというファイル1つで永続化します。
単一ページに一覧と登録・検索フォームをまとめ、初心者でも処理の流れを追いやすい構成にしました。コードの要点と、つまずきやすいエラーへの対処までまとめます。
Flaskの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。FlaskとSQLiteを使ってブックマーク管理アプリを作るカリキュラムを始めます。概要紹介。
FlaskとSQLiteの役割と使い方を学ぶ ブックマーク管理アプリの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。
タイトルとURLとタグの入力フォームを表示 登録フォームからPOSTでブックマークを追加bookmarksテーブルへINSERTして永続化 一覧をid降順で新しい順に表示 キーワードでタイトルとURLとタグを部分一致検索 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、Flask、Jinja2、pysqlite3-binary PythonのFlaskとSQLiteとは。
Flask:Flaskは、少ないコードでルーティングと画面表示を組み立てられるPythonのWebフレームワークSQLite:sqlite3は、追加のサーバを立てずにファイル1つでデータを保存できるPython標準のデータベースライブラリ Flaskで作るブックマーク管理アプリの要点。
titleとurlの両方があるときだけ登録 タグをnormalize_tagsで前後空白ごと除去 検索語を1つの%付き値で3カラムへ適用 ブックマーク管理アプリの要件定義。
登録後にトップの一覧ページへリダイレクト 登録したブックマークが一覧に表示 検索キーワードで一致分だけに絞り込み 該当なしのとき専用メッセージを表示 タグがカンマ区切りで分割表示 INTRO: Monaco Editorでブックマーク管理アプリを実装。コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。
LINE 002: osモジュールの読み込み。標準ライブラリのosを読み込んでいます。この後の行でファイルパスを組み立てるために使います。
LINE 003: sqlite3モジュールの読み込み。SQLiteデータベースを操作するためのsqlite3を読み込んでいます。ブックマークの保存や検索でこのモジュールを利用します。
LINE 004: tempfileモジュールの読み込み。一時ディレクトリの場所を取得するためにtempfileを読み込んでいます。書き込み可能な場所にDBを置く準備です。
LINE 006: Flaskの機能をまとめてインポート。Flask本体とリダイレクトやテンプレート描画、リクエスト取得、URL生成の機能をまとめて読み込んでいます。Webアプリ全体でこれらを使います。
LINE 009: Flaskアプリの生成。Flaskクラスからアプリケーション本体を作成しています。__name__を渡すことで、このファイルを基準に設定が読み込まれます。
LINE 011: DBファイルの保存先を決定。一時ディレクトリのパスとファイル名を結合し、bookmarks.dbの保存場所を作っています。読み取り専用の場所でも起動できるようにする工夫です。
LINE 014: DB接続を返す関数の定義。データベースへの接続を用意するget_db関数を定義しています。接続処理を一か所にまとめて再利用できるようにしています。
LINE 016: SQLiteへの接続。先ほど決めたDB_PATHを指定してSQLiteに接続しています。得られたconnを通してSQLを実行していきます。
LINE 017: カラム名アクセスの設定。row_factoryにsqlite3.Rowを設定し、取得した行にカラム名でアクセスできるようにしています。row["title"]のような書き方が可能になります。
LINE 018: 接続オブジェクトを返却。用意した接続connを呼び出し元へ返しています。これでほかの関数が同じ設定の接続を受け取れます。
RUN 1/7: DB接続を作るget_dbを確認する。get_dbでSQLiteへ接続でき、行をカラム名で読める設定になっているかを確認します。CHECK 1/7: 途中実行に成功。
row_factory: True RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 021: テーブル初期化関数の定義。
ブックマーク用のテーブルを準備するinit_db関数を定義しています。アプリ起動時にこの関数を呼び出して土台を整えます。LINE 023: DB接続の取得。
get_dbを呼び出してデータベース接続を取得しています。この接続を使ってテーブル作成のSQLを実行します。LINE 024: SQL実行の開始。
execute関数を呼び出してSQL文の実行を始めています。次の行から続く文字列でテーブル作成の命令を渡します。LINE 025: SQL文字列の開始。
三連引用符で複数行にわたるSQL文を書き始めています。改行を含む長い命令を読みやすく記述できます。LINE 026: テーブル作成の宣言。
bookmarksという名前のテーブルを作成する命令です。IF NOT EXISTSを付け、既に存在する場合は作り直さないようにしています。LINE 027: id列の定義。
自動で番号が振られる主キーidを定義しています。AUTOINCREMENTにより、登録ごとに一意の番号が割り当てられます。LINE 028: title列の定義。
ブックマークの表題を保存するtitle列を文字列型で定義しています。NOT NULL指定で空の値を許さないようにしています。LINE 029: url列の定義。
リンク先を保存するurl列を文字列型で定義しています。こちらもNOT NULLで必ず値が入るようにしています。LINE 030: tags列の定義。
タグをカンマ区切りで保存するtags列を定義しています。DEFAULT ''で、未入力時は空文字が入る仕様です。LINE 031: テーブル定義の閉じ括弧。
CREATE TABLEの列定義を閉じる括弧です。ここまでで4つの列を持つテーブルの構造が確定します。LINE 032: SQL文字列の終了。
三連引用符でSQL文の記述を終えています。ここまでの内容が一つの命令としてexecuteに渡されます。LINE 033: SQL実行呼び出しの終了。
executeの呼び出しを閉じる括弧です。これでテーブル作成のSQLが実際に発行されます。LINE 034: 変更の確定。
commitを呼び出してテーブル作成の結果をデータベースに確定させています。これを行わないと変更が保存されません。LINE 035: 接続のクローズ。
使い終わった接続をcloseで閉じています。リソースを解放し、処理をきれいに終える大切な後始末です。RUN 2/7: テーブルを準備するinit_dbを確認する。
init_dbを呼び、bookmarksテーブルが作成されるかを確認します。CHECK 2/7: 途中実行に成功。作成済みテーブル: ['bookmarks', 'sqlite_sequence'] RETURN 02: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 038: タグ正規化関数の定義。入力されたタグ文字列を整えるnormalize_tags関数を定義しています。
登録時と表示時の両方でこの関数を使い回します。LINE 040: タグをリストへ変換。カンマで区切った各タグの前後の空白を取り除き、空でないものだけをリストにまとめています。
余分な空白や空タグを除いてきれいな一覧を作ります。RUN 3/7: タグ正規化のnormalize_tagsを確認する。カンマ区切りの入力から前後空白と空要素が取り除かれるかを確認します。
CHECK 3/7: 途中実行に成功。['python', 'flask', 'web'] 空入力: [] RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 044: 起動時のテーブル準備。init_dbを呼び出して、アプリ起動と同時にテーブルを用意しています。これにより初回アクセスでもエラーにならず動作します。
RUN 4/7: 起動時のテーブル準備と件数を確認する。モジュール読み込みでinit_dbが実行され、現在の登録件数を数えられるかを確認します。CHECK 4/7: 途中実行に成功。
現在の登録件数: 0 RETURN 04: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 047: 画面テンプレートの定義開始。
PAGEという変数に画面のHTMLを三連引用符で書き始めています。この文字列をJinja2で描画して画面を表示します。LINE 048: HTML文書の宣言。
このファイルがHTML5の文書であることをブラウザに伝えています。ページの先頭に置く決まりの記述です。LINE 049: html要素と言語指定。
文書全体を囲むhtml要素を開き、lang属性で日本語ページであることを示しています。読み上げや翻訳の判断に役立ちます。LINE 050: head部分の開始。
ページの設定情報を書くhead要素を開いています。この中に文字コードやタイトルなどの情報を並べます。LINE 051: 文字コードの指定。
文字コードをUTF-8に指定しています。日本語などの文字が正しく表示されるようにするための設定です。LINE 052: 表示領域の設定。
viewportを設定し、スマートフォンなど画面幅に合わせて表示されるようにしています。レスポンシブ表示の基本となる記述です。LINE 053: ページタイトルの設定。
ブラウザのタブに表示されるタイトルを「ブックマーク管理アプリ」に設定しています。ページの内容が一目で分かります。LINE 054: スタイル定義の開始。
style要素を開き、ページの見た目を整えるCSSを書き始めています。この中に各要素の装飾を記述します。LINE 055: body全体のスタイル。
本文の書体や最大幅、余白、文字色をまとめて指定しています。全体が読みやすく中央寄せのレイアウトになります。LINE 056: 見出しのサイズ指定。
h1見出しの文字サイズを1.4remに設定しています。ページの主題を程よい大きさで目立たせます。LINE 057: フォームの下余白。
form要素の下に余白を付けています。登録フォームと検索フォームの間隔を空けて見やすくします。LINE 058: テキスト入力欄のスタイル。
テキスト入力欄に内側の余白と上下の間隔を設定しています。入力しやすく整った見た目になります。LINE 059: ボタンのスタイル。
ボタンに余白とマウスカーソルの形を指定しています。押せる要素であることが視覚的に分かりやすくなります。LINE 061: ブックマーク項目のスタイル。
一覧の各項目に枠線や角丸、余白を付けています。ブックマークがカードのように区切られて見やすくなります。LINE 062: タグ表示のスタイル。
タグを小さなラベル風に表示するための背景色や文字色、角丸を指定しています。本文と区別しやすい見た目にします。LINE 063: タグスタイルの続き。
前の行から続くタグの装飾で、内側の余白や右の間隔、文字サイズを設定しています。複数のタグが横に並んでも整って見えます。LINE 064: スタイル定義の終了。
style要素を閉じてCSSの記述を終えています。これでページ全体の見た目の設定が完了します。LINE 065: head部分の終了。
head要素を閉じています。ここまでで設定情報の記述が終わり、次から実際の表示内容に移ります。LINE 066: body部分の開始。
画面に表示される本文を囲むbody要素を開いています。ここから見出しやフォーム、一覧が並びます。LINE 067: ページ見出しの表示。
h1でアプリ名「ブックマーク管理アプリ」を表示しています。利用者が何のページかすぐ分かる見出しです。LINE 069: 登録フォームの開始。
新しいブックマークを登録するフォームを開いています。送信先はadd関数のURLで、method="post"でデータを送ります。LINE 070: タイトル入力欄。
ブックマークのタイトルを入力する欄です。requiredを付けているため未入力では送信できません。LINE 071: URL入力欄。
リンク先のURLを入力する欄です。size="40"で幅を広めに取り、requiredで必須入力にしています。LINE 072: タグ入力欄。
カンマ区切りでタグを入力する欄です。任意入力のためrequiredは付けていません。LINE 073: 登録ボタン。
フォームを送信する登録ボタンです。押すとpostでadd関数へ入力内容が送られます。LINE 074: 登録フォームの終了。
登録フォームを閉じています。ここまでが新規登録のための入力部分となります。LINE 076: 検索フォームの開始。
キーワード検索用のフォームを開いています。method="get"でindex関数へ検索語を送る仕組みです。LINE 077: 検索キーワード入力欄。
検索したい言葉を入力する欄です。value属性に前回の検索語queryを入れ、検索後も入力内容が残るようにしています。LINE 078: 検索ボタン。
入力したキーワードで検索を実行するボタンです。押すとgetで検索語がURLに付いて送られます。LINE 079: 検索フォームの終了。
検索フォームを閉じています。ここまでがキーワード検索の入力部分です。LINE 081: 一覧リストの開始。
ブックマークを並べる箇条書きのul要素を開いています。この中に各ブックマークが繰り返し表示されます。LINE 082: ブックマークの繰り返し開始。
Jinja2のforを使い、渡されたbookmarksを一件ずつ取り出しています。件数分だけ下の項目が繰り返し描画されます。LINE 083: 各ブックマークの項目。
一件分のブックマークを囲むli要素です。bookmark-itemクラスでカード風の見た目を適用しています。LINE 084: ブックマークのリンク表示。
b.urlをリンク先、b.titleを表示名にしたリンクを作っています。target="_blank"で別タブに開き、rel="noopener"で安全性に配慮しています。LINE 085: タグ表示領域の開始。
タグをまとめて表示するためのdivを開いています。この中にタグのラベルを並べていきます。LINE 086: タグの繰り返し開始。
b.tag_listからタグを一つずつ取り出すforループです。ブックマークが持つタグの数だけ表示を繰り返します。LINE 087: タグのラベル表示。
取り出した各タグをspanでラベル状に表示しています。tagクラスにより背景色付きの見やすい表示になります。LINE 088: タグ繰り返しの終了。
タグを1つずつ表示するための内側のforループをここで閉じています。1件のブックマークが持つすべてのタグを描画し終える区切りになります。LINE 089: タグ囲みdivの終了。
タグ一覧をまとめて囲んでいたdiv要素を閉じています。これでタイトルとタグの表示ブロックが完結します。LINE 090: ブックマーク項目の終了。
1件分のブックマークを表すli要素をここで閉じています。一覧の中の1行がこれで完成します。LINE 091: 一覧が空のときの分岐。
Jinja2のforに用意されたelse節で、bookmarksが空だった場合にのみ実行されます。表示するブックマークがないときの案内へ切り替わります。LINE 092: 該当なしの表示。
検索や一覧の結果が0件のときに、この行の案内メッセージを表示します。利用者に何も見つからなかったことを伝えます。LINE 093: 一覧繰り返しの終了。
ブックマークを1件ずつ描画していた外側のforループをここで閉じています。これで一覧部分の繰り返し処理が終わります。LINE 094: 一覧リストの終了。
ブックマーク一覧全体を囲んでいたul要素を閉じています。リスト表示のまとまりがここで完結します。LINE 095: 本文領域の終了。
ページの本文を表すbody要素を閉じています。画面に表示する中身がここまでで終わります。LINE 096: HTML文書の終了。
html要素を閉じてHTML文書全体の終わりを示しています。テンプレートの構造がこれで完成します。LINE 097: テンプレート文字列の終了。
三重引用符でPAGE変数に格納していたHTMLテンプレートの文字列をここで閉じています。この文字列を後で描画に使います。RUN 5/7: 画面テンプレートの用意を確認する。
PAGEに一覧の骨組みと該当なしの文言が含まれているかを確認します。CHECK 5/7: 途中実行に成功。一覧IDを含む: True 該当なし文言を含む: True RETURN 05: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 101: トップページのルート設定。@app.routeでURLの「/」にアクセスがあったとき、直後の関数を呼び出すよう対応付けています。
トップページの入り口になります。LINE 102: 一覧表示関数の定義。トップページの処理を担うindex関数を定義しています。
ここから一覧表示と検索の流れが始まります。LINE 103: 検索キーワードの取得。URLのクエリ文字列からqの値を取り出し、前後の空白をstripで取り除いています。
検索が指定されていなければ空文字になります。LINE 104: データベースへの接続。get_dbを呼び出してSQLiteへの接続を用意しています。
この接続を使ってブックマークを読み出します。LINE 105: 検索有無の分岐。検索キーワードが入力されているかどうかを判定しています。
値があれば絞り込み検索へ、なければ全件取得へ進みます。LINE 107: 部分一致パターンの作成。キーワードの前後に%を付けたLIKE検索用の文字列を組み立てています。
これで語句を含む項目を部分一致で探せます。LINE 108: 検索クエリの実行開始。conn.executeを呼び出して、絞り込み用のSQL文の実行を始めています。
この後に続く文字列がSQLの本体になります。LINE 109: 全カラムの取得指定。bookmarksテーブルからすべての列を取り出すSELECT文の前半を記述しています。
次の行の条件と組み合わせて1つのSQLになります。LINE 110: 検索条件の指定。タイトル・URL・タグのいずれかがキーワードに部分一致する行を対象にする条件です。
プレースホルダーを置き、あとから検索値を安全に差し込みます。LINE 111: 並び順の指定。idの降順で並べ替えるよう指定しています。
新しく登録したものほど上に表示される結果になります。LINE 112: 検索値の受け渡し。3つのプレースホルダーへ同じlikeパターンを順番に渡しています。
パラメータ化することでSQLインジェクションを防ぎ、安全に検索できます。LINE 113: 検索結果の取得。fetchallで一致したすべての行をまとめて取り出し、rowsに代入しています。
これが表示対象のブックマーク群になります。LINE 114: 検索なしの分岐。検索キーワードが空だった場合に実行される分岐です。
ここではすべてのブックマークを取得します。LINE 115: 全件取得の実行。条件を付けずに全ブックマークをidの降順で取得しています。
新しい順に並んだ全件がrowsに入ります。LINE 116: データベース接続の終了。必要なデータを読み終えたので、conn.closeで接続を閉じています。
使い終えた接続を確実に解放します。LINE 119: 表示用リストの準備。画面表示に整えたデータを入れるための空のリストを用意しています。
この後の繰り返しで1件ずつ追加していきます。LINE 120: 取得行の繰り返し。取り出したrowsを1行ずつ処理するためのループを開始しています。
各ブックマークに表示用の加工を施します。LINE 121: 行の辞書化。データベースの行オブジェクトをdictで辞書に変換しています。
項目を追加しやすい形に整える下準備です。LINE 122: タグ一覧の展開。カンマ区切りのタグ文字列をnormalize_tagsでリストに変換し、tag_listとして辞書に追加しています。
テンプレート側でタグを1つずつ表示できるようになります。LINE 123: 表示用リストへの追加。加工済みの1件を表示用リストbookmarksに追加しています。
全件を処理し終えると一覧データが完成します。LINE 125: テンプレートの描画。render_template_stringでPAGEテンプレートに一覧データと検索語を渡して描画しています。
この結果がブラウザへ返す画面になります。LINE 129: 登録用ルートの設定。URLの「/add」にPOSTでアクセスされたときだけ、直後の関数を呼び出すよう指定しています。
フォームからの登録を受け付ける入り口です。LINE 130: 登録処理関数の定義。ブックマークの登録処理を担うadd関数を定義しています。
ここから送信された内容の受け取りと保存が始まります。LINE 131: タイトルの受け取り。送信されたフォームからtitleの値を取り出し、前後の空白を取り除いています。
未入力のときは空文字になります。LINE 132: URLの受け取り。フォームからurlの値を取り出し、stripで前後の空白を整えています。
登録するリンク先の情報になります。LINE 133: タグの受け取り。フォームからtagsの値を取り出し、余分な空白を取り除いています。
あとで正規化してから保存します。LINE 135: 必須項目の確認。タイトルとURLの両方が入力されているかを判定しています。
どちらかが空なら保存せずに次へ進みます。LINE 137: タグの整形。normalize_tagsでタグを正規化し、カンマとスペースでつないで1つの文字列にまとめています。
データベースに保存する形式に整えます。LINE 138: データベースへの接続。get_dbを呼び出して保存用のSQLite接続を用意しています。
この接続を使ってデータを書き込みます。LINE 139: 登録クエリの実行開始。conn.executeを呼び出して、データを追加するINSERT文の実行を始めています。
続く行にSQLと値を記述します。LINE 140: INSERT文の記述。bookmarksテーブルのtitle・url・tagsへ値を追加するINSERT文です。
プレースホルダーの部分には、あとから安全に値を差し込みます。LINE 141: 登録値の受け渡し。3つのプレースホルダーへタイトル・URL・整形済みタグを順番に渡しています。
パラメータ化により安全に値を挿入できます。LINE 142: 実行呼び出しの終了。conn.executeの呼び出しを閉じて、INSERT文の実行指定を完了しています。
ここまでで登録内容がそろいます。LINE 143: 変更の確定。conn.commitで追加した内容をデータベースに確定させています。
これを実行しないと登録が保存されません。LINE 144: データベース接続の終了。保存が終わったのでconn.closeで接続を閉じています。
使い終えた接続を確実に片付けます。LINE 146: 一覧画面への転送。url_forでindexのURLを求め、redirectで一覧ページへ画面を移しています。
登録後に最新の一覧を表示させる流れです。RUN 6/7: 登録処理のリダイレクトを確認する。add関数へPOSTした後に一覧へリダイレクトされるかを確認します。
CHECK 6/7: 途中実行に成功。リダイレクトのステータス: 302 RETURN 06: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 150: 確認対象ルートの定義。動作確認したい画面のパスをROUTESにリストとしてまとめています。ここでは一覧ページの「/」を対象に指定しています。
RUN 7/7: 完成後の登録と一覧を確認する。登録した内容が一覧へ反映されるところまで通しで確認します。CHECK 7/7: 途中実行に成功。
一覧に含む: True RETURN 07: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。PREVIEW: 完成したブックマーク管理アプリを操作。
登録・検索・タグ表示を実際のブラウザ画面で確認します。確認1/4: トップページを表示して見出しを確認する。テストクライアントでトップページを取得し、見出しが表示されるかを確認します。
確認2/4: ブックマークを登録して一覧に表示する。登録フォームへの送信をPOSTで再現し、一覧へ追加されタグが並ぶかを確認します。確認3/4: キーワードで検索して絞り込む。
複数のブックマークを登録してからキーワード付きでアクセスし、一致分だけに絞られるかを確認します。確認4/4: 該当がないときのメッセージを確認する。登録されていないキーワードで検索し、該当なしのメッセージが表示されるかを確認します。
学習内容のまとめ。@app.routeでURLと処理関数を対応付けsqlite3.connectでDBファイルへ接続titleとurlの両方があるときだけ登録bookmarks.dbファイル1つで永続化 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
PythonのFlaskとSQLiteとは
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
マイクロフレームワークFlask
Flaskは、少ないコードでルーティングと画面表示を組み立てられるPythonのWebフレームワークです。データベースサーバやテンプレートエンジンを外部に用意しなくても、標準ライブラリと組み合わせるだけで小さなアプリが完成します。本教材では単一ファイルにルートとテンプレートをまとめており、処理の流れを追いやすくしています。
初めてWeb開発に触れる方でも、全体像をつかめるはずです。
ブックマーク管理アプリでFlaskが担う役割を実際のコードに対応させて挙げます
- @app.routeでURLと処理関数を対応付け
- render_template_stringでHTMLを返す
- request.formとrequest.argsで入力を受け取る
- redirectとurl_forで登録後に一覧へ戻す
- app.test_clientで画面の応答を検証
ファイル1つで永続化する標準ライブラリsqlite3
sqlite3は、追加のサーバを立てずにファイル1つでデータを保存できるPython標準のデータベースライブラリです。このアプリでは、bookmarks.dbというファイルへ登録内容をためていきます。接続やテーブル作成も数行で書けるため、学習の入り口として扱いやすい仕組みになっているのが特徴です。
SQL文を直接書きながら、データ操作の基本を体験できます。
このアプリでsqlite3をどう使って保存や検索を実現しているかを処理単位で挙げます
- sqlite3.connectでDBファイルへ接続
- row_factoryにsqlite3.Rowを設定
- CREATE TABLE IF NOT EXISTSでテーブル準備
- パラメータ化クエリでINSERTを実行
- commitとcloseで確定と後始末
Python・Flaskで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install Flask Jinja2 pysqlite3-binary
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install Flask Jinja2 pysqlite3-binary
- SQLite本体はpysqlite3-binaryを導入して用意します。これはSQLiteエンジンを同梱した配布パッケージなので、別途データベースサーバを立てなくても利用でき、アプリのコードはPython標準のsqlite3モジュール経由で接続します。Jinja2はFlaskの依存として入りますが、明示的にも導入しています。
- 初回起動時にアプリと同じフォルダへbookmarks.dbが自動作成されます。書き込み可能なディレクトリで実行してください。
PythonのFlaskとSQLiteで作るブックマーク管理アプリの要件定義
目的は、Flaskとsqlite3でブックマークを登録・一覧・検索でき、bookmarks.dbに保存した内容を単一ページで確認できるWebアプリを作ることです。
対象者として、Pythonの基本文法を学び終え、Flaskで小さなWebアプリのデータ保存と検索を実装する流れを学びたい人を想定しています。
完成物は、Flaskとsqlite3で作り、フォームから登録したブックマークを一覧表示しキーワードで検索できるWebアプリです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- タイトルとURLとタグの入力フォームを表示
- 登録フォームからPOSTでブックマークを追加
- bookmarksテーブルへINSERTして永続化
- 一覧をid降順で新しい順に表示
- キーワードでタイトルとURLとタグを部分一致検索
- タグをカンマ区切りで分割してバッジ表示
- 該当が0件のときに専用メッセージを表示
非機能要件
- bookmarks.dbファイル1つで永続化
- 書き込み可能な一時ディレクトリへDBファイルを配置
- パラメータ化クエリでSQLインジェクションを防止
- 起動時にCREATE TABLE IF NOT EXISTSでテーブル準備
- sqlite3.Rowでカラム名アクセスを有効化
- 単一ページに一覧とフォームを集約
- render_template_stringでJinja2描画
- titleとurlが両方あるときだけ登録
実装方針
今回はFlaskとSQLiteの基本動作を追いやすくするため、ブックマーク管理アプリ本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
ブックマーク管理アプリを安全に組み立てるための実装方針は次のとおりです。
- bookmarks.dbファイル1つで永続化
- 書き込み可能な一時ディレクトリへDBファイルを配置
- パラメータ化クエリでSQLインジェクションを防止
- 起動時にCREATE TABLE IF NOT EXISTSでテーブル準備
- sqlite3.Rowでカラム名アクセスを有効化
- 単一ページに一覧とフォームを集約
- render_template_stringでJinja2描画
- titleとurlが両方あるときだけ登録
完成と判断する条件
- 登録後にトップの一覧ページへリダイレクト
- 登録したブックマークが一覧に表示
- 検索キーワードで一致分だけに絞り込み
- 該当なしのとき専用メッセージを表示
- タグがカンマ区切りで分割表示
Flaskでブックマーク管理アプリを作る際の重要ポイント
このアプリの中核は、フォーム入力を受け取ってsqlite3へ保存し、一覧と検索結果をJinja2で描画する一連の流れにあります。登録ではtitleとurlがそろったときだけINSERTを実行し、未入力の送信は弾く仕組みです。検索ではキーワードをタイトル・URL・タグへLIKEで当てて絞り込んでいます。
タグについてはカンマ区切りを分割したうえで、画面ではタグバッジとして並べて表示する流れです。
登録から検索までの処理でつまずきやすい分岐や整形の要点を挙げます
- titleとurlの両方があるときだけ登録
- タグをnormalize_tagsで前後空白ごと除去
- 検索語を1つの%付き値で3カラムへ適用
- 一覧はid降順で新しい順に表示
- 該当0件でも専用メッセージを表示
normalize_tagsによるタグの正規化
normalize_tagsは、カンマで分割してから前後の空白を取り除き、空要素を捨てています。
保存するときはカンマとスペースで連結し、表示するときは再び分割してタグバッジへ展開します。
タグ入力を保存用と表示用に整える手順を示します
- カンマで分割して前後空白を除去
- 空要素を捨てて有効なタグだけ残す
- 保存時はカンマとスペースで連結
LIKEを使った部分一致検索
検索語の前後を%で挟んだパターンを作り、title・url・tagsの3カラムへORで当てています。
キーワードが空のときは条件を付けず、登録済みのブックマークを全件取得します。
キーワード検索で組み立てるSQL条件を示します
- 前後を%で挟んだ検索パターンを用意
- titleとurlとtagsをORで横断
- 空のキーワードでは全件を取得
一覧描画と該当なしの表示分岐
取得した行はtag_listを加えた辞書へ変換してから、テンプレートへ渡しています。
Jinja2のfor文にはelse節を付けているので、一覧が空のときは該当なしのメッセージを表示します。
テンプレートが一覧を描くときの分岐を示します
- 取得行をtag_list付きの辞書へ変換
- forループで各ブックマークを描画
- else節で該当なしメッセージを表示
Pythonでブックマーク管理アプリの完成コード
コードは大きく、DB接続の準備・テーブル作成・タグ正規化・画面テンプレートの4つに分かれます。まずFlaskアプリのapp変数とDBの保存先を決め、その後にDB接続やタグ処理のヘルパー関数を並べました。
SQLへ値を渡すときは文字列連結を避け、プレースホルダに差し込むパラメータ化クエリを使いました。記号混じりのURLでも壊れにくく、SQLインジェクションも防げます。
画面はrender_template_stringへ渡すPAGEという文字列テンプレートで、Jinja2が変数を埋め込んで描画します。登録フォームと検索フォームを同じページに配置しました。
このセクションの用語
- Jinja2
-
Flaskが標準で使うテンプレートエンジンです。{{ 変数 }}の記法でHTMLへ値を差し込みます。 - パラメータ化クエリ
- SQL文へ値を直接連結せず、プレースホルダ経由で安全に渡す書き方です。SQLインジェクション対策になります。
- row_factory
-
sqlite3が返す行の形式を決める設定です。sqlite3.Rowにするとカラム名で値を取り出せます。 - request.form
-
FlaskでPOST送信された値を受け取る入り口です。フォームのnameをキーに値を取り出します。 - ルーティング
- どのURLでどの関数を動かすかを対応づける仕組みです。
url_forは関数名からURLを組み立てます。
# 必要なライブラリの読み込み
import os
import sqlite3
import tempfile
from flask import Flask, redirect, render_template_string, request, url_for
# アプリケーションとデータベースの初期設定
app = Flask(__name__)
# 書き込み可能な一時ディレクトリにDBを置く(実行ディレクトリが読み取り専用でも起動できる)
DB_PATH = os.path.join(tempfile.gettempdir(), "bookmarks.db")
def get_db():
# SQLiteへ接続し、カラム名でアクセスできるようにする
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
# ブックマークを保存するテーブルを作成する(タグはカンマ区切りで保持)
conn = get_db()
conn.execute(
"""
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL,
tags TEXT NOT NULL DEFAULT ''
)
"""
)
conn.commit()
conn.close()
def normalize_tags(raw):
# カンマ区切りのタグ入力を正規化してリストにする
return [t.strip() for t in raw.split(",") if t.strip()]
# 起動時にテーブルを準備する
init_db()
# 画面テンプレート(Jinja2で描画)
PAGE = """
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ブックマーク管理アプリ</title>
<style>
body { font-family: sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; color: #222; }
h1 { font-size: 1.4rem; }
form { margin-bottom: 1.2rem; }
input[type=text] { padding: .4rem; margin: .2rem 0; }
button { padding: .4rem .9rem; cursor: pointer; }
#bookmark-list { list-style: none; padding: 0; }
.bookmark-item { border: 1px solid #ddd; border-radius: 6px; padding: .6rem .8rem; margin-bottom: .5rem; }
.tag { display: inline-block; background: #eef; color: #337; border-radius: 4px;
padding: .1rem .5rem; margin-right: .3rem; font-size: .8rem; }
</style>
</head>
<body>
<h1>ブックマーク管理アプリ</h1>
<form id="add-form" method="post" action="{{ url_for('add') }}">
<div><input type="text" id="bm-title" name="title" placeholder="タイトル" required></div>
<div><input type="text" id="bm-url" name="url" placeholder="URL" size="40" required></div>
<div><input type="text" id="bm-tags" name="tags" placeholder="タグ(カンマ区切り)" size="40"></div>
<button id="add-bookmark" type="submit">登録</button>
</form>
<form id="search-form" method="get" action="{{ url_for('index') }}">
<input type="text" id="search-input" name="q" value="{{ query }}" placeholder="キーワード検索">
<button id="search-btn" type="submit">検索</button>
</form>
<ul id="bookmark-list">
{% for b in bookmarks %}
<li class="bookmark-item">
<a href="{{ b.url }}" target="_blank" rel="noopener">{{ b.title }}</a>
<div>
{% for tag in b.tag_list %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
</li>
{% else %}
<li class="bookmark-item">該当するブックマークはありません。</li>
{% endfor %}
</ul>
</body>
</html>
"""
# 一覧表示とキーワード検索
@app.route("/")
def index():
query = request.args.get("q", "").strip()
conn = get_db()
if query:
# タイトル・URL・タグをLIKEで部分一致検索する
like = f"%{query}%"
rows = conn.execute(
"SELECT * FROM bookmarks "
"WHERE title LIKE ? OR url LIKE ? OR tags LIKE ? "
"ORDER BY id DESC",
(like, like, like),
).fetchall()
else:
rows = conn.execute("SELECT * FROM bookmarks ORDER BY id DESC").fetchall()
conn.close()
# タグ文字列を表示用のリストに展開する
bookmarks = []
for row in rows:
item = dict(row)
item["tag_list"] = normalize_tags(row["tags"])
bookmarks.append(item)
return render_template_string(PAGE, bookmarks=bookmarks, query=query)
# ブックマークの登録(SQLiteへ永続化)
@app.route("/add", methods=["POST"])
def add():
title = request.form.get("title", "").strip()
url = request.form.get("url", "").strip()
tags = request.form.get("tags", "").strip()
if title and url:
# タグを正規化し、パラメータ化クエリでINSERTする
tag_text = ", ".join(normalize_tags(tags))
conn = get_db()
conn.execute(
"INSERT INTO bookmarks (title, url, tags) VALUES (?, ?, ?)",
(title, url, tag_text),
)
conn.commit()
conn.close()
return redirect(url_for("index"))
# 動作確認したい画面
ROUTES = ["/"]
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
Flaskのapp変数とDB保存先の用意
app = Flask(__name__)
# 書き込み可能な一時ディレクトリにDBを置く(実行ディレクトリが読み取り専用でも起動できる)
DB_PATH = os.path.join(tempfile.gettempdir(), "bookmarks.db")Flask(__name__)でアプリ本体となるappを作ります。DBはtempfile.gettempdir()が返す一時フォルダに置き、実行場所が書き込み不可でも起動できるようにしています。
get_dbでsqlite3に接続する
def get_db():
# SQLiteへ接続し、カラム名でアクセスできるようにする
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return connsqlite3.connectでDBファイルへ接続します。row_factoryにsqlite3.Rowを設定すると、取得した行をb.urlのようにカラム名で読めるようになり、テンプレートが書きやすくなります。
CREATE TABLE IF NOT EXISTSで表を用意
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL,
tags TEXT NOT NULL DEFAULT ''
)ブックマークを保存するbookmarksテーブルを定義します。IF NOT EXISTSを付けているので、すでに表がある2回目以降の起動でもエラーにならず、そのまま使えます。
normalize_tagsでタグを正規化する
def normalize_tags(raw):
# カンマ区切りのタグ入力を正規化してリストにする
return [t.strip() for t in raw.split(",") if t.strip()]入力されたタグ文字列をカンマで分割し、前後の空白をstripで除きます。空の要素は捨てるので、「python, ,web」のような入力でも余計な空タグが残りません。
起動時にinit_dbを呼び出す
# 起動時にテーブルを準備する
init_db()アプリ読み込みの最後でinit_db()を1回実行し、テーブルを先に用意します。これを忘れると最初のクエリで表が無いと言われて止まるため、起動時の準備が重要です。
url_forで登録フォームの送信先を指定
<form id="add-form" method="post" action="{{ url_for('add') }}">
<div><input type="text" id="bm-title" name="title" placeholder="タイトル" required></div>
<div><input type="text" id="bm-url" name="url" placeholder="URL" size="40" required></div>登録フォームはmethod="post"で/addへ送ります。送信先をurl_for('add')で組み立てるので、ルートのパスを変えてもテンプレートを直さずに済みます。
参考:
©Python 3 documentation — sqlite3Instead, use the DB-API's parameter substitution. Put ? as a placeholder wherever you want to use a value, and then provide a tuple of values as the second argument of the cursor's execute() method.
ブックマーク登録と検索をPythonで確認
ローカルでアプリを起動し、外部通信なしで動作を確かめました。フォームにタイトル・URL・タグを入力して登録すると、一覧へそのまま反映されます。
続いて検索フォームにキーワードを入れると、タイトル・URL・タグを対象にした部分一致で候補が絞り込まれました。登録から検索までの一連の操作が、期待どおりに動くと確認できました。
記事執筆時の初期データは0件で、空の一覧から1件ずつ登録して増やす流れです。最初の1件を追加した瞬間に、リストへ項目が現れる様子を確認できています。
このセクションの用語
- 部分一致検索
- キーワードが一部に含まれる行を探す検索です。SQLの
LIKEで前後を%で囲んで実現します。 - バッジ
- 小さなラベル状の表示です。ここではタグを角丸の枠で囲って一覧に並べています。
画面キャプチャで確認できた操作は次のとおりです。
- タイトル・URL・タグを送信すると一覧に項目が追加される
- 登録したタグが各項目にバッジとして表示される
- キーワードでタイトル・URL・タグを部分一致で絞り込める



PythonのFlaskとSQLite利用時に起きやすいエラーと対処法
ここではFlaskとsqlite3の初心者がつまずきやすい実行エラーを、原因と対処に分けてまとめます。実際に必ず起きるものではなく、一般に遭遇しやすい例として押さえておくと安心です。
このセクションの用語
- OperationalError
-
sqlite3が操作中の問題を検知して投げる例外です。表が無い、ファイルを開けない、などで起きます。 - BuildError
-
Flaskがurl_forでURLを組み立てられないときの例外です。指定した関数名が見つからないと発生します。
| エラー例 | 原因 | 対処 |
|---|---|---|
| sqlite3.OperationalError: no such table | init_db()前にクエリを実行した | 起動時にinit_dbを呼びテーブルを作る |
| unable to open database file | DBの置き場所に書き込み権限がない | tempfileなど書き込み可能な場所を指定 |
| jinja2 TemplateSyntaxError | {{ }}や{% %}の閉じ忘れ | 波括弧の対応を見直して修正 |
| werkzeug BuildError for 'add' | url_forの名前がルート関数と不一致 | @app.route関数名と引数を揃える |
| sqlite3.ProgrammingError | 別スレッドで同じ接続を使い回した | リクエストごとにget_dbで接続を作る |
ブックマーク管理アプリで注意したい点
小さなアプリでも、DBの準備タイミングやテンプレートの記法で手が止まりがちです。特に起動時のinit_db()忘れと、フォームのname属性の付け忘れは初心者に多い落とし穴です。
フォームの各入力にはnameを必ず付けます。nameが無いと送信データに含まれず、サーバ側でrequest.formから値を取り出せません。
ポイントとしては、テーブル準備とフォーム設計の初歩を押さえることが大切です。
テーブル準備:起動時にinit_dbを実行
フォーム:入力にname属性を付与
接続管理:リクエストごとに接続を作る
ブックマーク管理アプリを応用できる場面
この作りは、URLとタグで情報を分類したい場面に幅広く応用できます。単一ファイルで動くので、個人利用から小さな共有まで手を広げやすいです。
| 使える場面 | 具体的な使い方 |
|---|---|
| 個人の学習リンク集 | 教材やドキュメントのURLをタグ別に登録し、検索窓で読み返す資料をすぐ絞り込む |
| チームの共有リンク管理 | 社内ツールや手順書のURLをタグで分類し、検索から必要なページへ素早く到達する |
| 調査メモの整理 | 調べ物で見つけた記事をタグ付きで蓄積し、後からテーマ単位で一覧して振り返る |
| ブックマーク移行の下地 | 既存のお気に入りをtitle・url・tagsで取り込み、自作アプリで一元管理する |
ブックマーク管理アプリ開発のまとめ
PythonのFlaskとsqlite3だけで、登録・一覧・検索がそろったブックマーク管理アプリを実際に動かせました。追加のサーバを使わず、ファイル1つで永続化できる手軽さが魅力です。
起動時のテーブル作成、パラメータ化クエリ、Jinja2テンプレートという基本を押さえれば、同じ型で別のデータ管理アプリにも応用できます。まずは手元で1件登録し、流れを体で覚えるのがおすすめです。
小さく作って動かした経験は、次に別のWebアプリへ挑むときの確かな土台になります。
このセクションの用語
- CRUD
- 作成・読み取り・更新・削除という4つの基本操作の総称です。データ管理アプリの土台になります。
さらに発展させる具体的なアイデアです。
- 各項目に編集・削除ボタンを付けてCRUDを完成させる
- タグをクリックすると同じタグの一覧へ飛べるようにする
- 登録日時のカラムを追加して新着順に並べ替える
参考にした一次情報
- ^ Flask公式ドキュメント. https://flask.palletsprojects.com/en/stable/, (参照26-07-28).
- ^ Python sqlite3 — SQLiteデータベース (公式). https://docs.python.org/ja/3/library/sqlite3.html, (参照26-07-28).
- ^ Jinja2公式ドキュメント. https://jinja.palletsprojects.com/en/stable/, (参照26-07-28).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。
ITやプログラミングに関するコラム
【Python】FastAPIで料金プラン見積もりシミュレーターを作ってみた
【Python】pandasとmatplotlibで在庫データのABC分析と構成比を可視化してみた
【Python】Flaskで社内FAQをカテゴリ検索できるWebアプリを作ってみた
【Python】argparseでJSON整形・構文検証・キー検索CLIを試してみた
【Python】NumPyとmatplotlibでモンテカルロ法による円周率推定と収束過程の可視化を試してみた
【Python】Playwrightでスクレイピングを試してみた
【CSS】notで複数の件を除外する方法
【Git】remote設定を変更する方法
【VBA】コメントアウトを設定する方法
x86とx64の違いを分かりやすく解説
ITやプログラミングに関するニュース
VercelがAI GatewayにSeedream 5.0 Proを追加、AI SDKのモデル指定で画像生成と編集が可能に
AWSがAmazon LocationのPlaces APIを強化、住所表記の指定と移動手段別の検索が可能に
VercelがトレースにTree・Waterfallビューを追加、ログ画面で処理の階層と所要時間を確認可能に
Googleがエージェント評価の再考を提唱、難易度を情報量で測るDiscovery Benchを解説
Google CloudがCloud Runサンドボックスを公開プレビューで提供、サービスヘルスは一般提供に
Google Cloud EMEAが英国金融の重要第三者に指定、イングランド銀行・PRA・FCAの直接監督下に
AWS DMS Schema ConversionがSQL Serverのオフライン変換に対応、ソースDBへ接続せずスキーマを変換可能に
EC2 G7インスタンスが米国東部(バージニア北部)で利用可能に、G6比でAI推論性能が最大4.6倍
SageMaker HyperPodが継続プロビジョニングでのAMIベース構成に対応、S3のスクリプト管理なしでSlurmクラスターを作成可能に
AWSがEMR on EKSでSparkトラブルシューティングエージェントに対応、失敗ジョブの原因分析を自然言語で依頼可能に
