WHYHOW系列之json文件处理

JSON是Javascript的数据交换格式,经常会遇到以JSON格式存储的数据,在R中应该如何正确的使用JSON格式呢?

目录


Why jsonlite

Why JSON

JSON是Javascript的数据交换格式,在互联网如此发达的今天,数据的交换和展示已经成为互联网服务的重要组成部分。在这当中,很大一部分的数据还是用来进行网页渲染的。传统地,也有一些使用XML作为数据交换格式的,然而随着Javascript的盛行,今天很大部分数据是使用JSON格式进行交换的。所以,读写JSON格式基本已经成为了必备技能。

另一方面,JSON格式结构简单,在Javascript里处理相当便捷,在其他程序语言中也比较容易,所以JSON也是一种值得学习的数据格式。

自省过程

刚才讲过了,JSON格式本身比较简单,所以读写库的编写并不困难。实际上市面上的JSON读写库可谓多如牛毛。那为什么又要说读写库有优劣呢?

我的看法是这样的:

  1. 读写速度不同。不同的读写库的读写速度差异是很大的,一个好的读写库应该有足够的效率。
  2. 数据化简功能强大。JSON格式本身比较简单,如果不对数据进行化简,那么读写库其实意义不大。我们是从JSON读入数据,读入数据的目的大部分时候是处理数据,所以,好的读写库是可以将JSON尽可能的转换成方便分析的数据格式,比如R的data.frame。如果JSON读写库可以提供优良的化简功能,即使效率稍差也是可以接受的。
  3. 数据写出接口有详细的控制。虽然大部分时候默认值可能就可以接受,强大的可定制输出的接口,也是优秀的库必备的技能。
  4. 接口设计简明。一个本来就不复杂的格式设计超级复杂的接口,本来就是逆天的存在。

以上就是我们寻找的好的JSON库的基本原则。下面我们行动起来:

Why JSONLITE

本文主要关注R下的JSON读写库,Python下的读写库正确的选择可能是SimpleJSON。

R下比较常见的JSON读写库有:

  1. rjson
  2. RJSONIO
  3. jsonlite

rjson可以完成json的读写,但是并不提供其他功能。RJSONIO是一个相对来讲比较完善的JSON库,核心读入函数fromJSON的选项很多:

fromJSON(content, handler = NULL,
          default.size = 100, depth = 150L, allowComments = TRUE,
           asText = isContent(content), data = NULL,
            maxChar = c(0L, nchar(content)), simplify = Strict,
             nullValue = NULL, simplifyWithNames = TRUE,
              encoding = NA_character_, stringFun = NULL, ...)

toJSON的选项也不少:

toJSON(x, container = isContainer(x, asIs, .level), 
        collapse = "\n", ..., .level = 1L,
         .withNames = length(x) > 0 && length(names(x)) > 0, .na = "null",
         .escapeEscapes = TRUE, pretty = FALSE, asIs = NA, .inf = " Infinity")

感觉起来应该是一个不错的IO库,然而经过测试它的读入化简功能不够强大,输出的控制能力有不够精细。整体来讲,还有一些可以提升的空间。不过最重要的是这个库的设计和上面的理想方式不同。

说那么多,jsonlite也该闪亮登场了。首先,这个库的题注是

A Robust, High Performance JSON Parser and Generator for R

这个是为什么,大家需要到jsonlite的官网自行查询。总之,这个库可以满足第一点。

其次,我们来看fromJSONtoJSON的选项:

fromJSON(txt, simplifyVector = TRUE, simplifyDataFrame = simplifyVector,
  simplifyMatrix = simplifyVector, flatten = FALSE, ...)

toJSON(x, dataframe = c("rows", "columns", "values"), matrix = c("rowmajor",
  "columnmajor"), Date = c("ISO8601", "epoch"), POSIXt = c("string",
  "ISO8601", "epoch", "mongo"), factor = c("string", "integer"),
  complex = c("string", "list"), raw = c("base64", "hex", "mongo"),
  null = c("list", "null"), na = c("null", "string"), auto_unbox = FALSE,
  digits = 4, pretty = FALSE, force = FALSE, ...)

我想不用解释太多了。fromJSON的选项简直叫做少得出奇,而提供的选项全部都是数据化简的选项。toJSON的选项比较丰富,这也正是一个写出库提供精细控制的体现。所以单从这个选项上来看,jsonlite已经超过了另外两个库。

