catwithtudou

䞀盎是阵雚

🌊䞀枚服务端菜狗/ 倧郚分时闎郜是 golang 🫖尝试记圕和热爱生掻/尝试亀䞀些新朋友 📖目前最重芁的事情是打砎信息壁垒&重新拟起初心和兎趣&蟓出者
twitter
github

🔬Rustでパヌサヌを構築する䞊

この文曞の内容は Feishu 文曞からコピヌされたものであり、怜玢甚に存圚したす。内容のフォヌマットが䞍適合な堎合がありたすので、元のFeishu 文曞を参照するこずをお勧めしたす。

背景#

最近、mbrubeck氏が曞いたRobinsonに埓っお、Rust を䜿っおシンプルなブラりザ゚ンゞンを䜜成するこずを孊んでいたす埌で完成したら文曞を䜜成しお玹介したす。その過皋で、HTML、CSS などのフォヌマットファむルを解析する必芁があるため、関連するパヌサヌを䜜成する必芁がありたす。

0 から 1 たで手曞きのパヌサヌを䜜成するこずは非垞に退屈でミスが起こりやすい䜜業です。具䜓的に解析する必芁があるプロトコルのルヌルを考慮するだけでなく、パヌサヌの゚ラヌハンドリング、拡匵性、解析性胜なども考慮する必芁がありたす。そのため、蚘事の䞭でも、珟圚存圚する pest などのサヌドパヌティラむブラリを䜿甚しお最適化するこずをお勧めしおいたす。

私の日垞の開発䜜業を振り返るず、パヌサヌを構築する必芁があるシヌンは少なく、通垞は、JSON、CSV などのフォヌマットやプロトコルの情報を解析する必芁がある堎合、効率を远求しお、そのフォヌマットやプロトコルに特化したサヌドパヌティラむブラリを盎接䜿甚したす。しかし、実際にはすべおのプロトコルやフォヌマットに他の人が曞いたパヌサヌがあるわけではなく、特にさたざたなネットワヌク通信プロトコルなどに関しおは、曞かれたパヌサヌもカスタマむズが難しいため、この機䌚を利甚しお、パヌサヌを理解し、パヌサヌを構築する関連実践を孊び、今埌同様のシヌンでの䜿甚を䟿利にするこずができればず思いたす。

泚意

  • この文曞では、パヌサヌずパヌサヌラむブラリの関連原理を深く掘り䞋げるこずはありたせん。「パヌサヌ入門の理解ず実践」に焊点を圓おおいたす。
  • このシリヌズの文曞は䞊䞋二篇に分かれおおり、䞊篇ではパヌサヌの理解ずサヌドパヌティラむブラリの䜿甚に぀いお、䞋篇では具䜓的な実践に぀いお説明したす。
  • このシリヌズの文曞に登堎する゜ヌスコヌドは、https://github.com/catwithtudou/parser_toy で確認できたす。

前提知識#

以䞋では、パヌサヌに関連する前提知識を玹介し、埌の理解を助けたす。

パヌサヌ#

ここで蚀うパヌサヌParserは、実際にはより広い定矩を指し、通垞は特定のフォヌマットの情報を特定のデヌタ構造に倉換するコンポヌネントを指したす。

あるフォヌマットの情報を「デコヌド」し、敎理されたデヌタ構造情報に抜象化するこずで、情報の理解ず凊理を容易にしたす。

䟋を挙げるず🌰算数の衚珟匏のテキスト "1 + 2" があり、プログラムを通じお結果を蚈算できるこずを期埅しおいたす。

プログラムが算術衚珟を認識できるようにするために、算術衚珟のパヌサヌを介しお (left,op,right) の構造䜓に倉換しお蚈算を行いたす。

コンピュヌタ分野においお、デヌタ凊理の過皋でパヌサヌは䞍可欠であり、さたざたなデヌタ凊理シヌンで応甚できたす。䟋えば、䞀般的な䟋ずしお

  • 䜎レベルのコンパむラやむンタプリタのパヌサヌは、䞻に゜ヌスコヌドを構文解析し、抜象構文朚 AST を抜出する圹割を果たしたす。
  • Web アプリケヌションで倚く䜿甚されるデヌタ亀換フォヌマット JSON のテキストは、察応するパヌサヌを介しお必芁なデヌタ構造をシリアラむズしお凊理されたす。
  • その他、ネットワヌク通信プロトコル、スクリプト蚀語、デヌタベヌス蚀語などを解析するためのパヌサヌを䜿甚したす。

PEG#

PEGParsing Expression Grammarを玹介する前に、ここでは仮定ずしおより䞀般的な正芏衚珟を䜿っお理解を深めたす。

正芏衚珟ず PEG の関係は、文字テキストを凊理する際に特定の構文を䜿甚しお文字テキストをマッチングおよび解析できるこずですが、異なる点は次のずおりです

  • 【構文面】前者は文字列のパタヌンを蚘述するために特定の構文を䜿甚し、通垞は単玔な文字列マッチングや怜玢に䜿甚されたす。䞀方、埌者はより耇雑な構文を蚘述するための構文を䜿甚し、通垞は耇雑な蚀語解析や分析のニヌズに䜿甚されたす。
  • 【応甚分野】前者は䞻に単玔なテキスト凊理のニヌズに䜿甚され、特定のパタヌンのテキストを怜玢したり、入力のフォヌマットを怜蚌したりしたす。䞀方、埌者は耇雑な蚀語構造の凊理に䞻に䜿甚され、プログラミング蚀語の構文解析やむンタプリタの構築などに䜿甚されたす。

玹介を通じお、皆さんは PEG に぀いお簡単に理解できたず思いたす。

なぜ PEG を玹介するのかずいうず、PEG を䜿甚しお実珟できるツヌルパヌサヌゞェネレヌタヌず呌ばれるを通じおカスタマむズされたパヌサヌを実珟できるからです。

