Python에서 JSON 데이터 작업

2024. 1. 7. 20:55python/intermediate

보세요, JSON이에요!

In [ ]:
# JSON

{
    "firstName": "Jane",
    "lastName": "Doe",
    "hobbies": ["running", "sky diving", "singing"],
    "age": 35,
    "children": [
        {
            "firstName": "Alice",
            "age": 6
        },
        {
            "firstName": "Bob",
            "age": 8
        }
    ]
}

보시다시피 JSON은 문자열 및 숫자와 같은 기본 유형을 지원합니다. , 중첩된 목록 및 개체도 포함됩니다.

Python은 기본적으로 JSON을 지원합니다!

In [ ]:
import json

약간의 어휘

JSON을 인코딩하는 과정을 일반적으로 직렬화라고 합니다. 이 용어는 데이터를 바이트 시리즈(따라서 직렬마샬링이라는 용어를 들을 수도 있지만 이는 완전히 다른 논의입니다. . 당연히 역직렬화는 JSON 표준에 저장되거나 전달된 데이터를 디코딩하는 상호 프로세스입니다.

JSON 직렬화

파이썬JSON
dict object
list,tuple array
str string
int, long, float number
True true
False false
None null

간단한 직렬화 예

In [ ]:
data = {
    "president": {
        "name": "Zaphod Beeblebrox",
        "species": "Betelgeusian"
    }
}

Python의 컨텍스트 관리자를 사용하면 data_file.json이라는 파일을 만들고 쓰기 모드로 열 수 있습니다. (JSON 파일은 편리하게 .json 확장자로 끝납니다.)

In [ ]:
with open("data_file.json", "w") as write_file:
    json.dump(data, write_file)

dump()은 (1) 직렬화할 데이터 객체와 (2) 바이트가 기록될 파일류 객체라는 두 개의 위치 인수를 취합니다.
또는 이 직렬화된 JSON 데이터를 프로그램에서 계속 사용하고 싶다면 이를 기본 Python str object에 쓸 수 있습니다.

In [ ]:
json_string = json.dumps(data)

몇 가지 유용한 키워드 인수

대부분의 사람들이 바꾸고 싶어하는 첫 번째 옵션은 공백입니다. indent 키워드 인수를 사용하여 중첩 구조의 들여쓰기 크기를 지정할 수 있습니다.

In [ ]:
json.dumps(data)
In [ ]:
json.dumps(data, indent=4)

또 다른 형식 지정 옵션은 separators 키워드 인수입니다. 기본적으로 이는 구분자 문자열의 2-튜플 (", ", ": ")이지만 압축 JSON의 일반적인 대안은 (",", ":")입니다.

JSON 역직렬화

json 라이브러리에는 JSON으로 인코딩된 데이터를 Python 객체로 변환하는 데 필요한 load() 및 loads()가 있습니다.

JSON파이썬
object dict
array list
string str
number(정수) int
number(유리수) float
true True
false False
null Non

실제로는 한 친구가 무언가를 한국어로 번역하게 하고 다른 친구가 그것을 다시 영어로 번역하게 하는 것과 비슷할 것입니다. 그럼에도 불구하고 가장 간단한 예는 다음과 같이 tuple을 인코딩하고 디코딩 후 list를 다시 가져오는 것입니다.

In [ ]:
blackjack_hand = (8, "Q")
blackjack_hand = (8, "Q")
encoded_hand = json.dumps(blackjack_hand)
decoded_hand = json.loads(encoded_hand)

blackjack_hand == decoded_hand
In [ ]:
type(blackjack_hand)
In [ ]:
type(decoded_hand)
In [ ]:
blackjack_hand == tuple(decoded_hand)

간단한 역직렬화 예

이번에는 메모리에서 조작하려는 일부 데이터가 디스크에 저장되어 있다고 가정해 보겠습니다. 여전히 컨텍스트 관리자를 사용하지만 이번에는 기존 data_file.json 읽기 모드를 엽니다.

In [ ]:
with open("data_file.json", "r") as read_file:
    data = json.load(read_file)

다른 프로그램에서 JSON 데이터를 가져왔거나 Python에서 JSON 형식의 데이터 문자열을 얻은 경우 loads()를 사용하여 쉽게 역직렬화할 수 있습니다. 문

In [ ]:
json_string = """
{
    "researcher": {
        "name": "Ford Prefect",
        "species": "Betelgeusian",
        "relatives": [
            {
                "name": "Zaphod Beeblebrox",
                "species": "Betelgeusian"
            }
        ]
    }
}
"""
data = json.loads(json_string)

실제 사례(sort of)

JSONPlaceholder 서비스에 API 요청을 해야 하므로 requests 패키지를 사용하여 무거운 작업을 수행하세요. 파일 상단에 다음 가져오기를 추가하세요.

In [ ]:
import json
import requests

계속해서 todos 엔드포인트에 대해 JSONPlaceholder API에 요청하세요. requests에 익숙하지 않은 경우 모든 작업을 대신해 주는 편리한 json() 방법이 있지만 사용법을 연습해 볼 수도 있습니다. json 응답 객체의 text 속성을 ​​역직렬화하는 라이브러리입니다.

In [ ]:
response = requests.get("https://jsonplaceholder.typicode.com/todos")
todos = json.loads(response.text)

대화형 모드에서 파일을 실행하고 직접 테스트해 보세요. 그 동안 todos 유형을 확인하세요.

In [ ]:
todos == response.json()
In [ ]:
type(todos)
In [ ]:
todos[:10]

브라우저에서 엔드포인트를 방문하여 데이터의 구조를 볼 수 있지만 다음은 샘플 TODO입니다.

In [ ]:
# JSON

{
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": 0    
}

각각의 고유한 userId 사용자를 가진 여러 사용자가 있으며, 각 작업에는 부울 completed 속성이 있습니다. 어떤 사용자가 가장 많은 작업을 완료했는지 확인할 수 있나요?

In [ ]:
# Map of userId to number of complete TODOs for that user
todos_by_user = {}

# Increment complete TODOs count for each user.
for todo in todos:
    if todo["completed"]:
        try:
            # Increment the existing user's count.
            todos_by_user[todo["userId"]] += 1
        except KeyError:
            # This user has not been seen. Set their count to 1.
            todos_by_user[todo["userId"]] = 1

# Create a sorted list of (userId, num_complete) pairs.
top_users = sorted(todos_by_user.items(), 
                   key=lambda x: x[1], reverse=True)

# Get the maximum number of complete TODOs.
max_complete = top_users[0][1]

# Create a list of all users who have completed
# the maximum number of TODOs.
users = []
for user, num_complete in top_users:
    if num_complete < max_complete:
        break
    users.append(str(user))

max_users = " and ".join(users)

요점은 이제 JSON 데이터를 일반 Python 객체로 조작할 수 있다는 것입니다!

In [ ]:
s = "s" if len(users) > 1 else ""
print(f"user{s} {max_users} completed {max_complete} TODOs")

마지막 작업을 위해 최대 TODO 수를 완료한 각 사용자에 대해 완료 TODO가 포함된 JSON 파일을 생성합니다.

In [ ]:
# Define a function to filter out completed TODOs 
# of users with max completed TODOS.
def keep(todo):
    is_complete = todo["completed"]
    has_max_count = str(todo["userId"]) in users
    return is_complete and has_max_count

# Write filtered TODOs to file.
with open("filtered_data_file.json", "w") as data_file:
    filtered_todos = list(filter(keep, todos))
    json.dump(filtered_todos, data_file, indent=2)

사용자 정의 Python 객체 인코딩 및 디코딩

In [ ]:
class Elf:
    def __init__(self, level, ability_scores=None):
        self.level = level
        self.ability_scores = {
            "str": 11, "dex": 12, "con": 10,
            "int": 16, "wis": 14, "cha": 13
        } if ability_scores is None else ability_scores
        self.hp = 10 + self.ability_scores["con"]
In [ ]:
elf = Elf(level=4)
json.dumps(elf)

json 모듈은 대부분의 내장 Python 유형을 처리할 수 있지만 기본적으로 사용자 정의 데이터 유형을 인코딩하는 방법을 이해하지 못합니다. 이는 둥근 구멍에 네모난 못을 맞추려는 것과 같습니다.

데이터 구조 단순화

JSON을 직접 인코딩하고 디코딩해 볼 수도 있지만 작업을 줄여줄 좀 더 영리한 솔루션이 있습니다. 사용자 정의 데이터 유형에서 JSON으로 바로 이동하는 대신 중간 단계를 진행할 수 있습니다.
기본적으로 더 복잡한 객체를 더 간단한 표현으로 변환하고, json 모듈은 이를 JSON으로 변환합니다.

In [ ]:
z = 3 + 8j
type(z)
In [ ]:
json.dumps(z)

복소수의 경우 실수부와 허수부만 알면 되며 두 부분 모두 complex 객체의 속성으로 액세스할 수 있습니다.

In [ ]:
z.real
In [ ]:
z.imag

동일한 숫자를 complex 생성자에 전달하면 eq 비교 연산자를 만족하기에 충분합니다.

In [ ]:
complex(3, 8) == z

사용자 정의 유형 인코딩

사용자 정의 개체를 JSON으로 변환하려면 dump() 메소드의 default 매개변수에 인코딩 기능을 제공하기만 하면 됩니다. json 모듈은 기본적으로 직렬화할 수 없는 모든 객체에 대해 이 함수를 호출합니다.

In [ ]:
def encode_complex(z):
    if isinstance(z, complex):
        return (z.real, z.imag)
    else:
        type_name = z.__class__.__name__
        raise TypeError(f"Object of type '{type_name}' is not JSON serializable")
In [ ]:
json.dumps(9 + 5j, default=encode_complex)
In [ ]:
json.dumps(elf, default=encode_complex)

다른 일반적인 접근 방식은 표준을 하위 클래스로 분류하고 JSONEncoder 해당 default() 메소드를 재정의하는 것입니다.

In [ ]:
class ComplexEncoder(json.JSONEncoder):
    def default(self, z):
        if isinstance(z, complex):
            return (z.real, z.imag)
        else:
            return super().default(z)

TypeError 직접 발생시키는 대신 기본 클래스에서 처리하도록 할 수 있습니다. cls 매개변수를 통해 dump() 메소드에서 직접 사용하거나 인코더의 인스턴스를 생성하고 해당 encode() 방법을 호출하여 이를 사용할 수 있습니다

In [ ]:
json.dumps(2 + 5j, cls=ComplexEncoder)
In [ ]:
encoder = ComplexEncoder()
encoder.encode(3 + 6j)

사용자 정의 유형 디코딩

ComplexEncoder을 사용하여 복소수를 인코딩한 다음 결과를 디코딩하려고 하면 다음과 같은 일이 발생합니다.

In [ ]:
complex_json = json.dumps(4 + 17j, cls=ComplexEncoder)
json.loads(complex_json)
In [ ]:
type(complex_json)

json 모듈에서는 모든 사용자 정의 유형이 JSON 표준에서 objects로 표현될 것으로 예상합니다. 다양성을 위해 이번에는 complex_data.json이라는 JSON 파일을 만들고 복소수를 나타내는 다음 object을 추가할 수 있습니다.

In [ ]:
# JSON

{
    "__complex__": 1,  # 출처에는 true로 되어 있는 데 NameError: name 'false' is not defined이라는 error 발생
    "real": 42,
    "imag": 36
}

그 "|__complex__" 키는 우리가 방금 이야기한 메타데이터입니다. 연관된 값이 무엇인지는 실제로 중요하지 않습니다. 이 작은 해킹을 작동시키려면 키가 존재하는지 확인하기만 하면 됩니다.

In [ ]:
def decode_complex(dct):
    if "__complex__" in dct:
        return complex(dct["real"], dct["imag"])
    return dct

"__complex__"이 사전에 없으면 개체를 반환하고 기본값을 그대로 두기만 하면 됩니다. 디코더가 처리합니다.

load() 메소드가 object 구문 분석을 시도할 때마다 기본 디코더가 데이터를 처리하기 전에 중재할 기회가 제공됩니다. 디코딩 기능을 object_hook 매개변수에 전달하면 됩니다.

In [ ]:
with open("complex_data.json") as complex_data:
    data = complex_data.read()
    z = json.loads(data, object_hook=decode_complex)
 
type(z)

object_hook는 dump() 메소드의 default 매개변수에 대응되는 것처럼 느껴질 수 있지만 실제로 비유는 여기서 시작하고 끝납니다.
이것은 하나의 객체에만 작동하지 않습니다. 이 복소수 목록을 complex_data.json에 넣고 스크립트를 다시 실행해 보세요.

In [ ]:
# JSON

[
  {
    "__complex__":1,
    "real":42,
    "imag":36
  },
  {
    "__complex__":1,
    "real":64,
    "imag":11
  }
]
In [ ]:
with open("complex_data.json") as complex_data:
    data = complex_data.read()
    numbers = json.loads(data, object_hook=decode_complex)
 
numbers
In [ ]:
출처 : https://realpython.com/python-json/