利用R處理大量的JSON資料 (Streaming Style)

這陣子我接了一個案子,要幫忙核桃運算開發他們產品BigObject Analytics的R Client。恰巧,他們的RESTful API在撈資料的時候,吐回來的格式是jsonlines

{"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的時候,這樣的解決方法是不能讓人滿意的!

亂七八糟的解法:
library(magrittr)  
src # 剛剛的文字資料  
strsplit(src, "\n") %>% sapply(fromJSON)  

話說最近用magrittr的pipeline style寫程式碼真的上癮了,害我寫python的時候覺得python更難用了... 而且還找不到這種pipeline style。抱歉扯遠了!

所以在不能漏氣驅使自己進步的動力下,我開始運用過去和JSON打交道的經驗簡單研究一下,目前在R 之中,要如何漂亮的處理這類的資料。

Imgur Imgur

R中處理JSON的套件

相信碰過這個問題的朋友不在少數,而大家的想法大概都類似:找個套件把問題解決掉就好啦!

但是處理JSON的套件在R裡面就有好幾個,這裡列出我用過的套件:

而三個套件都提供了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)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              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" 

由於太過驚嚇,所以我只好趕快檢查一下這東西到底是什麼:

> 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" ...

看起來是個... 長度五的向量??? 阿彌陀佛!

出處:http://www.appledaily.com.tw/appledaily/article/headline/20141103/36185130/

jsonlite

jsonlite則是直接噴錯,簡單明瞭!

> jsonlite::fromJSON(src)
Error: parse error: trailing garbage  
          h":"0.2","Species":"setosa"} {"Sepal.Length":"4.9","Sepal.Wi
                     (right here) ------^

我其實比較喜歡這樣子的風格:凡是不能處理的資料就噴錯,不要像rjson一樣不噴錯但是給錯誤不預期的結果。要是我沒注意到有掉資料,直接用到產品之中,就...

革命尚未成功,同志仍需努力

由於這種jsonlines格式的資料是非常非常的常見,所以如果R 沒有處理這類函數的功能,也太扯了吧!

所以於是我就看了一下這三個套件有沒有issues區可以討論,而目前看起來,只有jsonlite有上github。但是簡單看一下目前有開的issues,居然沒有要求這個套件處理jsonlines!這通常表示,問題可能已經被解決了...

離題一下,在造訪jsonlite套件的過程中,我也注意到原來jsonliteRJSONIO的繼承者阿!喵了一下Reverse Depends、Reverse Imports的套件名單,看來都和Hadley大大那幫人有扯上關係(httr、curl)。

果然,我找到了作者Jeroen Ooms在今年useR!研討會的一份投影片:Streaming Data IO in R還熱騰騰的!

裡面提到的stream_in這個函數,看起來不但是我需要的,而且還提供給R使用者以Streaming Style處理大量JSON物件的能力。引述Jeroen Ooms投影片的內容:

# This doesn't work...
fromJSON("hugefile.json")  
Error: cannot allocate vector of size 8.1 Gb  

在處理大量數據時,如果電腦不夠力,記憶體不夠,大家都常常會看到這類錯誤。

而Streaming Style是許多R 使用者陌生,但是在記憶體不足時非常有用的一種技巧。透過以下的Demo(也是取自Jeroen Ooms的投影片):

# 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,然後串接到gzconstream_in、中間處理資料的邏輯,最後由stream_out輸出到硬碟上。

Imgur 圖片出處:https://en.wikipedia.org/wiki/Production_line#/media/File:Krispy_Kreme_Doughnuts.jpg

其實這類connection的操作,我也是學R過後好久才知道的。不熟悉的朋友可以想像一下,上面的程式碼就是一段不停運作的生產線。

  • curl("")就是原料供應處,不斷把未加工的資料放到生產線上。
  • gzcon,不斷的以gzip格式將生產線上的資料解壓縮,再放回生產線上。
  • stream_in再不斷的讀取生產線上的資料,依照JSON的格式做解釋,並且轉換成R物件,放回生產線
  • function(df) { ... }則把生產線上的R物件拿出來,做過濾,再放回生產線上
  • stream_out則把生產線上的物件再以JSON的格式寫到硬碟之中

在組裝生產線的時候,除了定義各種操作之外,就是要安排順序。而R擁有許多的connection相關的函數,都是吃一個connection,再吐出一個connection。這種設計就是要讓使用者組裝生產線。

ps. 在軟體工程中,這是一種叫做Decorator Pattern的設計模式的範例。

因此,curl回傳一個connectiongzcon接過去處理、再來是stream_in... 以此類推。用這種寫法寫出來的程式,不需要一次把所有資料裝到記憶體之中(這就是fromJSON做的事情)。在資料爆炸的現代來說,這種技巧是窮人在機器記憶體不夠時,還是能用高效率處理問題的一種方法。對於很多資工背景的朋友來說,這種技巧可能是很基礎的吧!可是對於非資工背景出身的我來說,其實也是寫程式寫了好多年,才注意到這種技術。