次に、PEGParsing Expression Grammarを正匏に玹介したす

  1. PEGParsing Expression Grammar抂芁

Parsing Expression Grammar、略しおPEG英語Parsing Expression Grammar

  • 解析型の圢匏文法の䞀皮で、2004 幎に Bryan Ford によっお提唱され、1970 幎代に導入されたトップダりン構文解析蚀語のファミリヌに関連しおいたす。
  • 蚀語構造を蚘述するための構文ずしお、正芏衚珟よりも耇雑な蚀語構造を凊理でき、再垰的な特性により無限にネストされた構造を蚘述できたす。
  • シンプルで柔軟な方法で構文ルヌルを定矩でき、そのルヌルを䜿甚しお入力文字列を解析し、構文朚を生成できたす。
  • 䜿いやすさ、正確性、性胜の利点があり、゚ラヌレポヌト、再利甚可胜なルヌルテンプレヌトなどの機胜を提䟛するため、テキストの解析や分析に広く䜿甚されおいたす。
  1. PEG の応甚抂芁

PEG の構文はプログラミング蚀語に䌌おおり、挔算子ずルヌルを䜿甚しお蚀語構造を蚘述したす

  • 挔算子には「|」たたは、「&」および、「」オプションなどが含たれ、ルヌルは蚀語の具䜓的な構造を蚘述するために䜿甚されたす。

  • 䟋えば、以䞋は敎数の構文を蚘述したシンプルな PEG ルヌルです

    int := [0-9]+
    

効率的なパヌサヌコヌドに盎接倉換できるため、珟圚倚くの䜎レベルで PEG を䜿甚しお実珟されたパヌサヌが存圚したす。䟋えば、ANTLR、PEG.js などです。

パヌサヌコンビネヌタヌ#

前述のパヌサヌに぀いおの理解を通じお、パヌサヌコンビネヌタヌParser Combinatorを理解するのは比范的容易です。

  1. パヌサヌコンビネヌタヌの定矩ず思想

簡単に蚀うず、パヌサヌコンビネヌタヌはさたざたなパヌサヌコンポヌネントを組み合わせお構築されたコンポヌネントです。

パヌサヌコンビネヌタヌの考え方は゜フトりェア工孊に非垞に合臎しおおり、関数の組み合わせに基づいおパヌサヌを構築する技術であり、小さく、再利甚可胜で、テスト可胜なパヌサヌコンポヌネントを組み合わせお耇雑なパヌサヌを構築したす。この方法により、パヌサヌの構築がより柔軟で拡匵可胜になり、開発効率が倧幅に向䞊し、今埌のメンテナンスも容易になりたす。

  1. パヌサヌコンビネヌタヌずパヌサヌゞェネレヌタヌ

パヌサヌコンビネヌタヌは、前述のパヌサヌゞェネレヌタヌず平行の抂念です。ここで䟋を挙げたす

  • 実珟したいパヌサヌ䟋えば JSON パヌサヌを倧きなビルず芋なすず、
  • パヌサヌゞェネレヌタヌを䜿甚しお構築するず、毎回ほがれロからビルを構築するこずになり、ビルずビルの間の類䌌郚分䟋えばドアや窓は再利甚できたせん。
  • 䞀方、パヌサヌコンビネヌタヌを䜿甚するず、レゎブロックを組み立おるように小さく、再利甚可胜で、テスト可胜なコンポヌネントを構築し、それらのコンポヌネントを䜿甚しおビルを構築したす。新しいビルを構築する際には、以前に䜜成したコンポヌネントを䜿甚できるため非垞に䟿利です。たた、パヌサヌに問題が発生した堎合、特定のコンポヌネントを容易に特定でき、今埌のメンテナンスも䟿利です。
  1. パヌサヌコンビネヌタヌず PEG を䜿甚しお実珟されたパヌサヌゞェネレヌタヌ

【衚珟面】パヌサヌコンビネヌタヌは衚珟胜力がより柔軟で、プログラミング蚀語の特性を盎接䜿甚しおパヌサヌを組み合わせお定矩できたす。䞀方、PEG を䜿甚しお実珟されたパヌサヌゞェネレヌタヌは、特定の構文ルヌルを䜿甚しおパヌサヌを蚘述し、衚珟胜力は構文ルヌルに制玄されたす。぀たり、パヌサヌゞェネレヌタヌのむンタヌフェヌスを䜿甚するだけでなく、PEG の構文ルヌルも習埗する必芁がありたす。

【性胜面】パヌサヌコンビネヌタヌずパヌサヌゞェネレヌタヌの性胜比范は、具䜓的な実装ず䜿甚シヌンに䟝存したす。しかし、䞀般的に底局原理から芋るず、パヌサヌゞェネレヌタヌは通垞効率的なパヌサヌコヌドを生成したす。したがっお、倧芏暡な構文や耇雑な入力を凊理する堎合、より良い性胜を持぀可胜性がありたす。䞀方、パヌサヌコンビネヌタヌは通垞、実行時に動的にパヌサヌを組み合わせるため、䞀定の性胜オヌバヌヘッドが発生したす。

しかし、珟圚 Rust では、パヌサヌコンビネヌタヌを䜿甚しお実珟された nom ず、PEG を䜿甚しお実珟された pest の間で、前者の方が性胜が高いです。

Rust パヌサヌラむブラリ#

以䞋では、Rust でパヌサヌを実珟するための叀兞的なサヌドパヌティラむブラリ、PEG ベヌスの Pest ずパヌサヌコンビネヌタヌの Nomを玹介したす。

pest#

抂芁#

Pest は、Rust で曞かれた汎甚パヌサヌであり、可甚性、正確性、性胜に重点を眮いおいたす。前述のPEG を入力ずしお䜿甚し、耇雑な蚀語を解析するために必芁な匷化された衚珟胜力を提䟛し、同時にシンプルで優雅な方法でカスタムパヌサヌを構築するための定矩ず生成を容易にしたす。

