【Django】Model内の保存データをCSVファイルに自動/手動アップロード


投稿日 2020年8月29日 >> 更新日 2023年3月1日

今回はデータベースに保存されているデータをCSVファイルにエクスポートし、DjangoのモデルフィールドにあるFileFieldにファイルの保管先(ディレクトリ)を保存したのち、そのままCSVファイルをダウンロードできるという機能を構築したいと思います。

CSVファイルに保管する際は、Python標準ライブラリであるCSVモジュールか外部ライブラリのdjango-pandasを使用していきます。

ファイルの保管内容は、自動保管と手動保管の2つの実装をそれぞれ行っていきます。

  • 自動保管の場合

自動保管では、データが作成、もしくわ更新される度に最新の状態でデータがCSVファイルに書き込まれます。


  • 手動保管の場合

手動保管では、「ファイルの作成」というボタンを押すと最新の状態でデータがCSVファイルに書き込まれます。


これらの自動、もしくわ手動で作成されたCSVファイルは他のモデルで定義されているFileFieldへアップロードされ、そのままダウンロード可能な状態へと整います。

あとは煮るなり焼くなりしてもらえたら幸いです。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.6.9
pip 9.0.1
使用ライブラリ ライセンス
Django==3.1 BSD
django-cleanup==5.0.0 MIT
django-pandas==0.6.2 BSD

プロジェクトの準備

使用ライブラリ中にある「django-cleanup」と「django-pandas」ですが、前者はアップロードされているファイルが削除、もしくわ更新された際に実際のファイルも削除してくれるモジュールです。

後者の「django-pandas」は、モデルインスタンスをそのまま渡すだけでテーブル形式の構造に変換をし、データの分析やCSVファイルの保管を容易に実装することができます。

それぞれインストールを終えたら、設定ファイルに追記します。


$ pip install django-cleanup django-pandas

各モジュールとアプリを設定し、アップロードされるファイルのディレクトリを設定します。

# config/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_cleanup.apps.CleanupConfig',  # 設定
    'django_pandas',  # 設定
    'app.apps.AppConfig',  # アプリ名
]

....

STATIC_URL = '/static/'

import os

# ファイルの保管先ディレクトリ
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

ではsettings.pyの階層にある「urls.py」も編集します。

# config/urls.py

from django.contrib import admin
from django.urls import path, include

from django.conf import settings
from django.conf.urls.static import static

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

# 開発環境下でファイルを扱う際の設定
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

次にモデルの構築です。

イメージでは、TODOリストにカテゴリーが紐づいており、別途アップロード用のモデルがあります。

# app/models.py

from django.db import models


class Category(models.Model):
    """
    カテゴリー
    """
    name = models.CharField(max_length=50)

    def __str__(self):
        return self.name


class Todo(models.Model):
    """
    TODOリスト
    """
    title = models.CharField(max_length=255)
    category = models.ForeignKey(Category, on_delete=models.PROTECT)
    created_at = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.title


class FileUpload(models.Model):
    """
    アップロード
    """
    upload_dir = models.FileField(upload_to='%Y/%m/%d')
    created_at = models.DateField(auto_now_add=True)

    def __str__(self):
        return str(self.upload_dirname)

アップロード用に定義したFileUploadモデルの「models.FileFiled」に関してはこちらの記事で詳しく触れています。

「admin.py」も編集しておきましょう。

# app/admin.py

from django.contrib import admin
from .models import Category, Todo, FileUpload


admin.site.register(Category)
admin.site.register(Todo)
admin.site.register(FileUpload)

次にアプリディレクトリ内の「urls.py」と「views.py」を編集します。

# app/urls.py

from django.urls import path
from . import views


app_name = 'app'

urlpatterns = [
        path('', views.index, name='index'),
]
# app/views.py

from django.shortcuts import render
from .models import Todo, FileUpload


def index(request):
    """
    トップページ
    """
    todo_obj = Todo.objects.all()
    # 保存データからのアップロード用としてフィルターをかけて取得
    file_obj = FileUpload.objects.filter(upload_dir__icontains='auto_upload_file')
    context = {
            'todo_obj': todo_obj,
            'file_obj': file_obj,
    }
    return render(request, 'app/index.html', context)

