rust语言nom库常用接口使用示例

nom 库使用解析器组合和统一的 IResult 返回类型。每个解析器都是一个接受输入(例如 &str 或 &[u8])并返回 IResult 的函数。
以下是一些常用的 nom 接口及其使用示例:
核心类型:IResult
所有 nom 解析器都返回一个 IResult<I, O, E> 类型,这是一个 Result 的别名: 
// IResult<剩余输入, 输出结果, 错误类型>
type IResult<'a, O> = nom::IResult<&'a str, O>; 
常用接口(组合)使用示例
请确保您已在 Cargo.toml 中添加 nom = "7.1.3"(或最新版本),并在代码顶部引入必要的 use 语句。
nom 库常用接口
这里列出了一些最常用的接口,以及它们的作用:
 
接口/组合模块路径(通常)作用描述
tag() / tag_no_case() bytes::complete 匹配一个固定的字节序列或字符串。
char() character::complete 匹配一个特定的字符。
alpha1() / digit1() character::complete 匹配一个或多个字母/数字。
space0() / space1() character::complete 匹配零个/一个或多个空格(包含 \n)。
line_ending() character::complete 匹配行尾的换行符(\n 或 \r\n)。
alt() branch 尝试一系列解析器,返回第一个成功的解析器的结果。
map() / value() combinator map 用于转换解析结果;value 用于忽略解析结果,返回一个固定值。
opt() combinator 使一个解析器成为可选的,返回 Option<O>
many0() / many1() multi 匹配零个/一个或多个重复模式,返回一个 Vec<O>
tuple() sequence 按顺序运行多个解析器,并将结果收集到一个元组中。
separated_pair() sequence 解析 A <分隔符> B 结构,返回 A 和 B 的结果,忽略分隔符。
preceded() / terminated() sequence 解析 A B 或 B A 结构,只返回其中一个的结果(忽略另一个)。

当然以上接口仅是冰山一角,还有很多常用接口未列出,后续有时间我会结合项目写给大家,敬请期待。

1. tag() 和 char(): 匹配固定内容

  • tag(T): 匹配一个固定的字节序列或字符串。
  • char(C): 匹配一个特定的字符。
use nom::{bytes::complete::tag, character::complete::char, IResult};

fn parse_http(input: &str) -> IResult<&str, &str> {
    // 匹配 "http://" 字符串,返回匹配的字符串本身作为输出
    tag("http://")(input) 
}

fn parse_comma(input: &str) -> IResult<&str, char> {
    // 匹配单个逗号字符,返回 ' , ',保证intput以逗号开头,否则解析错误。
    char(',')(input) 
}

2. alpha1() 和 digit1(): 匹配字符类型

这些位于 character::complete 模块中,用于匹配特定类型的字符序列。
  • alpha1: 匹配一个或多个字母 [a-zA-Z]
  • digit1: 匹配一个或多个数字 [0-9] 
use nom::character::complete::{alpha1, digit1};
use nom::IResult;

fn parse_word(input: &str) -> IResult<&str, &str> {
    alpha1(input) // 匹配 "hello" 从 "hello world"
}

fn parse_number_str(input: &str) -> IResult<&str, &str> {
    digit1(input) // 匹配 "123" 从 "123xyz"
}

3. separated_pair(): 解析由分隔符隔开的两个元素

一个非常强大的序列组合子,用于解析 A <分隔符> B 的结构,并返回 A 和 B 的结果,忽略分隔符。
use nom::{
    character::complete::{alpha1, char},
    sequence::separated_pair,
    IResult,
};

fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> {
    // 尝试解析:键 (alpha1) : (char(':')) 值 (alpha1)
    separated_pair(
        alpha1,    // 解析第一个元素 (键)
        char(':'), // 解析分隔符 (冒号)
        alpha1     // 解析第二个元素 (值)
    )(input)
}
// parse_key_value("name:Alice") 会返回 Ok(("", ("name", "Alice")))