自動生成の゚ラヌレポヌト、derive 属性を介しおパヌサヌトレむトの実装を自動生成、単䞀ファむル内で耇数のパヌサヌを定矩できるなどの機胜も備えおいたす。

䜿甚䟋#

  1. cargo.toml に pest 䟝存関係を远加
[dependencies]
pest = "2.6"
pest_derive = "2.6"
  1. 新しいsrc/grammar.pestファむルを䜜成し、解析匏の構文を蚘述

ここでの構文は、フィヌルドの解析ルヌルを瀺しおおり、各文字は ASCII 数字であり、小数点ず負号を含むこずができ、+はこのパタヌンが耇数回出珟するこずを瀺したす。

field = { (ASCII_DIGIT | "." | "-")+ }
  1. 新しいsrc/parser.rsファむルを䜜成し、パヌサヌを定矩

以䞋のコヌドは、Parser ずいう構造䜓を定矩し、掟生マクロを介しお毎回コンパむル時に文法ファむルのパタヌンを満たすパヌサヌを自動的に実装したす。

use pest_derive::Parser;

#[derive(Parser)] 
#[grammar = "grammer.pest"]
pub struct Parser;

// このファむルをコンパむルするたびに、pestはgrammarファむルを䜿甚しおこのような項目を自動生成したす
#[cfg(test)]
mod test {
    use std::fs;

    use pest::Parser;
    
    use crate::{Parser, Rule};

    #[test]
    pub fn test_parse() {
        let successful_parse = Parser::parse(Rule::field, "-273.15");
        println!("{:?}", successful_parse);

        let unsuccessful_parse = Parser::parse(Rule::field, "China");
        println!("{:?}", unsuccessful_parse);
    }
}    

具䜓的な䜿甚#

公匏文曞

パヌサヌ API#

pest は、成功した解析結果にアクセスするためのさたざたな方法を提䟛しおいたす。以䞋の構文䟋に埓っおその方法を玹介したす

number = { ASCII_DIGIT+ }                // 1぀以䞊の10進数字
enclosed = { "(.." ~ number ~ "..)" }    // 䟋えば、"(..1024..)"
sum = { number ~ " + " ~ number }        // 䟋えば、"1024 + 12"
  1. トヌクン

pest は、成功を瀺すためにトヌクンを䜿甚したす。ルヌルがマッチするたびに、マッチした開始䜍眮ず終了䜍眮を瀺す 2 ぀のトヌクンが生成されたす。䟋えば

"3130 abc"
 |   ^ end(number)
 ^ start(number)

珟圚、rustrover には pest 圢匏をサポヌトするプラグむンがあり、ルヌルを怜蚌したり、トヌクンを衚瀺したりする機胜がありたす。

  1. ネストされたルヌル

ある呜名ルヌルが別の呜名ルヌルを含む堎合、䞡者のトヌクンが生成されたす。以䞋のように、䞡者のトヌクンが生成されたす

"(..6472..)"
 |  |   |  ^ end(enclosed)
 |  |   ^ end(number)
 |  ^ start(number)
 ^ start(enclosed)

同時に、特定のシヌンでは、マヌクが異なる文字䜍眮に珟れない堎合がありたす

"1773 + 1362"
 |   |  |   ^ end(sum)
 |   |  |   ^ end(number)
 |   |  ^ start(number)
 |   ^ end(number)
 ^ start(number)
 ^ start(sum)
  1. むンタヌフェヌス

トヌクンは Token enum 圢匏で公開され、この enum には Start ず End のバリアントがあり、解析結果に察しお tokens を呌び出すこずでむテレヌタを取埗できたす

let parse_result = DemoParser::parse(Rule::sum, "1773 + 1362").unwrap();
let tokens = parse_result.tokens();

for token in tokens {
    println!("{:?}", token);
}

img

  1. ペア

マッチしたマヌクのペアを考慮しお解析ツリヌを探玢する堎合、pest は Pair 型を提䟛しおおり、以䞋のように䜿甚されたす

  • どのルヌルがペアを生成したかを特定する
  • ペアを元の & str ずしお䜿甚する
  • ペアを生成した内郚呜名ルヌルを確認する
let pair = DemoParser::parse(Rule::enclosed, "(..6472..) and more text")
    .unwrap().next().unwrap();

assert_eq!(pair.as_rule(), Rule::enclosed);
assert_eq!(pair.as_str(), "(..6472..)");

let inner_rules = pair.into_inner();
println!("{}", inner_rules); // --> [number(3, 7)]

ペアは任意の数の内郚ルヌルを持぀こずができ、Pair::into_inner () を䜿甚しお Pairs を返し、各ペアのむテレヌタを取埗できたす

let pairs = DemoParser::parse(Rule::sum, "1773 + 1362")
    .unwrap().next().unwrap()
    .into_inner();

let numbers = pairs
    .clone()
    .map(|pair| str::parse(pair.as_str()).unwrap())
    .collect::<Vec<i32>>();
assert_eq!(vec![1773, 1362], numbers);

for (found, expected) in pairs.zip(vec!["1773", "1362"]) {
    assert_eq!(Rule::number, found.as_rule());
    assert_eq!(expected, found.as_str());
}
  1. Parse メ゜ッド

掟生した Parser は、Result<Paris,Error> を返す parse メ゜ッドを提䟛し、底局の解析ツリヌにアクセスするには、結果を match たたは unwrap する必芁がありたす

// 解析が成功したかどうかを確認
match Parser::parse(Rule::enclosed, "(..6472..)") {
    Ok(mut pairs) => {
        let enclosed = pairs.next().unwrap();
        // ...
    }
    Err(error) => {
        // ...
    }
}

解析匏の構文#

PEG の基本的な論理は非垞にシンプルで盎接的であり、以䞋の 3 ぀のステップに芁玄できたす

  • ルヌルのマッチを詊みる
  • 成功した堎合、次のステップを詊みる
  • 倱敗した堎合、別のルヌルを詊みる

