天天看點

從零編寫一個解析器(1)—— 解析數字

  • 文章名稱:從零編寫一個解析器(1)—— 解析數字
  • 參考位址:https://github.com/Geal/nom/blob/master/doc/making_a_new_parser_from_scratch.md
  • 文章來自:https://github.com/suhanyujie/my-parser-rs
  • 文章作者:suhanyujie
  • Tips:文章如果有任何錯誤之處,還請指正,謝謝~
  • 标簽:Rust,parser

長久以來,由于我在工作中使用 go 語言,是以時常會遇到需要将 sql 轉換為 struct 的需求,雖然在網上能夠找到一些将 sql、json 等轉換為 struct 的工具,但大都無法配置,要麼隻支援将 json 轉 struct,要麼轉換後 tag 的風格不符合我所需要的。

基于這種情況,我一直想自己寫一套可自定義的轉換工具,要想能靈活的轉換,先需要對源碼字元串(sql 或者 json)進行解析,是以,我們從這裡開始,逐漸學習如何實作一個解析器,最終的目标是可以靈活的将 sql、json 轉換為 go struct。

nom是 Rust 中一個強大的解析器庫,而我們就是要基于 nom 對源字元串進行解析。

本文的前半部分深度參考 nom 倉庫中的一個文檔。

萬丈高樓平地起,要想用 nom 寫好一個解析器,我們先要對 nom 進行一些了解,是以先從一些小示例開始,主要是通過一些 nom 自帶的函數來實作簡單的解析。

第一次解析

根據 文檔 中的介紹,我們先從解析一個括号中的數字 ——

(12345)

開始。先定義一個函數簽名,它用于把字元串

(12345)

解析成數字:

fn parse_u32(input: &[u8]) -> IResult<&[u8], u32>
           

parse_u32 是函數名稱,它接收一個 input 參數,

IResult

nom::IResult

,是 nom 中常用的結果傳回類型。可以通過文檔檢視其聲明和注釋:

/// Holds the result of parsing functions
///
/// It depends on the input type `I`, the output type `O`, and the error type `E`
/// (by default `(I, nom::ErrorKind)`)
///
/// The `Ok` side is a pair containing the remainder of the input (the part of the data that
/// was not parsed) and the produced value. The `Err` side contains an instance of `nom::Err`.
///
/// Outside of the parsing code, you can use the [Finish::finish] method to convert
/// it to a more common result type
pub type IResult<I, O, E = error::Error<I>> = Result<(I, O), Err<E>>;
           

它的類型由輸入、輸出的類型和錯誤類型而定,在傳回 Ok 時,它包含了輸入的剩餘部分以及解析結果;在傳回 Err 時,它包含的是

nom::Err

類型執行個體。

基于 nom 的解析器,大都是自下而上建構的,先編寫最小的解析單元,然後使用組合子将它們組合到更複雜的解析器中。

nom 中已經提供了很多的基礎的解析單元。利用這些解析單元,我們可以做兩種選擇:

  • 1.解析特定的内容
  • 2.組合更上層的解析器

圍繞這兩點,我們可以先開始嘗試 —— 解析

(12345)

很明顯,我們無法直接用基礎的解析器直接解析出

(12345)

中的數字部分,因為基礎解析器解析的内容是比較單調的,比如可以用來解析

aaa

97900

等這類比較由規律的單元。

既然無法直接解析

(12345)

,我們就需要手動組合這些基礎解析器。基礎的解析器大都位于

nom::*::complete

下,比如

nom::bytes::complete::tag

(12345)

由一個左小括号開始,緊跟着一批數字字元然後是右小括号結束。據此我們可以将其拆分為:

  • (

  • 12345

  • )

是以實作傳回解析

