11-Flask

Flask(Part.19)| 【画像のアップロードと利用(解説編)】

python| まとめ | 現役エンジニア&プログラミングスクール講師「python」のまとめページです。pythonに関して抑えておきたい知識や文法やにについて記事をまとめています。まとめページの下部には「おすすめの学習書籍」「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中...

目標

  • 画像のアップロードを行うプログラムを理解できる。
  • アップロードした画像を利用するプログラムを理解できる。

Flaskで画像を利用する方法

アプリケーションの追加と修正されたプログラム

商品モデルクラス(models.py)

# 商品モデル(テーブル)の定義
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    price = db.Column(db.Integer, nullable=False)
    category_id = db.Column(
        db.Integer, db.ForeignKey("category.id"), nullable=True
    )  # カテゴリID
    image_url = db.Column(db.String(255), nullable=True)  # 画像のURLを保存するカラム

    def __repr__(self):
        return f"<Product {self.id} - {self.name} - {self.price}>"

models.pyファイルの商品モデルクラスに画像のURLを保存するカラム用の属性を追加しました。

  • image_url は商品の画像URLを保存する文字列型のカラム(最大255文字)。
  • nullable=True なので、画像がなくても登録可能。

商品フォームクラスの修正(forms.py)

forms.pyファイルで、FileFieldとFileAllowedが利用できるように追加のライブラリをインポートしました。

from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed

これは Flask-WTF を使用してフォームを作成し、ファイルのアップロードを制限するためのものです。

続けて、forms.pyファイルの商品追加フォームクラスで商品画像用の項目を追記しました。

class AddProductForm(FlaskForm):
    product_name = StringField("商品名", validators=[DataRequired()])
    product_price = IntegerField(
        "価格",
        validators=[
            DataRequired(),
            NumberRange(min=1, message="価格は1以上である必要があります"),
        ],
    )
    category = SelectField("カテゴリー", coerce=int)  # カテゴリー選択フィールド
    product_image = FileField(
        "商品画像",
        validators=[
            FileAllowed(
                ["jpg", "png", "jpeg", "gif"], "画像ファイルのみ許可されています"
            )
        ],
    )

FileAllowed は、ファイルのアップロード時に許可するファイルの種類を制限 するためのバリデーションを提供します。
FileField は ファイルアップロード用のフォームフィールド を提供し、ユーザーがファイルを選択できる入力欄 を作成します。
バリデーション(FileAllowed など)と組み合わせて使用することで、不正なファイルのアップロードを防ぎ、安全なアップロードを実現 できます。
例えば、FileAllowed([“jpg”, “png”, “gif”]) と指定すると、画像ファイルのみ許可し、それ以外の拡張子を拒否します。

app.pyの修正

app.pyファイルでは、Flaskアプリケーションでファイルアップロード機能を実装するための基本的なモジュールをインポートし、商品の追加・編集のルートに画像用の項目を追記しました。

import os
from itertools import groupby

from flask import Flask, flash, redirect, render_template, request, url_for
from werkzeug.utils import secure_filename

from apps.productsapp.forms import AddProductForm, CategoryForm
from apps.productsapp.models import Category, Product, db  # models.pyからインポート
import os

OS(オペレーティングシステム)関連の処理を行う標準ライブラリです。
ファイルパスの操作や環境変数の取得などで使用されます。

UPLOAD_FOLDER = os.path.join(os.getcwd(), “uploads”) # 現在のディレクトリに”uploads”フォルダを作成

  • os.getcwd() → 現在のディレクトリを取得。
  • os.path.join() → 異なるOSでも正しいパスの書式で結合。
from werkzeug.utils import secure_filename

アップロードされたファイルのファイル名を安全な形式に変換する関数です。

ユーザーが ../../etc/passwd のような 危険なファイル名を指定した場合、不正なパスアクセスを防ぎます。また、”よくないファイル名.sh” のようなファイルの名前を適切に処理してくれます。

