見出し画像

DjangoでテーブルデータをCSV、XLS、JSON等へ変換するレシピ

このレシピでは、Djangoのモデルクラスで定義されたテーブルデータをCSV、XLS、JSONなどのさまざまな形式に変換する方法を実際に手を動かしながら学ぶことができます。

ライブラリdjango-import-exportを使用して実現します。

1.事前準備

まずはDjango環境の準備を行います。

仮想環境を作成してアクティベートし、必要なモジュールをインストールします。

python -m venv django-import-export
django-import-export\scripts\activate

Django本体とdjango-import-exportモジュールをインポートします。


pip install django django-import-export

次にDjangoプロジェクトを作成します。

django-admin startproject config .

次に、アプリケーション(app)を作成しておきます。

django-admin startapp app

config\settings.pyの初期カスタマイズを行います。

作成したアプリケーション(app)とdjango-export-importを有効にするためINSTALLED_APPSに以下のエントリーを追加します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app.apps.AppConfig',  #追加
    'import_export',   #追加
]

また、言語地タイムゾーンを日本に変更しておきます。

LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

上で事前準備は完了です。

2.モデルの定義

続いて今回扱うデータのモデルクラスを定義します。
ここでは、よくある例として以下のような書籍、カテゴリ名、著者を管理するモデルクラスを定義します。

app\models.pyに以下のモデルクラスを定義しましょう。

from django.db import models

class Category(models.Model):
    class Meta:
        verbose_name = "カテゴリ名"
        verbose_name_plural = "カテゴリ名"

    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class Author(models.Model):
    class Meta:
        verbose_name = "著者名"
        verbose_name_plural = "著者名"

    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class Book(models.Model):
    class Meta:
        verbose_name = "書籍"
        verbose_name_plural = "書籍"

    name = models.CharField(verbose_name="書籍名",max_length=255)
    author = models.ForeignKey(Author,verbose_name="著者名",on_delete = models.CASCADE ,blank=True, null=True)
    categories = models.ManyToManyField(Category,verbose_name="カテゴリ名",  blank=True)

    def __str__(self):
        return self.name

今回は、author列を外部参照(ForegnKey)、categories列は多対多(ManytoMany)として定義しています。

モデルを定義したので、データベースに反映させるためマイグレーションを実行します。

python manage.py makemigrations

Migrations for 'app':
  app\migrations\0001_initial.py
    - Create model Author
    - Create model Category
    - Create model Book
python manage.py migrate

Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying app.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

任意の名前で管理者ユーザを作成します。

python manage.py createsuperuser

adminサイトに作成したモデルを登録するためにapp\admin.pyに以下のコードを追記します。

from django.contrib import admin

from .models import *

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display=('pk','name')


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display=('pk','name')


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display=('pk','name', 'author')

http://127.0.0.1:8000/adminにアクセスして以下の通り3つのテーブルが表示されることを確認しておきます。

次に、動作確認用のデモデータをインポートします。

まず、アプリケーションフォルダ直下にfixuresフォルダ(app\fixtures)を作成します。

fixturesフォルダ直下に空のsample_data.yamlファイルを作成して、以下のコードをコピー&ペースとして保存します。

- model: app.category
  pk: 1
  fields:
    name: 文学・評論
- model: app.category
  pk: 2
  fields:
    name: ノンフィクション
- model: app.category
  pk: 3
  fields:
    name: ビジネス・経済
- model: app.category
  pk: 4
  fields:
    name: 歴史・地理
- model: app.category
  pk: 5
  fields:
    name: 政治・社会
- model: app.category
  pk: 6
  fields:
    name: 芸能・エンターテイメント
- model: app.category
  pk: 7
  fields:
    name: アート・建築・デザイン
- model: app.category
  pk: 8
  fields:
    name: 人文・思想・宗教
- model: app.category
  pk: 9
  fields:
    name: 暮らし・健康・料理