最后,jsonlite的功能是完整的,库里提供了校验JSON格式的函数validate(txt),提供了将R对象转换为JSON的serializeJSON(x, digits = 8, pretty = FALSE)unserializeJSON(txt)。同时,库里还提供了base64的解编码函数。

综上所述,jsonlite成为了R下读写JSON的不二选择,当然我也不拦着你用RJSONIO

How jsonlite

考虑到写出JSON的内容是不言自明的,下面主要讨论读入JSON的内容。

以下内容需要阅读R的str函数的原始输出

简单的读入

我们以读入新加坡天气的metadata为例,展示这一过程:

library(jsonlite)
txt <- readLines('../SG-Weather/metadata/2017-01-AirTemperature.json')
json <- fromJSON(txt)
str(json)
## List of 3
##  $ stations    :'data.frame':  18 obs. of  4 variables:
##   ..$ device_id: chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ location :'data.frame':  18 obs. of  2 variables:
##   .. ..$ latitude : num [1:18] 1.38 1.26 1.31 1.34 1.28 ...
##   .. ..$ longitude: num [1:18] 104 104 104 104 104 ...
##   ..$ name     : chr [1:18] "Ang Mo Kio Avenue 5" "Banyan Road" "East Coast Parkway" "Kim Chuan Road" ...
##   ..$ id       : chr [1:18] "S109" "S117" "S107" "S43" ...
##  $ reading_unit: chr "deg C"
##  $ reading_type: chr "DBT 1M F"

这里注意到jsonlite已经把读入的数据进行了化简,stations已经被转换为了data.frame

json$stations
##    device_id location.latitude location.longitude                    name
## 1       S109           1.37640           103.8492     Ang Mo Kio Avenue 5
## 2       S117           1.25600           103.6790             Banyan Road
## 3       S107           1.31350           103.9625      East Coast Parkway
## 4        S43           1.33990           103.8878          Kim Chuan Road
## 5       S108           1.27990           103.8703    Marina Gardens Drive
## 6        S44           1.34583           103.6817          Nanyang Avenue
## 7       S121           1.37288           103.7224  Old Choa Chu Kang Road
## 8       S106           1.41680           103.9673              Pulau Ubin
## 9        S06           1.35240           103.9007                     S06
## 10      S102           1.18900           103.7680        Semakau Landfill
## 11      S122           1.41731           103.8249          Sembawang Road
## 12       S96           1.31750           104.0307  Tanah Merah Coast Road
## 13      S115           1.29377           103.6184     Tuas South Avenue 3
## 14      S24B           1.36780           103.9980 Upper Changi Road North
## 15       S24           1.36780           103.9826 Upper Changi Road North
## 16      S116           1.28100           103.7540      West Coast Highway
## 17      S104           1.44387           103.7854      Woodlands Avenue 9
## 18      S100           1.41720           103.7485          Woodlands Road
##      id
## 1  S109
## 2  S117
## 3  S107
## 4   S43
## 5  S108
## 6   S44
## 7  S121
## 8  S106
## 9   S06
## 10 S102
## 11 S122
## 12  S96
## 13 S115
## 14 S24B
## 15  S24
## 16 S116
## 17 S104
## 18 S100

这个功能是很好,如果我们强制不使用化简功能的读入结果是这样的:

