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)


매우 잘 정리되어 있습니다.

하지만 JSON 데이터 100만개를 처리하는 데 시간이 얼마나 걸릴지 궁금해서 코드를 조금 수정했습니다.

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

재귀를 이용한 clean 함수는 따로 작성해야 하지만 이 코드를 사용하여 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만 정도부터는 검색 속도가 확실히 느리게 느껴진다.

그리고 예제 데이터는 깊이가 얕아서 재귀는 잘 되는데 깊이가 너무 깊으면 검색시 메모리 에러가 납니다.

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