IT/언어

[python] python으로 JSON 파일 다루는 기초적인 방법

개발자 두더지 2021. 5. 22. 00:48
728x90

 Python의 표준 라이브러리인 json모듈을 사용하면 JSON형식의 파일이나 문자열을 파스해서 사전형 dict등의 객체로써 읽어 들일 수 있다. 또한 JSON에 상당하는 객체를 정형화하여 JSON형식의 파일이나 문자열로써 출력하거나 저장하는 것도 가능하다. 

이번 포스팅을 통해서 살펴 볼 내용은 다음과 같다.

  • JSON문자열을 사전형(dict형)으로 변환 : josn.loads()

     - 순서대로 저장 : 인수 object_paris_hook

     - 바이트열을 변환

  • JSON파일을 사전형으로 읽어들이기 : josn.loads()
  • 읽어들인 사전형 데이터의 값을 취득, 변경, 삭제, 추가하기

     - 값의 변경

     - 요소의 삭제

     - 요소의 추가

  • 사전형(dict형)을 JSON문자열로 정형화하여 출력 : json.dumps()

     - 구분문자를 지정 : 인수 separators

     - 인덱스를 지정 : 인수 indent

     - 키로 정렬 : 인수 sort_keys

     - Unicode이스케이프 지정 : 인수 ensure_ascii

  • 사전형(dict형)데이터를 JSON파일로 저장 : json.dump()
  • JSON파일의 신규 생성, 갱신 (수정 및 추가기입)
  • JSON파일과 문자열을 다룰 때에 주의점 

     - Unicode이스케이프

     - 인용부

 또한, 편의상, 샘플 코드로 사전형 데이터로 한정하고 있지만, JSON내용에 따라 list객체로써 다룰 수 있는 경우도 있다.

 JSON 형식의 문자열이나 파일을 pandas.DataFrame으로 읽고 쓸 수도 있는데, 이 부분은 나중에 기회가 되면 다루도록 하겠다.

 또한, 참고로 JSON 형식의 Web API 얻어내기 위해서는 Requests를 사용하는 것이 편리하다. 이제부터 사용할 샘플 코드는 아래와 같이 모듈을 사전에 import해둬야한다. 아래의 모듈 전부 표준이므로 따로 설치할 필요는 없다.

import json
from collections import OrderedDict
import pprint

 참고로 pprint를 결과를 보기 좋게 출력하주므로 import했는데, 사실 JSON 처리 자체와 상관없다.

 

 

JSON문자열을 사전형으로 변환 : json.loads()


 JSON 형식의 문자열을 사전형으로 변환하기 위해서는 json.loads() 함수를 사용한다. 아래와 같이 첫 번째 인수에 문자열을 지정하면 사전형으로 변환된다.

s = r'{"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}'

print(s)
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

d = json.loads(s)

pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(d))
# <class 'dict'>

 문자열의 \u3042는 Unicode이스케이프 시퀀스이다. JSON에서는 전각 일본어등 ASCII문자에 포함되지 않는 문자가 Unicode이스케이프되어 있는 경우가 있다.

 예에서는 raw문자열을 사용하여 백슬래쉬를 \\가 아닌 \로 기재하고 있다. 또한, 애매한 것이, Unicode이스케이프 시퀀스와 raw문자열로 무효화하는 이스케이프 시퀀스는 별개의 것이라는 점이다.

 json.loads()와 곧 설명할 json.load()에서는 특히 어떠한 설정도 하지 않아도 Unicode이스케이프 시퀀스가 대응하는 문자열로 변환된다.

 읽어들인 사젼형 데이터의 값을 취득하거나 변경하는 것에 대해서는 조금 뒤에서 설명하도록 하겠다.

 

순서대로 저장 : 인수 object_pairs_hook

 Python3.6 까지는 언어 사양으로 사전형 (dict형 객체)은 요소의 순서를 유지해주지 않았다. 따라서, 변환된 데이터형에서 원본의 JSON문자열으로의 순서가 유지되지 않는 경우가 있다.

 인수 object_pairs_hook에 순서를 붙인 사전형인 OrderedDict형을 지정하면, 변환 값이 OrderedDict형이 되어, 순서가 유지된 채로 변환된다. OrderedDict를 import해둘 필요가 있다는 점을 주의하자.

od = json.loads(s, object_pairs_hook=OrderedDict)

pprint.pprint(od)
# OrderedDict([('C', 'あ'),
#              ('A', OrderedDict([('i', 1), ('j', 2)])),
#              ('B',
#               [OrderedDict([('X', 1), ('Y', 10)]),
#                OrderedDict([('X', 2), ('Y', 20)])])])

 

바이트열을 변환

