Django モデルのエクスポート方法

今回はDjangoアプリケーションのモデルをエクスポートする方法についての備忘記事です。

  • CSVエクスポート
  • 検索結果のエクスポート
  • Excelエクスポート
  • pandas DataFrameエクスポート


サンプルモデル


ブログアプリを想定して、カテゴリとタイトルだけの簡単なモデルを用意しました。
アプリケーション名はappとしています。

# app/models.py
from django.db import models


class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=12)

    def __str__(self):
        return self.name


class Post(models.Model):
    title = models.CharField('タイトル', max_length=32)
    category = models.ForeignKey(Category, on_delete=models.PROTECT)

    def __str__(self):
        return self.title


csvエクスポート

まずは通常のcsvエクスポートをしてみましょう。
アプリ内のurls.pyを以下のようにします。

# app/urls.py
from django.urls import path
from . import views

app_name = 'app'

urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    path('export/', views.export, name='export'),
]


トップページとエクスポート用のパターンを作成しました。
次にviews.pyを編集します。

# app/views.py
from django.views import generic
from .models import Post
from django.http import HttpResponse
import csv


class Top(generic.ListView):
    model = Post


def export(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="posts.csv"'
    writer = csv.writer(response)
    for post in Post.objects.all():
        writer.writerow([post.pk, post.title, post.category])
    return response


次に表示させるテンプレートを作っていきます。
まずは、base.htmlです。
今回はbootstrapを使っています。

<!-- app/templates/app/base.html -->
<!doctype html>
<html lang="ja">

<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  <title>Export Test</title>
</head>

<body>
  <!-- header -->
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="{% url 'app:top' %}">Export Test</a>
    <div class="navbar-nav">
      <a class="nav-item nav-link active" href="{% url 'app:top' %}">Home <span class="sr-only">(current)</span></a>
      <a class="nav-item nav-link active" href="/admin">Admin <span class="sr-only">(current)</span></a>
    </div>
  </nav>

  <!-- content -->
  <div class="container">
    {% block content %}{% endblock %}
  </div>

  <!-- Optional JavaScript -->
  <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>

</html>


次にpost_list.htmlを作成し、以下のようにします。

<!-- app/templates/app/post_list.html -->
{% extends "app/base.html" %}
{% block content %}
<div class="mt-4">
  <a class="btn btn-primary" href="{% url 'app:export' %}">export</a>
  {% for post in post_list  %}
  <article class="mt-4">
    <span class>{{ post.category }}</span>
    <h1>{{ post.title }}</h1>
  </article>
  {% endfor %}
</div>
{% endblock %}


とりあえず記事の一覧とexport用のボタンを用意しています。



exportボタンを押すとviews.pyの関数が呼ばれ、csvファイルがダウンロードされます。



csvのヘッダーを付けるには以下のようにします。

# app/views.py

def export(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="posts.csv"'

    writer = csv.writer(response)
    # 追加
    header = ['pk', 'title', 'category']
    writer.writerow(header)

    for post in Post.objects.all():
        writer.writerow([post.pk, post.title, post.category])
    return response



検索結果をエクスポート

この方法ですと、Postモデルの全件がダウンロードされます。
しかし何らかの検索を一覧画面で実行して、結果だけを出力したい、というケースがあると思います。

実装をする前に、簡単な検索フォームを作ります。
アプリ内にforms.pyを作成して以下のようにします。

# app/forms.py
from django import forms
from .models import Category, Post


class PostSearchForm(forms.Form):
    key_word = forms.CharField(
        label='検索キーワード',
        required=False,
        widget=forms.TextInput(attrs={'class': 'form-control',
                                      'autocomplete': 'off',
                                      'placeholder': 'Keyword',
                                      })
    )

    category = forms.ModelChoiceField(
        label='カテゴリ',
        required=False,
        queryset=Category.objects.order_by('name'),
        widget=forms.Select(attrs={'class': 'form-control'})
    )


キーワードとカテゴリだけの簡単な検索です。
そして、views.pyで読み込みます。

# app/views.py
from .forms import PostSearchForm  # 追加


class Top(generic.ListView):
    model = Post

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = form = PostSearchForm(self.request.GET or None)

        if form.is_valid():
            key_word = form.cleaned_data.get('key_word')
            if key_word:
                for word in key_word.split():
                    queryset = queryset.filter(title__icontains=word)

            category = form.cleaned_data.get('category')
            if category:
                queryset = queryset.filter(category=category)

        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form

        return context


次にpost_list.htmlでこのsearch_formを表示します。

<!-- app/templates/app/post_list.html -->
{% extends "app/base.html" %}
{% block content %}
<div class="mt-4">
  <a class="btn btn-primary" href="{% url 'app:export' %}">export</a>
  <!-- 追加 -->
  <form class="mt-2" id="search-form" name="search-form" method="GET">
    <div class="row">
      <div class="col-md-2">
        {{ search_form.key_word }}
      </div>
      <div class="col-md-2">
        {{ search_form.category }}
      </div>
      <div class="col-md-2">
        <button class="btn btn-sm btn-info ml-4" type="submit">Search</button>
      </div>
    </div>
  </form>
  {% for post in post_list  %}
  <article class="mt-4">
    <span class>{{ post.category }}</span>
    <h1>{{ post.title }}</h1>
  </article>
  {% endfor %}
</div>

{% endblock %}


このように検索フォームが表示されます。



カテゴリにPythonを指定して検索を実行してみます。



こうなりますね。
前置きが長くなりましたが、この状態でexportボタンを押した際に、Pythonを始める方法、というエントリだけダウンロードされてほしいわけです。

まず、post_list.html内のexportボタンを以下のようにします。

  <!-- 変更 -->
  <a class="btn btn-primary" href="{% url 'app:export' %}?{{ request.GET.urlencode }}">export</a>


こうすることで、key_word=&category=1というようなパラメータをエクスポート関数に渡せます。
渡されたパラメータを確認してみましょう。

# app/views.py


def export(request):
    print(request.GET)    
>>>
<QueryDict: {'key_word': [''], 'category': ['1']}>


あとはこのQueryDictを元に絞り込みしたうえで出力をすればいいわけです。

# app/views.py

def export(request):
    posts = Post.objects.all()
    # リクエストに応じて絞り込み
    key_word = request.GET.get('key_word')
    if key_word:
        for word in key_word.split():
            posts = posts.filter(title__icontains=word)

    category = request.GET.get('category')
    if category:
        posts = posts.filter(category=category)

    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="posts.csv"'

    writer = csv.writer(response)
    header = ['pk', 'title', 'category']
    writer.writerow(header)

    for post in posts:
        writer.writerow([post.pk, post.title, post.category])
    return response


これで一覧画面の検索に応じて出力されるようになります。


Excel


次にExcel形式での出力を実装していきます。
実務の現場ではExcelは良く使われるので、覚えておいて損はありません。

openpyxlを使います。

pip install openpyxl


excel出力用のパターンを追加します。

# app/urls.py

urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    path('export/', views.export, name='export'),
    path('export_excel', views.export_excel, name='export_excel'),  # 追加
]


次にviewです。

# app/views.py
import openpyxl # 追加


def export_excel(request):
    # 新規ブックを作成
    wb = openpyxl.Workbook()
    ws = wb.active
    response = HttpResponse(content_type='application/vnd.ms-excel')
    response['Content-Disposition'] = 'attachment; filename=posts.xlsx'

    ws.cell(1, 1).value = 'pk'
    ws.cell(1, 2).value = 'title'
    ws.cell(1, 3).value = 'category'
    k = 2
    for post in Post.objects.all():
        ws.cell(k, 1).value = post.pk
        ws.cell(k, 2).value = post.title
        # .nameをつけないとバグ
        ws.cell(k, 3).value = post.category.name
        k += 1

    wb.save(response)

    return response


テンプレートにリンクボタンを設置します。

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

...
 
<a class="btn btn-primary" href="{% url 'app:export' %}?{{ request.GET.urlencode }}">export</a>
<!-- 追加 -->
<a class="btn btn-success" href="{% url 'app:export_excel' %}">export excel</a>

...  




ボタンを押すとExcelでダウンロードされます。


pandas DataFrame

次にpandas DataFrameの出力です。
例えば何らかの複雑な処理をした結果を出力したい…というケースがあるかもしれません。
django-pandasを使うと、クエリセットを簡単にpandas DataFrame化できるので便利です。

pip install django-pandas


後はおなじみで以下のように編集していきます。

# app/urls.py

urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    path('export/', views.export, name='export'),
    path('export_excel', views.export_excel, name='export_excel'),
    # 追加
    path('export_pandas_df', views.export_pandas_dataframe, name='export_pandas_df') 
]


# app/views.py
from django_pandas.io import read_frame # 追加

def export_pandas_dataframe(request):
    df = read_frame(qs=Post.objects.all(), fieldnames=['pk', 'title', 'category'])
    response = HttpResponse(content_type='text/csv; charset=utf8')
    response['Content-Disposition'] = 'attachment; filename=posts.csv'
    # 実際に使う場合はここで何らかの処理が入る
    # とりあえず列名だけ変更
    df = df.rename(columns={'pk': '番号', 'title': 'タイトル', 'category': 'カテゴリ'})

    df.to_csv(path_or_buf=response, encoding='utf-8', index=None)

    return response


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

...

<a class="btn btn-primary" href="{% url 'app:export' %}?{{ request.GET.urlencode }}">export</a>
<a class="btn btn-success" href="{% url 'app:export_excel' %}">export excel</a>
<!-- 追加 -->
<a class="btn btn-warning" href="{% url 'app:export_pandas_df' %}">export pandas df</a>

...


今回は列名だけ変更してみました。
以下のように出力されるでしょう。


ソースコード

https://github.com/qlitre/django-export-test

TOPページ