Djangoで、ファイルアップロード

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月11日12:43に更新(約2日前)
2018年11月26日22:56に作成(約17日前)

概要

Djangoでファイルをアップロードする方法についてを紹介していきます。

これはかなりパターンが多く、大きく分けて単一ファイルのアップロード複数ファイルのアップロードの2つがあり、そしてモデルを利用するか否かでも変わります。

さらに、<input type="file" multiple>といったHTMLの少し特殊な要素を使う場合も考えられます。

まず、メディアファイルを扱うための設定をしておきましょう。

settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

urls.py

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

単一ファイルのアップロード

モデルを利用しない

アップロードした後に、そのファイルのURLだけ教えてくれれば良いというケースがあります。そのような場合、わざわざモデルを定義しなくても実現できます。

forms.py。アップロード用のフォームを定義してみます。

from django import forms
from django.core.files.storage import default_storage


class UploadForm(forms.Form):
    file = forms.FileField(label='ファイル')

    def save(self):
        upload_file = self.files['file']
        file_name = default_storage.save(upload_file.name, upload_file)
        return default_storage.url(file_name)

これはあくまで、モデルフォームではなく通常のフォームです。save()メソッドは上書きした訳ではありませんforms.Formにはsave()メソッドはなく、保存処理をするメソッドの名前として、そしてモデルフォームと似たような扱いができるようにsave()という名前にしているだけです。upload()とかそういう名前にしても大丈夫です。

self.files['file']ですが、これはアップロードされたファイルオブジェクトの取得です。フォームをインスタンス化するとき、form = MyForm(request.POST, files=request.FILES)のようにしますが、フォーム側からはself.filesrequest.FILESににアクセスできます。

もし今回のようにフォームを定義するのが面倒だった場合は、ビュー側でrequest.FILES['file']とすれば大体同じ感じで処理できます。

ちなみにですが、self.dataとするとrequest.GETrequest.POSTにもアクセスできます。

以下の部分は、ファイルの保存を行い、そのファイルURLのパスを返しています。/media/1_CIGg8bv.pngみたいな文字列を返します。ファイル名が被ったら、似たような名前を勝手につけてくれます。

        file_name = default_storage.save(upload_file.name, upload_file)
        return default_storage.url(file_name)

ビューです。

from django.views import generic
from .forms import UploadForm


class Upload(generic.FormView):
    form_class = UploadForm
    template_name = 'app/upload.html'

    def form_valid(self, form):
        download_url = form.save()
        context = {
            'download_url': download_url,
            'form': form,
        }
        return self.render_to_response(context)

フォームのsave()メソッドでファイルURLのパスが帰るので、それを取得してテンプレートへ渡しています。

テンプレートは、ちょっと適当に作りました。upload.htmlは以下のようにしておきます。

    <form action="" method="POST" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

    {% if download_url %}
        <a href="{{ download_url }}">ダウンロード</a>
    {% endif %}

enctype="multipart/form-data"を忘れないようにしましょう。

ファイルのフルなURLを返す必要がある場合は、HttpResponseとして返す例テンプレートでフルURLにする例も見てください。

モデルを利用する

アップロードされたファイルを管理したくなったら、モデルを利用しましょう。

from django.db import models


class UploadFile(models.Model):
    file = models.FileField('ファイル')

    def __str__(self):
        return self.file.url

forms.py。モデルフォームを利用するのが簡単でしょう。

from django import forms
from .models import UploadFile


class UploadModelForm(forms.ModelForm):

    class Meta:
        model = UploadFile
        fields = '__all__'

そしてビューです。特に言うこともないでしょう。

from django.views import generic
from .forms import UploadModelForm
from .models import UploadFile


class Upload(generic.CreateView):
    model = UploadFile
    form_class = UploadModelForm
    success_url = '/'

success_urlをテキトウにしていますが、reverse_lazy()等を使ってアップロードファイルの一覧ビューを指定したり、get_success_url()メソッドを上書きして更新ページにリダイレクトさせると良いでしょう。

uploadfile_form.html

    <form action="" method="POST" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

今回テンプレートへはファイルURLの表示を行っていません。アップロードしたファイルURLを確認したい場合は管理画面に行くか、自作のテンプレートならばモデルインスタンスのファイルorイメージフィールド名.urlで、URLのパス部分.../media/1_CIGg8bv.pngみたいな文字列が取得できます。今回の例ならば、モデルインスタンス.file.url です。