file = request.files[“file”]
filename = secure_filename(file.filename) # 安全なファイル名に変換
file.save(os.path.join(UPLOAD_FOLDER, filename)) # 指定フォルダに保存

secure_filename(file.filename) によって、特殊文字やディレクトリ操作を防止します。

次は画像ファイルのアップロード用のフォルダの準備を行っています。この記述はimport文のすぐ後、アプリケーションの初期化「app = Flask(__name__)」の前に記述します。

# パスの設定
UPLOAD_FOLDER = os.path.join(os.getcwd(), "apps", "productsapp", "static", "images")
# ディレクトリが存在しない場合に作成
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)
  • os.getcwd() → 現在の作業ディレクトリ(実行中のアプリのフォルダ)を取得します。
  • os.path.join() → パスを適切に結合します。
  • “apps/productsapp/static/images” → 画像を保存するフォルダのパスを指定します。
  • os.path.exists(UPLOAD_FOLDER) → フォルダがすでに存在するかチェックします。
  • os.makedirs(UPLOAD_FOLDER) → フォルダが存在しない場合、新しく作成します。

画像の形式やサイズの設定を行います。

# アップロードの設定
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER  # 画像保存先ディレクトリ
app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif"}  # 許可する画像拡張子
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 最大アップロードサイズ(16MB)

app.config[“MAX_CONTENT_LENGTH”] = 16 * 1024 * 1024 # 最大アップロードサイズ(16MB)このように記述すると最大アップロードサイズを指定できます。このようにすることで、サーバーへの負荷を軽減できます。

数値 ✕ 1024 ✕ 1024の計算式で、数値MBを指定します。
例:8 ✕ 1024 ✕ 1024 は 8MB を指定したことになります。

アップロードされたファイルが許可される拡張子のものかをチェックする関数を定義します。この記述は全てのルートよりも前に記述します。

# アップロードが許可されている拡張子かをチェック
def allowed_file(filename):
    return (
        "." in filename
        and filename.rsplit(".", 1)[ 1 ].lower() in app.config["ALLOWED_EXTENSIONS"]
    )

rsplit(“.”, 1)[ 1 ] → ファイル名の最後の . 以降の部分(拡張子)を取得
lower() → 大文字・小文字を区別せずにチェック

ルート:商品追加ページの編集

# ルート: 商品追加ページ
@app.route("/product/add", methods=["GET", "POST"])
def add_product():
    form = AddProductForm()
    form.set_category_choices()

    if form.validate_on_submit():
        # フォームのデータを取得
        name = form.product_name.data
        price = form.product_price.data
        category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files.get("product_image")
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)  # ファイル名を安全に処理
                image_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
                image.save(image_path)  # 画像を保存

                # 画像のパスをデータベースに保存
                image_url = f"images/{filename}"  # アップロードされた画像のURL
            else:
                image_url = "images/noimage.png"
        else:
            image_url = "images/noimage.png"

        # 商品の追加
        new_product = Product(
            name=name, price=price, category_id=category_id, image_url=image_url
        )
        db.session.add(new_product)
        db.session.commit()

        return redirect(url_for("product_list"))

    return render_template("product_form.html", form=form, title="商品追加フォーム")
if “product_image” in request.files:

フォームから送信されたデータに “product_image” というキー(ファイル)が含まれているかを確認します。ファイルがアップロードされていない場合、この処理をスキップします。

image = request.files.get(“product_image”)

“product_image” というキーのファイルを取得します。.get() を使うことで、キーが存在しない場合でも None が返り、エラーを回避できます。

if image and allowed_file(image.filename):

image が None ではなく、ファイルが正常に取得できているか確認します。allowed_file(image.filename) で 拡張子が許可されているかチェックします。
allowed_file() は、アップロードが許可されている拡張子かをチェック関数でした。

filename = secure_filename(image.filename)

