跳到主要內容

Rust - 以 JSON 為資料來源建立 Polars DataFrame

緣起

在科技的蓬勃發展中,資料處理和分析變得愈發重要。Rust 語言的 Polars 框架為我們提供了一個強大的工具,讓資料操作變得更加容易且高效。這個框架支援以 JSON 格式匯入資料並建立 Dataframe,為我們提供了一個直觀且方便的方式來操作資料。

本文開始

本文的目的,為使用 JSON 為資料來源,在 Rust 中建立 Polars 的 Dataframe。在以下的文章裡,我們將在 Cargo.toml 中,使用如下的設定:

[dependencies] polars = { version = "0.36.2", features = ["json"] }

撰文的當下,Polars 的最新版本為 0.36.2。參考 Polars 的官方文件,它提供了以下的程式碼段落,以匯入 JSON 檔案內容:

use polars::prelude::*;

let mut file = std::fs::File::open("docs/data/path.json").unwrap();
let df = JsonReader::new(&mut file).finish().unwrap();

到這裡,就能輕鬆的利用一個 JSON 檔案的內容,建立出 Dataframe 物件。

好了,故事結束,收工。
.
.
.
.
.
如果故事真的就這麼簡單,我想也就沒有撰寫這篇文章的必要了。

想想上面程式段落的例子,過程裡需要透過一個存在於檔案系統中的 JSON 檔案為媒介,儲存我們要處理的資料,再透過「JsonReader」讀入的 Dataframe 中處理。如果這些資料是從網路上取得(例如呼叫RESTful API),以字串變數的形式存在於系統之中,例如下面的內容:

let msg = r#"[
    {"id": 1, "name": "A", "age": 10},
    {"id": 2, "name": "B", "age": 20}
]"#;

面對這樣的情境,若將資料先落地後再重新讀入,實在是有些不切實際,還可能因為受到磁碟 I/O 的效能影響,而拖慢了程式執行的效率。

那為了避免上述的問題,我們應該怎麼處理呢?

使用「Serde」框架,手動建立 Dataframe

在 Rust 語言的生態系中,有一個「Serde」框架可以協助我們處理 JSON 格式的訊息或檔案。為了使用「Serde」,我們需要調整「Cargo.toml」的內容如下:

[dependencies] polars = { version = "0.36.2", features = ["json"] }
serde = { version = "1", features = ["serde_derive", "derive"] }
serde_json = "1"

但在開始程式碼內容前,我們先說說整個方法的處理邏輯如下:

  1. 定義訊息格式;
  2. 把訊息的內容,透過「Serde」框架轉換為物件或清單(這個視訊息的內容,是否有陣列存在);
  3. 建立 Polars Dataframe。

定義訊息格式

這個動作裡,需要先定義好一個結構(struct);參考了 JSON 訊息的內容,我們將「User」這個結構定義如下:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    id: i32,
    name: String,
    age: i32,
}

這樣做的好處是,在匯入前就明確定義要處理的資料欄位、名稱,以及資料型態。我們會利用這個結構,來匯入 JSON 訊息。

匯入 JSON 訊息

這個步驟裡,我們會使用到「serde_json::from_str()」來匯入 JSON 訊息,具體的方式如下:

// Parse JSON string into a vector of Item structs
let users: Vec<User> = serde_json::from_str(msg).unwrap();

由於 JSON 訊息裡記載的是一個具有陣列型態的資料內容,因此在處理上,就應以「Vec<User>」來宣告,以同時儲存多個不同的「User」。

建立 Polars Dataframe

最後的重頭戲來了!!我們的目標是建立出 Polars Dataframe 物件,以進行後續資料的處理與操作。參考以下的程式片段,以完成這個作業:

use polars::prelude::*;

// Create a DataFrame from the vector of structs
let df = DataFrame::new(vec![
    Series::new("id", users.iter().map(|i| i.id).collect::<Vec<_>>()),
    Series::new("name", users.iter().map(|i| i.name.clone()).collect::<Vec<_>>()),
    Series::new("age", users.iter().map(|i| i.age).collect::<Vec<_>>())
    ]).unwrap();

// Display the DataFrame
println!("{:?}", df);

這裡使用的方式,是將「users」裡的各個欄位透過「iterator」的方式「抽出」,產生對應的資料序列(Series)後,再逐一填寫到 Dataframe 中,最終產生我們的 Dataframe - 「df」。

除了手動建立 Dataframe 的方式外,有沒有什麼比較好的方式,可以用來建立 Dataframe 呢?

透過「Cursor」建立字元串流

其實在 Rust 語言的「std::io」標準庫裡,有一個「Cursor」的 Wrapper 結構,它可以把一個字串轉換為資料串流,以進行 I/O 讀取操作;這個用法有點像我們在 Java 語言裡,會把「String」物件轉換為「StringReader」,再逐字或逐行的讀入這個字串內容。

相關的做法如下:

use polars::prelude::*;

// Create a Cursor from the input string
let cur = Cursor::new(msg);
let df = JsonReader::new(cur).finish().unwrap();

這個方法只用了短短的兩行,就完成了上述的 Dataframe 生成作業,是不是很方便?

結論

在本文中,我們探討了使用 Rust 語言的 Polars 框架處理 JSON 訊息的方法。從檔案匯入的方式進一步擴展,介紹了在處理來自網路的 JSON 字串時,如何避免因資料落地後再讀入的效能問題。透過 Serde 框架,我們定義了資料結構,並展示了如何手動建立 Dataframe,以及透過 std::io::Cursor 結構建立字元串流的方式,使產生 Dataframe 的作業更為簡單方便。這樣的處理方式不僅提高了效能,也讓資料處理更具彈性。

我尚在浩瀚的 Rust 世界中探索各種發展的可能。文章的內容皆為我實際操作後所撰寫下的過程及心得,雖已儘可能追求正確與嚴謹,但內容仍難免因觀念或理解而有所差誤,還望看到此文的同好先進,不忘指點改正,謝謝您。

最後附上本篇文章所用到的完整程式碼供大家參考:

use std::io::Cursor;

use polars::prelude::*;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    id: i32,
    name: String,
    age: i32,
}

fn main() {
    // Define your JSON message as a string
    let msg = r#"[
        {"id": 1, "name": "A", "age": 10},
        {"id": 2, "name": "B", "age": 20}
    ]"#;

    // Parse JSON string into a vector of Item structs
    let users: Vec<User> = serde_json::from_str(msg).unwrap();

    // Create a DataFrame from the vector of structs
    let df = DataFrame::new(vec![
        Series::new("id", users.iter().map(|i| i.id).collect::<Vec<_>>()),
        Series::new("name", users.iter().map(|i| i.name.clone()).collect::<Vec<_>>()),
        Series::new("age", users.iter().map(|i| i.age).collect::<Vec<_>>())
        ]).unwrap();

    // Display the DataFrame
    println!("{:?}", df);

   
    // Create a Cursor from the input string
    let cur = Cursor::new(msg);
    let df = JsonReader::new(cur).finish().unwrap();

    // Display the DataFrame
    println!("{:?}", df);

}


留言

這個網誌中的熱門文章

青山瀑布、老梅外拍

班上的第二次外拍,不過今天的狀況不大好,不太能抓到感覺,很多景都是看人家拍就跟著拍(除了那隻不知名的小蟲除外);到了老梅更慘,沒有減光鏡,只能夠拍出海浪拍打岩岸的畫面,平常看到人家拍老梅那種細細柔柔的味道,一點都沒辦法呈現。 不過我不覺得這次外拍是失敗的,至少我大概知道使用減光鏡能怎樣表現,也拍到了那張岩石上奇特的小蟲。 Ps. 文華大哥謝謝你啦!教我擺個葉子,讓畫面更有感覺。 :) Fig. 1 蠻有詩意的畫面,不過我覺得流水的型狀還不夠漂亮就是了。 Fig. 2 老師評語:在不是楓紅的季節,擺張綠色的葉子,清楚的五爪在畫面上顯的相當有張力。不過Fig1改採直幅構圖,感覺應該會更好。 Fig. 3 老師評語:只有黑與白,顏色比較單調。不過曝光控調的還不錯。 Fig. 4 流水澎湃你感覺到了嗎? 老師評語:同上一張,顏色單調;可以考慮用些植物做為前景來搭配。 Fig. 5 老師評語:綠葉沒有透光的感覺,看起來暗暗的;加上後方白色的瀑布,主體不明顯。 Fig. 6 老師評語:光影變化、曝光控制還不錯,下方的石頭可以考慮裁切掉,或著留更多成為前景。 Fig. 7 我很喜歡這張照片,不錯的光影變化,充滿生命力的感覺。 老師評語:特別的光影變化,還有生動昆蟲。雖然昆蟲的細節因顏色不明顯,不過和特別的光影變化搭配,加上石頭的曲線,乃是相當不錯。 Fig. 8 老師評語:沒有用減光鏡,所以石頭上有輕微的反光,顏色不夠生動。不過構圖上,使用石槽將視線由左下往右上引導至浪花,感覺還不錯。 Fig. 9 老梅奇特的岩岸地形。 老師評語:利用直幅構圖,表現石槽及延伸感。 Fig. 10 海水拍打在岩岸上,濺起陣陣的水花。 老師評語:快門速度要快不快、要慢不慢,可以再斟酌一下。 Fig. 11 老師評語:拍人物時沒有打閃光,導致眼睛有陰影而無神。可以透過閃光燈補光,製造眼神光。

[潛水器材] 防寒衣

新一年度的潛水季準備到來,預訂的潛水訓練班也即將開課。為了充實個人的基本學能,花了些時間在網路及書本上,研讀並整理相關的潛水常識。就先談談潛水活動所常使用到的個人裝備--防寒衣開始吧! 空氣的熱傳導係數為0.025 W/(m·K),而水的熱傳係數為0.6 W/(m·K)。依此推算,水的熱傳導速度,約比空氣快24倍 [1][2] 。 正由於水對熱的傳導速度,要比在空氣中來得快。當潛水員入水之後,人體與水接觸並加上水流的作用,尤其是在清澈的淡水,或是冰冷的環境下,身體的熱量很快就會被帶走。長時間的體溫流失,最終將會造成人體失溫。防寒衣的主要功用,就是在人體與水之間,建立起一層隔離層。透過隔離層的建立,可以有效的將水與人體隔絕,不再接觸並減少體溫流失。 目前市面上主要的防寒衣有以下三類: 濕式防寒衣 (Wet Suit) 主要的材質為Neoprene / Polychloroprene [3] 。當潛水員穿著以Neoprene製作的防寒衣下水時,衣料中的空隙將會被水分子填滿,同時這些水分子將被潛水員的體溫加熱。由於衣料中的水分子被鎖住在空隙中不再流動,因此可以將熱量保持在身體的四週,並大幅的減少體溫的散失,進而達到防寒保溫的作用。 如上所說,濕式防寒衣必須將熱量保留在身體的四周,因此應避免冷水由防寒衣的縫隙進出。選購防漢寒衣時,應特別注意剪裁是否合身,活動是否方便等。 半乾式防寒衣 (Semi-Wet Suit) 你也可以稱為半濕式防寒衣,一般材質亦使用Neoprene。但與濕式防寒衣不同的是,半乾式防寒衣在軀幹與四肢連接處,採用水密設計,可以阻擋冷水進入防寒衣內,以達到防寒效果。不過,由於半乾式防寒衣並不是完全將身體密封於防寒衣內,水仍然有機會從皮膚與防寒衣間的細縫滲入;而且位於亞熱帶地區的台灣,使用半乾式防寒衣的機率並不高,因此購買前應多加考慮。 乾式防寒衣 (Dry Suit) 乾式防寒衣通常以防水材質製作,常用於冰冷海域及極地低溫環境下潛水。由於乾式防寒衣應用的範圍,較多於專業/技術潛水領域,一般休閒潛水較少使用,故不在此多做討論。 除了防止體溫的流失,防寒衣另有一個很重要的功能,就是保護潛水員不受水中生物的螫咬。水底世界中有各式各樣的生物,為了在這充滿競爭及挑戰的環境中生存,牠們各自發展出不同的生存策略,而帶有毒螫...

翻滾吧…鯨鯨!!

翻滾吧…鯨鯨!! , a photo by 我是歐嚕嚕 (I'm Olulu...) on Flickr. Information: Camera: LOMO LC-A Film: Kodak EB3