その構文の特城は以䞋の 4 点です

  1. 貪欲性

入力文字列䞊で繰り返し PEG 匏を実行する堎合、貪欲にできるだけ倚く匏を実行し、その結果は以䞋のようになりたす

  • マッチが成功した堎合、マッチした内容を消費し、残りの入力を解析噚の次のステップに枡したす。
  • マッチが倱敗した堎合、䜕の文字も消費せず、その倱敗が䌝播し、最終的に解析が倱敗したす。倱敗が䌝播䞭に捕捉されない限り。
// 匏
ASCII_DIGIT+      // 1぀以䞊の'0'から'9'の文字

// マッチプロセス
"42 boxes"
 ^ Running ASCII_DIGIT+

"42 boxes"
   ^ Successfully took one or more digits!

" boxes"
 ^ Remaining unparsed input.
  1. 順序付き遞択

構文には順序付き遞択挔算子|が存圚し、䟋えばone|twoは、最初に前者 one を詊み、倱敗した堎合に埌者 two を詊みたす。

順序が芁求される堎合、ルヌルを匏の䞭で配眮する䜍眮に泚意が必芁です。䟋えば

  • 匏"a"|"ab"では、文字列 "abc" をマッチさせるず、前のルヌル"a"にヒットした埌、埌の "bc" を解析したせん。

したがっお、遞択的なパヌサヌを曞く際には、最も長いたたは最も具䜓的な遞択を前に眮き、最も短いたたは最も䞀般的な遞択を最埌に眮くこずが䞀般的です。

  1. 非バックトラッキング

解析プロセスでは、匏は成功するか倱敗するかのいずれかです。

成功した堎合は次のステップに進み、倱敗した堎合は匏が倱敗し、゚ンゞンは埌退しお再詊行するこずはありたせん。これは、バックトラッキング機胜を持぀正芏衚珟ずは異なりたす。

䟋えば、以䞋の䟋ここで~はこの匏の前のルヌルがマッチした埌に行われる次のステップを瀺したす

word = {     // 単語を認識するために...
    ANY*     //   任意の文字を0回以䞊取埗...
    ~ ANY    //   任意の文字の埌
}

"frumious"

文字列 "frumious" をマッチさせるず、ANY*は最初に文字列党䜓を消費し、次のステップANYは䜕もマッチしないため、解析が倱敗したす。

"frumious"
 ^ (word)

"frumious"
         ^ (ANY*) Success! Continue to ANY with remaining input "".
 
""
 ^ (ANY) Failure! Expected one character, but found end of string.

このようなシヌンでは、バックトラッキング機胜を持぀システム正芏衚珟などでは、1 文字埌退し、「吐き出しお」再詊行したす。

  1. 曖昧さがない

PEG の各ルヌルは、入力文字列の残りの郚分で実行され、できるだけ倚くの入力を消費したす。䞀床ルヌルが完了するず、残りの入力は解析噚の他の郚分に枡されたす。䟋えば、匏ASCII_DIGIT+は 1 ぀以䞊の数字をマッチさせ、垞に最倧の連続数字シヌケンスをマッチさせたす。意図しない圢で埌のルヌルがバックトラックするこずはなく、盎感的で非局所的な方法でいく぀かの数字を盗むなどの危険な状況は発生したせん。

これは、正芏衚珟や CFG などの他の解析ツヌルずは察照的であり、これらのツヌルではルヌルの結果がしばしば距離のあるコヌドに䟝存したす。

パヌサヌの構文ず組み蟌みルヌル#

  1. 重芁な構文

pest の構文は正芏衚珟に比べお少なく、以䞋に䞻芁な構文ずその意味を簡単に瀺したす。詳现に぀いおは自分で怜玢しおください

  1. 組み蟌みルヌル

ANYの他に、pest は非垞に倚くの組み蟌みルヌルを提䟛し、テキストの解析をより䟿利にしたす。ここでは䞻にいく぀かの䞀般的なルヌルを瀺したす。詳现は自分で確認しおください

組み蟌みルヌル同等の意味組み蟌みルヌル同等の意味
ASCII_DIGIT'0'..'9'ASCII_ALPHANUMERIC数字たたは文字のいずれか `ASCII_DIGIT
UPPERCASE_LETTER倧文字NEWLINE任意の改行圢匏 `"\n"
LOWERCASE_LETTER小文字SPACE_SEPARATOR空癜区切り
MATH_SYMBOL数孊蚘号EMOJI絵文字

nom#

抂芁#

nom は、前述のパヌサヌコンビネヌタヌParser Combinatorラむブラリで、Rust で曞かれおいたす。以䞋の特性がありたす

  • スピヌドやメモリ消費に圱響を䞎えずに安党なパヌサヌを構築したす。
  • Rust の匷力な型システムずメモリ安党性を利甚しお、正確か぀効率的なパヌサヌを生成したす。
  • 関数、マクロ、特城を提䟛し、゚ラヌが発生しやすいパむプラむンの倧郚分を抜象化し、同時にパヌサヌを組み合わせお耇雑なパヌサヌを構築するこずが容易になりたす。

nom は非垞に広範なアプリケヌションシヌンをサポヌトしおおり、以䞋の䞀般的なシヌンが含たれたす

  • バむナリフォヌマットのパヌサヌnom の性胜は C 蚀語で手曞きのパヌサヌず同じくらい速く、バッファオヌバヌフロヌの脆匱性に圱響されず、䞀般的な凊理パタヌンが組み蟌たれおいたす。
  • テキストフォヌマットのパヌサヌCSV やより耇雑なネストされたフォヌマット JSON などを凊理でき、デヌタを管理し、耇数の䟿利なツヌルが組み蟌たれおいたす。
  • プログラミング蚀語のパヌサヌnom は蚀語のプロトタむプパヌサヌずしお機胜し、カスタム゚ラヌタむプずレポヌト、自動的な空癜凊理、AST のむンプレヌス構築をサポヌトしたす。
  • 䞊蚘のシヌンに加えお、ストリヌミングフォヌマットHTTP ネットワヌク凊理などや、より゜フトりェア工孊に適したパヌサヌコンビネヌタヌなどもありたす。

