與大多數程式員一樣,我經常需要標識存在於文字文件中的組件和結構,這些文檔包括:記錄檔、設定檔、分隔的資料以及格式更自由的(但還是半結構化的)報表格式。所有這些文檔都擁有它們自己的“小語言”,用於規定什麼能夠出現在文檔內。
我編寫處理這些非正式解析任務的程式的方法總是有點象大雜燴,其中包括定製狀態機器、Regex以及上下文驅動的字串測試。這些程式中的模式大概總是這樣:“讀一些文本,弄清是否可以用它來做些什麼,然後可能再多讀一些文本,一直嘗試下去。”
各種形式的解析器將文檔中組件和結構的描述提煉成簡明、清晰和 說明性的規則,該規則規定了如何標識文檔的組成部分。這裡,說明性方面是最令人信服的。我所有的舊的特別的解析器都採用了這種風格:讀一些字元、作決定、累加一些變數、清空、重複。正如本專欄關於函數型編程的部分文章中所評述的,程式流的方法風格相對來說容易出錯並且難以維護。
正式解析器幾乎總是使用擴充巴科斯範式(Extended Backus-Naur Form(EBNF))上的變體來描述它們所描述語言的“文法”。我們在這裡研究的工具是這樣做的,流行的編譯器開發工具 YACC(及其變體)也是這樣做的。基本上,EBNF 文法對您可能在文檔中找到的 組件賦予名稱;另外,經常將較小的組件組成較大的組件。由運算子 ― 通常和您在Regex中看到的符號相同 ― 來指定小組件在較大的組件中出現的頻率和順序。在解析器交談(parser-talk)中,文法中每個命名的組件稱為一個“產品(production)”。
可能讀者甚至還不知道 EBNF,卻已經看到過啟動並執行 EBNF 描述了。例如,大家熟悉的 Python 語言參考大全(Python Language Reference)定義了浮點數在 Python 中是什麼樣子:
EBNF 樣式的浮點數描述
floatnumber: pointfloat | exponentfloat
pointfloat: [intpart] fraction | intpart "."
exponentfloat: (nonzerodigit digit* | pointfloat) exponent
intpart: nonzerodigit digit* | "0"
fraction: "." digit+
exponent: ("e"|"E") ["+"|"-"] digit+
或者您可能見過以 EBNF 樣式定義的 XML DTD 元素。例如,developerWorks 教程的 <body> 類似於:
developerWorks DTD 中 EBNF 樣式的描述
<!ELEMENT body ((example-column | image-column)?, text-column) >
拼字稍有不同,但是量化、交替和定序這些一般概念都存在於所有 EBNF 樣式的語言文法中。
使用 SimpleParse 構建標記列表
SimpleParse 是一個有趣的工具。要使用這個模組,您需要底層模組 mxTextTools ,它用 C 實現了一個“標記引擎”。 mxTextTools (請參閱本文後面的 參考資料)的功能強大,但是相當難用。一旦在 mxTextTools 上放置了 SimpleParse 後,工作就簡單多了。
使用 SimpleParse 確實很簡單,因為不需要考慮 mxTextTools 的大部分複雜性。首先,應該建立一種 EBNF 樣式的文法,用來描述要處理的語言。第二步是調用 mxTextTools 來建立一個 標記列表,當文法應用於文檔時,該列表描述所有成功的產品。最後,使用 mxTextTools 返回的標記列表來進行實際操作。
對於本文,我們要解析的“語言”是“智能 ASCII”所使用的一組標記代碼,這些代碼用來表示諸如黑體、模組名以及書籍標題之類的內容。這就是先前使用 mxTextTools 來標識的同一種語言,在先前的部分中,使用Regex和狀態機器。該語言比完整的程式設計語言簡單得多,但已經足夠複雜而有代表性。
這裡,我們可能需要回顧一下。 mxTextTools 提供給我們的“標記列表”是什麼東西?這基本上是一個嵌套結構,它只是給出了每個產品在源文本中匹配的字元位移量。 mxTextTools 快速遍曆源文本,但是它不對源文本本身 做任何操作(至少當使用 SimpleParse 文法時不進行任何操作)。讓我們研究一個簡化的標記列表:
從 SimpleParse 文法產生的標記列表
(1,
[('plain',
0,
15,
[('word', 0, 4, [('alphanums', 0, 4, [])]),
('whitespace', 4, 5, []),
('word', 5, 10, [('alphanums', 5, 10, [])]),
('whitespace', 10, 11, []),
('word', 11, 14, [('alphanums', 11, 14, [])]),
('whitespace', 14, 15, [])]),
('markup',
15,
27,
...
289)
中間的省略符號表示了一批更多的匹配。但是我們看到的部分敘述了下列內容。根產品(“para”)取得成功並結束於位移量 289 處(源文本的長度)。子產品“plain”的位移量為 0 到 15。“plain”子產品本身由更小的產品組成。在“plain”產品之後,“markup”產品的位移量為 15 到 27。這裡省略了詳細資料,但是第一個“markup”由組件組成,並且源文本中稍後還有另外的產品取得成功。