json.org의 JSON 소개 내용에 따르면, JSON (JavaScript Object Notation) 은 XML, YAML 과 함께 효율적으로 데이터를 저장하고 교환(exchange data)하는데 사용하는 텍스트 데이터 포맷 중의 하나입니다. JSON은 사람이 읽고 쓰기에 쉬우며, 또한 기계가 파싱하고 생성하기도에 쉽습니다. JSON은 그 이름에서 유추할 수 있듯이 JavaScript의 프로그래밍 언어의 부분에 기반하고 있으며, C-family 프로그램밍 언어 (C, C++, C#, Java, JavaScript, Perl, Python 등)의 규약을 따르고 있어서 C-family 프로그래밍 언어 간 데이터를 교환하는데 적합합니다. 

JSON은 아래의 두개의 구조로 이루어져 있습니다. 

  • 이름/값 쌍의 집합 (A collection of name/value pairs): object, record, struct, dictionary, hash table, keyed list, associative array
  • 정렬된 값의 리스트 (An ordered list of values): array, vector, list, sequence

홍길동이라는 이름의 학생에 대한 정보를 포함하고 있는 JSON 데이터 포맷의 예를 들어보겠습니다.

{

    "1.FirstName": "Gildong",
    "2.LastName": "Hong",
    "3.Age": 20,
    "4.University": "Yonsei University",
    "5.Courses": [
        {
            "Classes": [
                "Probability",
                "Generalized Linear Model",
                "Categorical Data Analysis"
            ],
            "Major": "Statistics"
        },
        {
            "Classes": [
                "Data Structure",
                "Programming",
                "Algorithms"
            ],
            "Minor": "ComputerScience"
        }
    ]
}


그러면, 이번 포스팅에서는 

(1) Python 객체를 JSON 데이터로 쓰기, 직렬화, 인코딩 (Write Python object to JSON, Serialization, Encoding)

(2) JSON 포맷 데이터를 Python 객체로 읽기, 역직렬화, 디코딩 (Read JSON to Python, Deserialization, Decoding)

하는 방법을 소개하겠습니다. 


위의 Python - JSON 간 변환 표(conversion table b/w python and JSON)에서 보는 바와 같이, python의 list, tuple 이 JSON의 array로 변환되며, JSON의 array는 pythonhon의 list로 변환됩니다. 따라서 Python의 tuple을 JSON으로 변환하면 JSON array가 되며, 이를 다시 Python으로 재변환하면 이땐 python의 tuple이 아니라 list로 변환된다는 점은 인식하고 사용하기 바랍니다. 


 (1) Python 객체를 JSON 데이터로 쓰기, 직렬화, 인코딩: json.dumps()
      (Write Python object to JSON, Serialization, Encoding)

python 객체를 JSON 데이터로 만들어서 쓰기 위해서는 파이썬의 내장 json 모듈이 필요합니다. 

 import json


아래와 같은 홍길동 이라는 학생의 정보를 담고 있는 사전형 자료(dictionary)를 json.dump()와 json.dumps() 의 두가지 방법으로 JSON 포맷 데이터로 만들어보겠습니다. 

student_data = {
    "1.FirstName": "Gildong",
    "2.LastName": "Hong",
    "3.Age": 20, 
    "4.University": "Yonsei University",
    "5.Courses": [
        {
            "Major": "Statistics", 
            "Classes": ["Probability", 
                        "Generalized Linear Model", 
                        "Categorical Data Analysis"]
        }, 
        {
            "Minor": "ComputerScience", 
            "Classes": ["Data Structure", 
                        "Programming", 
                        "Algorithms"]
        }
    ]
} 


(2-1) with open(): json.dump() 를 사용해서 JSON 포맷 데이터를 디스크에 쓰기

with open("student_file.json", "w") 로 "student_file.json" 이름의 파일을 쓰기("w") 모드로 열어놓고, json.dump(student_data, json_file) 로 직렬화해서 JSON으로 내보내고자 하는 객체 student_data를, 직렬화된 데이터가 쓰여질 파일 json_file 에 쓰기를 해주었습니다. 

import json

with open("student_file.json", "w") as json_file:

    json.dump(student_data, json_file)


그러면 아래의 화면캡쳐에서 보는 바와 같이 'student_file.json'이라는 이름의 JSON 포맷 데이터가 새로 생성되었음을 알 수 있습니다.



(2-2) json.dumps()를 사용해서 JSON 포맷 데이터를 메모리에 만들기

만약 메모리 상에 JSON 포맷 데이터를 만들어놓고 python에서 계속 작업을 하려면 json.dumps() 를 사용합니다.

import json

st_json = json.dumps(student_data)

print(st_json)

{"5.Courses": [{"Classes": ["Probability", "Generalized Linear Model", "Categorical Data Analysis"], "Major": "Statistics"}, {"Minor": "ComputerScience", "Classes": ["Data Structure", "Programming", "Algorithms"]}], "3.Age": 20, "2.LastName": "Hong", "4.University": "Yonsei University", "1.FirstName": "Gildong"}


이때 만약 json.dumps()가 아니라 json.dump() 처럼 's'를 빼먹으면 TypeError가 발생하므로 주의하세요. 

# use json.dumps() instead of json.dump()

st_json = json.dump(student_data)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-ac881d00bbcb> in <module>()
----> 1 st_json = json.dump(student_data)

TypeError: dump() missing 1 required positional argument: 'fp'


json.dumps()로 파이썬 객체를 직렬화해서 JSON으로 쓸 때 사람이 보기에 좀더 쉽도록 'indent = int' 로 들여쓰기(indentation) 옵션을 설정할 수 있습니다. 아래 예시는 indent=4 로 설정한건데요, 한결 보기에 가독성이 좋아졌습니다. 


import json

st_json2 = json.dumps(student_data, indent=4)

print(st_json2)

{

    "3.Age": 20,
    "5.Courses": [
        {
            "Classes": [
                "Probability",
                "Generalized Linear Model",
                "Categorical Data Analysis"
            ],
            "Major": "Statistics"
        },
        {
            "Minor": "ComputerScience",
            "Classes": [
                "Data Structure",
                "Programming",
                "Algorithms"
            ]
        }
    ],
    "1.FirstName": "Gildong",
    "4.University": "Yonsei University",
    "2.LastName": "Hong"
}


'sort_keys=True' 를 설정해주면 키(keys)를 기준으로 정렬해서 직렬화하여 내보낼 수도 있습니다. 

import json

st_json3 = json.dumps(student_data, indent=4, sort_keys=True)

print(st_json3)

{
    "1.FirstName": "Gildong",
    "2.LastName": "Hong",
    "3.Age": 20,
    "4.University": "Yonsei University",
    "5.Courses": [
        {
            "Classes": [
                "Probability",
                "Generalized Linear Model",
                "Categorical Data Analysis"
            ],
            "Major": "Statistics"
        },
        {
            "Classes": [
                "Data Structure",
                "Programming",
                "Algorithms"
            ],
            "Minor": "ComputerScience"
        }
    ]
}




 (2) JSON 포맷 데이터를 Python 객체로 읽기, 역직렬화, 디코딩: json.loads()
      (Read JSON to Python, Deserialization, Decoding)


(2-1) 디스크에 있는 JSON 포맷 데이터를 json.load()를 사용하여 Python 객체로 읽어오기 (역직렬화, 디코딩 하기)

이어서, (1)번에서 with open(): json.dump() 로 만들어놓은 JSON 포맷의 데이터 "student_file.json" 를 Python 으로 역질렬화(deserialization)해서 읽어와 보겠습니다. with open("student_file.json", "r") 로 읽기 모드("r")로 JSON파일을 열어 후에, json.load(st_json)으로 디코딩하였습니다. 

import json

with open("student_file.json", "r") as st_json:

    st_python = json.load(st_json)


st_python

{'1.FirstName': 'Gildong',
 '2.LastName': 'Hong',
 '3.Age': 20,
 '4.University': 'Yonsei University',
 '5.Courses': [{'Classes': ['Probability',
    'Generalized Linear Model',
    'Categorical Data Analysis'],
   'Major': 'Statistics'},
  {'Classes': ['Data Structure', 'Programming', 'Algorithms'],
   'Minor': 'ComputerScience'}]}


이때 json.loads() 처럼 's'를 붙이면 TypeError: the JSON object must be str, not 'TextIOWrapper'가 발생합니다. (json.loads()가 아니라 json.load() 를 사용해야 함)

# use json.load() instead of json.loads()

with open("student_json_file.json", "r") as st_json:

    st_python = json.loads(st_json)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-53-c39634419df6> in <module>()
      1 with open("student_json_file.json", "r") as st_json:
----> 2     st_python = json.loads(st_json)

C:\Users\admin\Anaconda3\lib\json\__init__.py in loads(s, encoding, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    310     if not isinstance(s, str):
    311         raise TypeError('the JSON object must be str, not {!r}'.format(
--> 312                             s.__class__.__name__))
    313     if s.startswith(u'\ufeff'):
    314         raise JSONDecodeError("Unexpected UTF-8 BOM (decode using utf-8-sig)",

TypeError: the JSON object must be str, not 'TextIOWrapper'



(2-2) 메모리에 있는 JSON 포맷 데이터를 json.loads()로 Python 객체로 읽기 (역직렬화, 디코딩하기)

import json

st_python2 = json.loads(st_json3)

st_python2

{'1.FirstName': 'Gildong',
 '2.LastName': 'Hong',
 '3.Age': 20,
 '4.University': 'Yonsei University',
 '5.Courses': [{'Classes': ['Probability',
    'Generalized Linear Model',
    'Categorical Data Analysis'],
   'Major': 'Statistics'},
  {'Classes': ['Data Structure', 'Programming', 'Algorithms'],
   'Minor': 'ComputerScience'}]}


이때 만약 json.loads() 대신에 's'를 빼고 json.load()를 사용하면 AttributeError: 'str' object has no attribute 'read' 가 발생하니 주의하기 바랍니다. 

# use json.loads() instead of json.load()

st_python2 = json.load(st_json3)

---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)
<ipython-input-54-9de49903fef6> in <module>()
----> 1 st_python2 = json.load(st_json3)

C:\Users\admin\Anaconda3\lib\json\__init__.py in load(fp, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    263 
    264     """
--> 265     return loads(fp.read(),
    266         cls=cls, object_hook=object_hook,
    267         parse_float=parse_float, parse_int=parse_int,

AttributeError: 'str' object has no attribute 'read'


많은 도움이 되었기를 바랍니다. 

다음번 포스팅에서는 '웹(API)으로 부터 JSON 포맷 자료를 Python으로 읽어와서 pandas DataFrame으로 만드는 방법(https://rfriend.tistory.com/475)을 소개하겠습니다. 

Python으로 XML 파일 읽기, 쓰기는 https://rfriend.tistory.com/477 를 참고하세요. 

Python으로 YAML 파일 읽기, 쓰기는 https://rfriend.tistory.com/540 를 참고하세요. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요.  :-)


728x90
반응형
Posted by Rfriend
,

개발 프로젝트에 가보면 서버와 클라이언트 간 데이터 전송할 때 XML 보다 기능은 적지만 보다 간단하고 파싱도 빠른 JSON(JavaScript Object Notation, (/ˈsən/ JAY-sən)) 데이터 포맷을 많이 사용합니다. 


JSON 은 JavaScript Object Notation 이라는 이름처럼 JavaScript 에서 유래하기는 했습니다만, 프로그래밍 언어에 독립적으로 기능하는 데이터 포맷입니다. 


JSON 이 무엇인가를 이해하는데 있어, 어떤 사람을 기술하는데 JSON 표기를 사용한 아래의 예제를 참고하시면 도움이 될 듯 합니다. 



[ 사람을 기술하는데 사용한 JSON 표기 예시 (JSON representation describing a person) ]


{
  "firstName": "John",
  "lastName": "Smith",
  "isAlive": true,
  "age": 25,
  "address": {
    "streetAddress": "21 2nd Street",
    "city": "New York",
    "state": "NY",
    "postalCode": "10021-3100"
  },
  "phoneNumbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "office",
      "number": "646 555-4567"
    },
    {
      "type": "mobile",
      "number": "123 456-7890"
    }
  ],
  "children": [],
  "spouse": null
}

 * source : https://en.wikipedia.org/wiki/JSON




개발하는 분이라면 위 예제의 JSON 데이터 포맷이 아주 익숙할 것입니다만, 개발 경험이 없는 분석가의 경우 매우 생소하게 느낄 수 있습니다.  R에서 사용하는 데이터 구조로 스칼라, 벡터, 행렬, 배열, 데이터프레임, 리스트 등이 있는데요, 특히 행렬, 데이터프레임의 경우 2차원의 행(row)과 열(column)로 데이터셋이 구성이 되어 있습니다. JSON과는 많이 다르기 때문에 처음 JSON을 본 분석가는 아마 '이거 뭐지?' 하고 당황할 것 같습니다 (제가 그랬어요. ^^;).


그나마 위의 SJON 데이터는 들여쓰기(indentation)이 이쁘게 되어 있어서 가독성이 좋은 것이구요, 개발자들이 건네주는 SJON 파일을 보면 아래처럼 들여쓰기 없이 옆으로 죽~ 이어져 있어서 가독성이 매우 떨어지다 보니 더 당황하게 되는거 같습니다. @@~


{"firstName":"John","lastName":"Smith","isAlive":true,"age":25,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null} 



그래서 보통은 JSON parser 의 도움을 받아서 들여쓰기(indentation)을 해서 데이터 구조, 위계체계를 살펴보곤 합니다. 

아래는 http://www.jsonparseronline.com/ 이라는 사이트에 들어가서 JSON 데이터(왼쪽)를 parsing (오른쪽) 해본 것입니다. 


[ JSON parser (http://www.jsonparseronline.com) ]





위의 데이터를 자세히 보시면 "address""phoneNumbers"는 다른 항목과는 달리 하위에 nested data 들을 가지고 있습니다. 일종의 데이터 위계체계가 있는 셈인데요, 아래 예시처럼 어떤 사람에 대한 데이터를 JSON 데이터 포맷으로 구조화하는 방식이 합리적이고 효율적으로 보입니다. 다만, R이나 Python (numpy, pandas 모듈), SAS, SPSS 등의 분석 툴을 사용하는 분석가라면 생소할 수 있다는점을 빼면 말이지요. 





서론이 길었습니다. 


이번 포스팅에는 R의 jsonlite 패키지를 사용해서 


(1) JSON 포맷의 데이터를 R DataFrame 으로 변환

     (converting from JSON to R DataFrame)


(2) R DataFrame 을 JSON 포맷의 데이터로 변환

     (converting from R DataFrame to JSON)


하는 방법에 대해서 알아보겠습니다. 


jsonlite 패키지를 사용하면 JSON 데이터 포맷을 구조를 R이 알아서 잘 이해를 해서 R 분석가가 익숙한 R DataFrame으로 자동으로 바꾸어 주며, 그 반대로 R DataFrame을 JSON 데이터 포맷으로도 바꾸어주니 매우 편리한 패키지입니다. 



먼저, jsonlite 패키지와 (웹에서 JSON 데이터를 호출할 때 사용하는) httr 패키지를 설치하고 불러와 보겠습니다. 



install.packages("jsonlite")

library(jsonlite)


install.packages("httr")

library(httr)




 (1) JSON 포맷의 데이터를 R DataFrame 으로 변환 (converting from JSON to R DataFrame)

      : fromJSON() 함수


아래는 jsonlite 패키지의 fromJSON() 함수를 사용해서 "https://api.github.com/users/hadley/repos" 의 github API 로부터 JSON 데이터를 요청(request)해서 R DataFrame으로 변환해 본 예제입니다. 



[ (R로 불러오기 전의) JSON 원본 데이터 포맷 (https://api.github.com/users/hadley/repos) ]





자세히 보면 "owner" 는 nested data 로 login, id 등의 데이터를 가지고 있습니다. 






30개의 관측치와 69개의 변수를 가지고 있는 DataFrame으로 잘 변환이 되었음을 알 수 있습니다. 


[ R jsonlite package를 사용해서 JSON 데이터를 R DataFrame 으로 변환한 모습 ]



> # (1) converting JSON to R DataFrame

> df_repos <- fromJSON("https://api.github.com/users/hadley/repos")

> str(df_repos)

'data.frame': 30 obs. of  69 variables:

 $ id               : int  40423928 40544418 14984909 12241750 5154874 9324319 20228011 82348 888200 3116998 ...

 $ name             : chr  "15-state-of-the-union" "15-student-papers" "500lines" "adv-r" ...

 $ full_name        : chr  "hadley/15-state-of-the-union" "hadley/15-student-papers" "hadley/500lines" "hadley/adv-r" ...

 $ owner            :'data.frame': 30 obs. of  17 variables:

  ..$ login              : chr  "hadley" "hadley" "hadley" "hadley" ...

  ..$ id                 : int  4196 4196 4196 4196 4196 4196 4196 4196 4196 4196 ...

  ..$ avatar_url         : chr  "https://avatars0.githubusercontent.com/u/4196?v=3" "https://avatars0.githubusercontent.com/u/4196?v=3" "https://avatars0.githubusercontent.com/u/4196?v=3" "https://avatars0.githubusercontent.com/u/4196?v=3" ...

  ..$ gravatar_id        : chr  "" "" "" "" ...

  ..$ url                : chr  "https://api.github.com/users/hadley" "https://api.github.com/users/hadley" "https://api.github.com/users/hadley" "https://api.github.com/users/hadley" ...

  ..$ html_url           : chr  "https://github.com/hadley" "https://github.com/hadley" "https://github.com/hadley" "https://github.com/hadley" ...

  ..$ followers_url      : chr  "https://api.github.com/users/hadley/followers" "https://api.github.com/users/hadley/followers" "https://api.github.com/users/hadley/followers" "https://api.github.com/users/hadley/followers" ...

  ..$ following_url      : chr  "https://api.github.com/users/hadley/following{/other_user}" "https://api.github.com/users/hadley/following{/other_user}" "https://api.github.com/users/hadley/following{/other_user}" "https://api.github.com/users/hadley/following{/other_user}" ... 

 ...이하 생략 ...


> names(df_repos)

 [1] "id"                "name"              "full_name"         "owner"             "private"          

 [6] "html_url"          "description"       "fork"              "url"               "forks_url"        

[11] "keys_url"          "collaborators_url" "teams_url"         "hooks_url"         "issue_events_url" 

[16] "events_url"        "assignees_url"     "branches_url"      "tags_url"          "blobs_url"        

[21] "git_tags_url"      "git_refs_url"      "trees_url"         "statuses_url"      "languages_url"    

[26] "stargazers_url"    "contributors_url"  "subscribers_url"   "subscription_url"  "commits_url"      

[31] "git_commits_url"   "comments_url"      "issue_comment_url" "contents_url"      "compare_url"      

[36] "merges_url"        "archive_url"       "downloads_url"     "issues_url"        "pulls_url"        

[41] "milestones_url"    "notifications_url" "labels_url"        "releases_url"      "deployments_url"  

[46] "created_at"        "updated_at"        "pushed_at"         "git_url"           "ssh_url"          

[51] "clone_url"         "svn_url"           "homepage"          "size"              "stargazers_count" 

[56] "watchers_count"    "language"          "has_issues"        "has_projects"      "has_downloads"    

[61] "has_wiki"          "has_pages"         "forks_count"       "mirror_url"        "open_issues_count"

[66] "forks"             "open_issues"       "watchers"          "default_branch"   





nested data 를 가진 "owner" 에 딸린 데이터 항목으로 login, id 등 총 17개 데이터 항목이 있군요. 



> # nested DataFrame in owner

> names(df_repos$owner)

 [1] "login"               "id"                  "avatar_url"          "gravatar_id"        

 [5] "url"                 "html_url"            "followers_url"       "following_url"      

 [9] "gists_url"           "starred_url"         "subscriptions_url"   "organizations_url"  

[13] "repos_url"           "events_url"          "received_events_url" "type"               

[17] "site_admin"

 




nested data 를 가진 "owner" 변수에 대해 하위 변수까지 위계 구조를 반영해서 데이터를 indexing 해오는 방법은 아래를 참고하세요. 4가지 방법 모두 동일한 결과를 반환합니다. 



> # different indexing, the same results

> df_repos[1:3,]$owner$login

[1] "hadley" "hadley" "hadley"

> df_repos[1:3,"owner"]$login

[1] "hadley" "hadley" "hadley"

> df_repos$owner[1:3,"login"]

[1] "hadley" "hadley" "hadley"

> df_repos$owner[1:3,]$login

[1] "hadley" "hadley" "hadley"

 




 (2) R DataFrame 을 JSON 포맷의 데이터로 변환 (converting from R DataFrame to JSON)

       : toJSON() 함수


위에서 JSON 포맷 데이터를 웹에서 호출해서 "df_repos"라는 이름의 R DataFrame으로 변환을 했었는데요, 이번에는 jsonlite패키지의 toJSON() 함수를 사용해서 거꾸로 R DataFrame 을 원래의 JSON 데이터 포맷으로 변환해보겠습니다. 



> # (2) converting R DataFrame to JSON

> json_repos <- toJSON(df_repos)

 




R DataFrame 데이터를 JSON 데이터 포맷으로 변환한 결과를 cat() 함수, prettify() 함수, minify() 함수를 사용해서 차례대로 살펴보겠습니다. 


cat() 함수, minify() 함수는 R의 head() 함수와 기능이 비슷합니다. 

 


> cat(json_repos)

[{"id":40423928,"name":"15-state-of-the-union","full_name":"hadley/15-state-of-the-union","owner":{"login":"hadley","id":4196,"avatar_url":"https://avatars0.githubusercontent.com/u/4196?v=3","gravatar_id":"","url":"https://api.github.com/users/hadley","html_url":"https://github.com/hadley","followers_url":"https://api.github.com/users/hadley/followers","following_url":"https://api.github.com/users/hadley/following{/other_user}","gists_url":"https://api.github.com/users/hadley/gists{/gist_id}","starred_url":"https://api.github.com/users/hadley/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hadley/subscriptions","organizations_url":"https://api.github.com/users/hadley/orgs","repos_url":"https://api.github.com/users/hadley/repos","events_url":"https://api.github.com/users/hadley/events{/privacy}","received_events_url":"https://api.github.com/users/hadley/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/hadley/15-s... <truncated>

 




# not including indentation, whitespace

minify(json_repos)

 






prettify() 함수는 들여쓰기, 공백을 사용해서 JSON 데이터를 파싱해서 표기해줌으로써 아래에 화면캡쳐해 놓은 것처럼 가독성이 매우 좋습니다.  nested data 를 가진 "owner" 데이터에 대해서 아래처럼 정확하게 원래의 JSON 데이터포맷으로 변환을 해놨습니다. jsonlite 패키지 참 똑똑하지요? ^^



# including indentation, whitespace

prettify(json_repos, indent = 4)




JSON 데이터 포맷 관련해서 serializeJSON, stream_in, stream_out 등 추가적인 기능이 필요한 분은 아래 [Reference]의 jsonlite package 매뉴얼을 참고하시기 바랍니다. 


[Reference] https://cran.r-project.org/web/packages/jsonlite/jsonlite.pdf


많은 도움이 되었기를 바랍니다. 


이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. ^^



728x90
반응형
Posted by Rfriend
,