【Django】CSVファイルをインポートしてDBにデータを登録する

# はじめに
（MySQLとか使ってて）初期データをインポートしたい時なんかはHeidiSQLみたいなクライアントソフトを使えばいいんですが、勉強がてら作ってみました。
# モデルの例
こんなモデルを用意してみました。
WeaponテーブルのデータをCSVでインポートしたいと思います。
外部キーも使いたかったので、サブウェポンとスペシャルを無理やりテーブルにして外部キーとして参照させています。

```Python:models.py
class SubWeapon(models.Model):
    # TODO: Define fields here
    name = models.CharField('名前', blank=True, max_length=100)
    overview = models.TextField('概要', blank=True)


    class Meta:
        verbose_name = 'サブウェポン'
        verbose_name_plural = 'サブウェポン一覧'

    def __str__(self):
        return(self.name)


class Special(models.Model):
    # TODO: Define fields here
    name = models.CharField('名前', blank=True, max_length=100)
    overview = models.TextField('概要', blank=True)

    class Meta:
        verbose_name = 'スペシャル'
        verbose_name_plural = 'スペシャル一覧'

    def __str__(self):
        return(self.name)


class Weapon(models.Model):
    # TODO: Define fields here
    no = models.IntegerField('番号', blank=True, null=True)
    name = models.CharField('ブキ名', blank=True, max_length=100)
    CATEGORY = (
        ('0', 'シューター'),
        ('1', 'マニューバー'),
        ('2', 'ブラスター'),
        ('3', 'スピナー'),
        ('4', 'ローラー'),
        ('5', 'フデ'),
        ('6', 'チャージャー'),
        ('7', 'スロッシャー'),
        ('8', 'シェルター'),
        ('9', 'ガイザー'),
    )
    category = models.CharField(blank=True, max_length=2, choices=CATEGORY)
    sub = models.ForeignKey(SubWeapon, on_delete=models.PROTECT, null=True, blank=True)
    special = models.ForeignKey(Special, on_delete=models.PROTECT, null=True, blank=True)
    gauge = models.IntegerField('スペシャル必要ポイント', blank=True, null=True)
    release_condition = models.CharField('解放条件', blank=True, max_length=100)
    range = models.IntegerField('射程', blank=True, null=True)
    continuous_power = models.IntegerField('連射力', blank=True, null=True)
    fixed_num = models.IntegerField('確定数', blank=True, null=True)

    class Meta:
        verbose_name = 'ブキ'
        verbose_name_plural = 'ブキ一覧'

    def __unicode__(self):
        return(self.name)
```
ちなみにスプラトゥーン2のブキは2018/4/17時点で80種類近く登場しているので、これらの情報をDjangoの管理画面から1つ1つ登録するなんてのは愚の骨頂です。

# CSVファイルの用意
作ったモデルを元に、以下のようなCSVファイルを用意します。
※実際にインポートするときはヘッダー行は消してください。

また、`models.py`で`category`をchoicesで指定し、その選択肢を0～9の数字で設定したため、カテゴリの列には数字を入力する必要があります。
選択肢を数字ではなく、文字列にしておけばそのまま文字列で大丈夫なはずです。（未検証）

```CSV
#番号,名前,カテゴリ,サブウェポン,スペシャル,スペシャル必要ポイント,解放条件,射程,連射力,確定数
101,わかばシューター,0,スプラッシュボム,インクアーマー,180,最初から,35,75,4
102,もみじシューター,0,ロボットボム,アメフラシ,180,ランク4,35,75,4
103,スプラシューター,0,クイックボム,スーパーチャクチ,200,ランク2,50,60,3
104,スプラシューターコラボ,0,スプラッシュボム,ジェットパック,210,ランク4,50,60,3
：
：
```

# views.py
続いて `views.py` を書いていきます。

