JSON 파싱

요청 및 json 패키지를 이용하여 JSON 데이터를 파싱할 때 아래와 같이 각 키의 데이터를 사전 형식으로 변환하고 값을 변경하여 데이터를 변경한다.

data = {"key1":"value1"}
data("key1") = "value2"

특정 값을 가진 키를 찾기 위해 전체 데이터를 스캔하려고 합니다.

다음 코드로 전체를 확인(for, if 사용)하면서 원하는 값의 키를 찾았습니다.

data = {
 "key1":"value1",
 "key2":"value2",
 "key3":"value3",
 "key4":"value4",
 "key5":"value5",
}

for key in data:
  if "value3" == data(key):
    print(key)
    break

간단한 데이터의 경우 위의 찾기 방법으로 충분하지만, 데이터의 수가 매우 많거나 복잡한 경우에도 같은 방법으로 값을 찾아 변경할 수 있습니까?

아래와 같이 사람 정보가 있는 JSON 데이터에서 “-“, “N/A”, “” 값을 가진 키를 찾아 제거하고 싶은데 각각의 데이터 유형이 다르고 모양이 일정하지 않습니다. 사람의 수가 매우 많다고 가정합니다.

{
 "person_1":{
  "name": {"first":"David", "middle":"", "last":"Smith"},
  "age":12,
  "address":"-",
  "education":{"highschool":"N/A","college":"-"},
  "hobbies":("running", "swimming", "-"),
 },
 "person_2":{
  "name": {"first":"Mason", "middle":"henry", "last":"Thomas"},
  "age":21,
  "address":"-",
  "education":{"highschool":"N/A", "college":"", "Universitry":"Stanford"},
  "hobbies":("coding", "-", ""),
 },
 ...
}

먼저 위의 형식으로 임의의 양의 데이터를 생성하는 함수를 만들었습니다.

아래 이미지를 보면 패턴이 있지만 데이터가 잘 구성되어 있는 것 같습니다.

def create_random_data(num:int) -> dict:
    import random
    
    data={}
    random_string = lambda x: "".join((chr(random.randint(97, 122)) for tmp in range(x)))

    for i in range(num):
        key = f"person_{i+1}"
        data(key) = {
            "name":{"first":random_string(random.randint(0, 8)), "middle":random_string(random.randint(0, 8)), "last":random_string(random.randint(0, 8))}, 
            "age":random.randint(10, 50),
            "address":"-",
            "education":{"highschool":random_string(random.randint(0, 8)), "college":"", "Universitry":random_string(random.randint(0, 8))},
            "hobbies":("coding", "-", "", random_string(random.randint(0, 8))),
        }

    return data
    
print(create_random_data(5))


이제 그것을 파싱하는 함수를 만들어야 하는데, 위의 데이터의 형태를 보았을 때 가장 문제가 되는 부분이 데이터의 깊이라고 생각했습니다.

데이터의 깊이에 관계없이 데이터를 반복할 수 있도록 재귀를 사용하는 구문 분석 함수를 작성해 보겠습니다.

def clean(data):
    remove_keyword = ("N/A", "-", "")

    for key in list(data):
        value = data(key)

        if type(value) == dict: 
            clean(value)
            if value == {}:
                data.pop(key)

        if type(value) == list:
            for x in (keyword for keyword in remove_keyword if keyword in value):
                for _ in range(value.count(x)):
                    value.remove(x)

        if type(value) == str:
          if value in remove_keyword:
            data.pop(key)

datas = create_random_data(5)
print(datas)
clean(datas.copy())
print(datas)


꽤 잘 정리되어 있습니다. 그런데 100만 JSON 데이터를 처리하는 데 시간이 얼마나 걸릴지 궁금해서 코드를 조금 더 수정했습니다.

https://hwan001.co.kr/178 함수의 실행 시간을 측정하는 데코레이터가 있습니다.

재귀를 사용하는 cleanup 함수는 별도로 작성해야 하지만 이 코드를 사용하여 100만 건의 생성 시간과 cleanup 시간을 측정해 보자.

def my_decorator(func):
    def wrapped_func(*args):
        import time
        start_r = time.perf_counter()
        start_p = time.process_time()

        ret = func(*args)

        end_r = time.perf_counter()
        end_p = time.process_time()
        
        elapsed_r = end_r - start_r
        elapsed_p = end_p - start_p
        print(f'{func.__name__} : {elapsed_r:.6f}sec (Perf_Counter) / {elapsed_p:.6f}sec (Process Time)')
        
        return ret
    return wrapped_func

@my_decorator
def create_random_data(num:int) -> dict:
    import random

    data={}
    random_string = lambda x: "".join((chr(random.randint(97, 122)) for tmp in range(x)))

    for i in range(num):
        key = f"person_{i+1}"
        data(key) = {
            "name":{"first":random_string(random.randint(0, 8)), "middle":random_string(random.randint(0, 8)), "last":random_string(random.randint(0, 8))}, 
            "age":random.randint(10, 50),
            "address":"-",
            "education":{"highschool":random_string(random.randint(0, 8)), "college":"", "Universitry":random_string(random.randint(0, 8))},
            "hobbies":("coding", "-", "", random_string(random.randint(0, 8))),
        }

    return data

def clean(data):
    remove_keyword = ("N/A", "", "-")

    for key in list(data):
        value = data(key)

        if type(value) == dict: 
            clean(value)
            if value == {}:
                data.pop(key)

        if type(value) == list:
            for x in (keyword for keyword in remove_keyword if keyword in value):
                for _ in range(value.count(x)):
                    value.remove(x)

        if type(value) == str:
          if value in remove_keyword:
            data.pop(key)


datas = create_random_data(1000000)
print(len(datas), datas, "\n")

import time
start_r = time.perf_counter()
start_p = time.process_time()

clean(datas.copy())

end_r = time.perf_counter()
end_p = time.process_time()
        
elapsed_r = end_r - start_r
elapsed_p = end_p - start_p
print(f'{"clean"} : {elapsed_r:.6f}sec (Perf_Counter) / {elapsed_p:.6f}sec (Process Time)')
print(len(datas), datas)

데이터가 너무 길어서 키워드 갯수와 일부 데이터(키워드가 줄어들지 않음)를 찍어봤습니다.


생산하는

정리하다

만드는 데 129.3초, 정리하는 데 13초가 걸렸습니다. 100만 정도부터는 검색 속도가 확실히 느리게 느껴진다.

또한 샘플 데이터는 깊이가 얕아서 재귀함수는 잘 되나 깊이가 너무 크면 검색시 메모리 오류가 발생한다.

재귀가 아닌 큐나 스택을 사용하는 방식으로 바뀌어야 하고, 실제로 사용하기 위해서는 좀 더 효율적인 알고리즘이 필요할 것 같습니다.