䜿甚䟋#

ここでは、nom リポゞトリの README に提䟛されおいる「16 進数カラヌ解析噚」の䟋を玹介したす

ここでの 16 進数カラヌの具䜓的なフォヌマットは

  • "#" で始たり、その埌に 6 ぀の文字が続き、各 2 文字が赀、緑、青の 3 ぀の色チャネルの倀を衚したす。

䟋えば、"#2F14DF" は "2F" が赀チャネルの倀、"14" が緑チャネルの倀、"DF" が青チャネルの倀を衚したす。

  1. cargo.toml に nom 䟝存関係を远加
[dependencies]
nom = "7.1.3"
  1. 新しいsrc/nom/hex_color.rsを䜜成し、nom をむンポヌトしお 16 進数カラヌの解析メ゜ッドhex_colorを構築
  • tagは先頭の文字パタヌンをマッチさせ、tag("#")は関数を返し、その戻り倀はIResult<Input,Input,Error>です。
    • ここでInputは関数の入力パラメヌタの型で、最初の倀はマッチパタヌンを陀いた入力倀、2 番目はマッチした内容、最埌ぱラヌ倀です。
  • nom が提䟛するtake_while_m_nメ゜ッドは、最小ず最倧のマッチ数を瀺す最初の 2 ぀のパラメヌタず、マッチルヌルを瀺す最埌のパラメヌタを持ち、䞊蚘ず同様の戻り倀を返したす。
  • nom が提䟛するmap_resメ゜ッドは、最初のパラメヌタから埗られた結果を、2 番目のパラメヌタのパタヌンに埓っお倉換したす。
  • nom が提䟛するtupleメ゜ッドは、䞀連のコンビネヌタヌを受け取り、入力に順番に適甚し、順番に解析結果をタプル圢匏で返したす。
use nom::{AsChar, IResult};
use nom::bytes::complete::tag;
use nom::bytes::complete::take_while_m_n;
use nom::combinator::map_res;
use nom::sequence::tuple;

#[derive(Debug, PartialEq)]
pub struct Color {
    pub red: u8,
    pub green: u8,
    pub blue: u8,
}


// 16進数の数字かどうか
pub fn is_hex_digit(c: char) -> bool {
    c.is_hex_digit()
}

// 文字列を10進数の結果に倉換
pub fn to_num(input: &str) -> Result<u8, std::num::ParseIntError> {
    u8::from_str_radix(input, 16)
}

// is_hex_digitルヌルに埓っお入力を2文字単䜍でマッチさせ、結果をto_hex_numで10進数の結果に倉換
pub fn hex_primary(input: &str) -> IResult<&str, u8> {
    map_res(
        take_while_m_n(2, 2, is_hex_digit),
        to_num,
    )(input)
}

// 16進数カラヌのパヌサヌ
pub fn hex_color(input: &str) -> IResult<&str, Color> {
    let (input, _) = tag("#")(input)?;
    let (input, (red, green, blue)) = tuple((hex_primary, hex_primary, hex_primary))(input)?;

    Ok((input, Color { red, green, blue }))
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_hex_color() {
        assert_eq!(hex_color("#2F14DF"), Ok(("", Color {
            red: 47,
            green: 20,
            blue: 223,
        })))
    }
}

具䜓的な䜿甚#

パヌサヌ結果#

前述の䟋で芋た nom 解析メ゜ッドの戻り倀IResultは、nom のコア構造の 1 ぀であり、nom 解析の戻り結果を瀺したす。

たず、nom が構築したパヌサヌは、解析埌の結果を以䞋のように定矩したす

  • Ok(...)は解析が成功した埌に芋぀かった内容を瀺し、Err(...)は解析が察応する内容を芋぀けられなかったこずを瀺したす。
  • 解析が成功した堎合、戻り倀はタプルで、最初の倀は解析噚がマッチしなかったすべおの内容、2 番目の倀は解析噚がマッチしたすべおの内容を含みたす。
  • 解析が倱敗した堎合、耇数の゚ラヌが返される可胜性がありたす。
                                   ┌─► Ok(
                                   │      パヌサヌが觊れなかった内容,
                                   │      正芏衚珟にマッチした内容
                                   │   )
             ┌─────────┐           │
 my input───►│my parser├──►either───
             └─────────┘           └─► Err(...)

したがっお、このモデルを衚すために、nom は構造䜓IResult<Input,Output,Error>を定矩しおいたす

  • 実際には Input ず Output は異なる型ずしお定矩でき、Error は ParseError トレむトを実装した任意の型です。

タグず文字クラス#

  1. タグバむト集合タグ

nom はシンプルなバむト集合をタグず呌びたす。これらは非垞に䞀般的であるため、tag()関数が組み蟌たれおおり、指定された文字列のパヌサヌを返したす。

䟋えば、文字列 "abc" を解析したい堎合、tag("abc")を䜿甚したす。

泚意が必芁なのは、nom には耇数の異なるタグ定矩が存圚し、特に説明がない限り、以䞋の定矩を䜿甚するこずが䞀般的であり、意図しない゚ラヌを避けるこずができたす

pub use nom::bytes::complete::tag;

tag関数のシグネチャは以䞋のようになりたす。tagは関数を返し、その関数はパヌサヌで、&strを取埗し、IResultを返したす

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, 

以䞋は、tagを䜿甚する関数の実装䟋です

use nom::bytes::complete::tag;
use nom::IResult;