Python3.6이후의 버전부터는 json.loads()의 첫 번째인수로써 문자열뿐만이 아니라 바이트열(bytes형)을 지정할 수 있게 됐다.

b = s.encode()

print(b)
# b'{"C": "\\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}'

print(type(b))
# <class 'bytes'>

db = json.loads(b)

pprint.pprint(db, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(db))
# <class 'dict'>

 이전 버전에서는 바이트열을 지정할 수 없으므로, decode()메소드로 문자열으로 변환한 후에 json.loads()에 전달할 필요가 있다.

sb = b.decode()

print(sb)
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(type(sb))
# <class 'str'>

dsb = json.loads(sb)

pprint.pprint(dsb, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(dsb))
# <class 'dict'>

 최종적으로 json.loads()에 전달하기 때문에 특별히 신경쓸 필요는 없지만, decode() 메소드의 첫 번째 인수 encoding 에 'unicode-escape'를 지정하면, Unicode 이스케이프 시퀀스가 변환된 문자열을 취득할 수 있다. 

sb_u = b.decode('unicode-escape')

print(sb_u)
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(type(sb_u))
# <class 'str'>

dsb_u = json.loads(sb_u)

pprint.pprint(dsb_u, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(dsb_u))
# <class 'dict'>

 json.loads()의 출력 결과는 동일하다.

 

 

JSON 파일을 사전형으로 읽어들이기 : json.load()


JSON 파일을 사전형으로 읽어 들이기 위해서는 json.load()함수를 사용한다. 첫 번째 인수로 파일 객체를 지정하는 것 이외에는 json.loads()와 동일하다. 파일 객체는 내장함수 open()등으로 취득할 수 있다.

 json.loads()의 열과 동일한 문자열로 작성한 파일을 사용한다.

with open('data/src/test.json') as f:
    print(f.read())
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

 json.load()로 파일로 부터 읽어 들인다.

with open('data/src/test.json') as f:
    df = json.load(f)

pprint.pprint(df, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(df))
# <class 'dict'>

 

 

읽어들인 사젼형의 값을 취득, 변경, 삭제, 추가하기


 위에서 언급했듯, json.load(), json.loads()로 JSON 파일, 문자열의 내용을 사전형 데이터로 취득할 수 있었다. 여기서 부터는 취득한 사전형 객체의 조작에 대해 살펴보도록 할 것이다.

 또한, 순서가 붙은 OrderedDict는 사전형 dict의 서브 클래스이므로, 이하의 조작 그대로 사용할 수 있다.

pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(d['A'])
# {'i': 1, 'j': 2}

print(d['A']['i'])
# 1

 사전형의 리스트가 요소인 경우에는, 리스트의 요소를 [인덱스]로 지정한다.

print(d['B'])
# [{'X': 1, 'Y': 10}, {'X': 2, 'Y': 20}]

print(d['B'][0])
# {'X': 1, 'Y': 10}

print(d['B'][0]['X'])
# 1

 당연하지만, 그 값을 다른 변수에 대입하는 것도 가능하다.

value = d['B'][1]['Y']
print(value)
# 20

 존재하지 않는 키를 지정하면 에러가 발생하지만, get() 메소드를 사용하면 존재하지 않는 키를 지정하면 None을 반환한다.

# print(d['D'])
# KeyError: 'D'

print(d.get('D'))
# None

 

값의 변경

[키] 를 지정하여 새로운 값을 대입할 수 있다.

d['C'] = 'ん'
pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'ん'}

 

요소의 삭제

pop() 메소드로 키를 지정하여 요소(키와 값의 쌍) 을 삭제할 수 있다.

d.pop('C')
pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}]}

pop()외의 del문으로도 삭제할 수 있다.

 

요소의 추가

[키] 로 새로운 [키] (기존에 존재하지 않은 키)를 지정하여 값을 대입하면, 새로운 키와 값이 요소로 추가된다.

d['C'] = 'あ'
pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

 다른 사전형을 연계하거나, 여러 개의 요소를 하나로 엮어서 추가하고 싶은 경우는 update() 메소드를 사용한다. 

 

 

사전형 데이터를 JSON문자열로써 정형화해서 출력 : json.dumps()


 사전형 데이터를 JSON형식의 문자열로써 출력하기 위해서는 json.dumps()를 사용한다. 첫 번째 인수에 사젼형 데이터를 지정하면, JSON 형식의 문자열을 취득할 수 있다.

pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

sd = json.dumps(d)

print(sd)
# {"A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}], "C": "\u3042"}

print(type(sd))
# <class 'str'>

 기본적으로는 전각 일본어 문자등의 ASCII가 아닌 문자가 Unicode 이스케이프되어 출력된다. 인수 ensure_ascii = False로 하면, Unicode 이스케이프되지 않는다.

 OrderedDict형의 객체도 json.dumps()의 첫 번째 인수로 지정할 수 있다. 