テンプレートファイルの「index.html」は以下のようになります。

<!-- app/templates/app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>

            <h2>Todoリスト</h2>
            <br>

            <!-- file_objにデータがあれば表示、無ければ「ありません」 -->
            {% if file_obj %}
                <h2>NEWファイル</h2>
                {% for file in file_obj %}
                    <p>
                        {{ file.created_at }}
                        {{ file.upload_dir.name }}
                        <a href="{{ file.upload_dir.url }}">ダウンロード</a>
                    </p>
                {% endfor %}
            {% else %}
                <h2>現在ファイルはありません</h2>
            {% endif %}
            <br>

            <!-- TODOのリストを表示 -->
            {% for todo in todo_obj %}
                <p>
                    {{ todo.created_at }}
                    {{ todo.title }}
                </p>
            {% endfor %}

        </body>
</html>

ではモデルをデータベースに反映させ、管理者として登録します。


$ python3 manage.py makemigrations app

$ python3 manage.py migrate app

$ python3 manage.py createsuperuser

幾つかデータを保存したら準備完了です。

CSVファイル作成用のPythonファイルを作成

アプリディレクトリ内に、保存データをCSVファイルへと処理するPythonファイル(「model_create_file.py」)を新規作成します。

app
  |--__pycache__
  |--migrations
  |--templates
  |--__init__.py
  |--...
  |--model_create_file.py  # 新規作成
  |--...

標準ライブラリのCSVモジュールを使用した例

# app/model_create_file.py

from django.conf import settings
from .models import Todo, FileUpload
import os

# mediaルートとFileUploadモデルに渡す相対パス
path = os.path.join('auto_upload_file', 'todo.csv')
# ファイルを書き込む際に渡す絶対パス
media_path = os.path.join(settings.MEDIA_ROOT, path)

# 一時的にFileUploadモデル内に保存されている相対パスを取得
file_dir = FileUpload.objects.filter(upload_dir=path)

# Todoモデルのオブジェクトを取得
todo = Todo

def create_csv():
    """
    CSVモジュールを使用してファイルを作成
    """
    import csv

    todo_head = todo._meta.get_fields() # フィールドのインスタンスを取得
    todo_head_value = [row.name for row in todo_head]  # フィールド名を取得
    todo_obj = todo.objects.all()
    # 各フィールドの要素をリスト内リストとして取得
    todo_values = [[row.id, row.title, row.category.name, row.created_at] for row in todo_obj]
    todo_values.insert(0, todo_head_value)  # ヘッダー(列名)用にフィールド名を挿入

    # 絶対パスに指定されたファイルへ要素を書き込む
    with open(media_path, 'wt') as f:
        csvout = csv.writer(f)
        csvout.writerows(todo_values)

    # 検索した相対パスが無ければ新規作成
    if not file_dir:
        FileUpload.objects.create(upload_dir=path)

「create_csv()」関数が呼ばれると、上から順番に処理が始まります。

関数内で定義されている処理は、CSVモジュールで処理できるように整えているだけです。

詳しくこちらの記事でも説明しています。

関数外(上部)では関数内で必要とされる要素を取得していますが、このあと「django-pandas」を使用したCSVファイルへ書き込む関数も定義するので、同じ要素を利用するために分けました。

ではdjango-pandasを使用した関数も定義しておきます。

外部ライブラリのdjango-pandasを使用した例

settings.pyに「django_pandas」として明記されていれば呼び出すことができます。

# config/settings.py

INSTALLED_APPS = [
    ...
    'django_pandas',
    ...
]

では「model_create_file.py」の下部に「django_pandas」を使用した関数を定義します。

# app/model_create_file.py

from django.conf import settings
from .models import Todo, FileUpload
import os

# mediaルートとFileUploadモデルに渡す相対パス
path = os.path.join('auto_upload_file', 'todo.csv')
# ファイルを書き込む際に渡す絶対パス
media_path = os.path.join(settings.MEDIA_ROOT, path)