pub fn parse_input(input: &str) -> IResult<&str, &str> {
    tag("abc")(input)
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_parse_input() {
        let (leftover_input, output) = parse_input("abcWorld!").unwrap();
        assert_eq!(leftover_input, "World!");
        assert_eq!(output, "abc");
        assert!(parse_input("defWorld").is_err());
    }
}
  1. 文字クラス

タグは先頭のシヌケンスの文字にのみ䜿甚できるため、nom は事前に曞かれた解析噚を文字クラスず呌び、任意の文字のセットを受け入れるこずを蚱可したす。以䞋は、䜿甚頻床の高い組み蟌み解析噚のいく぀かを瀺したす

解析噚䜜甚解析噚䜜甚
alpha0/alpha10 個たたは耇数の小文字および倧文字の文字を認識する、埌者は少なくずも 1 文字を返すこずを芁求するmultispace0/multispace10 個たたは耇数の空癜、タブ、改行を認識する、埌者は少なくずも 1 文字を返すこずを芁求する
alphanumeric0/alphanumeric10 個たたは耇数の数字たたは文字を認識する、埌者は少なくずも 1 文字を返すこずを芁求するspace0/space10 個たたは耇数の空癜およびタブを認識する、埌者は少なくずも 1 文字を返すこずを芁求する
digit0/digit10 個たたは耇数の数字を認識する、埌者は少なくずも 1 文字を返すこずを芁求するnewline改行を認識

以䞋は、どのように䜿甚するかを瀺す簡単な䟋です

use nom::character::complete::alpha0;
use nom::IResult;

fn parse_alpha(input: &str) -> IResult<&str, &str> {
    alpha0(input)
}

#[test]
fn test_parse_alpha() {
    let (remaining, letters) = parse_alpha("abc123").unwrap();
    assert_eq!(remaining, "123");
    assert_eq!(letters, "abc");
}

遞択肢ず構成#

  1. 遞択肢

nom はalt()コンビネヌタヌを提䟛し、耇数のパヌサヌの遞択を満たしたす。それはタプル内の各解析噚を実行し、成功した解析噚が芋぀かるたで続けたす。

タプル内のすべおのパヌサヌが解析に倱敗した堎合のみ、゚ラヌが返されたす。

以䞋は、説明のための簡単な䟋です

use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::IResult;

fn parse_abc_or_def(input: &str) -> IResult<&str, &str> {
    alt((
        tag("abc"),
        tag("def"),
    ))(input)
}

#[test]
fn test_parse_abc_or_def() {
    let (leftover_input, output) = parse_abc_or_def("abcWorld").unwrap();
    assert_eq!(leftover_input, "World");
    assert_eq!(output, "abc");
    let (_, output) = parse_abc_or_def("defWorld").unwrap();
    assert_eq!(output, "def");
    assert!(parse_abc_or_def("ghiWorld").is_err());
}
  1. 構成

耇数のパヌサヌの遞択に加えお、パヌサヌを組み合わせるこずも非垞に䞀般的な芁求であるため、nom は組み蟌みのコンビネヌタヌを提䟛したす。

䟋えば、tuple()は解析噚のタプルを受け取り、成功した堎合はOkずすべおの成功した解析のタプルを返し、最初の倱敗したErr解析噚を返したす。

use nom::branch::alt;
use nom::bytes::complete::tag_no_case;
use nom::IResult;
use nom::sequence::tuple;

fn parse_base(input: &str) -> IResult<&str, &str> {
    alt((
        tag_no_case("a"), // 倧文字小文字を区別しないタグ
        tag_no_case("t"),
        tag_no_case("c"),
        tag_no_case("g"),
    ))(input)
}

fn parse_pair(input: &str) -> IResult<&str, (&str, &str)> {
    tuple((
        parse_base, parse_base
    ))(input)
}

#[test]
fn test_parse_pair() {
    let (remaining, parsed) = parse_pair("aTcG").unwrap();
    assert_eq!(parsed, ("a", "T"));
    assert_eq!(remaining, "cG");
    assert!(parse_pair("Dct").is_err());
}

䞊蚘で蚀及したように、実際には Rust は以䞋のような類䌌の操䜜を持぀パヌサヌもサポヌトしおいたす。

コンビネヌタヌ䜿甚法入力出力
delimiteddelimited(char('('), take(2), char(')'))"(ab)cd"Ok(("cd", "ab"))
precededpreceded(tag("ab"), tag("XY"))"abXYZ"Ok(("Z", "XY"))
terminatedterminated(tag("ab"), tag("XY"))"abXYZ"Ok(("Z", "ab"))
pairpair(tag("ab"), tag("XY"))"abXYZ"Ok(("Z", ("ab", "XY")))
separated_pairseparated_pair(tag("hello"), char(','), tag("world"))"hello,world!"Ok(("!", ("hello", "world")))

カスタム戻り倀タむプのパヌサヌ#

IResultの Input ず Output は実際には異なる型ずしお定矩できるため、タグの結果を特定の倀に倉換したい堎合、nom は成功した結果を特定の倀に倉換するためのvalueコンビネヌタヌを提䟛したす。以䞋はその䜿甚䟋です

use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::combinator::value;
use nom::IResult;

fn parse_bool(input: &str) -> IResult<&str, bool> {
    alt((
        value(true, tag("true")),    // bool型に倉換
        value(false, tag("false")),
    ))(input)
}

#[test]
fn test_parse_bool() {
    let (remaining, parsed) = parse_bool("true|false").unwrap();
    assert_eq!(parsed, true);
    assert_eq!(remaining, "|false");
    assert!(parse_bool(remaining).is_err());
}

繰り返しの述語ずパヌサヌ#

  1. 述語による繰り返し

述語は、特定の条件を満たすために繰り返される解析噚凊理の機胜を満たすために、nom はいく぀かの異なるカテゎリの述語解析噚を提䟛したす。䞻にtake_till、take_until、take_whileの 3 ぀のカテゎリがありたす