複数ファイルのアップロード

ファイルを複数アップロードしたくなると、少し複雑になってきます。

フォームセット(モデルなし)

Djangoで、フォームセットを使うシリーズでは主にモデルフォームセット、インラインフォームセットを紹介しました。しかし、モデルに紐づかない通常のフォームセットというのも作ることができます。

まずですが、以下のようなフォームを定義しておきます。

from django import forms
from django.core.files.storage import default_storage


class UploadForm(forms.Form):
    file = forms.FileField(label='ファイル')

    def save(self):
        # フォームセットからは、ここのsaveは呼ばれないよ
        upload_file = self.files['file']
        file_name = default_storage.save(upload_file.name, upload_file)
        return default_storage.url(file_name)


class BaseUploadFormSet(forms.BaseFormSet):

    def save(self):
        # ['/media/1.png', '/media/2.png']のようなファイルのURLが入ったリストになる
        url_list = []

        # これはrequest.FILESの全てを取り出すので、他フォームでアップロードされたファイルも取得する。
        # 確実にしたいならば、self.files.items() 等でキーも取り出しつつ
        # そのキー名にfile(UploadFormのフィールド名)が含まれているか、等を確認しましょう。
        for upload_file in self.files.values():
            file_name = default_storage.save(upload_file.name, upload_file)
            file_path = default_storage.url(file_name)
            url_list.append(file_path)
        return url_list


UploadFormSet = forms.formset_factory(UploadForm, formset=BaseUploadFormSet, extra=5)

上で説明した単一ファイルのアップロード モデルなしで、UploadFormという、save()の呼び出しでアップロードファイルのURLを返す(モデルフォームじゃない)通常のフォームを作成しました。それのフォームセットです。

フォームセットオブジェクトはforms.formset_factory()で作るのですが、今回はフォームセットにファイルの保存処理を定義したいので、BaseUploadFormSetというクラスを定義してsave()メソッドを作っています。送信されたファイルを保存し、それらのURLをリストで返します。

そのカスタマイズしたフォームセットクラスを、formset_factory()のformset引数に指定しています。モデルフォームセットやインラインフォームセットでも、今回のようにフォームセット自体の挙動をカスタマイズできます。

ビューは、関数ビューで作ってみました。

from django.shortcuts import render
from .forms import UploadFormSet


def upload(request):
    formset = UploadFormSet(request.POST or None, files=request.FILES or None)
    download_url_list = []

    if request.method == 'POST' and formset.is_valid():
        download_url_list = formset.save()

    context = {
        'download_url_list': download_url_list,
        'formset': formset,
    }
    return render(request, 'app/upload.html', context)

アップロードされたファイルは、files=request.FILESのようにしてフォームに渡すことを忘れないようにしましょう。

