파이썬 객체(object), csv, json을 자유롭게 변환해서 입출력 하자.
목표
파이썬에서 오브젝트(클래스) 생성 후 객체의 프로퍼티(attribute)를 csv에 저장하고, CsvManager 클래스를 만들어서 원하는 데이터가 있는 row를 손쉽게 입출력 하는 것.객체(Object) <-> JSON <-> csv 상호 변환이 가능하다.* 특징 : 필자가 구현하고 싶은 건 nested object(맞는 표현인지는 잘 모르겠다...)도 json으로 변환되어 csv로 들어갈 수 있어야 된다는 게 가장 큰 차이점이다. 구글링해도 잘 안나와서 필자가 개고생해가며 발코드로 구현을 했다...
|
예를 들면 아래와 같은 객체(testobj)가 있다고 하면, TestClass의 property(속성) 대로 엑셀에 column을 맞춰서 예쁘게 넣기가 쉽지가 않다.
class TestClass():
def __init__(self, field1, field2, field3, keyword, rank):
self.field1 = field1
self.field2 = field2
self.field3 = TestClass2(999,888,'text1','text2')
self.keyword = keyword
self.rank = rank
class TestClass2():
def __init__(self,a,b,c,d):
self.a = a
self.b = b
self.c = TestClass3('text3','text4',321431211,2498918)
self.d = d
class TestClass3():
def __init__(self,e,f,g,h):
self.e = e
self.f = f
self.g = g
self.h = h
testobj = TestClass('여성의류','원피스','봄원피스','키워드테스트',100)
만약에 이미 존재하는 csv파일의 제일 아래에 testobj를 맞춰서 넣는다고 하면, 아래와 같이 될 것이다.
field1, field2, keyword, rank까지는 괜찮은데 field3이라는 property는 꽤나 복잡한 구조를 가지고 있다.
오브젝트를 어떻게 csv에 예쁘게 넣을 수 있는 지 고민하고 고생하다가, 그래도 나름의 꿀팁(?)인 것 같아서 글로 남기게 되었다.
그래서 첫 번째는, 기본이라 할 수 있는 csv 입/출력을 먼저 해볼 것이다.
먼저 import부터 해주자. 나는 라이브러리 사용을 굉장히 좋아한다.
import csv
import json
import numpy
import pandas
import re
from collections import namedtuple
json은 json관련된 작업을 할 때 유용하고, numpy는 일반적인 배열, 행렬 등의 연산이 파이썬에서 기본적으로 제공하는 자료형보다 훨씬 빠른 자료구조(?)를 제공한다고 한다.
pandas는 마찬가지로 csv관련 작업을 할 때 기본 제공되는 csv 라이브러리보다 훨씬 유용한 기능들이 제공된다.
class CsvManager:
def __init__(self, file):
self.file = file
self.data = pandas.read_csv(file)
self.header = self.data.columns
def reset(self, file):
self.file = file
self.data = pandas.read_csv(file)
self.header = self.data.columns
def save(self):
self.data.to_csv(self.file,index=False)
그리고 CsvManager를 대강 작성해보자. csv파일에 있는 데이터가 self.data에 세팅될 것이다.
데이터 참조는 self.data.iloc[행 번호]로 참조하면 dict형식으로 값이 리턴될 것이다.
구글에 검색하면 레퍼런스를 찾을 수 있을 것임.
a = CsvManager('./파일명.csv')
위와 같이 써주고, 이제 csv파일에 데이터를 row 단위로 add/remove 하는 함수를 작성해야된다.
먼저 csv에 특정 데이터를 1개의 row에 add해주는 함수를 작성을 해야된다.
그러기 위해선 먼저 오브젝트를 dictionary(딕셔너리)로 변환해주는 함수부터 작성을 할 것이다.
딕셔너리가 무엇인가?
딕셔너리(dictionary)
파이썬에서 딕셔너리(dictionary)란 사전형 데이터를 의미하며, key와 value를 1대1로 대응시킨 형태입니다.
이때 하나의 key에는 하나의 value만이 대응됩니다.
사전에서 단어를 찾으면 그에 대한 해설이 있는 것을 상상하면 쉽게 이해할 수 있습니다.
이 때, key 값은 절대로 변하지 않으며 value 값은 변경할 수 있습니다.
튜플과 다르게 key-value 쌍 자체를 수정하거나 삭제할 수 있기 때문에 유용하게 사용할 수 있습니다.
예제
dic = {1 : "My" , 2:"Name", 3:"Is", 4:"Python"}
dic[1] = My ...
출처 : http://tcpschool.com/python/types_dictionary
class CsvManager:
@staticmethod
def convertObjToJson(obj):
result = {}
for key in obj.__dict__:
if hasattr(getattr(obj,key), '__dict__'):
flag = True
temp = getattr(obj,key)
for underKey in temp.__dict__:
if hasattr(getattr(temp,underKey),'__dict__'):
flag = False
break
if flag == False:
result.update( [(key, CsvManager.convertObjToJson(getattr(obj,key)))] )
else:
result.update( [(key, temp.__dict__)] )
else:
result.update([( key, getattr(obj,key) )])
return result
함수 이름은 convertObjToJson이긴 한데 사실상 convertObjToDict라는 함수 명이 더 적절할 것 같다.
위 함수에 인자로 object(객체)를 넣으면 딕셔너리 형태로 변환해준다.
설명을 하자면, 첫 반복문에서 객체의 모든 프로퍼티를 참조한다.
TestClass로 예시를 들면 field1, field2, field3, keyword, rank가 key에 들어오게 된다.
getattr(obj,field1)을 하면 field1은 단순히 스트링 값('여성의류')이므로 __dict__라는 attr은 존재하지 않는다.
따라서 else:인 result.update([(key,getattr(obj,key))])가 실행된다.
field2, keyword, rank도 마찬가지로 단순한 스트링이므로 __dict__라는 속성은 존재하지 않아서 else로 가게 된다.
update함수는 dictionary에서 key, value쌍을 추가해주는 함수이다.
field3의 경우엔 value가 object이므로 __dict__가 존재한다. 그 아래의 for underKey in temp.__dict__:는 무슨 구문이냐면, field3의 value에 해당하는 object를 temp에 가져와서, 그 object의 모든 프로퍼티를 참조하는게 underKey가 된다.
flag 변수를 만든 이유는, 재귀로 계속해서 object를 깊이 파고 들어가다가 더 이상 프로퍼티중에 object가 하나도 없는 object가 나올 때, 그 object의 key와 dict쌍을 update해주기 위함이다.
만약 flag가 False면, 그 오브젝트의 프로퍼티 중에 오브젝트가 하나라도 존재하므로, convertObjToJson 함수를 한번 더 호출하여 한 단계 더 깊은 곳에서 똑같은 과정으로 탐색하라는 뜻이다.
아무튼 위와 같이 해서 딕셔너리를 얻을 수 있으니 이제 이 데이터를 csv의 row로 변환할 일만 남았다.
def addRow(self, obj):
mydict = self.convertObjToJson(obj)
df = pandas.DataFrame([mydict])
self.data = self.data.append(df)
self.data.to_csv(self.file,index=False)
CsvManager에 addRow 함수를 만들어서, 오브젝트를 row로 append할 수 있게 해준다.
위에 a = CsvManager()라고 선언해 뒀으니, a.addRow(testobj) 이런 식으로 사용하면 된다.
사용한 결과를 아래에 보여주도록 하겠다.
마지막 Row가 잘 추가된 것을 확인할 수 있는데, field3에 우리가 넣은 오브젝트가 중첩된(nested) json 형태로 잘 들어간 것을 확인할 수 있다.
이제 이걸 꺼내 쓸 수만 있다면 완벽하지 않은가?
나는 컬럼값이 특정 값에 해당하는 row들을 뽑아내고 싶어서, 아래와 같은 함수 CsvManager에 정의했다.
def consumeRow(self, colName, key):
returnData = self.data[self.data[colName] == key]
returnList = []
for row in returnData.iloc:
returnObject = {}
for col in returnData.columns:
strData = str(row[col])
if isValidJson(strData):
replacedString = str(strData.replace("'","\""))
returnObject[col] = json.loads(replacedString, object_hook=Generic.from_dict)
else:
returnObject[col] = row[col]
returnList.append(returnObject)
self.data = self.data[self.data[colName] != key]
self.data.to_csv(self.file,index=False)
return returnList
따라서 consumeRow('keyword','키워드입니다')라고 사용하게 되면 keyword 컬럼 값이 '키워드입니다' 에 해당하는 모든 Row들의 데이터를 출력하게 된다.
isValidJson함수의 역할은 strData(스트링 데이터)를 받았을 때 그게 dict형식이냐 아니냐를 판단하는 함수다. dict형식이 아니면 단순히 key : value 쌍이므로 returnObject[col]에 row[col]값을 대입을 한다. dict형식이라면 returnObject[col] 자체에 오브젝트를 넣어줘야 되므로 json.loads에 object_hook=Generic.from_dict를 옵션으로 주었다.
class Generic:
@classmethod
def from_dict(cls, dict):
obj = cls()
obj.__dict__.update(dict)
return obj
따라서 dict string이 객체로 예쁘게 탄생하게 된다.
isValidJson 함수는 너무 더럽게 구현해서, 여러분이 예쁘게 다시 작성해줬으면 하는 바람에 올리지 않도록 하겠다.
스트링을 인자로 받아서, {'key':'value' , 'key':{'key':'value'}} 꼴인지 아니면 단순 스트링인지 판단해서 True False를 리턴해주면 된다.
newDict = a.consumeRow('keyword','키워드입니다')
print(newDict)
print(newDict[1]['field3'].c.e)
자 이제 다 끝나서 실행만 남았다. csv에 아래와 같이 넣고 위 코드를 실행시켜 보자.
field1,field2,field3,keyword,rank
패션의류,여성의류,니트/스웨터,니트,1
패션의류,여성의류,니트/스웨터,여성브이넥니트,2
남성의류,니트,봄니트,니트1,3
남성의류,니트,여름니트,니트2,4
남성의류,니트,가을니트,니트3,5
여성의류,원피스,"{'a': 999, 'b': 888, 'c': {'e': 'text3', 'f': 'text4', 'g': 321431211, 'h': 2498918}, 'd': 'text2'}",키워드입니다,6
여성의류,원피스,"{'a': 999, 'b': 888, 'c': {'e': 'text3', 'f': 'text4', 'g': 321431211, 'h': 2498918}, 'd': 'text2'}",키워드입니다,7
실행 결과
[{'field1': '여성의류', 'field2': '원피스', 'field3': <__main__.Generic object at 0x000001EDE482DC70>, 'keyword': '키워드입니다', 'rank': 6}, {'field1': '여성
의류', 'field2': '원피스', 'field3': <__main__.Generic object at 0x000001EDE482DCA0>, 'keyword': '키워드입니다', 'rank': 7}]
text3
결과가 2 row가 나왔으므로, newDict[1]으로 두번째 row를 참조했다.
따라서 newDict[1]['field3'].c.e는 text3이 잘 출력이 되었다.
만약에 newDict[1]['field3'].c.f를 참조하면 text4가 출력이 될 것이다.
그리고 keyword가 '키워드입니다'인 row를 consume(소모) 했으므로 csv파일을 다시 열어보면
아래와 같이 되어있을 것이다.
field1,field2,field3,keyword,rank
패션의류,여성의류,니트/스웨터,니트,1
패션의류,여성의류,니트/스웨터,여성브이넥니트,2
남성의류,니트,봄니트,니트1,3
남성의류,니트,여름니트,니트2,4
남성의류,니트,가을니트,니트3,5
'파이썬' 카테고리의 다른 글
파이썬으로 주격 조사(은/는) 구분하기 (0) | 2020.03.21 |
---|