ユーザーがアップロードしたファイルのファイル名を安全なものに変換します。
secure_filename() によって 特殊文字やディレクトリ操作(../../ など)を防ぎます。

image_path = os.path.join(app.config[“UPLOAD_FOLDER”], filename)

os.path.join(app.config[“UPLOAD_FOLDER”], filename) で 保存パスを作成します。

image.save(image_path)

image.save(image_path) で 実際にファイルを保存します。

f”images/{filename}”

画像のURLを 相対パス形式 で作成します。

データベースに保存する画像のパスを設定します。

画像が存在しない場合はnoimage.pngを指定してデータベースに保存します。

image_url = f”images/{filename}” # アップロードされた画像のURL
else:
image_url = “images/noimage.png”
else:
image_url = “images/noimage.png”

ルート:商品編集ページの編集

# ルート: 商品を編集
@app.route("/product/edit/<int:product_id>", methods=["GET", "POST"])
def edit_product(product_id):
    product = Product.query.get_or_404(product_id)
    form = AddProductForm(obj=product)
    form.set_category_choices()

    if form.validate_on_submit():
        product.name = form.product_name.data
        product.price = form.product_price.data
        product.category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files["product_image"]
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)
                image_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)

                # もしすでに画像があれば削除
                if product.image_url and product.image_url != "images/noimage.png":
                    old_image_path = os.path.join(
                        app.config["UPLOAD_FOLDER"], os.path.basename(product.image_url)
                    )
                    if os.path.exists(old_image_path):
                        os.remove(old_image_path)

                # 新しい画像を保存
                image.save(image_path)
                product.image_url = f"images/{filename}"  # 新しい画像URLを保存

        db.session.commit()
        flash("商品情報を更新しました", "success")
        return redirect(url_for("product_list"))

    return render_template(
        "product_form.html", form=form, title="商品編集", product=product
    )
if “product_image” in request.files:

フォームから送信されたデータに “product_image” というキーが含まれているかを確認します。

image = request.files[“product_image”]

もし存在すれば、 request.files[“product_image”] で画像ファイルを取得します。

if image and allowed_file(image.filename):

画像が正常に取得され、かつ allowed_file 関数で指定した拡張子(例:png, jpg など)が許可されているかチェックします。

filename = secure_filename(image.filename)

secure_filename() を使用して、ユーザーがアップロードしたファイル名を安全な形式に変換します。これによって、特殊文字やディレクトリ操作を防ぎます。

image_path = os.path.join(app.config[“UPLOAD_FOLDER”], filename)

アップロードされたファイルのパスを取得します。

if product.image_url and product.image_url != “images/noimage.png”:

product.image_url が空、かつ noimage.png でないかをチェックします。

old_image_path = os.path.join(
app.config[“UPLOAD_FOLDER”], os.path.basename(product.image_url)
)

もし 既存の商品画像があれば(product.image_url が空でなく、かつ noimage.png でない場合)、その画像を削除します。os.path.basename(product.image_url) で既存の画像名を取り出し、保存先パスを作成します。

if os.path.exists(old_image_path):

既存の画像名を取り出し、保存先パスを作成したものが、存在しているか(画像が存在するか)をチェックします。

os.remove(old_image_path)

os.remove(old_image_path) でその画像ファイルを削除します。

image.save(image_path)

新しくアップロードされた画像を指定のパス(image_path)に保存します。

product.image_url = f”images/{filename}”

商品オブジェクト(product)の image_url 属性に、新しく保存した画像の相対パスを設定します。

product_form.htmlの編集

formタブのリクエストに「add_product」もしくは「edit_product」が選択できるようにしました。このようにすることで、追加の時には「add_product」、編集の時は「edit_product」が選択されるようになりました。

<form method="POST" action="{{ url_for('edit_product', product_id=product.id) if product else url_for('add_product') }}"
    enctype="multipart/form-data">

product_form.htmlに次の項目を追加しました。