4. alt(): 尝试多个解析器(分支选择)

尝试一系列解析器,返回第一个成功的解析器的结果。
use nom::{
    branch::alt,
    bytes::complete::tag,
    IResult,
};

fn parse_unit(input: &str) -> IResult<&str, &str> {
    // 尝试匹配 "kg" 或 "lbs"
    alt((tag("kg"), tag("lbs"), tag("g")))(input)
}
// parse_unit("lbs rocks") 会返回 Ok((" rocks", "lbs"))

5. map(): 转换解析结果

将一个解析器的输出传递给一个自定义函数,以转换其类型或结构。
use nom::combinator::map;
use nom::character::complete::digit1;
use nom::IResult;

fn parse_u32(input: &str) -> IResult<&str, u32> {
    // 1. 使用 digit1 解析器得到 &str
    // 2. 使用 map 将 &str 转换为 u32
    map(
        digit1, 
        |digit_str: &str| digit_str.parse::<u32>().unwrap()
    )(input)
}
// parse_u32("123 apple") 会返回 Ok((" apple", 123u32))

6.many0匹配零个或多个重复模式

这里不再赘述many1。

use nom::{

IResult,

number::streaming::be_u16,

multi::many0,

combinator::complete,

};


fn parse_data(input: &[u8]) -> IResult<&[u8], Vec<u16>> { many0(complete(be_u16))(input)
//many0(be_u16)(input) } let many0_input:
&[u8] = &[0x01, 0x02, 0x03, 0x04]; match parse_data(many0_input) { Ok((rem, res)) => println!("成功: 剩余 {:?}, 结果 {:?}", rem, res), Err(e) => println!("失败: {:?}", e), }

需要注意的是:上述be_u16函数使用的是use nom::number::streaming::be_u16。需要注意nom 处理流式解析(Streaming)与完整解析(Complete)的核心差异。

1). 核心结论
  • streaming::be_u16:它是“悲观”的。如果字节不够,它认为数据还没传完,返回 Incomplete
  • complete::be_u16:它是“果断”的。如果字节不够,它认为数据已经损坏或结束,返回 Error
  • many0 的特性:遇到 Error 停止循环并返回成功;遇到 Incomplete 则向上抛出 Incomplete
2). 为什么 streaming 版本加 complete 才有救?
当你使用 streaming::be_u16 时:
  • 不加 complete:剩下 1 字节时,be_u16 返回 Incompletemany0 看到这个信号后,会认为“还没解析完,等更多数据”,于是整个 many0 也返回 Incomplete。这导致你拿不到已经解析好的数据。
  • 增加 complete(...) 包装:complete 组合子的唯一作用是拦截 Incomplete 并将其转变为 Error
    • 当 be_u16 返回 Incomplete 时,被 complete 强制转成了 Error
    • many0 接收到 Error,认为“循环到此为止”,于是把之前解析好的 Vec 返回给你。
3). 为什么 complete 版本加不加都一样?
当你使用 nom::number::complete::be_u16 时:
  • 这个解析器本身就自带“数据已完整”的假设。
  • 如果剩下 1 字节,它直接返回 Error,永远不会返回 Incomplete
  • 既然它不返回 Incomplete,那么外层的 complete() 组合子就抓不到任何 Incomplete 信号,自然也就什么都不做(原样透传 Error)。
  • many0 接收到这个 Error,正常停止循环。
4). 总结对比表
 
解析器类型剩余单字节时的行为many0(parser) 的结果many0(complete(parser)) 的结果
streaming::be_u16 返回 Incomplete Incomplete (继续等待) Ok (停止循环并返回结果)
complete::be_u16 返回 Error Ok (停止循环并返回结果) Ok (行为一致)

以上Demo的完整版如下:

use nom::{
    bytes::complete::tag,
    character::complete::{alpha1, char, digit1},
    sequence::separated_pair,
    branch::alt,
    combinator::map,
    IResult,
};

