用 Perl 模塊進行解析
Perl 是用于文本分析的一種出色語言。內置的操作符使得文本搜索、替換和模式匹配輕而易舉。程序員在學習 Perl 時,常常會試著編寫一些自己的例程來解析文本和數據。幸運的是,CPAN(綜合 Perl 檔案網絡(Comprehensive Perl Archive Network);請參閱 參考資料)匯集了大量模塊,有些模塊把您從文本和數據分析的困境中解救出來。
將 Perl 模塊用于解析、記載和分析
Damian Conway 開發的 Parse::RecDescent 是一個對文本進行記載和解析的功能強大的工具。Kim Ryan 開發的 Lingua::EN::Fathom 可以分析一個文件或一個文本塊,并產生有關其輸入的各種統計信息。請參閱 參考資料,獲取這兩個工具。
Parse::RecDescent 的缺點是:由于它使用可擴展文法規則并且實時地進行記載和解析,所以比較慢。如果沒有正確使用該模塊,性能就會降低。優點是:Parse::RecDescent 擅長記載和解析。下一章演示一個移植到 Parse::RecDescent 文法中的記載文法。記載總是比任何其它工具更好地執行任務。雖然 chef.pl 腳本(請參閱 參考資料)比用 chef.1 記載文法編譯的 C 程序運行得慢些,但它做得更多。請確保您了解自己的工具并將適當的工具用于作業。
改編現有的記載文法
John Hagerman 的 Swedish Chef 記載文法是出色的簡單文本過濾器示例。它還非常有趣,會給許多計算機科學與工程專業的學生在畢業前夕帶來歡樂。我將展示一個使用 Parse::RecDescent 模塊將 chef.l 文法移植到 Perl 中的示例(Parse::RecDescent 模塊并不是實現這一任務的理想選擇 ― Parse::Lex 模塊會更好些)。這一節只準備介紹構建 Parse::RecDescent 語法的規則,將包括操作、記憶狀態、拒絕產品和對文本進行記載。請記住,自己試一試 chef.pl 腳本 ― 您很可能會對此著迷。
chef.pl 腳本幾乎是 chef.l 記載文法完全一樣的副本。 $niw 變量在啟動時設置為 0,因為許多規則測試它來判斷它們應該被接受還是被拒絕。 $niw 表示“不在文字中(not in word)”,而當解析器在文字內時,它設置為 1。如果 Parse::RecDescent 的偽指令中指定的變量非零,則該偽指令會拒絕該規則。因此,請牢記 $niw = 0 意味著解析器不在文字內。
skip 變量設置為 '' (空字符串),所以所有輸入(包括空格)都轉至標志偽指令。此外,chef 規則以 \z 結束,\z 表示字符串的結束。通常使用 \Z ,但那還可以匹配 Perl 中的換行,它們也都可以在輸入中。
chef 規則:文法以 chef 規則開始。chef 規則匹配許多標志,直至表示字符串結束的 \z 。chef 規則的那兩個元素稱為“產品”。任何規則都必須由產品組成。操作可以是產品的一部分;它由花括號 {} 標出,并包含 Perl 代碼。它不匹配任何事物 ― 操作僅用于執行。
token 規則:token 規則可以匹配任何數或序列,這些數和序列是我為匹配 chef.1 文法而指定的(有些隨意)。我將說明一些示例,以便使文法對應清晰。
一個文字/非文字字符的基本文法定義:
chef.pl: NW: /[^A-Za-z']/
chef.l: WC [A-Za-z']
chef.l: NW [^A-Za-z']
an 規則:最簡單的規則不取決于任何情況。an 規則是一個很好的示例:每次它看到 “an” ,就打印 “un” 。而且,它將 $niw 設置為 1(記住,這意味著在文字內)。
chef.l: "an" { BEGIN INW; printf("un"); }
ax 規則:下一個更復雜的規則是 ax 規則。它表明:如果出現一個 “a” ,而且后跟一個文字字符 WC ,就打印 “e” 。 ...WC 產品語法意味著,文字字符必須跟在 a 之后,但不在匹配中使用。因此,通過使用 an 和 ax 規則, “aan” 會產生 “eun” 。規則將 $niw 設置為 1(在文字內)。
chef.l: "a"/{WC} { BEGIN INW; printf("e"); }
en 規則:en 規則的作用完全象 ax 規則,但希望后跟 NW (非文字)產品。這意味著 “en” 必須在文字結尾。
chef.pl: en: /en/ ...NW { $niw = 1; print "ee" } chef.l: "en"/{NW} { BEGIN INW; printf("ee"); }
ew 規則:僅當在文字內,ew 規則才成功。也就是說如果 $niw 為 0,您就拒絕它。
chef.pl: ew: /ew/ { $niw = 1; print "oo" } chef.l: "ew" { BEGIN INW; printf("oo"); }
i 規則:僅當在文字內,而且尚未看到另一個 i ,i 規則才會成功。它將 $i_seen 增加為 1,僅當看到非文字字符或換行時, $i_seen 才設置回 0。
chef.pl: i: /i/ { $niw=1;$i_seen=1; print "ee" } chef.l: "i" { BEGIN INW; printf(i_seen++ ? "i" : "ee"); }
句結束(end of sectence)規則:將打印任意數量后跟空格的句結束標記符 [.!?] ,再跟著打印著名的(或臭名昭著的,隨您喜歡)“ Bork Bork Bork! ”消息。實際行為會與初始的 chef 過濾器略有偏差,僅僅是因為我更喜歡那樣(一個人可能永遠都不會有足夠多的 Bork 消息)。 $item[1] 語法意味著不會匹配空格,因為它們對 Parse::RecDescent 而言是 $item[2] 。
chef.pl: end_of_sentence: /[.?!]+/ /\s+/ { $niw = 0; $i_seen = 0; print $item[1] . "\nBork Bork Bork!\n" } chef.l: [.!?]$ { BEGIN NIW; i_seen = 0; printf("%c\nBork Bork Bork!", yytext[0]); }
動態擴展文法
extend-grammar.pl 腳本是從與 Parse::RecDescent 模塊一起提供的 demo_selfmod.pl 腳本發展而來的,它演示了可擴展文法。最初,文法只由一個恰當的名稱 Ted(不太謙虛地說,這是個很棒的名稱)組成。name 規則匹配 1 到 n 個文字字符(根據 Perl 的 \w 語法 )。 do_you_know 規則匹配由任意數目的空格分隔、任意大小寫組合的“do you know”。這就是 Perl i 匹配修飾符派上用場的地方,這樣您就可以以簡單的方式表達簡單概念。
proper_name: /Ted/ name: /\w+/ do_you_know: /do/i /you/i /know/i
擴展文法處理(extend-grammar process)規則:這個 process 規則可以由一個查詢或定義組成。除非它找到一個查詢或定義,否則它會都在主循環中觸發消息。
process: query | definition ... and later ... while (<>) { $parse->process($_) or print "Enter a query (do you know ...)" "or a definition (... exists)\n"; }
擴展文法查詢(extend-grammar query)規則:這個 query 規則由 do_you_know 產品組成,后跟一個名稱或恰當的名稱。對于名稱,操作是打印它未知的消息。對于恰當的名稱(由 proper_name 規則定義的),操作是打印出已知消息。同名的兩個規則等價于一個帶有兩個可選擇產品的規則,所以:
query: do_you_know proper_name { print "I know " . $item{proper_name} . ",sure!\n" } query: do_you_know name { print $item{name} . " does not exist in my little world", ", sorry.\n" }
等價于:
query: do_you_know proper_name { print "I know " . $item{proper_name} . ",sure!\n" } | do_you_know name { print $item{name} . " does not exist in my little world", ", sorry.\n" }
擴展文法定義(extend-grammar definition)規則:該可擴展文法的核心是 definition 規則。如果名稱后面跟著 “exists” ,那么操作將用 proper_name 的新規則擴展解析器。在文法運行時,可以修改這一正在執行的特別的文法。
definition: name /exists/i { $thisparser->Extend("proper_name: '$item{name}'"); print "\"$item{name}\" is now a valid proper name\n"; }
$thisparser 是對正在執行文法操作的解析器的引用。除了 Extend,您也可以使用 Replace 方法(例如,想想 C 中的 #ifdef 語句)來更改規則的內容。
要執行 extend-grammar.pl(請參閱 參考資料中的完整清單),只要運行它并輸入“ do you know ”或“ exists ”即可。任何其它內容都不會匹配解析器的處理規則,因而會被拒絕。proper_name 規則以“Ted”(一個已知恰當的名稱)開始。
分析 C/C++ 源代碼注釋以提高可讀性
stat-comments.pl 腳本(請參閱 參考資料中的清單)將 demo_decomment.pl 腳本的文法用于解析 C/C++ 代碼以抽取注釋。demo_decomment.pl 腳本與 Parse::RecDescent 模塊一起提供。
另外,stat-comments.pl 腳本使用 Lingua::EN::Fathom 模塊來分析由 Parse::RecDescent 文法解析出的注釋。
首先,stat-comments.pl 創建文法(在腳本末尾的 BEGIN 塊中的 $Grammar 變量中)。文法的程序規則返回散列引用,其中包含作為文本的代碼、注釋和字符串。有關其余的文法,請參閱 Parse::RecDescent 文檔。(C 解析文法也與 Java 程序的源代碼一起使用。)
然后,stat-comments.pl 讀入文本輸入或腳本末尾處提供的樣本數據,并進行檢查以判斷注釋是否被完全獲取。將 $/ 設置為 undef 的作用是將所有輸入(包括換行)立刻讀入 $text 中。
輸入循環,并檢查注釋是否可以進行 undef $/;
my $text = @ARGV ? <> : ; my $parts = $parser->program($text) or die "malformed C program"; # only work with comments of length > 0 die "No comments found in input" unless length $parts->{comments};
接著,stat-comments.pl 將注釋標志轉換成句點,這樣就用句點分隔注釋。這樣做只是為了整潔,不會對最終的統計信息產生很大的影響。最后,創建 Lingua::EN::Fathom 對象,并將封裝的文本塊(請參閱 Text::Wrap)傳遞給它,供其分析。然后,打印出報告。
stat-comments.pl 的結尾:
$parts->{comments} =~ s#//#. #g; $parts->{comments} =~ s#/\*#. #g; $parts->{comments} =~ s#\*/#. #g; # we can now evaluate the comments (stored in $parts->{comments}) my $fathom = new Lingua::EN::Fathom; $fathom->analyse_block(wrap('', '', $parts->{comments})); # voila, the readability report! print($fathom->report);
stat-comments.pl 腳本的潛在應用是在代碼質量控制方面。文檔編制良好的程序更易于維護,這是眾所周知的事實,而且許多組織都非常想這樣做。stat-comments.pl 腳本不會區分好注釋和差注釋。然而,它的確會告訴代碼管理員,某個程序員的注釋是極其簡潔的、異常冗長的還是對非編程人員來說是很難理解的。自己嘗試一下:在帶有以“ We should raise... ”開頭的長注釋塊和不帶這一長注釋塊這兩種情況下,運行 stat-comments.pl,并查看統計信息的差異。顯然,軟件項目經理會有效地使用這些統計信息。
stat-comments.pl 腳本是一個有價值的軟件項目管理工具(但僅在正確使用時)。就象函數點計數、每天的代碼行數以及許多其它可供軟件項目經理使用的統計信息一樣,stat-comments.pl 產生的統計信息必須看作是現實世界的補充,而不是其基本要素。許多編程編得很好的程序員寫注釋不行,而編得不好的程序員能寫出出色的注釋。重要的是了解他們的工作方法,并認識到其中的模式和變化。
最后說明
目前可用的 Perl 模塊可以使任何解析任務更加容易。除了 Parse::RecDescent 之外,還有 Parse::Lex、Parse::CLex 和 Parse::Yapp,它們都可以從 CPAN 獲得。查看它們,并研究哪一個最適合于您的情況。在這個列表中,Parse::RecDescent 是最靈活和功能最強大的模塊。
如需很好使用 Parse::RecDescent 的示例,請參閱 Abigail 的 CPAN RFC::RFC822::Address。沒有 Parse::RecDescent,即使利用 Perl 正則表達式的功能,也幾乎不可能對 RFC 822 電子郵件地址進行語法驗證。
如果嘗試創建自己的工具,那么很難進行文本分析。CPAN Lingua 模塊可以在任何文本分析任務中為您提供功能和靈活性。請訪問 CPAN 網站(請參閱 參考資料),獲取更多其它 Lingua 模塊。
本文展示了可以如何有效地重用 Perl 代碼。與 Parse::RecDescent 模塊一起提供的演示腳本提供了在此介紹的三個腳本中兩個腳本的基礎。感謝 Damian Conway 編寫了 Parse::RecDescent 模塊,Helmut Jarausch 編寫了 demo_decomment.pl 腳本,Kim Ryan 編寫了出色的 Lingua::EN::Fathom,以及 John Hagerman 編寫了初始的 chef 過濾器文法。