You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Rust Polars中通过自定义函数将Utf8 Series转为List<Utf8> Series

问题描述

我有一个Polars DataFrame的Utf8列,想要将其转换为List<Utf8>列。具体场景是提取每行HTML文档文本,用soup解析所有<p>标签的段落,将段落文本收集为Vec<String>或Vec<&str>,已编写独立解析函数:

fn parse_paragraph(s: &str) -> Vec<&str> {
    let soup = Soup::new(s);
    soup.tag(p).find_all().iter().map(|&p| p.text()).collect()
}

我参考Polars文档写了字符串分割的简化示例代码:

use polars::prelude::*;

fn vector_split(text: &str) -> Vec<&str> {
    text.split(' ').collect()
}

fn vector_split_series(s: &Series) -> PolarsResult<Series> {
    let output : Series = s.utf8()
        .expect("Text data")
        .into_iter()
        .map(|t| t.map(vector_split))
        .collect();
    Ok(output)
}

fn main() {
    let df = df! [
        "text" => ["a cat on the mat", "a bat on the hat", "a gnat on the rat"]
    ].unwrap();

    df.clone().lazy()
        .select([
            col("text").apply(|s| vector_split_series(&s), GetOutput::default())
                .alias("words")
        ])
        .collect();
}

(注:我知道Utf8 Series有内置split函数,此处仅为简化示例)

运行cargo check时出现编译错误:

error[E0277]: a value of type `polars::prelude::Series` cannot be built from an iterator over elements of type `Option<Vec<&str>>`
    --> src/main.rs:11:27
     |
11   |       let output : Series = s.utf8()
     |  ___________________________^
12   | |         .expect("Text data")
13   | |         .into_iter()
14   | |         .map(|t| t.map(vector_split))
     | |_____________________________________^ value of type `polars::prelude::Series` cannot be built from `std::iter::Iterator<Item=Option<Vec<&str>>>`
15   |           .collect();
     |            ------- required by a bound introduced by this call
     |
     = help: the trait `FromIterator<Option<Vec<&str>>>` is not implemented for `polars::prelude::Series`
     = help: the following other types implement trait `FromIterator<A>`:
               <polars::prelude::Series as FromIterator<&'a bool>>
               <polars::prelude::Series as FromIterator<&'a f32>>
               <polars::prelude::Series as FromIterator<&'a f64>>
               <polars::prelude::Series as FromIterator<&'a i32>>
               <polars::prelude::Series as FromIterator<&'a i64>>
               <polars::prelude::Series as FromIterator<&'a str>>
               <polars::prelude::Series as FromIterator<&'a u32>>
               <polars::prelude::Series as FromIterator<&'a u64>>
             and 15 others
note: required by a bound in `std::iter::Iterator::collect`

请问这种转换场景的正确实现范式是什么?有没有更简便的自定义函数应用方式?


解决方案

核心问题分析

编译错误源于两个关键点:

  1. 生命周期不匹配:Vec<&str>中的引用依赖原字符串的生命周期,但Polars的List列需要拥有所有权的数据(如Vec<String>),否则会导致引用失效。
  2. Series构造方式错误:直接通过collect()从Option<Vec<&str>>迭代器构建Series不被支持,需要显式使用List类型的Builder来构造。

正确实现方式

1. 调整函数返回拥有所有权的类型

先把解析函数改成返回Vec<String>,彻底规避生命周期问题:

fn parse_paragraph(s: &str) -> Vec<String> {
    let soup = Soup::new(s);
    soup.tag("p").find_all().iter().map(|p| p.text().to_string()).collect()
}

fn vector_split(text: &str) -> Vec<String> {
    text.split(' ').map(|s| s.to_string()).collect()
}

2. 使用Builder构造List Series

处理整个Series时,用ListUtf8Builder来构建目标Series:

fn vector_split_series(s: &Series) -> PolarsResult<Series> {
    let utf8_series = s.utf8().expect("Text data");
    let mut builder = ListUtf8Builder::with_capacity(utf8_series.len());

    for opt_str in utf8_series.into_iter() {
        match opt_str {
            Some(s) => builder.append_slice(&vector_split(s)),
            None => builder.append_null(),
        }
    }

    Ok(builder.finish().into_series())
}

3. 用map_elements简化Lazy API实现

Polars Lazy API提供的map_elements可以直接处理单个元素的转换,无需手动操作整个Series,代码更简洁:

use polars::prelude::*;

fn vector_split(text: &str) -> Vec<String> {
    text.split(' ').map(|s| s.to_string()).collect()
}

fn main() -> PolarsResult<()> {
    let df = df! [
        "text" => ["a cat on the mat", "a bat on the hat", "a gnat on the rat"]
    ]?;

    let result_df = df.lazy()
        .select([
            col("text")
                .map_elements(
                    |s: &str| vector_split(s),
                    GetOutput::from_type(DataType::List(Box::new(DataType::Utf8)))
                )
                .alias("words")
        ])
        .collect()?;

    println!("{:?}", result_df);
    Ok(())
}

4. HTML解析场景完整示例

将parse_paragraph集成到Lazy API中的完整代码:

use polars::prelude::*;
use soup::Soup;

fn parse_paragraph(s: &str) -> Vec<String> {
    let soup = Soup::new(s);
    soup.tag("p").find_all().iter().map(|p| p.text().to_string()).collect()
}

fn main() -> PolarsResult<()> {
    let df = df! [
        "html" => [
            "<p>第一段</p><p>第二段</p>",
            "<p>Hello</p><p>World</p>",
            "<div>无段落</div>"
        ]
    ]?;

    let result_df = df.lazy()
        .select([
            col("html")
                .map_elements(
                    |html_str: &str| parse_paragraph(html_str),
                    GetOutput::from_type(DataType::List(Box::new(DataType::Utf8)))
                )
                .alias("paragraphs")
        ])
        .collect()?;

    println!("{:?}", result_df);
    Ok(())
}

关键要点

  • 优先使用拥有所有权的类型(如String)作为List元素,避免引用生命周期问题。
  • Lazy API场景下,首选map_elements处理单元素转换,代码更简洁且Polars会自动优化执行逻辑。
  • 若需手动处理整个Series,使用对应类型的Builder(如ListUtf8Builder)构造Series,而非直接collect()。

内容的提问来源于stack exchange,提问作者BrettW

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 09:36:10