Djangoでcsvをインポートするならdjango-import-exportが便利

Django

DjangoAdminでcsvをインポートしたかった

DjangoAdminでcsvをインポートする機能を付けたかったのですが、これを自分で実装したら大分面倒だなと思い、パッケージを探していたら良さそうなものがありました。それがこちら。

Dark Mode

django-import-export (this link opens in a new window) by django-import-export (this link opens in a new window)

Django application and library for importing and exporting data with admin integration.

とても便利なのですが、ドキュメントがそこまで充実しておらず、癖がちょっとあったので、使い方を紹介したいと思います。

ModelResourceのサブクラスを作成し、ModelAdminのサブクラスで設定する

基本的な使い方は以下のような感じです。
django-import-exportでは、csvの一行目をカラム名として扱います。なので、このカラム名がDjangoのモデルのフィールド名と合っていれば、ModelResourceのサブクラスでモデル名とカラム名を書き、ModelAdminのサブクラスでresource_classとformatsを指定するだけです。

今回は、csvファイルのインポートだけだったので、ImportMixinを指定していますが、これを指定しなければエクスポートも出来ます。また、フォーマットもcsv以外にもyamlやjson、tsvなどの指定も出来ます。

Model

from django.db import models

class Book(models.Model):
    author = models.ForeignKey(Author, null=True, on_delete=models.CASCADE)
    name = models.CharField(max_length=300)
    status = models.BooleanField(default=True)

    class Meta:
        verbose_name = 'Book'

class Author(models.Model):
    name = models.CharField(max_length=50, unique=True)

    class Meta:
        verbose_name = 'Author'

Admin

from django.contrib import admin
from import_export.resources import ModelResource
from import_export.admin import ImportMixin
from import_export.formats import base_formats
from books.models import Book

class BookResource(ModelResource):
    class Meta:
        model = Book

        import_order = ('id', 'name', 'status')
        import_id_fields = ['id']

class BookAdmin(ImportMixin, admin.ModelAdmin):
    list_display = ('id', 'name', 'status')
    resource_class = BookResource
    formats = [base_formats.CSV]

admin.site.register(Book, BookAdmin)

csvのカラム名とモデルのフィールド名が違う場合

こういうケースが殆どだと思いますが、自分も例外なくcsvのカラム名とモデルのフィールド名が違いまして、ここで一度詰まりました。

そういった場合には、fieldをoverwriteすることで可能でした。やり方はこのような感じで、ModelResource内でfieldを追加してあげます。

from import_export.resources import ModelResource
from import_export.fields import Field

class BookResource(ModelResource):
    id = Field(attribute='id', column_name='UUID')
    name = Field(attribute='name', column_name='Book Name')
    status = Field(attribute='status', column_name='Status')

    class Meta:
        model = Book
        import_order = ('id', 'name', 'status')
        import_id_fields = ['id']

このようにFieldを追加してあげて、column_nameにcsv側のカラム名をしてあげることで、簡単にマッチングさせることが出来ました。

ForeignKeyの先にある値とマッチングさせたい

モデル側では、ForeignKeyを指定していて、その先にある値を使ってPrimaryKeyを取ってきてセットしたいということがありました。このパッケージでは、そういったこともとても簡単に出来ます。

from import_export.resources import ModelResource
from import_export.fields import Field

class BookResource(ModelResource):
    id = Field(attribute='id', column_name='UUID')
    author = Field(attribute='author', column_name='Author', widget=ForeignKeyWidget(Author, 'name'))
    name = Field(attribute='name', column_name='Book Name')
    status = Field(attribute='status', column_name='Status')

    class Meta:
        model = Book
        import_order = ('id', 'author', 'name', 'status')
        import_id_fields = ['id']

この例では、csvファイル側ではauthorのidは分からず、authorのnameしか取れないというような状態です。
そこで、既にAuthorモデル側に入っているデータとnameでマッチしたidをForeignKeyWidgetを使用することで引いてくることが出来ました。とても便利ですね。

validationが掛かる前にデータを書き換えたい

django-import-exportでは、インポート前にvalidationが掛かります。もし必要があれば、その前にデータを書き換える必要があります。

例えば、statusをBooleanで設定しているにも関わらず、csv側はOK/NGで書かれているなどです。
そういった場合には、データを読み込み、validationが掛かる前にbefore_import_rowでデータを書き換えましょう。

class BookResource(ModelResource):
    class Meta:
        model = Book
        import_order = ('id', 'name', 'status')
        import_id_fields = ['id']

    def before_import_row(self, row, row_number=None, **kwargs):
        row['status'] = True if row['status'] == 'OK' else False

before_import_rowは、行毎にデータを判定出来るので、rowにキーを指定してあげれば値を取得出来ます。また、その値をチェックし、上記のように入れ直してあげることで、データの書き換えが可能です。

インポート時にエラーのあった行はスキップする

ForeignKeyが存在しない場合や、フィールドの型が違う場合などでvalidationに引っかかった場合も、それらの行はスキップしてインポートに進みたい場合もあるかと思います。

そのような場合は、確認画面でエラー内容は表示しつつ、インポートまで進めるように以下の行を追加することで可能になります。

class SubModelResource(ModelResource):
    def get_field_names(self):
        names = []
        for field in self.get_fields():
            names.append(self.get_field_name(field))
        return names

    def import_row(self, row, instance_loader, **kwargs):
        # overriding import_row to ignore errors and skip rows that fail to import
        # without failing the entire import
        import_result = super(ModelResource, self).import_row(
            row, instance_loader, **kwargs
        )

        if import_result.import_type == RowResult.IMPORT_TYPE_ERROR:
            import_result.diff = [
                row.get(name, '') for name in self.get_field_names()
            ]

            # Add a column with the error message
            import_result.diff.append(
                "Errors: {}".format(
                    [err.error for err in import_result.errors]
                )
            )
            # clear errors and mark the record to skip
            import_result.errors = []
            import_result.import_type = RowResult.IMPORT_TYPE_SKIP

        return import_result

最後に

ちょっと癖のあるパッケージですが、使いこなすととても便利です。
他にもManyToManyFieldの場合など使っていない要素もあるので、また使う機会があれば、追記していきたいと思います。

コメントを残す