개발 프로젝트에 가보면 서버와 클라이언트 간 데이터 전송할 때 XML 보다 기능은 적지만 보다 간단하고 파싱도 빠른 JSON(JavaScript Object Notation, ( 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
많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. ^^