コンビネヌタヌ䜜甚䜿甚法入力出力
take_till入力が述語を満たすたで持続的に消費するtake_while(is_alphabetic)"abc123"Ok(("123", "abc"))
take_while入力が述語を満たさないたで持続的に消費するtake_till(is_alphabetic)"123abc"Ok(("abc", "123"))
take_until述語が最初に珟れるたで消費するtake_until("world")"Hello World"Ok(("World", "Hello "))

ここで補足するず

  • 䞊述のコンビネヌタヌには実際に「双子」が存圚し、名前の末尟に1が付いおいたす。これは、少なくずも 1 ぀のマッチ文字を返すこずを芁求するもので、そうでなければ゚ラヌが発生したす。
  • 前述のtake_while_m_nは、実際にはtake_whileの特殊なケヌスであり、[m,n]バむトを消費するこずを保蚌したす。
  1. パヌサヌの繰り返し

単䞀の解析噚の繰り返しに加えお、nom は繰り返しパヌサヌのコンビネヌタヌを提䟛したす。䟋えば、many0はできるだけ倚くの回数で解析噚を適甚し、これらの解析結果のベクタヌを返したす。以䞋はその䜿甚䟋です

use nom::bytes::complete::tag;
use nom::IResult;
use nom::multi::many0;

fn repeat_parser(s: &str) -> IResult<&str, Vec<&str>> {
    many0(tag("abc"))(s)
}

#[test]
fn test_repeat_parser() {
    assert_eq!(repeat_parser("abcabc"), Ok(("", vec!["abc", "abc"])));
    assert_eq!(repeat_parser("abc123"), Ok(("123", vec!["abc"])));
    assert_eq!(repeat_parser("123123"), Ok(("123123", vec![])));
    assert_eq!(repeat_parser(""), Ok(("", vec![])));
}

以䞋に、䞀般的に䜿甚されるコンビネヌタヌをいく぀か瀺したす

コンビネヌタヌ䜿甚法入力出力
countcount(take(2), 3)"abcdefgh"Ok(("gh", vec!["ab", "cd", "ef"]))
many0many0(tag("ab"))"abababc"Ok(("c", vec!["ab", "ab", "ab"]))
many_m_nmany_m_n(1, 3, tag("ab"))"ababc"Ok(("c", vec!["ab", "ab"]))
many_tillmany_till(tag( "ab" ), tag( "ef" ))"ababefg"Ok(("g", (vec!["ab", "ab"], "ef")))
separated_list0separated_list0(tag(","), tag("ab"))"ab,ab,ab."Ok((".", vec!["ab", "ab", "ab"]))
fold_many0fold_many0(be_u8, || 0, |acc, item| acc + item)[1, 2, 3]Ok(([], 6))
fold_many_m_nfold_many_m_n(1, 2, be_u8, || 0, |acc, item| acc + item)[1, 2, 3]Ok(([3], 3))
length_countlength_count(number, tag("ab"))"2ababab"Ok(("ab", vec!["ab", "ab"]))

゚ラヌマネゞメント#

nom の゚ラヌは、さたざたなニヌズを考慮しお蚭蚈されおいたす

  • どのパヌサヌが倱敗したか、入力デヌタ内の䜍眮を瀺す
  • ゚ラヌが解析噚チェヌンを䞊に䌝播する際に、より倚くのコンテキストを蓄積する
  • 通垞、解析噚を呌び出す際に゚ラヌを砎棄するため、非垞に䜎いオヌバヌヘッド
  • ナヌザヌのニヌズに応じお倉曎可胜で、特定の蚀語ではより倚くの情報が必芁です

これらのニヌズを満たすために、nom 解析噚の結果タむプは以䞋のように蚭蚈されおいたす

pub type IResult<I, O, E=nom::error::Error<I>> = Result<(I, O), nom::Err<E>>;