json <- fromJSON(txt, simplifyVector = FALSE)
str(json)
## List of 3
##  $ stations    :List of 18
##   ..$ :List of 4
##   .. ..$ device_id: chr "S109"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.38
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Ang Mo Kio Avenue 5"
##   .. ..$ id       : chr "S109"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S117"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.26
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Banyan Road"
##   .. ..$ id       : chr "S117"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S107"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.31
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "East Coast Parkway"
##   .. ..$ id       : chr "S107"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S43"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.34
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Kim Chuan Road"
##   .. ..$ id       : chr "S43"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S108"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.28
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Marina Gardens Drive"
##   .. ..$ id       : chr "S108"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S44"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.35
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Nanyang Avenue"
##   .. ..$ id       : chr "S44"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S121"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.37
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Old Choa Chu Kang Road"
##   .. ..$ id       : chr "S121"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S106"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.42
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Pulau Ubin"
##   .. ..$ id       : chr "S106"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S06"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.35
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "S06"
##   .. ..$ id       : chr "S06"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S102"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.19
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Semakau Landfill"
##   .. ..$ id       : chr "S102"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S122"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.42
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Sembawang Road"
##   .. ..$ id       : chr "S122"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S96"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.32
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Tanah Merah Coast Road"
##   .. ..$ id       : chr "S96"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S115"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.29
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Tuas South Avenue 3"
##   .. ..$ id       : chr "S115"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S24B"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.37
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Upper Changi Road North"
##   .. ..$ id       : chr "S24B"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S24"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.37
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Upper Changi Road North"
##   .. ..$ id       : chr "S24"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S116"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.28
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "West Coast Highway"
##   .. ..$ id       : chr "S116"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S104"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.44
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Woodlands Avenue 9"
##   .. ..$ id       : chr "S104"
##   ..$ :List of 4
##   .. ..$ device_id: chr "S100"
##   .. ..$ location :List of 2
##   .. .. ..$ latitude : num 1.42
##   .. .. ..$ longitude: num 104
##   .. ..$ name     : chr "Woodlands Road"
##   .. ..$ id       : chr "S100"
##  $ reading_unit: chr "deg C"
##  $ reading_type: chr "DBT 1M F"

可以看到这就是文件原始的结构,读入这样的文件当然我们更希望的是获得一个已经化简的结果。这也是为什么上面一直强调化简的原因。

压平数据

细心的读者应该看到上面化简完的结果里(注意看str的输出),location是单一的一列,而下面下属两个子列。我将上述代码再执行一次:

json <- fromJSON(txt)
str(json)
## List of 3
##  $ stations    :'data.frame':  18 obs. of  4 variables:
##   ..$ device_id: chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ location :'data.frame':  18 obs. of  2 variables:
##   .. ..$ latitude : num [1:18] 1.38 1.26 1.31 1.34 1.28 ...
##   .. ..$ longitude: num [1:18] 104 104 104 104 104 ...
##   ..$ name     : chr [1:18] "Ang Mo Kio Avenue 5" "Banyan Road" "East Coast Parkway" "Kim Chuan Road" ...
##   ..$ id       : chr [1:18] "S109" "S117" "S107" "S43" ...
##  $ reading_unit: chr "deg C"
##  $ reading_type: chr "DBT 1M F"
  ..$ location :'data.frame': 18 obs. of  2 variables:
    .. ..$ latitude : num [1:18] 1.38 1.26 1.31 1.34 1.28 ...
    .. ..$ longitude: num [1:18] 104 104 104 104 104 ...

注意到了吧,location一列是两层结构,访问时需要这样操作:

json$stations$location$latitude
##  [1] 1.37640 1.25600 1.31350 1.33990 1.27990 1.34583 1.37288 1.41680
##  [9] 1.35240 1.18900 1.41731 1.31750 1.29377 1.36780 1.36780 1.28100
## [17] 1.44387 1.41720

这样的结构对于dplyr一类的工具可能就不是很方便了,比如:

library(dplyr)
json$stations %>% summarise(lat = mean(location$latitude))
## Error: Each variable must be a 1d atomic vector or list.
## Problem variables: 'location'

当然你可以先将数据转换程序要的格式,然后再做这个操作,不过jsonlite提供了压平功能就能直接解决后顾之忧。在需要的时候可以使用:

json <- fromJSON(txt, flatten = TRUE)
str(json)
## List of 3
##  $ stations    :'data.frame':  18 obs. of  5 variables:
##   ..$ device_id         : chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ name              : chr [1:18] "Ang Mo Kio Avenue 5" "Banyan Road" "East Coast Parkway" "Kim Chuan Road" ...
##   ..$ id                : chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ location.latitude : num [1:18] 1.38 1.26 1.31 1.34 1.28 ...
##   ..$ location.longitude: num [1:18] 104 104 104 104 104 ...
##  $ reading_unit: chr "deg C"
##  $ reading_type: chr "DBT 1M F"

可以看到,stations已经压平了,location变成了location.latitudelocation.longitude两列了。这一点从直接的打印上很难看出来:

