Djangoで顧客管理システムを作るテストをするのに、ダミーの顧客情報を作る方法を以前紹介した。
(個人情報のダミーデータを作ってくれるサイト)
そこで作ったダミーの顧客データをDjangoに入力するのに、どのようにしたらいいのか調べてみると・・・
manage.pyにloaddata というFixtureを入力するコマンドがあることを知った。
> manage.py loaddata data.json
Fixture は XML, JSON, YAML形式で書いていたらOKらしい。
manage.py dumpdata appname
で出力したデータがそのままFixtureに使えるそうなので、それを参考にCSVファイルからJSON形式のFixtureを作るスクリプトを作ってみた。
JSON形式のFixtueの構造
まず始めにJSON形式のFixtureの構造は以下のようになっている。
[
{"pk": 1,
"model": "customer.item",
"fields":
{"category": "\u30af\u30ed\u30b9",
"code": "a-silvercloth",
"name": "\u9280\u307f\u304c\u304d\u30af\u30ed\u30b9\uff082\u679a\u5165\uff09",
"price": "525",
"memo": "\u9280\u78e8\u304d\u30af\u30ed\u30b9\u30022\u679a\u5165\u308a\u3002",
"unit": "\u500b"}
},
{"pk": 2,
"model": "customer.item",
"fields":
{"category": "\u30d0\u30f3\u30b0\u30eb",
"code": "ba-ki-l0001",
"name": "\u30ec\u30c7\u30a3\u30b9\u30d0\u30f3\u30b0\u30eb\u3000\u300e\u83ca\u6c34\u300f",
"price": "70350",
"memo": "",
"unit": "\u500b"}
}
]
Objectで書かれた各データの配列になっていて、各データのObjectは
pk — PrimaryKeyの値
model — Djangoのモデル名 アプリ名.モデルクラス名 の形式で表記
fields — 各フィールドのフィールド名と値のObject
となっている。
ちなみにJSONの構文は以下のページを見れば良く分かる。
JSONの紹介
手書きでも書けるので、初期データを放り込むのにカンタンなものなら手書きで書いてもいいんだけど、顧客ダミーデータは5000件あるのでそれを手書きで変換するのはめんどくさい。
そこで、カンタンに変換できるツールを作ってみた。
CSVデータをJSONのFixtureに変換するツール(Pythonスクリプト)
ツールの構成は
1.変換用スクリプト make_fixture.py
2.変換設定ファイル(JSON形式)
からなっていて、コマンドラインで利用する。
使い方は、変換設定ファイルを書いて、コマンドラインから
make_fixture.py -s 設定ファイル -i 変換元CSVファイル -o 変換後の出力ファイル
とするだけ。
各ファイルはUTF-8で使うことを前提にしている(UTF-8でしかテストしていない)
例として前回作ったダミーの顧客情報を変換する設定ファイルを載せてみる。
変換元のCSVデータはこんな感じ
姓,名,姓(カタカナ),名(カタカナ),性別,電話番号,携帯電話,メールアドレス,郵便番号,住所,,,,,生年月日,年齢 早川,順,ハヤカワ,ジュン,M,03-0043-5967,080-1129-0749,srkpfajun112@ftapigbq.uh,229-0032,神奈川県,相模原市,矢部,2-8,グランド矢部403,1980/03/09,29 都築,武雄,ツズキ,タケオ,M,03-8521-5608,080-4747-0980,takeotsuzuki@cukt.npkbr.fi,393-0046,長野県,諏訪郡下諏訪町,東赤砂,4-5-15,,1988/05/27,21 内藤,果歩,ナイトウ,カホ,F,03-7080-8426,090-0443-2574,jyqydqafb-kaho64604@pljimta.qc,794-0015,愛媛県,今治市,常盤町,1-3-18,,1979/07/05,30 岩永,紅葉,イワナガ,クレハ,F,03-7063-2531,080-1587-4004,dyjkpsnkureha28732@yoiyr.ks,382-0806,長野県,上高井郡高山村,なかひら,3-11-16,,1944/03/13,65 永野,志保,ナガノ,シホ,F,03-7739-4442,,shiho5190@voyifkg.pd,297-0075,千葉県,茂原市,押日,2-11-11,,1944/08/21,65
※これは、完全にダミーのデータなのでこの世に存在しない人の情報ですが、偶然にも存在してしまった場合はごめんなさい。
この、CSVデータをDjangoの以下のモデル用のFixtureに変換する。
class Customer(models.Model):
SEX_CHOICES = (
('M', u'男性'),
('F', u'女性'),
)
AGE_CHOICES = (
('-', u'---'),
('-20', u'20最未満'),
('20-29', u'20歳-29歳'),
('30-39', u'30歳-39歳'),
('40-49', u'40歳-49歳'),
('50-', u'50歳以上'),
)
JOB_CHOICES = (
('-', u'---'),
(u'会社員', u'会社員'),
(u'自営業', u'自営業'),
(u'公務員', u'公務員'),
(u'団体職員', u'団体職員'),
(u'主婦', u'主婦'),
(u'学生', u'学生'),
(u'その他', u'その他'),
)
sei = models.CharField(u'姓', max_length=100)
mei = models.CharField(u'名', max_length=100)
kanasei = models.CharField(u'かな姓', max_length=100)
kanamei = models.CharField(u'かな名', max_length=100)
zip = models.CharField(u'郵便番号', max_length=10)
pref = models.CharField(u'都道府県', max_length=20)
add1 = models.CharField(u'住所1', max_length=256)
add2 = models.CharField(u'住所2', max_length=256, blank=True)
tel1 = models.CharField(u'TEL1', max_length= 20)
tel1_is_mobile = models.BooleanField(u'TEL1は携帯電話', default= False)
tel2 = models.CharField(u'TEL2', max_length = 20, blank=True)
tel2_is_mobile = models.BooleanField(u'TEL2は携帯電話', default= False)
fax = models.CharField(u'FAX', max_length = 20, blank=True)
email1 = models.CharField(u'email1', max_length=100)
email1_is_mobile = models.BooleanField(u'email1は携帯メール', default= False)
email2 = models.CharField(u'email2', max_length=100, blank=True)
email2_is_mobile = models.BooleanField(u'email2は携帯メール', default= False)
is_inform = models.BooleanField(u'お知らせメール許可', default=True)
sex = models.CharField(u'性別', max_length=1, choices=SEX_CHOICES)
birthday = models.DateField(u'生年月日', null=True, blank=True)
age = models.IntegerField(u'年齢', null=True, blank=True)
ages = models.CharField(u'年代', max_length=10, choices=AGE_CHOICES, blank=True)
jobclass = models.CharField(u'職業区分', max_length=20, choices=JOB_CHOICES)
occupation = models.CharField(u'職業', max_length=100, blank=True)
first_contact = models.DateField(u'初回接触', null=True, blank=True)
first_buy = models.DateField(u'初回購入日', null=True, blank=True)
モデルと元データを比べてみたら分かると思うが、元データにはモデルの全てのフィールドが定義されているわけではなく、また、
元データのフィールドの値を加工しなければモデルの合わないデータも存在する。
住所の項目などでは、CSVのいくつかのフィールドを結合しなければモデルの住所データに合わないし、
誕生日の日付の形式はPythonで使っているyyyy-mm-dd 形式に変換しなければいけない。
(Python側で日付のフォーマット形式を変更することも出来そうだけど、今回はデータを変換して使う)
また、携帯電話項目には tel2_is_mobile を常に True としたデータを作りたい。
ということで、以下のような設定ファイルで、モデルにあるJSON形式のFixtureを作れるようにした。
{"model": "customer.customer",
"header": true,
"fields": [{"field": "sei", "col": [0]},
{"field": "mei", "col": [1]},
{"field": "kanasei", "col": [2]},
{"field": "kanamei", "col": [3]},
{"field": "sex" , "col": [4]},
{"field": "tel1", "col": [5]},
{"field": "tel2", "col": [6]},
{"field": "tel2_is_mobile", "static": true},
{"field": "email1", "col": [7]},
{"field": "zip", "col": [8]},
{"field": "pref", "col": [9]},
{"field": "add1", "col": [10, 11, 12]},
{"field": "add2", "col": [13]},
{"field": "is_inform", "static": true},
{"field": "birthday", "col": [14], "replace": ["/", "-"]},
{"field": "age", "col": [15]}
]
}
設定の意味は以下の通り
model — Djangoのモデル名 アプリ名.モデルクラス名 を小文字で
header — CSVファイルにヘッダー行があるか? ある場合(1行目がデータでない場合) true
fields — データのフィールドの定義をObjectの配列で設定
—- fields 内の意味 —-
ひとつのフィールドはひとつのObject
field — モデルのフィールド名 Django のモデルのフィールド名
col — CSVの何カラム目を当てはめるのかの配列
複数のカラムを指定するとそれらを結合したものをそのフィールドのデータとする
例 {“field”: “add1″, “col”: [10, 11, 12]}
CSVの10カラム目、11カラム目、12カラム目を結合したデータをadd1に入力する。
static — 常に一定のデータを入力したいときに利用
例 {“field”: “is_inform”, “static”: true}
is_inform は常にTrueを入力する。
replace — CSVのカラム内のデータの一部を置換したデータにして入力する
例 {“field”: “birthday”, “col”: [14], “replace”: ["/", "-"]}
birthday フィールドにはCSVの14カラム目のデータの / を - に置換して入力
2010/02/12 を 2010-02-12 に変換する
replace は re.subを使っているので、正規表現が使える(と思う)
select — 選択データを入力する
例 {“field”: “is_inform”, “col”: [16], “select”: {“Y”: true, “N”: false}}
is_inform にはCSVの16カラム目のデータがYの場合は True、 Nの場合はFalseを入力する。
上のデータを変換するとこんな感じになる。
[{"pk": 1, "model": "customer.customer", "fields": {"tel1": "03-0043-5967", "is_inform": true, "add2": "\u30b0\u30e9\u30f3\u30c9\u77e2\u90e8403", "add1": "\u76f8\u6a21\u539f\u5e02\u77e2\u90e82-8", "zip": "229-0032", "age": "29", "pref": "\u795e\u5948\u5ddd\u770c", "mei": "\u9806", "kanamei": "\u30b8\u30e5\u30f3", "birthday": "1980-03-09", "sex": "M", "tel2": "080-1129-0749", "kanasei": "\u30cf\u30e4\u30ab\u30ef", "tel2_is_mobile": true, "sei": "\u65e9\u5ddd", "email1": "srkpfajun112@ftapigbq.uh"}}, {"pk": 2, "model": "customer.customer", "fields": {"tel1": "03-8521-5608", "is_inform": true, "add2": "", "add1": "\u8acf\u8a2a\u90e1\u4e0b\u8acf\u8a2a\u753a\u6771\u8d64\u78024-5-15", "zip": "393-0046", "age": "21", "pref": "\u9577\u91ce\u770c", "mei": "\u6b66\u96c4", "kanamei": "\u30bf\u30b1\u30aa", "birthday": "1988-05-27", "sex": "M", "tel2": "080-4747-0980", "kanasei": "\u30c4\u30ba\u30ad", "tel2_is_mobile": true, "sei": "\u90fd\u7bc9", "email1": "takeotsuzuki@cukt.npkbr.fi"}}, {"pk": 3, 以下省略
さて、肝心のツールのコードは以下
<追記 2010/02/13>
以前のコードでUTF-8回りでうまく動かないところがあった(select がうまく動いていなかった)ので、修正したソースに変更。
追加機能として
type -- 数値型の型指定をする場合に使う。type指定していない場合は文字列としてデータを扱う。
例 {"field": "price", "col": [2], "type": "integer"}
priceフィールドには2番目のカラムのデータをinteger型で入力。
型指定は現在のところ integer とfloat だけ存在している。
< 追記 2010/02/15 >
余計なデバッグ用print文があったので削除。
make_fixture.py
# -*- coding: utf-8 -*-
import csv
import cStringIO
import json
import codecs
import re
import sys
from optparse import OptionParser
# PythonのリファレンスマニュアルよりUnicode対応csv.reader csv.writerラッパークラス
# http://pythonjp.sourceforge.jp/dev/library/csv.html#csv-examples
class UTF8Recoder:
"""
Iterator that reads an encoded stream and reencodes the input to UTF-8
"""
def __init__(self, f, encoding):
self.reader = codecs.getreader(encoding)(f)
def __iter__(self):
return self
def next(self):
return self.reader.next().encode("utf-8")
class UnicodeReader:
"""
A CSV reader which will iterate over lines in the CSV file "f",
which is encoded in the given encoding.
"""
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
f = UTF8Recoder(f, encoding)
self.reader = csv.reader(f, dialect=dialect, **kwds)
def next(self):
row = self.reader.next()
return [unicode(s, "utf-8") for s in row]
def __iter__(self):
return self
class UnicodeWriter:
"""
A CSV writer which will write rows to CSV file "f",
which is encoded in the given encoding.
"""
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
# Redirect output to a queue
self.queue = cStringIO.StringIO()
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
self.stream = f
self.encoder = codecs.getincrementalencoder(encoding)()
def writerow(self, row):
self.writer.writerow([s.encode("utf-8") for s in row])
# Fetch UTF-8 output from the queue ...
data = self.queue.getvalue()
data = data.decode("utf-8")
# ... and reencode it into the target encoding
data = self.encoder.encode(data)
# write to the target stream
self.stream.write(data)
# empty queue
self.queue.truncate(0)
def writerows(self, rows):
for row in rows:
self.writerow(row)
# CSVをDjango用のFixtureに変換するクラス
class CSV2Fixture:
def __init__(self, settingf, inputf, outputf):
self.setting_file = settingf
self.data_file = inputf
self.fixture = outputf
self.setting = json.load(open(self.setting_file, 'r'))
self.csvdata = UnicodeReader(open(self.data_file, 'r'))
self.data = []
self.model_name = self.setting['model']
self.index = 1
def load_one_line(self, line):
fields = {}
for field in self.setting['fields']:
fieldname = field['field']
data = ''
#-- カラムからデータを取り出す場合
try:
if field['col']:
for col in field['col']:
data += line[col]
try:
#-- 選択値データの場合 --
if field['select']:
data = field['select'][data]
except KeyError:
pass
try:
#-- データの変換がある場合 --
if field['replace']:
data = re.sub(field['replace'][0], field['replace'][1], data)
except KeyError:
pass
try:
#-- データが数値の場合 --
if field['type']:
if field['type'] == 'integer':
data = int(data)
elif field['type'] == 'float':
data = float(data)
except KeyError:
pass
except:
pass
#-- 固定値データの場合 --
try:
if field['static']:
data = field['static']
except:
pass
fields[fieldname] = data
return fields
def execute(self):
#ヘッダー処理 settingファイル内で "header": true の場合、1行目をフィールド名とみなして1行目を取り込まない
try:
if self.setting['header']:
self.csvdata.next()
except:
pass
#データの処理
for line in self.csvdata:
a=self.load_one_line(line)
if 'pk' in a:
self.data.append({'model': self.model_name,
'fields': a})
else:
self.data.append({'pk': self.index,
'model': self.model_name,
'fields': a})
self.index += 1
json_data = json.JSONEncoder().encode(self.data)
outf = codecs.open(self.fixture, 'w', encoding='utf-8')
outf.write(json_data)
outf.close()
if __name__ == '__main__':
parser = OptionParser()
parser.add_option("-s", "--setting", dest="settingfile", default="setting.json", help="Setting File (JSON)")
parser.add_option("-i", "--input", dest="inputfile", default="input.csv", help="Input File (CSV)")
parser.add_option("-o", "--output", dest="outputfile", default="output.json", help="Output File (JSON): This file is Fixture for Django apprication model.")
(options, args) = parser.parse_args(sys.argv)
x = CSV2Fixture(options.settingfile, options.inputfile, options.outputfile)
x.execute()
カンタンなコードの割にはかなり便利である。
よければ自由に使ってみてください。
もう少し発展させて、リレーションのあるモデル用のCSV-JSON Fixtureの変換も出来るようにしたいところです。







