在Rust中无需指定列类型,如何读取扁平化JSON为Polars DataFrame?
Rust中读取扁平化JSON到Polars DataFrame的问题
问题背景
给定如下扁平化JSON示例:
{ "data": [ { "requestId": "IBM", "date": "2024-03-19", "sales": 61860, "company": "International Business Machines", "price": 193.34, "score": 7 }, { "requestId": "AAPL", "date": "2024-03-19", "sales": 383285, "company": "Apple Inc.", "price": 176.08, "score": 9 }, { "requestId": "MSFT", "date": "2024-03-19", "sales": 211915, "company": "Microsoft Corporation", "price": 421.41, "score": 7 } ] }
数据仅包含整数、浮点数和字符串,存在两个问题:
- 如何无需为每个列标注数据类型,将该结构读取为Polars DataFrame?
- 当存在200+动态变化的列时,使用如下结构体中的
HashMap<String, serde_json::Value>来动态存储列是否合适?
#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct Row { requestId: String, date: String, #[serde(flatten)] company_data: HashMap<String, serde_json::Value>, }
解决方案
1. 无需指定列类型读取JSON为Polars DataFrame
方法一:使用Polars内置JSON读取功能
Polars支持直接读取JSON并自动推断列类型,无需手动指定。可通过JsonReader读取文件或内存中的JSON字符串:
use polars::prelude::*; use std::fs::File; fn main() -> Result<(), Box<dyn std::error::Error>> { // 读取本地JSON文件 let file = File::open("data.json")?; let df = JsonReader::new(file) .with_infer_schema_length(Some(100)) // 指定用于推断类型的样本行数,默认100 .finish()? .explode(["data"])? // 展开外层的data数组 .unnest(["data"])?; // 将嵌套结构体拆分为独立列 println!("{}", df); Ok(()) }
Polars会自动根据样本数据识别整数、浮点数、字符串等类型,全程无需手动定义列类型。
方法二:Serde解析后转换为DataFrame
若需要预处理步骤,可先用Serde将JSON解析为动态结构,再转换为Polars DataFrame:
use serde_json::Value; use polars::prelude::*; use std::fs::read_to_string; fn main() -> Result<(), Box<dyn std::error::Error>> { let json_str = read_to_string("data.json")?; let json_value: Value = serde_json::from_str(&json_str)?; // 提取data数组 let data = json_value.get("data").unwrap().as_array().unwrap(); // 收集所有唯一列名 let keys: Vec<&str> = data.iter() .flat_map(|obj| obj.as_object().unwrap().keys().map(|k| k.as_str())) .collect::<std::collections::HashSet<_>>() .into_iter() .collect(); let mut columns = Vec::new(); for key in keys { // 提取列值并转换为对应类型的Series let values: Vec<Value> = data.iter() .map(|obj| obj.get(key).unwrap().clone()) .collect(); let series = match &values[0] { Value::String(_) => Series::new(key, values.into_iter().map(|v| v.as_str().unwrap().to_string()).collect()), Value::Number(n) if n.is_i64() => Series::new(key, values.into_iter().map(|v| v.as_i64().unwrap()).collect()), Value::Number(n) if n.is_f64() => Series::new(key, values.into_iter().map(|v| v.as_f64().unwrap()).collect()), _ => panic!("不支持的数据类型"), }; columns.push(series); } let df = DataFrame::new(columns)?; println!("{}", df); Ok(()) }
这种方式适合需要自定义预处理逻辑的场景,但手动类型判断会增加代码复杂度。
2. 200+动态列使用HashMap的适用性分析
可行场景
- 当列名、数量完全不可预测,无法提前定义结构体字段时,
HashMap<String, serde_json::Value>可以灵活容纳任意键值对,作为中间解析步骤是可行的。
核心缺点
- 性能损耗大:HashMap的哈希查找、
serde_json::Value的动态类型判断都会带来额外开销,200+列的情况下,每一行遍历HashMap会显著降低处理效率。 - 类型不安全:动态类型需要手动判断,容易出现运行时错误,无法利用Rust编译期类型检查的优势。
- 转换繁琐:需要手动将HashMap中的值转换为Polars Series,重复代码多且易出错。
更优替代方案
- 优先用Polars内置功能:直接通过
JsonReader自动推断Schema,Polars会高效处理动态列,自动优化类型,无需手动维护HashMap。 - 用
serde_json::Map替代HashMap:serde_json::Map是Serde内置的有序键值结构,更适配JSON对象解析,能保持列顺序,转换为DataFrame更方便。 - 动态结构体生成:若列名半固定,可通过宏结合Serde动态生成结构体,但实现复杂,仅适合特定场景。
综上,200+动态列场景下,优先选择Polars内置的JSON读取功能;若必须用Serde解析,建议用serde_json::Map替代HashMap以减少性能损耗。
内容的提问来源于Stack Exchange,提问作者Trevor Seibert
相关产品推荐
相关产品推荐

