如何在Rust的nom库中实现带序号校验的编号列表解析器?
解决方法
先理清楚核心问题
你遇到的类型错误主要有两个原因:
- 原代码用了
FnOnce约束,它只能被调用一次,但循环里需要反复生成对应序号的分隔符解析器,必须换成允许多次调用的Fn。 - 动态生成
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
相关产品推荐
相关产品推荐

