peg.js 簡介
PEG.js 是一個非常簡潔的 javascript 庫,它採用 PEG 並生成可以直接從 javascript 調用的解析器程序。從他們的網站:
“PEG.js 是一個簡單的 JavaScript 解析器生成器,可生成具有出色錯誤報告的快速解析器。您可以使用它來處理複雜的數據或計算機語言,並輕鬆構建轉換器、解釋器、編譯器和其他工具。”
我計劃寫一個關於如何使用 PEG.js 編寫編程語言的完整系列,所以我想我在這里為以前沒有使用過 PEG.js 的人提供一個介紹。大多數人可能不會在正則上編寫語言解析器,所以我也會在解決一些問題的背景下討論 peg,人們也可能會使用正則表達式來解決這些問題。如果您是來這裡專門了解 Peg 或熟悉什麼語法,請隨時跳到入門部分。
激勵示例:正則表達式地獄
我覺得大多數人對正則表達式有一種愛恨交織的關係。編寫複雜的正則表達式幾乎總是一個壞主意,因為在我看來,它會給其他開發人員或你未來的自己帶來巨大的可讀性問題。也就是說,如果明智地使用正則表達式顯然會非常有用。
例如,使用 grep 查找文件通常是 regex 的一個很好的用例。然而,有些東西正則表達式無法解析(例如 HTML),然後還有更大的類別可能不應該單獨用正則表達式來解決。
如果您發現自己想要編寫另一個不可讀的正則表達式,可以考慮另一種選擇,例如 PEG。
過度還原的 PEG 有點像 regex++。 Peg 或 Parser 表達式語法與上下文無關語法非常相似,它允許您將類似正則表達式的規則組合到一個更大的解析器中。它以聲明性、遞歸的方式執行此操作。
等等什麼語法?
語法是一種“語言中的語言”,因為它是一種表達語言的方式。例如,英語有語法,但它是一種比上下文無關語法更寬鬆的語法類型。如果您想了解更多來自 The coding train 的 Daniel Shiffman 在描述上下文無關語法方面做得很好。 Pegs 與上下文無關文法非常相似,只是它們沒有歧義,即對於給定的輸入,只有一種有效的解析方式。
Peg.js 可以成為“正則表達式地獄”的一個很好的解決方案,並且可以用於構建更複雜的工具,例如 dsl 解析器、自定義查詢語言甚至是新的編程語言。我一直對語言解析器的工作方式非常感興趣,我認為這是一個很好的例子,所以在本文中,我們將介紹 PEG.JS,並討論在解析查詢語言時可能遇到的一些基本挑戰。主頁>
如何安裝/開始
如果您想快速開始並使用 PEG.js,他們在 https://pegjs.org/online 上有一個非常酷的在線交互式編輯器,儘管遺憾的是沒有暗模式;)
他們文檔的第一部分很好地向您展示瞭如何在您的機器上安裝和設置 peg,但基本上只是
npm install -g pegjs
然後,您應該能夠將有效的 pegjs 語法傳遞給 peg cli 以生成語法:
pegjs hello.pegjs
或者如果您需要在運行時生成解析器:
var peg = require("pegjs");
var grammar = "start = ('a' / 'b')+";
var parser = peg.generate(grammar);
parser.parse("abba"); // returns ["a", "b", "b", "a"]
這會生成一個匹配任何數字或 a 字符或 b 字符的語法。 eg:abb aabbbabab 和 bbbbbba 都會解析,但 cabbbbabbbcccc 不會。
基本規則:
- peg 語法是一個規則列表,它是從上到下解釋的。這一點非常重要 - 起始規則是語法的“根”,因此任何無法從根到達的規則實際上都不是語法的一部分。
- 規則看起來像變量聲明,它們由名稱和解析表達式組成。一個簡單的解析表達式看起來很像一個正則表達式,但重要的是它們還可以包含其他規則。
簡單字符串匹配
start = 'hello world' // returns 'hello world'
請注意,這與 hello world 完全匹配,缺少或多餘的字符將導致解析器拋出錯誤
簡單表達式:
integer = [0-9] // "1"
這將匹配單個字符 0-9 並且類似於正則表達式,我們可以使用 + 和 * 分別匹配“至少一個”和“零或多個”:
integer = [0-9]+ // parsing 1 returns ['1']
integer = [0-9]+ // parsing '' throws error
integer = [0-9]*') // parsing '124' returns ['1','2','4'],
請注意,通過添加 * 或 + 解析器返回匹配的單個值數組,與正則表達式不同,我們也可以在規則上使用這些數量修飾符:
float = integer+ '.' integer+
integer = [0-9]
格式化
Peg.js 最酷的特性之一是能夠使用與規則相鄰的 javascript 來控制其返回值。它通過使用變量名標記表達式的一部分並將 js 函數附加到規則的末尾來工作,如下所示:
integer = digits:[0-9] { return digits.join() }
// parsing '124' now returns '124' instead of ['1','2','4'],
或表達式
or 表達式 '/' 在規則中非常有用。噸
number = float / integer / bigint / imaginary
為了避免歧義,Peg 將規則解析為第一個有效的解析器表達式。例如:如果 start=a/b 並且我們的輸入可以匹配 a 和 b PEG.js 將使用 a 來解析子表達式。
遞歸定義
遞歸在 peg.js 中有幾個用途。首先,我們可以使用它來描述嵌套或樹狀結構,例如 HTML 或 JSON,但我們也可以使用它來描述事物的平面列表 - 這與 Haskell 等函數式語言如何根據遞歸頭對定義列表非常相似&尾值:
commaSeparatedIntegerList
= integer ',' commaSeparatedIntegerList
/ integer
integer = [0-9]
例子:
parse:'1':它缺少逗號,因此文本無法匹配第一個解析器表達式,但它匹配第二個解析器表達式(整數)。
解析'1,2'它匹配第一個表達式'消耗1,然後遞歸地嘗試匹配2。2是一個有效的commaSeparatedIntegerList,因為它是一個整數,所以1,2會解析。
這個過程可以無限期地或更準確地繼續下去,直到堆棧溢出。
把所有東西放在一起,我們可以很容易地構建一個糟糕的 mans json 解析器:
object = "{" keyValueList? "}"
keyValueList = keyValue ',' keyValueList / keyValue
keyValue = key ":" value
key = [a-zA-Z]+
value = string / intArray / object
string = "'"[a-zA-Z]+ "'"
intArray
= '[' integer ',' intArray ']'
/ integer
integer = [0-9]+
這將成功處理諸如“{foo:'bar',fip:1,goo:1,a:{a:[1,2,3]}}”之類的輸入,但在某些明顯有效的輸入上失敗,例如在鍵/值之間包含空格或換行符,並且需要一些額外的格式來產生有用的輸出,我將把它作為練習留給讀者。
暫時將其與正則表達式進行語法比較 - 確保它佔用更多空間,但 pegs 仍然相當簡潔,允許我們:
- 命名事物並
- 遞歸地構建更複雜的解析器。
這使您可以一次專注於程序的較小部分,從而減少對大腦工作記憶的總體需求。我希望您同意 PEG 是生成解析器的絕佳工具,並在下次您希望簡化複雜的正則表達式時考慮使用它們。
非常感謝閱讀!
如果喜歡它,請通過為這篇文章鼓掌告訴我,並在 youtube 和 twitter 上關注我,以隨時了解我所有的最新內容。