json$stations
##    device_id                    name   id location.latitude
## 1       S109     Ang Mo Kio Avenue 5 S109           1.37640
## 2       S117             Banyan Road S117           1.25600
## 3       S107      East Coast Parkway S107           1.31350
## 4        S43          Kim Chuan Road  S43           1.33990
## 5       S108    Marina Gardens Drive S108           1.27990
## 6        S44          Nanyang Avenue  S44           1.34583
## 7       S121  Old Choa Chu Kang Road S121           1.37288
## 8       S106              Pulau Ubin S106           1.41680
## 9        S06                     S06  S06           1.35240
## 10      S102        Semakau Landfill S102           1.18900
## 11      S122          Sembawang Road S122           1.41731
## 12       S96  Tanah Merah Coast Road  S96           1.31750
## 13      S115     Tuas South Avenue 3 S115           1.29377
## 14      S24B Upper Changi Road North S24B           1.36780
## 15       S24 Upper Changi Road North  S24           1.36780
## 16      S116      West Coast Highway S116           1.28100
## 17      S104      Woodlands Avenue 9 S104           1.44387
## 18      S100          Woodlands Road S100           1.41720
##    location.longitude
## 1            103.8492
## 2            103.6790
## 3            103.9625
## 4            103.8878
## 5            103.8703
## 6            103.6817
## 7            103.7224
## 8            103.9673
## 9            103.9007
## 10           103.7680
## 11           103.8249
## 12           104.0307
## 13           103.6184
## 14           103.9980
## 15           103.9826
## 16           103.7540
## 17           103.7854
## 18           103.7485

这也是为什么需要使用str的原因,当然你也可以用colnames(json$stations)来查看列名,不过这是个鸡生蛋的问题,既然你都知道结构了又何必去查看呢?

有了这样的结构以后,dplyr就能比较如鱼得水了:

json$stations %>% summarise(lat = mean(location.latitude))
##        lat
## 1 1.341603

其他功能

jsonlite还有一些其他功能,值得一体的是校验JSON合法性的validate:

validate(txt)
## [1] TRUE

还有一些直接从网络或者文件直接读取JSON的协助函数,比如

read_json(path, simplifyVector = FALSE, ...)

write_json(x, path, ...)

注意到默认从文件读入关闭了化简功能以提高效率。

json <- read_json('../SG-Weather/metadata/2017-01-AirTemperature.json',simplifyVector = T, flatten=T)
str(json)
## List of 3
##  $ stations    :'data.frame':  18 obs. of  5 variables:
##   ..$ device_id         : chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ name              : chr [1:18] "Ang Mo Kio Avenue 5" "Banyan Road" "East Coast Parkway" "Kim Chuan Road" ...
##   ..$ id                : chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ location.latitude : num [1:18] 1.38 1.26 1.31 1.34 1.28 ...
##   ..$ location.longitude: num [1:18] 104 104 104 104 104 ...
##  $ reading_unit: chr "deg C"
##  $ reading_type: chr "DBT 1M F"

可以看到以上结果和先读入再处理的结果是一样的。但是这两个函数的存在意义其实是不明的,因为fromJSON本身就可以直接接收文件和网络地址输入:

json <- fromJSON('../SG-Weather/metadata/2017-01-AirTemperature.json',flatten = T)
str(json)
## List of 3
##  $ stations    :'data.frame':  18 obs. of  5 variables:
##   ..$ device_id         : chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ name              : chr [1:18] "Ang Mo Kio Avenue 5" "Banyan Road" "East Coast Parkway" "Kim Chuan Road" ...
##   ..$ id                : chr [1:18] "S109" "S117" "S107" "S43" ...
##   ..$ location.latitude : num [1:18] 1.38 1.26 1.31 1.34 1.28 ...
##   ..$ location.longitude: num [1:18] 104 104 104 104 104 ...
##  $ reading_unit: chr "deg C"
##  $ reading_type: chr "DBT 1M F"

JSON的写出和base64相关的功能没有太多可说的,直接参看文档即可。

以下内容可以略过

序列化R对象

最后需要提一句序列化R对象的问题,jsonlite中可以使用如下两个函数完成这个工作:

serializeJSON(x, digits = 8, pretty = FALSE)

unserializeJSON(txt)

比如:

txt = serializeJSON(json, pretty = T)
identical(unserializeJSON(txt),json)
## [1] TRUE

当然这个东西不是万能的,比如ggplot的图像就不能做到序列化,还是老老实实的用save吧。

library(ggplot2)
p <- ggplot(mtcars,aes(mpg,cyl)) + 
  geom_point()
txt = serializeJSON(p)
identical(unserializeJSON(txt),p)
## [1] FALSE

WHYHOW
R jsonlite Tutorial WhyHow

评论