// tag demo.
fn parse_http(input: &str) -> IResult<&str, &str> {
    // 匹配 "https://" 字符串,返回匹配的字符串本身作为输出
    tag("https://")(input) 
}

// char demo.
fn parse_comma(input: &str) -> IResult<&str, char> {
    // 匹配单个逗号字符,返回 ' , ',保证intput以逗号开头,否则解析错误。
    char(',')(input) 
}

// alpha1 demo.
fn parse_word(input: &str) -> IResult<&str, &str> {
    alpha1(input) // 匹配 "hello" 从 "hello world"
}

// digit1 demo.
fn parse_number_str(input: &str) -> IResult<&str, &str> {
    digit1(input) // 匹配 "123" 从 "123xyz"
}

// separated_pair demo.定义一个解析键值对的函数
fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> {
    // 使用 separated_pair 组合子
    // 1. 解析第一个元素 (键)
    // 2. 解析分隔符 (冒号)
    // 3. 解析第二个元素 (值)
    separated_pair(
        alpha1,       // 匹配一个或多个字母作为键
        char(':'),    // 匹配单个冒号作为分隔符
        alpha1        // 匹配一个或多个字母作为值
    )(input)
}

// alt demo.
fn parse_unit(input: &str) -> IResult<&str, &str> {
    // 尝试匹配 "kg" 或 "lbs"
    alt((tag("kg"), tag("lbs"), tag("g")))(input)
}

// map demo.
fn parse_u32(input: &str) -> IResult<&str, u32> {
    // 1. 使用 digit1 解析器得到 &str
    // 2. 使用 map 将 &str 转换为 u32
    map(
        digit1, 
        |digit_str: &str| digit_str.parse::<u32>().unwrap()
    )(input)
}

