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

如何在Rust的nom库中实现带序号校验的编号列表解析器?

解决方法

先理清楚核心问题

你遇到的类型错误主要有两个原因:

  1. 原代码用了FnOnce约束,它只能被调用一次,但循环里需要反复生成对应序号的分隔符解析器,必须换成允许多次调用的Fn。
  2. 动态生成tag(&index.to_string())会触发生命周期问题——临时字符串用完就销毁,解析器持有的引用会悬空。不如换个思路:先解析数字,再校验是否等于预期序号,既安全又符合nom的解析逻辑。

修正后的自定义解析器代码

use nom::{
    error::{ErrorKind, ParseError},
    sequence::tuple,
    Err, IResult, InputLength, Parser,
};
use nom::character::complete::{digit1, space1};
use nom::combinator::map_res;

pub fn separated_numbered_list1<I, O, E, F, S>(
    sep: S,
    mut f: F,
) -> impl FnMut(I) -> IResult<I, Vec<O>, E>
where
    I: Clone + InputLength + PartialEq,
    F: Parser<I, O, E>,
    // 分隔符闭包接收预期序号,返回对应的解析器
    S: Fn(i32) -> impl Parser<I, (), E>,
    E: ParseError<I>,
{
    move |mut i: I| {
        let mut res = Vec::new();
        let mut expected_index = 2; // 第一个元素是1开头,下一个要匹配2

        // 先解析第一个元素
        match f.parse(i.clone()) {
            Err(e) => return Err(e),
            Ok((i1, o)) => {
                res.push(o);
                i = i1;
            }
        }

        loop {
            let len = i.input_len();
            // 用当前预期的序号生成分隔符解析器
            match sep(expected_index).parse(i.clone()) {
                Err(Err::Error(_)) => return Ok((i, res)),
                Err(e) => return Err(e),
                Ok((i1, _)) => {
                    // 防死循环:必须消耗输入
                    if i1.input_len() == len {
                        return Err(Err::Error(E::from_error_kind(i1, ErrorKind::SeparatedList)));
                    }

                    // 解析分隔符后面的元素
                    match f.parse(i1.clone()) {
                        Err(Err::Error(_)) => return Ok((i, res)),
                        Err(e) => return Err(e),
                        Ok((i2, o)) => {
                            res.push(o);
                            i = i2;
                        }
                    }
                }
            }
            expected_index += 1;
        }
    }
}

实际使用示例

use nom::character::complete::take_while1;
use nom::sequence::preceded;
use nom::error::Error;

// 元素允许字母和空格,支持解析"Whole Milk"这类带空格的内容
fn is_valid_element_char(c: char) -> bool {
    c.is_alphabetic() || c.is_whitespace()
}

fn parser(input: &str) -> IResult<&str, Vec<&str>, Error<&str>> {
    preceded(
        tuple((digit1, tag(". "))), // 先处理开头的"1. "
        separated_numbered_list1(
            // 定义分隔符解析逻辑:匹配" [序号]. "并校验序号是否符合预期
            |expected| {
                tuple((
                    space1,
                    map_res(digit1, |s: &str| s.parse::<i32>()), // 把解析到的数字转为整数
                    tag(". "),
                ))
                .filter(|&(_, num, _)| num == expected) // 校验序号是否和预期一致
                .map(|_| ()) // 忽略解析结果,只关心匹配是否成功
            },
            take_while1(is_valid_element_char), // 解析列表元素内容
        ),
    )(input)
}

fn main() {
    let valid_input = "1. Milk 2. Bread 3. Bacon";
    println!("{:?}", parser(valid_input)); // 成功输出:Ok(("", ["Milk", "Bread", "Bacon"]))

    let invalid_input1 = "1. Milk 3. Bread 4. Bacon";
    println!("{:?}", parser(invalid_input1)); // 解析失败,第二个序号应为2而非3

    let invalid_input2 = "1. Milk 8. Bread 1. Bacon";
    println!("{:?}", parser(invalid_input2)); // 解析失败,第二个序号不符合递增要求
}

关键改动说明

  • 闭包约束调整:将FnOnce替换为Fn(i32) -> impl Parser<I, (), E>,允许循环中反复调用闭包生成对应序号的解析器。
  • 序号校验逻辑:放弃动态生成tag的方案,改为先解析数字再校验,彻底规避生命周期问题,也更贴合nom的解析范式。
  • 保留防死循环检查:和官方separated_list1保持一致,确保每次解析都消耗输入,避免无限循环。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 22:25:30