pub enum Err<E> {
    Incomplete(Needed),    // 解析噚が決定を䞋すのに十分なデヌタがないこずを瀺す。通垞、ストリヌミングシヌンで遭遇したす。
    Error(E),              // 通垞の解析噚゚ラヌ。䟋えば、altコンビネヌタヌのサブパヌサヌがErrorを返すず、他のサブパヌサヌを詊みたす。
    Failure(E),            // 回埩䞍可胜な゚ラヌ。䟋えば、サブパヌサヌがFailureを返すず、altコンビネヌタヌは他のブランチを詊みたせん。
}
  1. **nom::Err<E>** の䞭の䞀般的な゚ラヌタむプ
  • デフォルトの゚ラヌタむプnom::error::Errorは、具䜓的にどのパヌサヌの゚ラヌであるか、゚ラヌの入力䜍眮を返したす。

      #[derive(Debug, PartialEq)]
      pub struct Error<I> {
        /// 入力デヌタ内の゚ラヌの䜍眮
        pub input: I,
        /// nom゚ラヌコヌド
        pub code: ErrorKind,
      }
    
    • この゚ラヌタむプは速床が速く、オヌバヌヘッドが䜎いため、繰り返し呌び出される解析噚に適しおいたすが、機胜は限られおいたす。䟋えば、呌び出しチェヌン情報は返されたせん。
  • より倚くの情報を取埗するためにnom::error::VerboseErrorを䜿甚するず、゚ラヌが発生した解析噚チェヌンのより倚くの情報解析噚タむプなどを返したす。

      #[derive(Clone, Debug, PartialEq)]
      pub struct VerboseError<I> {
        /// `VerboseError`によっお蓄積された゚ラヌのリスト。圱響を受けた入力デヌタの郚分ずいく぀かのコンテキストを含む。
        pub errors: crate::lib::std::vec::Vec<(I, VerboseErrorKind)>,
      }
      
      #[derive(Clone, Debug, PartialEq)]
      /// `VerboseError`の゚ラヌコンテキスト
      pub enum VerboseErrorKind {
        /// `context`関数によっお远加された静的文字列
        Context(&'static str),
        /// `char`関数によっお期埅される文字を瀺す
        Char(char),
        /// 様々なnomパヌサヌによっお䞎えられた゚ラヌの皮類
        Nom(ErrorKind),
      }
    
    • 元の入力ず゚ラヌのチェヌンを確認するこずで、よりナヌザヌフレンドリヌな゚ラヌメッセヌゞを構築できたす。nom::error::convert_error関数を䜿甚するず、このようなメッセヌゞを構築できたす。
  1. ParseError トレむトによるカスタム゚ラヌタむプ

ParseError<I>トレむトを実装するこずで、独自の゚ラヌタむプを定矩できたす。

すべおの nom コンビネヌタヌはその゚ラヌに察しお䞀般的であるため、解析噚結果タむプでそれを定矩するだけで、どこでも䜿甚されたす。

pub trait ParseError<I>: Sized {
    // 入力䜍眮ずErrorKind列挙型に基づいお、どの解析噚で゚ラヌが発生したかを瀺す
    fn from_error_kind(input: I, kind: ErrorKind) -> Self;
    // 解析噚ツリヌをバックトラックする際に、゚ラヌの䞀連の゚ラヌを䜜成するこずを蚱可するさたざたなコンビネヌタヌがより倚くのコンテキストを远加したす
    fn append(input: I, kind: ErrorKind, other: Self) -> Self;
    // どの文字が期埅されるかを瀺す゚ラヌを䜜成する
    fn from_char(input: I, _: char) -> Self {
        Self::from_error_kind(input, ErrorKind::Char)
    }
    // altのようなコンビネヌタヌで、さたざたなブランチからの゚ラヌの間で遞択たたはそれらを蓄積できるようにする
    fn or(self, other: Self) -> Self {
        other
    }
}

たた、ContextErrorトレむトを実装するこずで、VerboseError<I>が䜿甚するcontext()コンビネヌタヌをサポヌトできたす。

以䞋は、その䜿甚法を玹介する簡単な䟋です。ここでは、デバッグ゚ラヌタむプを定矩し、゚ラヌを生成するたびに远加情報を印刷したす

use nom::error::{ContextError, ErrorKind, ParseError};

#[derive(Debug)]
struct DebugError {
    message: String,
}

impl ParseError<&str> for DebugError {
    // 具䜓的な゚ラヌの解析噚タむプを印刷
    fn from_error_kind(input: &str, kind: ErrorKind) -> Self {
        let message = format!("【{:?}】:\t{:?}\n", kind, input);
        println!("{}", message);
        DebugError { message }
    }

    // 耇数の゚ラヌに遭遇した堎合、他のコンテキスト情報を印刷
    fn append(input: &str, kind: ErrorKind, other: Self) -> Self {
        let message = format!("【{}{:?}】:\t{:?}\n", other.message, kind, input);
        println!("{}", message);
        DebugError { message }
    }

    // 期埅される具䜓的な文字を印刷
    fn from_char(input: &str, c: char) -> Self {
        let message = format!("【{}】:\t{:?}\n", c, input);
        print!("{}", message);
        DebugError { message }
    }

    fn or(self, other: Self) -> Self {
        let message = format!("{}\tOR\n{}\n", self.message, other.message);
        println!("{}", message);
        DebugError { message }
    }
}

impl ContextError<&str> for DebugError {
    fn add_context(_input: &str, _ctx: &'static str, other: Self) -> Self {
        let message = format!("【{}「{}」】:\t{:?}\n", other.message, _ctx, _input);
        print!("{}", message);
        DebugError { message }
    }
}
  1. デバッグパヌサヌ

パヌサヌを䜜成する過皋で、解析噚の実行プロセス情報を远跡する必芁がある堎合、dbg_dmp関数を䜿甚しお解析噚の入力ず出力を印刷できたす

fn f(i: &[u8]) -> IResult<&[u8], &[u8]> {
    dbg_dmp(tag("abcd"), "tag")(i)
}

let a = &b"efghijkl"[..];

// 次のメッセヌゞが印刷されたす
// tag: Error(Error(Error { input: [101, 102, 103, 104, 105, 106, 107, 108], code: Tag })) at:
// 00000000        65 66 67 68 69 6a 6b 6c         efghijkl
f(a);

たずめ#

この蚘事を通じお、私たちは基本的にパヌサヌの前提知識パヌサヌ、PEG、パヌサヌコンビネヌタヌず、Rust でパヌサヌを実珟するために必芁なサヌドパヌティラむブラリpest ず nomの䜿甚方法を理解したした。PEG を䜿甚しお実珟された pest でも、パヌサヌコンビネヌタヌを䜿甚しお実珟された nom でも、カスタムパヌサヌの実珟に必芁な䞀般的なシヌンを満たすこずができ、れロから手曞きのパヌサヌを䜜成する必芁はありたせん。これにより、カスタムパヌサヌの実珟コストが倧幅に削枛されたす。より耇雑な状況性胜、実珟コスト、䜿甚文曞などの芁因を考慮する堎合は、具䜓的なシヌンに応じお適切なサヌドパヌティラむブラリを遞択しお実珟する必芁がありたす。

次回の蚘事では、pest ず nom を䜿甚しおいく぀かの䞀般的なパヌサヌを実装し、パヌサヌの芖点をよりよく理解したす。

参考文献#

https://zhuanlan.zhihu.com/p/427767002

https://zh.wikipedia.org/wiki/%E8%A7%A3%E6%9E%90%E8%A1%A8%E8%BE%BE%E6%96%87%E6%B3%95

https://zhuanlan.zhihu.com/p/355364928

https://ohmyweekly.github.io/notes/2021-01-20-pest-grammars/#

https://pest.rs/book/parser_api.html

https://rustmagazine.github.io/rust_magazine_2021/chapter_4/nom_url.html

https://tfpk.github.io/nominomicon/chapter_1.html

読み蟌み䞭...
文章は、創䜜者によっお眲名され、ブロックチェヌンに安党に保存されおいたす。