<label for="product_image">商品画像</label>
<!-- 既存の画像がある場合は表示 -->
{% if product.image_url %}
    <div>
        <img src="{{ url_for('static', filename=product.image_url) }}" alt="現在の画像" style="width: 100px; height: 100px;">
        <p>※新しい画像を選択すると上書きされます</p>
    </div>
{% endif %}
<label for="product_image" class="file-upload-label">画像を選択</label>
<input type="file" id="product_image" name="product_image" accept="image/*">

フォームフィールド(この場合は画像選択フィールド)のラベルを作成します。このようにすると、クラス名を利用してスタイルを適用しやすくなります。

for=”product_image” によって、ラベルをクリックすると 画像選択フィールド(タグ)にフォーカスが移るようになります。

{% if product.image_url %}:では、product.image_url が存在する場合、このブロックが実行されます。product.image_url は、画像の相対パスであると仮定しています(例: “images/photo.jpg”)。

url_for(‘static’, filename=product.image_url) は、Flaskの static フォルダ内の画像へのURLを生成します。具体的には、static/images/photo.jpg のように、画像のURLを自動的に生成します。

style=”width: 100px; height: 100px;”では画像を 100×100 ピクセルにリサイズしています。

product_form.htmlのformの閉じタグの下にJavaScriptを追記します。このプログラムは選択したファイルを表示するプログラムです。選択した画像ファイルのファイル名をボタンへ反映します。

<script>
    document.getElementById("product_image").addEventListener("change", function () {
        const fileName = this.files.length > 0 ? this.files[0].name : "画像を選択";
        document.querySelector(".file-upload-label").textContent = fileName;
    });
</script>

products.htmlの編集

products.htmlで商品の名前と価格を表示する部分よりも前に次の内容を入力しました。

       <!-- 商品画像の表示 -->
        {% if product.image_url %}
        <img src="{{ url_for('static', filename=product.image_url) }}" alt="{{ product.name }}"
            style="width: 50px; height: 50px; margin-right: 10px; border-right: 2px;">
        {% endif %}

スタイルシートの編集

画像を選択するボタンにスタイルを適用しました。

/* ファイル選択ボタンを隠す */
input[type="file"] {
    display: none;
}

/* カスタムアップロードボタンのスタイル */
.file-upload-label {
    display: inline-block;
    padding: 10px 20px;
    background-color: #f2491f;
    color: white;
    text-align: center;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

/* ホバー時のスタイル */
.file-upload-label:hover {
    background-color: #45a049;
}

今回は以上になります。

「python」おすすめ書籍 ベスト3 | 現役エンジニア&プログラミングスクール講師「python」の学習でお勧めしたい書籍をご紹介しています。お勧めする理由としては、考え方、イメージなどを適切に捉えていること、「生のpython」に焦点をあてて解説をしている書籍であることなどが理由です。勿論、この他にも良い書籍はありますが、特に質の高かったものを選んで記事にしています。ページの下部には「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中。...

ブックマークのすすめ

「ほわほわぶろぐ」を常に検索するのが面倒だという方はブックマークをお勧めします。ブックマークの設定は別記事にて掲載しています。

「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】について解説している記事です。削除方法も掲載しています。...
【パソコン選び】失敗しないための重要ポイント | 現役エンジニア&プログラミングスクール講師【パソコン選び】失敗しないための重要ポイントについての記事です。パソコンのタイプと購入時に検討すべき点・家電量販店で見かけるCPUの見方・購入者が必要とするメモリ容量・HDDとSSDについて・ディスプレイの種類・バッテリーの持ち時間や保証・Officeソフト・ウィルス対策ソフトについて書いています。...
RELATED POST
11-Flask

Flask(Part.9)| 【Flaskでデータベースを利用する方法(2)解説】

2025年3月2日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site
11-Flask

Flask(Part.10)| 【アプリケーション名のリファクタリング】

2025年3月3日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site