- model: app.category
  pk: 10
  fields:
    name: サイエンス・テクノロジー
- model: app.category
  pk: 11
  fields:
    name: 趣味・実用
- model: app.author
  pk: 1
  fields:
    name: フィリップ・プルマン
- model: app.author
  pk: 2
  fields:
    name: 山舩晃太郎
- model: app.author
  pk: 3
  fields:
    name: 松田美智子
- model: app.author
  pk: 4
  fields:
    name: 沢木耕太郎
- model: app.author
  pk: 5
  fields:
    name: 落合陽一
- model: app.author
  pk: 6
  fields:
    name: 佐藤辰彦
- model: app.author
  pk: 7
  fields:
    name: 山舩晃太郎
- model: app.author
  pk: 8
  fields:
    name: 山田ルイ53世
- model: app.author
  pk: 9
  fields:
    name: 西岡文彦
- model: app.author
  pk: 10
  fields:
    name: 猪木武徳
- model: app.author
  pk: 11
  fields:
    name: 緑慎也
- model: app.author
  pk: 12
  fields:
    name: 中坊徹次
- model: app.author
  pk: 13
  fields:
    name: 林寧彦
- model: app.book
  pk: 1
  fields:
    name: ダーク・マテリアルズII 神秘の短剣〔上〕
    author: 1
    categories:
    - 1
- model: app.book
  pk: 2
  fields:
    name: ダーク・マテリアルズII 神秘の短剣〔下〕
    author: 1
    categories:
    - 1
- model: app.book
  pk: 3
  fields:
    name: 沈没船博士、海の底で歴史の謎を追う
    author: 2
    categories:
    - 2
- model: app.book
  pk: 4
  fields:
    name: 仁義なき戦い 菅原文太伝
    author: 3
    categories:
    - 2
- model: app.book
  pk: 5
  fields:
    name: オリンピア1936 ナチスの森で
    author: 4
    categories:
    - 2
- model: app.book
  pk: 6
  fields:
    name: 半歩先を読む思考法
    author: 5
    categories:
    - 3
- model: app.book
  pk: 7
  fields:
    name: 世界を変える知財力
    author: 6
    categories:
    - 3
- model: app.book
  pk: 8
  fields:
    name: 沈没船博士、海の底で歴史の謎を追う
    author: 7
    categories:
    - 4
- model: app.book
  pk: 9
  fields:
    name: 一発屋芸人列伝
    author: 8
    categories:
    - 6
- model: app.book
  pk: 10
  fields:
    name: ビジネス戦略から読む美術史
    author: 9
    categories:
    - 7
- model: app.book
  pk: 11
  fields:
    name: 社会思想としてのクラシック音楽
    author: 10
    categories:
    - 8
- model: app.book
  pk: 12
  fields:
    name: 認知症の新しい常識
    author: 11
    categories:
    - 9
- model: app.book
  pk: 13
  fields:
    name: 絶滅魚クニマスの発見―私たちは「この種」から何を学ぶか―
    author: 12
    categories:
    - 10
- model: app.book
  pk: 14
  fields:
    name: 陶芸は生きがいになる
    author: 13
    categories:
    - 11

以下のコマンドを実行してサンプルデータをインポートします。

python manage.py loaddata --format=yaml app/fixtures/sample_data.yaml

Installed 38 object(s) from 1 fixture(s)

http://127.0.0.1:8000/adminにアクセスし、以下の様に「カテゴリ名、書籍、著者名」のサンプルデータが登録されていることが確認できればOKです。

3.Export/Import機能をadminサイトに追加する

デフォルトではadminサイト上にデータをExport、Importできる機能は存在していませんが、django-import-exportライブラリーを使うことで簡単に実装することができます。

django-import-exportの設定はadmin.pyで行います。

まず、書籍(Book)テーブルに対してexport,import機能を追加します。

app\admin.pyを以下の通りカスタマイズましょう。