pprint.pprint(od)
# OrderedDict([('C', 'あ'),
#              ('A', OrderedDict([('i', 1), ('j', 2)])),
#              ('B',
#               [OrderedDict([('X', 1), ('Y', 10)]),
#                OrderedDict([('X', 2), ('Y', 20)])])])

sod = json.dumps(od)

print(sod)
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(type(sod))
# <class 'str'>

 아래와 같이, 이외의 인수를 설정하여 정형화하는 것도 가능하다/

 

구분 문자를 지정 : 인수 separators

기본적으로는 키와 값 사이에 :, 요소간은 , 로 구분된다. 인수 separators로 각각의 구분 문자를 튜플로 지정할 수 있어, 공백 없애거나, 완전히 다른 문자열로하는 것도 가능하다.

print(json.dumps(d, separators=(',', ':')))
# {"A":{"i":1,"j":2},"B":[{"X":1,"Y":10},{"X":2,"Y":20}],"C":"\u3042"}

print(json.dumps(d, separators=(' / ', '-> ')))
# {"A"-> {"i"-> 1 / "j"-> 2} / "B"-> [{"X"-> 1 / "Y"-> 10} / {"X"-> 2 / "Y"-> 20}] / "C"-> "\u3042"}

 

인덱스를 지정 : 인수 indent

인수 indent로 인덱스폭 (문자 수)를 지정하면, 요소마다 개항, 들여쓰기 된다.

print(json.dumps(d, indent=4))
# {
#     "A": {
#         "i": 1,
#         "j": 2
#     },
#     "B": [
#         {
#             "X": 1,
#             "Y": 10
#         },
#         {
#             "X": 2,
#             "Y": 20
#         }
#     ],
#     "C": "\u3042"
# }

디폴트로 indent = None이므로 개행하지 않는다. indent = 0으로 지정하면, 인덱스 없이 개행된다.

 

키로 정렬 : 인수 sort_keys

인수 sort_key = True로 지정하면, 사전형 데이터의 요소가 키를 기준으로 정렬된다.

print(json.dumps(od))
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(json.dumps(od, sort_keys=True))
# {"A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}], "C": "\u3042"}

 

Unicode이스케이프 지정 : 인수 ensure_ascii

지금까지의 예와 같이, 기본적으로 전각 일본어 등 ASCII가 아닌 문자는 Unicode이스케이프되어 출력된다.

인수 ensure_ascii = True로 하면, Unicode이스케이프되지 않는다. 일본어의 전각 문자등이 그대로 출력된다.

print(json.dumps(od, ensure_ascii=False))
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

 

 

사젼형 데이터를 JSON 파일으로써 저장 : json.dump()


 사전형 데이터를 JSON파일로써 저장하기 위해서는 json.dump()를 사용한다. 두 번째 인수로 파일 객체를 지정하는 것 외에는 json.dumps()와 동일하다. 첫 번째 인수로 사전형 데이터를 지정하고, indent등 정형화를 위한 다른 인수도 동일한 방법으로 지정하면 된다.

 open()으로 파일을 열때에는 쓰기모드 'w'를 지정할 필요가 있을 필요가 있음에 주의하자.

with open('data/dst/test2.json', 'w') as f:
    json.dump(d, f, indent=4)

 결과를 확인하자.

with open('data/dst/test2.json') as f:
    print(f.read())
# {
#     "A": {
#         "i": 1,
#         "j": 2
#     },
#     "B": [
#         {
#             "X": 1,
#             "Y": 10
#         },
#         {
#             "X": 2,
#             "Y": 20
#         }
#     ],
#     "C": "\u3042"
# }

 기존 파일의 경로를 지정하면 덮어쓰기, 존재하지 않는 파일 경로를 지정하면 신규 작성이 된다. 그러나, 파일을 바로 위까지의 디렉토리(폴더)는 존재하지 않으면 에러 (FileNotFoundError예외)가 발생한다.

 

 

JSON 파일의 신규 작성, 갱신 (수정 및 추가 기입)


JSON 파일을 신규작성, 갱신 (수정 및 추가 기입)하는 흐름을 예로 설명하도록 하겠다.

 

JSON 파일의 신규작성

원본 데이터가 되는 사전형 객체를 작성한다.

d_new = {'A': 100, 'B': 'abc', 'C': 'あいうえお'}

open()으로 경로를 지정, json.dump()로 적절하게 정형화하여 저장한다.

with open('data/dst/test_new.json', 'w') as f:
    json.dump(d_new, f, indent=2, ensure_ascii=False)