# 一時的にFileUploadモデル内に保存されている相対パスを取得
file_dir = FileUpload.objects.filter(upload_dir=path)

# Todoモデルのオブジェクトを取得
todo = Todo

...

def pandas_csv():
    """
    django-pandasを使用したCSVファイルの作成
    """
    from django_pandas.io import read_frame

    query = todo.objects.all()
    df = read_frame(query)  # インスタンスを渡す
    df.to_csv(media_path)  # CSVファイルの作成

    # 検索した相対パスが無ければ新規作成
    if not file_dir:
        FileUpload.objects.create(upload_dir=path)

「pandas_csv()」関数内「df」変数名はpandasで使われているDataFrameの略で、出力すると以下のようなテーブル形式へと変換されます。

   id  title category  created_at
0  18  test1        A  2020-08-26
1  19  test2        B  2020-08-26
2  20  test3        C  2020-08-26
3  21  test4        A  2020-08-26
4  22  test5        C  2020-08-26

DataFrame化された変数のメソッド「to_csv()」引数にファイル名までのルートパスを渡すだけでCSVファイルを作成することができます。

では、「model_create_file.py」には以下の2つの関数ができあがりました。

# app/model_create_file.py

....
....

def create_csv():
    """
    CSVモジュールを使用したCSVファイル作成
    """
    import csv

    todo_head = todo._meta.get_fields()
    todo_head_value = [row.name for row in todo_head]
    todo_obj = todo.objects.all()
    todo_values = [[row.id, row.title, row.category.name, row.created_at] for row in todo_obj]
    todo_values.insert(0, todo_head_value)

    with open(media_path, 'wt') as f:
        csvout = csv.writer(f)
        csvout.writerows(todo_values)


def pandas_csv():
    """
    django-pandasを使用したCSVファイル作成
    """
    from django_pandas.io import read_frame

    query = todo.objects.all()
    df = read_frame(query)
    df.to_csv(media_path)

この双方どちらかの関数を呼び出すだけ、ファイルまでの相対パスをFileUploadモデルに保存し、そのルート上へファイルを書き込むようになります。

※今回削除といった機能は加えないため、管理画面等でFileUploadモデルの要素を削除します。

ではDjangoの「shell」コマンドを開いて実際に機能するか試してみます。


$ python3 manage.py shell
>>>
>>> from app.models import FileUpload
>>>
>>> # モデル内の要素が空であることを確認
>>> FileUpload.objects.all()
<QuerySet []>
>>>
>>> # CSVファイル作成用のファイルをインポート
>>> from app.model_create_file import create_csv, pandas_csv
>>>
>>> # CSVモジュールの関数を実行
>>> create_csv()
>>>
>>> # モデル内の要素を確認
>>> FileUpload.objects.all()
<QuerySet [<FileUpload: auto_upload_file/todo.csv>]>
>>>
>>> #モデル内の要素を一旦削除
>>> FileUpload.objects.all().delete()
(1, {'app.FileUpload': 1})
>>>
>>> # モデル内の要素が空であることを確認
>>> FileUpload.objects.all()
<QuerySet []>
>>>
>>> # django-pandasの関数を実行
>>> pandas_csv()
>>>
>>> # モデル内の要素を確認
>>> FileUpload.objects.all()
<QuerySet [<FileUpload: auto_upload_file/todo.csv>]>
>>>
>>> # shellを起動したままCSVファイルの中身を確認する
>>> import subprocess
>>>
>>> # Unixコマンドを指定し、ファイルの中身を取得
>>> subprocess.call(['cat', 'media/auto_upload_file/todo.csv'])
,id,title,category,created_at
0,18,test1,A,2020-08-26
1,19,test2,B,2020-08-26
2,20,test3,C,2020-08-26
3,21,test4,A,2020-08-26
4,22,test5,C,2020-08-26
5,23,test6,C,2020-08-26
6,24,test7,A,2020-08-26
7,25,test8,B,2020-08-26
8,26,test9,A,2020-08-26
9,27,test10,B,2020-08-26
10,28,test11,A,2020-08-26
11,29,test12,B,2020-08-27
0
>>>
>>> # モデル内の要素を一旦削除
>>> FileUpload.objects.all().delete()
(1, {'app.FileUpload': 1})
>>>
>>> quit()