(

的解析器的可以選擇

nom::bytes::complete::tag

它的函數簽名是:

pub fn tag<T, Input, Error: ParseError<Input>>(
    tag: T
) -> impl Fn(Input) -> IResult<Input, Input, Error>
where
    Input: InputTake + Compare<T>,
    T: InputLength + Clone,
           

可以看到該函數的傳回值是

impl Fn(Input) -> IResult<Input, Input, Error>

—— 即一個閉包。該閉包可以解析源字元串中特定的字元串。

比如你像解析

(

開頭的字元串(如

(123)

),則可以寫成

tag("(")

;如果你想解析

##

開頭的字元串(如

## someMdTitle

),則可以寫成:

tag("##")

用一個單元測試試試:

fn test_tag1() {
    // part 1
    fn my_parser1(s: &str) -> IResult<&str, &str> {
        tag("(")(s)
    }
    let res = my_parser1("(123)");
    assert_eq!(res, Ok(("123)", "(")));
    // part 2
    fn my_parser2(s: &str) -> IResult<&str, &str> {
        tag("##")(s)
    }
    assert_eq!(my_parser2("## someMdTitle"), Ok((" someMdTitle", "##")));
}
           

單元測試中的第 1 部分中,聲明了一個解析

(

的解析器,然後調用解析器解析字元串:

my_parser1("(123)");

然後斷言,傳回

Ok()

,其中包含的值是一個元組,第 0 個元素是解析完剩餘的字元串

"123)"

,第 1 個值是解析結果

(

單元測試中的第 2 部分中,聲明了一個解析

##

的解析器,然後調用該解析器解析字元串:

my_parser2("## someMdTitle")

,并斷言其傳回值

傳回

Ok()

,其中包含的值是一個元組,第 0 個元素是解析完剩餘的字元串

someMdTitle

,第 1 個值是解析結果

##

好了,雖然有點初級,但至少我們起步了!

加速

雖然我們可以通過簡單的解析器解析出需要的字元串,但我們的目标可不是單純地解析出

(

或者

##

之類地單調字元,我們的首要目标是解析出字元串(

(12345)

)中括号中的數字!

此時,我們就需要用到組合子。通過組合子對不同基礎解析器的組合,可以組合出更複雜的解析器。

nom 倉庫中提供了一個分類組合子的文檔。

從其中,我們可以找到一個适用于我們場景的組合子,例如:delimited

文檔中對該解析器的描述是:用第一個解析器比對一個對象,然後丢棄它;然後用第二個解析器比對特定的内容,并擷取它;最後用第三個解析器比對對象,并将對象丢棄。

剛好适用于我們的

(12345)

  • 1.用第一個解析器解析出

    (

    ,并丢棄;
  • 2.然後用第二個解析器比對出

    12345

  • 3.最後用第三個解析器解析出

    )

    并丢棄。

用代碼實作如下:

fn parse_u32(input: &[u8]) -> IResult<&[u8], &[u8]> {
    delimited(tag("("), digit0, tag(")"))(input)
}
           

是的,隻有一行代碼,就能實作解析

(12345)

。這個函數中,我們可能對

digit0

比較陌生,它是 delimited 函數中的第二個解析器,nom 中封裝好的。

它位于

nom::character::complete::digit0

解析數字

細心的朋友可能注意到,

parse_u32

函數傳回值是

IResult<&[u8], &[u8]>

,也就是說,在成功時,它傳回的輸入的剩餘資料的類型是

&[u8]

;解析的結果的類型也是

&[u8]

這是否和我們所說的拿到數值不太一緻?

是的,文章開頭,我們的函數簽名是

fn parse_u32(input: &[u8]) -> IResult<&[u8], u32>

我們期望剩餘的輸入是

&[u8]

,解析的結果是

u32

類型。是以,我們需要在基于 parse_u32 的基礎上,将

&[u8]

類型的解析結果轉換成 String,再将字元串轉換成 u32 類型:

fn parse_u32_ver1(input: &[u8]) -> IResult<&[u8], u32> {
    let mut my_parser = delimited(tag("("), digit0, tag(")"));
    let res = my_parser(input);
    match res {
        Ok((remain, raw)) => {
            let s1 = String::from_utf8_lossy(raw);
            let num: u32 = s1.parse().unwrap();
            Ok((remain, num))
        }
        Err(err) => Err(err),
    }
}
           

我們通過

String::from_utf8_lossy(raw);

将解析結果轉成 utf-8 編碼的字元串,通過

let num: u32 = s1.parse().unwrap();

将字元串轉換成 u32 類型。

寫個單測驗證一下:

fn test_parse_u32_ver1() {
    assert_eq!(
        parse_u32_ver1("(12345)".as_bytes()),
        Ok(("".as_bytes(), 12345))
    );

    assert_eq!(parse_u32_ver1("(0)".as_bytes()), Ok(("".as_bytes(), 0)));
}
           

太棒了,驗證通過!

至此,總算是完成了一個簡單的解析器,但距離解析 sql、json 還很遠,不着急,慢慢來。我們下一章來試試如何解析字元串。

參考

  • https://github.com/Geal/nom/blob/master/doc/making_a_new_parser_from_scratch.md