```Python:views.py
from django.shortcuts import render
from .models import SubWeapon, Special, Weapon
import csv
from io import TextIOWrapper, StringIO


def upload(request):
    if 'csv' in request.FILES:
        form_data = TextIOWrapper(request.FILES['csv'].file, encoding='utf-8')
        csv_file = csv.reader(form_data)
        for line in csv_file:
            weapon, created = Weapon.objects.get_or_create(name=line[1])
            weapon.no = line[0]
            weapon.name = line[1]
            weapon.category = line[2]
            weapon.sub = SubWeapon.objects.get(name=line[3])
            weapon.special = Special.objects.get(name=line[4])
            weapon.gauge = line[5]
            weapon.release_condition = line[6]
            weapon.range = line[7]
            weapon.continuous_power = line[8]
            weapon.fixed_num = line[9]
            weapon.save()

        return render(request, 'YpurApp/upload.html')

    else:
        return render(request, 'YourApp/upload.html')
```
最初にモデルやら必要なモジュールやらを読み込んでおきます。
ポイントはhtmlの`<input>`タグで指定した`name`を拾ってくるのでここの指定を間違えないことです。（上の例では`csv`と指定しています。）
`request.FILES`で受け取ったCSVファイルを`csv.reader`で読み込み、読み込んだファイルをfor文で回し、1行ずつ取り出します。

`line`はリスト形式でCSVの1行のデータが1つずつ入っています。
`line[0]`は番号、`line[1]`は名前、といった感じ。

```Python
weapon, created = Weapon.objects.get_or_create(name=line[1])
```
データの重複登録を避けるため、`get_or_create`を使ってWeaponテーブルに既に同じ名前のものが存在していれば新たにレコードは登録せずにgetするだけにします。

外部キーである`sub`と`special`はそれぞれのテーブルからCSVに書かれたデータと一致するものをgetすればセット可能です。

```Python
weapon.sub = SubWeapon.objects.get(name=line[3])
weapon.special = Special.objects.get(name=line[4])
```
for文の処理が全て終わればテンプレートを描画します。
今回はサクセスメッセージなどは表示させず、単純にアップロード画面を表示するだけにしています。

# urls.py
ブラウザからアクセスできるようにマッピングします。

```Python:urls.py
from django.urls import path
from . import views

app_name = 'YourAppName'
urlpatterns = [
    # :
    # :
    path('upload/', views.upload, name='upload'),
    # :
    # :
]
```

# upload.html
標準のファイル選択ボタンは味気ないので[Dropify](https://github.com/JeremyFagis/dropify)を利用しています。
↓このサイトを参考にさせて頂きました。
[あのダサいデザインとはおさらば! input[type=file] をかっこ良く表示できる jQuery プラグイン『Dropify』の使い方](http://phiary.me/jquery-plugin-dropify/)

あとCSSフレームワークに[Materialize](http://materializecss.com/)も利用しています。

```html:
{% extends "base.html" %}
{% load static %}
{% block title %}ブキデータアップロード{% endblock %}
{% block extrahead %}
<script>
$(function(){
  $('.dropify').dropify();
})
</script>
{% endblock %}

{% block content %}
<h3>アップロードするよ</h3>
<form action="{% url 'YourAppName:upload' %}" method="post" enctype="multipart/form-data">
  {% csrf_token %}
  <input type="file" class="dropify" data-default-file="ファイルを選択してください" name="csv">
  <button type="submit" class="btn" name="button">アップロード</button>
</form>

{% endblock content %}
```
![2018-04-17_15h15_18.png](https://qiita-image-store.s3.amazonaws.com/0/73844/206b5fe6-38d6-6768-6eb2-5a3893c19825.png)

# 完成
あとはこの画面からCSVファイルをインポートしてエラーが出なければOKです。
Djangoの管理画面にログインし、データがと登録されているか確認しましょう。

![2018-04-17_15h42_47.png](https://qiita-image-store.s3.amazonaws.com/0/73844/11bd728b-d165-afb9-2697-943004b28d3d.png)

無事登録できました。
※Django IDが3桁になってるのは色々試行錯誤した結果です。

# 終わりに
本当はアップロードされたファイルの拡張子がちゃんとCSVかどうかとか、入力データが正しいかとか、ちゃんとバリデーションチェックしないといけませんが、今回は一切考慮していません。
本番で提供するアプリケーションではもっとちゃんと実装しましょう（自戒）

あとCSVデータ作るのが一番しんどかった。