しっかり機能していることが分かりました。

冒頭でも言いましたが、「django-cleanup」のおかげでアップロードされているファイル(相対パス)を削除するだけで、実際のファイルも削除されています。

この作成した関数をDjangoプロジェクトの任意の場所に当てはめることによってファイルが自動で作成されるのか、もしくわ手動で作成されるのかを実行できるようになります。

CSVファイルの自動作成

自動作成の仕組みは、管理画面、もしくわフォーム画面においてデータが保存された際に保存されているデータをCSVファイルに移してアップロードされるという流れです。

そのために、ModelFormを継承し、saveメソッドをオーバーライドして独自の処理を実行させる必要があります。

なので、アプリディレクトリ内に「forms.py」を新規作成します。

# app/forms.py

from django import forms
from .models import Todo
from .model_create_file import create_csv, pandas_csv


class TodoForm(forms.ModelForm):
    """
    Todoモデルフォーム
    """
    model = Todo
    fields = '__all__'

    def save(self, commit=True):
        """
        カスタムメソッド
        """
        todo_create = super(TodoForm, self).save(commit=False)
        todo_create.save()

        # データの保存が終わるとCSVファイル作成用の関数が呼ばれる
        create_csv()
        # pandas_csv()

        return todo_create

ModelFormのsaveメソッドをカスタムする場合は、引数の「commi=False」を指定し、フィールドに渡された要素を取得してからデータを保存する必要があります。

そして戻り値として保存されたモデルインスタンスを渡します。

詳しくは公式ドキュメントにて

今回自作のフォーム画面は作成しませんが、このモデルフォールを管理画面で使えるように、adminのform属性に渡します。

それでは「admin.py」を編集します。

# app/admin.py

from django.contrib import admin
from .models import Category, Todo, FileUpload
from .forms import TodoForm

class TodoAdmin(admin.ModelAdmin):
    form = TodoForm  # form属性にオーバーライドされたフォームを代入


admin.site.register(Category)
admin.site.register(Todo, TodoAdmin)  # 追記
admin.site.register(FileUpload)

これにて、Todoモデルに要素が保存されるとCSVファイルも一緒に作成されることになります。

適当な要素をTodoモデルに保存すると

FileUploadモデルにてファイルがアップロードされています。

もちろん外部のファイル(PCなど)を直接アップロードすることもできます。

トップページへ移動すると、以下のように表示されていることが分かります。

CSVファイルの手動作成

手動作成の仕組みは、テンプレートファイルにformタグを設置し、「request.method」が「POST」として呼ばれた際にCSVファイルが作成されます。

では「views.py」を編集します。

# app/views.py

from django.shortcuts import render, redirect
from .models import Todo, FileUpload
from .forms import FileUploadForm
from .model_create_file import create_csv, pandas_csv


def index(request):
    """
    トップページ
    """
    todo_obj = Todo.objects.all()
    file_obj = FileUpload.objects.filter(upload_dir__icontains='auto_upload_file')

    # フォームのボタンがクリックされるとファイルが作成される
    if request.method == "POST":
        pandas_csv()
        # create_csv()
        return redirect('app:index')
    context = {
            'todo_obj': todo_obj,
            'file_obj': file_obj,
    }
    return render(request, 'app/index.html', context)

次に「index.html」にformタグを追記します。

<!-- app/templates/app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>

            <h2>Todoリスト</h2>
            <br>

            <form action="{% 'app:index' %}" method="POST">
                {% csrf_token %}
                <button type="submit">CSVファイルの作成</button>
            </form>

            ....
            ....

        </body>
</html>

アップロードされているファイルを削除したのち、トップページを表示してみます。

「CSVファイルの作成」ボタンをクリックしてみると

保存データからファイルを作成したCSVファイルのみ表示されれば成功です。

今回は簡易的な実装を試みましたが、ユーザー権限でファイルを隠したり、ファイルの中身を表示することが可能なので、余力のある方は試してみて下さい。

それでは以上となります。

最後までご覧いただきありがとうございました。