본문 바로가기

파이썬

파이썬 객체(class), json, csv를 자유롭게 변환해서 입출력 하자.

파이썬 객체(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를 맞춰서 넣는다고 하면, 아래와 같이 될 것이다.

vscode의 Excel Viewer라는 익스텐션이다.

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