fn main() {
    let tag_input = "https://i.cnblogs.com/posts/edit;postId=19295839";
    
    // IResult 的 Ok 变体包含剩余未解析的输入 和 解析结果(一个元组)
    match parse_http(tag_input) {
        Ok((remaining_input, value)) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!("ret值: '{}'", value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }

    let comma_input = ",comma.";
    match parse_comma(comma_input) {
        Ok((remaining_input, value)) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!(" 值: '{}'", value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }
    
    let alpha1_input = "hello, sun.";
    match parse_word(alpha1_input) {
        Ok((remaining_input, value)) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!(" 值: '{}'", value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }

    let digit1_input = "123, moon.";
    match parse_number_str(digit1_input) {
        Ok((remaining_input, value)) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!(" 值: '{}'", value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }

    let input = "name:Alice,one";
    
    // IResult 的 Ok 变体包含剩余未解析的输入 和 解析结果(一个元组)
    match parse_key_value(input) {
        Ok((remaining_input, (key, value))) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!("键: '{}', 值: '{}'", key, value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }
    
    let alt_input = "lbs,rocks";
     match parse_unit(alt_input) {
        Ok((remaining_input, value)) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!("值: '{}'", value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }
    
    let map_input = "369, river.";
     match parse_u32(map_input) {
        Ok((remaining_input, value)) => {
            println!("成功解析!");
            println!("剩余输入: '{}'", remaining_input);
            println!("值: '{}'", value);
        }
        Err(e) => {
            eprintln!("解析失败: {:?}", e);
        }
    }

}
View Code

last. 实战举例

这里以解析http报文为例进行说明。

新建http解析工程

cargo new httpparser_example

编辑Cargo.toml文件,添加以下配置nom依赖。

[dependencies]
nom = "7.1.3" 
将以下代码复制到你的 src/main.rs 文件中。
use nom::{
    branch::alt,
    bytes::complete::{tag, take_until},
    character::complete::{alpha1, line_ending, space1},
    combinator::{map, opt, value},
    multi::many0,
    sequence::{separated_pair, terminated},
    IResult,
};

// 定义一个结构体来保存解析结果
#[derive(Debug, PartialEq, Eq)]
struct Request<'a> {
    method: &'a str,
    path: &'a str,
    headers: Vec<(&'a str, &'a str)>,
}

// -- 解析器组合子函数 --

// 解析 HTTP 方法 (GET, POST, PUT...)
// 使用 alt 尝试匹配多个 tag
fn parse_method(input: &str) -> IResult<&str, &str> {
    alt((tag("GET"), tag("POST"), tag("PUT"), tag("DELETE")))(input)
}

// 解析路径,直到遇到空格
fn parse_path(input: &str) -> IResult<&str, &str> {
    take_until(" ")(input)
}

// 解析 HTTP 版本号 (例如 HTTP/1.1),并使用 value 忽略它,只返回一个固定的字符串 "HTTP/1.1"
fn parse_version(input: &str) -> IResult<&str, &str> {
    value(tag("HTTP/1.1"), tag("HTTP/1.1"))(input)
}

// 解析请求行的第一行: METHOD PATH VERSION
fn parse_request_line(input: &str) -> IResult<&str, (&str, &str, &str)> {
    // 使用 space1 作为分隔符分隔三个部分
    let (input, method) = parse_method(input)?;
    let (input, _) = space1(input)?;
    let (input, path) = parse_path(input)?;
    let (input, _) = space1(input)?;
    let (input, version) = parse_version(input)?;
    let (input, _) = line_ending(input)?; // 匹配行尾换行符

    Ok((input, (method, path, version)))
}

// 解析单个头部字段: Key: Value
fn parse_header(input: &str) -> IResult<&str, (&str, &str)> {
    // 使用 separated_pair 解析被冒号和空格分隔的 Key 和 Value
    separated_pair(
        alpha1,                     // Key: 字母序列
        tag(": "),                  // Separator: ": "
        take_until("\r\n"),         // Value: 直到行尾的所有内容
    )(input)
}

// 解析所有头部(多行)
fn parse_headers(input: &str) -> IResult<&str, Vec<(&str, &str)>> {
    // 使用 many0 匹配零个或多个 parse_header 模式
    // 并且每个模式后面都必须跟一个换行符 (\r\n) (使用 terminated)
    many0(
        terminated(parse_header, line_ending)
    )(input)
}

// 组合所有解析器,形成完整的请求解析器
fn parse_request(input: &str) -> IResult<&str, Request> {
    // 使用 map 将解析结果转换为我们定义的 Request 结构体
    let (input, (method, path, _version)) = parse_request_line(input)?;
    let (input, headers) = parse_headers(input)?;
    
    // 忽略可选的空行
    let (input, _) = opt(line_ending)(input)?; 

    // 使用 map 的闭包返回最终的 Request 结构体
    Ok((input, Request { method, path, headers }))
}


fn main() {
    let http_request_input = "\
GET /home/index.html HTTP/1.1\r\n\
Host: localhost:8080\r\n\
User-Agent: Rust-nom-Demo\r\n\
Accept: */*\r\n\
\r\n";

    match parse_request(http_request_input) {
        Ok((remaining_input, request)) => {
            println!("--- 成功解析 HTTP 请求 ---");
            println!("剩余未解析的输入(Payload): '{}'", remaining_input);
            println!("方法: {}", request.method);
            println!("路径: {}", request.path);
            println!("头部数量: {}", request.headers.len());
            println!("头部详情: {:?}", request.headers);
        }
        Err(e) => {
            eprintln!("解析失败错误: {:?}", e);
        }
    }
}
运行示例
在终端中,确保你在项目根目录,然后运行 cargo run
示例输出
--- 成功解析 HTTP 请求 ---
剩余未解析的输入(Payload): ''
方法: GET
路径: /home/index.html
头部数量: 3
头部详情: [("Host", "localhost:8080"), ("User-Agent", "Rust-nom-Demo"), ("Accept", "*/*")]

参考资料

1.Playing with Nom and parser combinators

 

posted @ 2025-12-02 09:33  PKICA  阅读(13)  评论(0)    收藏  举报