作者Wush978 (拒看低质媒体)
看板R_Language
标题[心得] R处理大量的JSON资料(Streaming Style)
时间Tue Sep 15 21:28:17 2015
[关键字]: R, JSON, Streaming
[重点摘要]:
这阵子我接了一个案子,要帮忙[核桃运算](
http://www.macrodatalab.com/#/)开发他们
产品BigObject Analytics的R Client。恰巧,他们的RESTful API在捞资料的时候,吐回
来的格式是[jsonlines](
http://jsonlines.org/):
```
{"Sepal.Length":"5.1","Sepal.Width":"3.5","Petal.Length":"1.4","Petal.Width":"0.2","Species":"setosa"}
{"Sepal.Length":"4.9","Sepal.Width":"3.0","Petal.Length":"1.4","Petal.Width":"0.2","Species":"setosa"}
{"Sepal.Length":"4.7","Sepal.Width":"3.2","Petal.Length":"1.3","Petal.Width":"0.2","Species":"setosa"}
{"Sepal.Length":"4.6","Sepal.Width":"3.1","Petal.Length":"1.5","Petal.Width":"0.2","Species":"setosa"}
{"Sepal.Length":"5.0","Sepal.Width":"3.6","Petal.Length":"1.4","Petal.Width":"0.2","Species":"setosa"}
{"Sepal.Length":"5.4","Sepal.Width":"3.9","Petal.Length":"1.7","Petal.Width":"0.4","Species":"setosa"}
```
由於负担起底层Client的责任,这是我第一次要正面迎战这样的资料。以前我遇到这种资
料,都是先乱七八糟的解掉,反正当下能用就好了。但是在写Client的时候,这样的解决
方法是不能让人满意的!
###### 乱七八糟的解法:
```r
library(magrittr)
src # 刚刚的文字资料
strsplit(src, "\n") %>% sapply(fromJSON)
```
话说最近用`magrittr`的pipeline style写程式码真的上瘾了,害我写python的时候觉得
python更难用了... 而且还找不到这种pipeline style。抱歉扯远了!
所以在<del>不能漏气</del>驱使自己进步的动力下,我开始运用过去和JSON打交道的经
验简单研究一下,目前在R 之中,要如何漂亮的处理这类的资料。
### R中处理JSON的套件
相信碰过这个问题的朋友不在少数,而大家的想法大概都类似:找个套件把问题解决掉就
好啦!
但是处理JSON的套件在R里面就有好几个,这里列出我用过的套件:
- [rjson](
https://cran.r-project.org/web/packages/rjson/index.html)
- [RJSONIO](
https://cran.r-project.org/web/packages/RJSONIO/index.html)
- [jsonlite](
https://cran.r-project.org/web/packages/jsonlite/index.html)
而三个套件都提供了`fromJSON`函数,而偏偏三个函数的`fromJSON`都不能用:
#### rjson
`rjson::fromJSON`只处理第一行,後面的资料就当成没看到了。
```
> rjson::fromJSON(src)
$Sepal.Length
[1] "5.1"
$Sepal.Width
[1] "3.5"
$Petal.Length
[1] "1.4"
$Petal.Width
[1] "0.2"
$Species
[1] "setosa"
```
#### RJSONIO
`RJSONIO::fromJSON`则回传了意味不明的一个... 东西?
```
> RJSONIO::fromJSON(src)
# 中间太可怕了,已经删掉
Species
"setosa\"}{\"Sepal.Length\":\"4.9\",\"Sepal.Width\":\"3.0\",\"Petal.Length\":\"1.4\",\"Petal.Width\":\"0.2\",\"Species\":\"setosa\"}{\"Sepal.Length\":\"4.7\",\"Sepal.Width\":\"3.2\",\"Petal.Length\":\"1.3\",\"Petal.Width\":\"0.2\",\"Species\":\"setosa\"}{\"Sepal.Length\":\"4.6\",\"Sepal.Width\":\"3.1\",\"Petal.Length\":\"1.5\",\"Petal.Width\":\"0.2\",\"Species\":\"setosa\"}{\"Sepal.Length\":\"5.0\",\"Sepal.Width\":\"3.6\",\"Petal.Length\":\"1.4\",\"Petal.Width\":\"0.2\",\"Species\":\"setosa\"}{\"Sepal.Len
gth\":\"5.4\",\"Sepal.Width\":\"3.9\",\"Petal.Length\":\"1.7\",\"Petal.Width\":\"0.4\",\"Species\":\"setosa"
```
由於太过惊吓,所以我只好赶快检查一下这东西到底是什麽:
```r
> str(.Last.value)
Named chr [1:5] "5.1" "3.5" "1.4" "0.2" ...
- attr(*, "names")= chr [1:5] "Sepal.Length" "Sepal.Width" "Petal.Length"
"Petal.Width" ...
```
看起来是个... 长度五的向量??? 阿弥陀佛!
#### jsonlite
`jsonlite`则是直接喷错,简单明了!
```r
> jsonlite::fromJSON(src)
Error: parse error: trailing garbage
h":"0.2","Species":"setosa"} {"Sepal.Length":"4.9","Sepal.Wi
(right here) ------^
```
我其实比较喜欢这样子的风格:凡是不能处理的资料就喷错,不要像`rjson`一样不喷错
但是给<del>错误</del>不预期的结果。要是我没注意到有掉资料,直接用到产品之中,
就...
### 革命尚未成功,同志仍需努力
由於这种jsonlines格式的资料是非常非常的常见,所以如果R 没有处理这类函数的功能
,也太扯了吧!
所以於是我就看了一下这三个套件有没有issues区可以讨论,而目前看起来,只有
`jsonlite`有上github。但是简单看一下目前有开的issues,居然没有要求这个套件处理
jsonlines!这通常表示,问题可能已经被解决了...
离题一下,在造访`jsonlite`套件的过程中,我也注意到原来`jsonlite`是`RJSONIO`的
继承者阿!喵了一下Reverse Depends、Reverse Imports的套件名单,看来都和Hadley大
大那帮人有扯上关系(httr、curl)。
果然,我找到了作者Jeroen Ooms在今年useR!研讨会的一份投影片:[Streaming Data
IO in R](
https://jeroenooms.github.io/mongo-slides/#1)还热腾腾的!
里面提到的`stream_in`这个函数,看起来不但是我需要的,而且还提供给R使用者以
Streaming Style处理大量JSON物件的能力。引述Jeroen Ooms投影片的内容:
```r
# This doesn't work...
fromJSON("hugefile.json")
Error: cannot allocate vector of size 8.1 Gb
```
在处理大量数据时,如果电脑不够力,记忆体不够,大家都常常会看到这类错误。
而Streaming Style是许多R 使用者陌生,但是在记忆体不足时非常有用的一种技巧。透
过以下的Demo(也是取自Jeroen Ooms的投影片):
```r
# Calculate delay for flights over 1000 miles
library(dplyr)
library(curl)
con <- gzcon(curl("
http://jeroenooms.github.io/data/nycflights13.json.gz"))
output <- file(tmp <- tempfile(), open = "wb")
stream_in(con, function(df){
df <- filter(df, distance > 1000)
df <- mutate(df, delta = dep_delay - arr_delay)
stream_out(df, output, verbose = FALSE)
})
close(output)
```
这段程式码中,R 先透过`curl`拿到一个来自网路的`connection`,然後串接到`gzcon`
、`stream_in`、中间处理资料的逻辑,最後由`stream_out`输出到硬碟上。
其实这类connection的操作,我也是学R过後好久才知道的。不熟悉的朋友可以想像一下
,上面的程式码就是一段不停运作的生产线。
- `curl("")`就是原料供应处,不断把未加工的资料放到生产线上。
- `gzcon`,不断的以[gzip](
https://zh.wikipedia.org/zh-tw/Gzip)格式将生产线上的
资料解压缩,再放回生产线上。
- `stream_in`再不断的读取生产线上的资料,依照JSON的格式做解释,并且转换成R物件
,放回生产线
- `function(df) { ... }`则把生产线上的R物件拿出来,做过滤,再放回生产线上
- `stream_out`则把生产线上的物件再以JSON的格式写到硬碟之中
在组装生产线的时候,除了定义各种操作之外,就是要安排顺序。而R拥有许多的
`connection`相关的函数,都是吃一个`connection`,再吐出一个`connection`。这种设
计就是要让使用者组装生产线。
ps. 在软体工程中,这是一种叫做[Decorator
Pattern](
https://en.wikipedia.org/wiki/Decorator_pattern)的设计模式的范例。
因此,`curl`回传一个`connection`,`gzcon`接过去处理、再来是`stream_in`... 以此
类推。用这种写法写出来的程式,不需要一次把所有资料装到记忆体之中(这就是
`fromJSON`做的事情)。在资料爆炸的现代来说,这种技巧是穷人在机器记忆体不够时,
还是能用高效率处理问题的一种方法。对於很多资工背景的朋友来说,这种技巧可能是很
基础的吧!可是对於非资工背景出身的我来说,其实也是写程式写了好多年,才注意到这
种技术。
部落格好读版:
http://wush.ghost.io/r-jsonlines/
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 118.161.38.94
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/R_Language/M.1442323700.A.B59.html
※ 编辑: Wush978 (118.161.38.94), 09/15/2015 21:28:59
1F:推 e002311: 超赞的 09/15 21:42
2F:推 celestialgod: 推,虽然看不太懂XD 09/15 22:00
3F:推 celestialgod: 我也很苦恼为啥MATLAB没有PIPE.. 09/15 22:02
4F:→ Wush978: 看不懂吗 >_< 那我再找时间来想怎麽教connection 09/15 22:09
5F:→ celestialgod: 其实主要看不懂是为啥这样可以处理jsonlines 09/15 22:21
6F:推 naturalsmen: 意思是以後BigObject也能用R做串连了吗?? 09/16 13:02
7F:推 evilove: 大推无私心得分享 09/19 22:18