from django.contrib import admin
from .models import *
from import_export import resources  #追加
from import_export.admin import ImportExportModelAdmin  #追加


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display=('pk','name')


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display=('pk','name')


#ここから下を追加
class BookResource(resources.ModelResource):
    class Meta:
        model = Book

#ここまで追加


@admin.register(Book)
class BookAdmin(ImportExportModelAdmin): # admin.ModelAdmin > ImportExportModelAdminに変更
    list_display=('pk','name', 'author')
    resource_class = BookResource  #追加

【コードの解説】

まず以下のコードでdjango-import-exportの機能をインポートします。

from import_export import resources  #追加
from import_export.admin import ImportExportModelAdmin  #追加

次にresources.ModelResourceを承継したクラス(今回はBookResource)を定義します。
このクラス内でdjango-import-exportに関する設定を行います。

#ここから下を追加
class BookResource(resources.ModelResource):
    class Meta:
        model = Book
#ここまで追加

MetaクラスではModelResourceの挙動を設定する様々なオプションが用意されています。
以下が代表的なオプションです。

model = Bookと指定することでexport,import対象をBookモデルクラスに指定します。

最後に以下の部分で、承継元のクラスを admin.ModelAdmin > ImportExportModelAdminに変更し、resource_classに先ほど設定したBookResourceクラスを指定します。

@admin.register(Book)
class BookAdmin(ImportExportModelAdmin): # admin.ModelAdmin > ImportExportModelAdminに変更
    list_display=('pk','name', 'author')
    resource_class = BookResource  #追加

以上が、最もシンプルなdjango-import-export機能の実装例です。

adminサイトにアクセスして「書籍テーブル」をクリックしてみましょう。
以下の通り、画面右上に「インポート」、「エクスポート」というボタンが追加されます。

それでは「エクスポート」ボタンを押してみましょう。

以下の通り、Exportファイルの種類が選択できるので、「xlsx」を選んでExcelファイル形式でエクスポートします。

すると、「モデルクラス名-yyyy-mm-dd.xlsx」という名称のExcelファイルがダウンロードされます。

ダウンロードされたファイルをExcelで開いてみましょう。

下図のように1行目にBOOOKモデルの列名、2行目以降にデータが記載されたファイルが生成されていることが確認できます。

4.データのインポート機能

データのインポートは、「インポート」ボタンから行うことができます。

先ほどエクスポートしたExcelファイルを開いて、「C2セル」の値を以下の通り数値「1」に変更しておきます。

このExcelファイルを「インポート」ボタンからインポートしてみましょう。

以下の画面が表示されるので、インポートするExcelファイルを指定し、フォーマットで「xlsx」を選び「確定」ボタンを押します。

すると、以下の様にプレビュー画面が表示されます。
自動的に変更箇所に色がつくため、変更箇所がわかるようになっています。

なお、デフォルトでは以下の通りすべてのデータについて「更新」扱いでインポート処理が実行されます。

問題なければ「インポートの実行」ボタンを押すとデータがインポートされます。

5.変更データのみインポートしたい場合

先ほどは変更がないデータも含めて全行がインポートされましたが、変更箇所のみインポートしたいケースもあると思います。
その場合は、Metaクラス内にskip_unchanged=Trueを設定します。

class BookResource(resources.ModelResource):
    class Meta:
        model = Book
        skip_unchanged = True  #追加

以下の通り、インポート時に変更箇所意外はスキップされるようになります。

6.ファイルの種類を限定したい場合

ファイルの種類を限定したい場合は、ImportExportMixinクラスのformatsを設定します。

BookAdminクラスを以下の様に変更します。

<変更前>

@admin.register(Book)
class BookAdmin(ImportExportModelAdmin):
    list_display=('pk','name', 'author')
    resource_class = BookResource  #追加

<変更後>

from import_export.admin import ImportExportMixins #追加
from import_export.formats import base_formats     #追加