결과를 확인한다.

with open('data/dst/test_new.json') as f:
    print(f.read())
# {
#   "A": 100,
#   "B": "abc",
#   "C": "あいうえお"
# }

 

JSON 파일의 갱신 (수정 및 추가 기입)

open()으로 경로를 지정하고, json.load()로 기존의 JSON 파일을 읽어들인다. 원본의 순서를 유지하고 싶은 경우애는, 인수 object_pairs_hook = OrederedDict를 지정한다.

with open('data/dst/test_new.json') as f:
    d_update = json.load(f, object_pairs_hook=OrderedDict)

print(d_update)
# OrderedDict([('A', 100), ('B', 'abc'), ('C', 'あいうえお')])

사전형 객체를 적절히 갱신한다.

d_update['A'] = 200
d_update.pop('B')
d_update['D'] = 'new value'

print(d_update)
# OrderedDict([('A', 200), ('C', 'あいうえお'), ('D', 'new value')])

open()으로 경로를 지정하여, json.dump()로 정형화하여 저장한다. 편의상, 예에서는 새로운 경로를 지정하고 있지만, 원래의 파일 경로를 지정하면 덮어쓰기 된다.

with open('data/dst/test_new_update.json', 'w') as f:
    json.dump(d_update, f, indent=2, ensure_ascii=False)

결과를 확인한다.

with open('data/dst/test_new_update.json') as f:
    print(f.read())
# {
#   "A": 200,
#   "C": "あいうえお",
#   "D": "new value"
# }

 

 

JSON 파일과 문자열을 다룰 때의 주의점


json모듈을 사용하고 있는 경우에 특별히 별도로 신경쓰지 않아도 문제없이 처리되지만, json모듈을 사용하지 않고 처리할 때에는 주의해야할 점들이 있다.

 

Unicode 이스케이프 

 특히 Web API로 취득할 수 있는 JSON은 보안을 위해 전각 일본어 등 ASCII가 아닌 문자는 Unicode 이스케이프되는 경우가 있다.

 json.load(), json.loads()에서는 Unicode이스케이프 시퀀스 \uXXX를 적당히 처리해주지만, 텍스트 파일로써 읽어들일 경우는 인코딩으로 'umicode-escape'를 지정하지 않으면 Unicode이스케이프된 채의 문자열이 된다. JSON 파일을 읽어들일 때에는 일본어가 문자가 깨진 경우는 이것이 원인이된다.

 인코딩은 open()함수의 인수로 지정한다.

with open('data/src/test.json') as f:
    print(f.read())
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

with open('data/src/test.json', encoding='unicode-escape') as f:
    print(f.read())
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

 위에서도 설명하였지만, Unicode이스케이프된 바이트열을 문자열로 디코딩할 경우에도 주의가 필요하다.

 Python표준 라이브러리의 urllib.request.urlopen()는 바이트열을 리턴하므로, Web API에 액세스해서 취득한 바이트열을 인코딩하지 않고 디코딩하면 Unicode 이스케이프된 채로의 문자열이 된다.

 인코딩은 decode()메소드의 인수로 지정한다.

print(b)
# b'{"C": "\\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}'

print(b.decode())
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(b.decode(encoding='unicode-escape'))
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

 또한, 바이트열의 경우에도 최종적으로 json.loads()로 사전형으로 변환하는 경우, json.loads()의 내부에 Unicode이스케이프 시퀀스가 처리되므로 특별히 문제가 없다. 

 

인용부

 JSON에서는 키나 문자열을 포함한 인용부가 큰 따옴표 " 여야한다.

  json.dump(), json.dumps()에서는 적당히 처리해주지만, 독자적으로 문자열을 생성하여 인용부가 작은 따옴표 '로 되어 있는 경우, json.load(), json.loads()에서 에러가 발생한다.

 예를 들어, 사전형 객체를 str()을 이용해 문자열로 변환시키면 인용부로써 작은 따옴표가 사용되므로, json.load()에서 에러가 발생한다.

d = {"A": 100, "B": 'abc', "C": 'あいうえお'}

print(str(d))
# {'A': 100, 'B': 'abc', 'C': 'あいうえお'}

# print(json.loads(str(d)))
# JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

 json.dumps()로 문자열으로 변환해주면 OK이다.

print(json.dumps(d))
# {"A": 100, "B": "abc", "C": "\u3042\u3044\u3046\u3048\u304a"}

print(json.loads(json.dumps(d)))
# {'A': 100, 'B': 'abc', 'C': 'あいうえお'}

print(type(json.loads(json.dumps(d))))
# <class 'dict'>

 참고자료

https://note.nkmk.me/python-json-load-dump/

728x90