[心得] R處理大量的JSON資料(Streaming Style)

看板R_Language作者 (拒看低質媒體)時間10年前 (2015/09/15 21:28), 10年前編輯推噓5(502)
留言7則, 5人參與, 最新討論串1/1
[關鍵字]: 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
推,雖然看不太懂XD
09/15 22:00, 2F

09/15 22:02, , 3F
我也很苦惱為啥MATLAB沒有PIPE..
09/15 22:02, 3F

09/15 22:09, , 4F
看不懂嗎 >_< 那我再找時間來想怎麼教connection
09/15 22:09, 4F

09/15 22:21, , 5F
其實主要看不懂是為啥這樣可以處理jsonlines
09/15 22:21, 5F

09/16 13:02, , 6F
意思是以後BigObject也能用R做串連了嗎??
09/16 13:02, 6F

09/19 22:18, , 7F
大推無私心得分享
09/19 22:18, 7F
文章代碼(AID): #1L-1pqjP (R_Language)
文章代碼(AID): #1L-1pqjP (R_Language)