@admin.register(Book)
class BookAdmin(ImportExportMixin, admin.ModelAdmin): #承継クラスを変更
    list_display=('pk','name', 'author')
    resource_class = BookResource
    formats = [base_formats.XLSX]   #追加

これで、以下の様にフォーマットがXSLXに限定されます。

7.エクスポート or インポートだけに限定したい場合

エクスポート or インポートだけに限定したい場合は、それぞれ専用のクラスを利用します。

エクスポート機能だけ提供

BookAdminクラスの承継元クラスにExportMixinを指定します。

from import_export.admin import ImportExportMixin, ExportMixin , ImportMixin  #修正

@admin.register(Book)
class BookAdmin(ExportMixin, admin.ModelAdmin): #承継クラスを変更
    list_display=('pk','name', 'author')
    resource_class = BookResource
    formats = [base_formats.XLSX]

以下の通り、「エクスポート」ボタンだけが表示されるようになります。

インポート機能だけ提供したい場合は、承継元クラスをExportMixiImportMixinに変更するだけでOKです。

8.データのエクスポート

ここではデータのエクスポートにフォーカスして様々な設定方法を見ていきます。

エクスポートデータの文字コードを変更したい場合

文字コードの設定はExportMixinクラス内のto_encoding属性で設定することができます。

なお、ImportExportMixinクラスImportMixin, ExportMixinを承継したクラスになっているため、ImportExportMixinでもto_encoding属性で設定することができます。

また、デフォルトでto_encodingutf-8に設定されています。

以下は、文字コードをutf-8-sigに設定する例です。


@admin.register(Book)
class BookAdmin(ImportExportMixin, admin.ModelAdmin):
    list_display=('pk','name', 'author')
    resource_class = BookResource
    formats = [base_formats.XLSX]
    to_encoding = "utf-8-sig"

エスポートファイルの列名を日本語で表示したい場合

デフォルトの設定のままでは、以下の通りモデルクラスのフィールド名そのままで列名が表示されます。

分かりやすい列名でファイルを生成するにはひと手間必要になります。

以下の様にBookResourceクラスにget_export_headersを追加することで、モデルクラスの各フィールドに指定したverbose_nameを列名にしてファイルを生成することができます。

class BookResource(resources.ModelResource):
    class Meta:
        model = Book
        skip_unchanged = True

    #ここから下を追加
    def get_export_headers(self):
        #Exportデータのヘッダをモデルのverbose_nameに変更する関数
        headers = []
        for field in self.get_fields():
            model_fields = self.Meta.model._meta.get_fields()
            header = next((
                str(x.verbose_name) for x in model_fields
                if x.name == field.column_name
            ), field.column_name)
            headers.append(header)
        return headers

以下の様にモデルクラス(models.py)のverbose_nameで指定した名称で列名が表示されます。

外部参照列(ForeignKey)の値を書き込みたい場合

デフォルトでは、外部参照している列の値は参照先テーブルのID名で書き込まれてしまいます。
このままではExportしたデータの確認がまともにできません。

普通は参照先のデータの値で確認したいので、参照先テーブルの値を表示するようにカスタマイズします。

このようなカスタマイズを行うにはModelResourceクラスを承継したクラス内にdehydrate_<fieldname> メソッドを定義します。

今回の例では、以下の通りBookResourceクラス内にdehydrate_authorメソッドを定義します。

class BookResource(resources.ModelResource):
    class Meta:
        model = Book
        skip_unchanged = True

    
    def get_export_headers(self):
        #Exportデータのヘッダをモデルのverbose_nameに変更する関数
        headers = []
        for field in self.get_fields():
            model_fields = self.Meta.model._meta.get_fields()
            header = next((
                str(x.verbose_name) for x in model_fields
                if x.name == field.column_name
            ), field.column_name)
            headers.append(header)
        return headers
    
    
    #ここから下を追加
    def dehydrate_author(self, book):
        #外部テーブル(authoor)の値でExportする
        return '%s' % (book.author)

