[心得] R處理大量的JSON資料(Streaming Style)
[關鍵字]: 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://www.ptt.cc/bbs/R_Language/M.1442323700.A.B59.html
※ 編輯: Wush978 (118.161.38.94), 09/15/2015 21:28:59
推
09/15 21:42, , 1F
09/15 21:42, 1F
推
09/15 22:00, , 2F
09/15 22:00, 2F
推
09/15 22:02, , 3F
09/15 22:02, 3F
→
09/15 22:09, , 4F
09/15 22:09, 4F
→
09/15 22:21, , 5F
09/15 22:21, 5F
推
09/16 13:02, , 6F
09/16 13:02, 6F
推
09/19 22:18, , 7F
09/19 22:18, 7F
R_Language 近期熱門文章
PTT數位生活區 即時熱門文章