読者です 読者をやめる 読者になる 読者になる

nanahara.log

プログラミング関係のノート

PythonでCSVをDBにインポート(with Flask-Script and SQLAlchemy)

Python Flask

Flask + SQLAlchemyを使っていて、フィクスチャ、サンプルデータ、マスタデータ等をDBに入れたいが、何かしらのファイル形式(今回はCSV)で書いたものをDBへとインポートする仕組みを作りたい

LOAD DATA INFILE 使えばええやんって話もあるけど、インポートの時点である程度のバリデートもできる(実際はデータを入れてから流すケースが多そうではあるが)ので、一考の価値はありかと

例えば、下記のようなテーブル(Message)に対して

mysql> desc message;
+------------+-----------+------+-----+---------+----------------+
| Field      | Type      | Null | Key | Default | Extra          |
+------------+-----------+------+-----+---------+----------------+
| message_id | int(11)   | NO   | PRI | NULL    | auto_increment |
| author     | text      | YES  |     | NULL    |                |
| text       | text      | YES  |     | NULL    |                |
+------------+-----------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

以下のようなCSV(message.csv)を用意して、データを取り込むようにする

message_id,author,text
1,Tom,This is a sample message
2,Ken,Oops!

Flask-Scriptとは

Flaskで外部スクリプトを簡単に書くためのextention。

Djangoでいうmanage.pyのようなものが実現できる

Flask-Script — Flask-Script 0.4.0 documentation

@manager.command
def hello():
    "Just say hello"
    print "hello"
python manage.py hello
> hello

↑は公式の引用だが、こういった形で使用できる

CSVをDBにインポートする

CSVに対してmodel(SQLAlchemy)を動的にロードしDBに突っ込む

将来のmodelの追加・変更を考えるとインポートのロジック自体にmodelの情報を持たせたくない

ここではCSVのファイル名をクラス名、CSVの1行目をクラスの各属性の情報として役立てるようにしている

from flask_script import Manager
from skeleton import app, db
import csv
import importlib
import os


manager = Manager(app)


@manager.command
def import_csv():
    fixtures_dir = app.config['FIXTURES_DIR']
    models = importlib.import_module('skeleton.models')

    for file_name in os.listdir(fixtures_dir):
        class_name = file_name.replace(".csv", "").capitalize()
        Klass = getattr(models, class_name)
        with open(fixtures_dir + '/' + file_name, encoding='utf-8') as csv_file:
            reader = csv.reader(csv_file, delimiter=',')
            header = next(reader)
            for row in reader:
                klass = Klass()
                for i in range(len(header)):
                    setattr(klass, header[i], row[i])
                db.session.add(klass)
            db.session.commit()


if __name__ == "__main__":
    manager.run()

件数多ければbulk insertにする必要がある

Pythonメモ

  • importlib : モジュールの動的ロードしてくれるライブラリ
  • getattr : object の指名された属性の値を返す組み込み関数。getattr(x, ‘foobar’) は x.foobar と等価
  • setattr : 値を属性に関連付ける組み込み関数。setattr(x, ‘foobar’, 123) は x.foobar = 123 と等価

31.5. importlib — import の実装 — Python 3.6.0 ドキュメント
2. 組み込み関数 — Python 3.6.0 ドキュメント

modelやdb周りの簡素な設定も含んだコードはこれ