dehydrate_authorメソッドの引数に対象となるモデルクラス名を小文字で指定します。
今回は、Bookモデルのauthorフィールドの名称を表示させたいので、return文にbook.authorのように指定します。

この状態でエクスポートを実行すると、以下の様に著者名が書き込まれていることが確認できます。

ManyToManyフィールドの値を書き込みたい場合

今回、カテゴリ名がManyToManyフィールドになっていますが、これもデフォルトでは参照先テーブルのID番号が書き込まれてしまいます。

実際のカテゴリ名が表示されるようにカスタマイズを行うにはModelResourceクラスのウィジェットを利用します。

以下の様にBookResourceクラスをカスタマイズします。

from import_export import fields   #追加
from import_export.widgets import ManyToManyWidget   #追加


class BookResource(resources.ModelResource):

    #many to manyフィールドのカテゴリ名でExport
    categories = fields.Field(attribute='categories', widget=ManyToManyWidget(Category, field="name")) #追加

    class Meta:
        model = Book
        skip_unchanged = True
    #以下省略

【コードの解説】

まず、ManyToManyフィールド用のウィジェットクラスとdjango-import-exportライブラリーのfieldsをインポートします。

from import_export import fields   #追加
from import_export.widgets import ManyToManyWidget   #追加

ManyToManyフィールドの値をエクスポートファイルに書き込むには、以下の通り対象のフィールド名(categories)をオーバーライドして定義します。

#many to manyフィールドのカテゴリ名でExport
categories = fields.Field(attribute='categories', widget=ManyToManyWidget(Category, field="name"))

再度ExcelファイルにExportすると以下の様にカテゴリ名が実際の値で書き込まれていることが確認できます。

エクスポート列と順序を指定したい場合

先ほどの例では、カテゴリ名が一番左側に書き込まれています。

また、デフォルトではID列も書き込まれてしまいます。

必要な列名だけ指定を行い、且つ、指定した順序列で書き込むには、 BookResourceクラスのMetaクラス内でfieldsexport_orderパラメータを指定します。

fieldsパラメータにはExport対象とするフィールドを指定します。
export_orderパラメータにはExportファイルの列の順序を指定します。

class BookResource(resources.ModelResource):
    #many to manyフィールドのカテゴリ名でExport
    categories = fields.Field(attribute='categories', widget=ManyToManyWidget(Category, field="name"))
    class Meta:
        model = Book
        skip_unchanged = True
        fields = [ 'name', 'author','categories']
        export_order = ['author', 'name','categories']

上記のようにfieldsexport_orderを設定すると、以下のようなExcelファイルが生成されます。
列は、著者名、書籍名、カテゴリ名の順になり、ID列は指定していないため除外されています。

adminサイトのアクション操作にExport機能を追加する

adminサイト画面上でExportしたいデータ(行)を選択して、操作メニューからデータをExportできるようなカスタマイズが可能です。

これは非常に簡単で、BookAdminクラスの承継元クラスにExportActionModelAdminを追加するだけで実現できます。

from import_export.admin import ExportActionModelAdmin   #追加


@admin.register(Book)
class BookAdmin(ImportExportMixin, ExportActionModelAdmin, admin.ModelAdmin):  #変更
    list_display=('pk','name', 'author')
    resource_class = BookResource
    formats = [base_formats.XLSX]

以下の通り、操作メニュー欄に「選択した書籍をエクスポート」メニューが追加されます。
フォーマットを選択して「実行」ボタンを押すことで選択した行のみExcelファイルとしてダウンロードできるようになります。

以上でこのレシピは終了です。
お疲れさまでした!


主にITテクノロジー系に興味があります。 【現在興味があるもの】 python、Django,統計学、機械学習、ディープラーニングなど。 技術系ブログもやってます。 https://sinyblog.com/