テンプレートは非常に単純です。

    <form action="" method="POST", enctype="multipart/form-data">
        {{ formset.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

    {% for download_url in download_url_list %}
        <p><a href="{{ download_url }}">ダウンロード</a></p>
    {% endfor %}

モデルフォームセット(モデルあり)

これは非常に簡単です。モデルフォームセットや場合によってはインラインフォームセットも良ければご覧ください。

forms.py

from django import forms
from .models import UploadFile


class UploadModelForm(forms.ModelForm):

    class Meta:
        model = UploadFile
        fields = '__all__'


UploadModelFormSet = forms.modelformset_factory(
    UploadFile, form=UploadModelForm,
    extra=3
)

views.py

from django.shortcuts import redirect, render
from .forms import UploadModelFormSet


def upload(request):
    formset = UploadModelFormSet(request.POST or None, files=request.FILES or None)

    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('app:upload')

    context = {
        'formset': formset,
    }
    return render(request, 'app/upload.html', context)

テンプレートは非常に単純です。

    <form action="" method="POST", enctype="multipart/form-data">
        {{ formset.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

input type="file" multiple(モデルなし)

スマホ環境では使えないこともあるのですが、こちらも紹介しておきます。

input type="file" multiple>な要素を使うと、Ctrlを押しながら複数のファイルを選択することができます。 複数ファイルを選択できる

まずフォームです。

from django import forms
from django.core.files.storage import default_storage


class UploadForm(forms.Form):
    file = forms.FileField(
        label='ファイル',
        widget=forms.ClearableFileInput(attrs={'multiple': True})
    )

    def save(self):
        url_list = []
        for upload_file in self.files.getlist('file'):
            file_name = default_storage.save(upload_file.name, upload_file)
            file_path = default_storage.url(file_name)
            url_list.append(file_path)
        return url_list

< input type="file">を、<input type="file" multiple>に変更する必要があります。そのため、widget引数を上書きしてそのように設定します。

保存処理ですが、getlist()は今回のように複数データを受け取る場合に使います。複数選択可能なselect要素や、checkboxなどの送信データをフォームオブジェクトを介さないで読み取る場合にも使ったりします。そして、ファイルのURLが詰まったリストを返します。

ビューもシンプルです。

from django.views import generic
from .forms import UploadForm


class Upload(generic.FormView):
    form_class = UploadForm
    template_name = 'app/upload.html'

    def form_valid(self, form):
        download_url_list = form.save()
        context = {
            'download_url_list': download_url_list,
            'form': form,
        }
        return self.render_to_response(context)

テンプレートもシンプルです。

    <form action="" method="POST", enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

    {% for download_url in download_url_list %}
        <p><a href="{{ download_url }}">ダウンロード</a></p>
    {% endfor %}

input type="file" multiple(モデルあり)

FileFieldImageFieldは基本的に1つのファイルを扱うようにできていますので、複数ファイルを扱えるように修正する必要があります。

ビューとテンプレートは単純に作れます。

from django.views import generic
from .forms import UploadModelForm
from .models import UploadFile


class Upload(generic.CreateView):
    model = UploadFile
    form_class = UploadModelForm
    success_url = '/'

uploadfile_form.html

    <form action="" method="POST" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

そしてフォームです。

from django import forms
from .models import UploadFile


class UploadModelForm(forms.ModelForm):
    file = forms.FileField(
        label='ファイル',
        widget=forms.ClearableFileInput(attrs={'multiple': True})
    )

    class Meta:
        model = UploadFile
        fields = '__all__'

    def save(self, commit=True):
        # 全てのアップロードファイルを取得
        upload_files = self.files.getlist('file')

        # このモデルフォーム自体は、あくまで1つのモデルインスタンスと紐づきます。
        # アップロードされたファイルのうち、どれか1つをフォームに紐づけつつ
        # 残りのファイルは今ここでUploadFile.objects.createで作成する。
        self.instance.file = upload_files[0]
        other_files = upload_files[1:]
        for file in other_files:
            UploadFile.objects.create(file=file)  # 残りのファイル作成
        return super().save(commit)

やっていることはシンプルで、コメントにあるように、モデルフォーム自体に1つのファイルを紐づけつつ、残りのファイルは適当に作成します。

このコードはcommit=Falseには対応しておらず、フォームに紐づいた1つ以外はcommit=Falseでも保存がされてしまいます。もしそれに対応するならば、以下のような感じになるでしょう。

    def save(self, commit=True):
        # 全てのアップロードファイルを取得
        upload_files = self.files.getlist('file')

        # このモデルフォーム自体は、あくまで1つのモデルインスタンスと紐づきます。
        # アップロードされたファイルのうち、どれか1つをフォームに紐づけつつ
        # 残りのファイルは今ここでUploadFile.objects.createで作成する。
        self.instance.file = upload_files[0]
        self.instance.others = []
        other_files = upload_files[1:]
        for file in other_files:
            file_obj = UploadFile(file=file)
            if commit:  # commit=Trueならsave()を呼び出す
                file_obj.save()
            self.instance.others.append(file_obj)
        return super().save(commit)

モデルフォームのsave()で返されるモデルインスタンスに、othersという属性を持たせ、他ファイルのモデルインスタンスをそこに格納します。

ビューでは、以下のような感じで使います。

class Upload(generic.CreateView):
    model = UploadFile
    form_class = UploadModelForm
    success_url = '/'

    def form_valid(self, form):
        # commt=False
        main_file = form.save(commit=False)

        # commit=Trueで保存する
        for other_file in main_file.others:
            other_file.save()
        main_file.save()
        ...
        ...

requestsからアップロード

外から、HTTPライブラリなどを使ってアップロードしたい場合もあるかもしれません。そのような場合は、今回の内容とPython、requestsを使ったファイルアップロードを参考にしてください。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする