趣味编程:从字符串中提取信息(参考答案 - 上)
這次“趣味編程”的目的是解析字符串,從一個指定模式的字符串中提取信息。對于目前這個問題,解決方案有很多種,例如直接拆分,使用正則表達式,或是如現在本文這般按照順序解析。總結果上來說,這些做法都是可取的,不過現在我打算舉出的做法是我認為最為“典型”也最有“學習”和“展現”價值的解決方案:基于狀態機的順序字符解析。也歡迎您對此其他的做法進行深入分析。
您可能需要重新閱讀上一篇文章來回憶字符串解析的具體規則,起始歸納起來,它只有以下三點:
至于最終結果,便是將一個text字符串,拆分成一個token group列表:
static List<List<string>> Parse(string text) { ... }在這里,我們使用List<string>來表示一個token group(即token列表)。自然,表現方式可以有所不同。例如您的Parse方法如果返回“列表的數組”、“數組的列表”或是“數組的數組”都是沒有任何問題的。
下面的做法基于winter-cn在上一篇文章后面的回復,再加以簡單的修改和注釋后得到的結果。這個做法的思路和我在“出題”時已經準備的“參考答案”不謀而合,但是winter-cn的實現要比我的更為簡單、因此我的代碼就不拿出來獻丑了,我們現在一起來欣賞高手的勞動成果。
winter-cn的做法,是將“解析”工作拆分為5種狀態,每種狀態對應一種解析邏輯,而每種解析邏輯除了處理當前字符(改變一些公共狀態)以外,還會返回處理下一個字符所使用的“解析邏輯”——這就是狀態的遷移。winter-cn原有的做法是使用Func<char, object>來表示解析邏輯的類型,這樣在每次得到新狀態之后,還需要將其轉化為Func<char, object>。不過為了更清晰地表達這樣一種邏輯,我們也可以定義一個返回自身類型的“遞歸”的委托類型:
delegate StateParser StateParser(char ch);在現在的實現中,我們把它解析過程分解為5個狀態,分別對應不同“時刻”下的解析邏輯:
static List<List<string>> Parse(string text) {StateParser p1 = null; // 用于解析token的起始字符StateParser p2 = null; // 用于解析作為分隔符的“-”的下一個字符StateParser p3 = null; // 用于解析token中或結尾的單引號的下一個字符StateParser p4 = null; // 用于解析單引號外的token字符StateParser p5 = null; // 用于解析單引號內的token字符var currentToken = new StringBuilder(); // 用于構建當前的token(即收集字符)var currentTokenGroup = new List<string>(); // 用于構建當前的token group(即收集token)var result = new List<List<string>>(); // 用于保存結果(即收集token group)...return result; }p1至p5便是所謂的“狀態”,也就是“解析邏輯”,它們都會操作currentToken,currentTokenGroup和result三個數據,并返回下一個狀態。狀態的劃分并非只有一種,不同的狀態劃分方式會形成不同的邏輯。我們接下來便要根據這樣的劃分方式,為每個狀態指定實現了。在實現的過程中,我們需要時刻遵守“當前”狀態的邏輯細節,以及其他狀態的職責,這樣實現狀態的遷移似乎也并不是一件困難的事情。
首先是p1,它的作用是解析token的第一個字符:
// 解析token的起始字符 p1 = ch => {if (ch == '-'){// 如果token中需要包含單引號或“-”,// 那么這個token在表示的時候一定需要用一對單引號包裹起來throw new ArgumentException();}if (ch == '\''){// 如果起始字符是單引號,// 則開始解析單引號內的token字符return p5;}else{// 如果是普通字符,則作為當前token的字符,// 并開始解析單引號外的token字符currentToken.Append(ch);return p4;}};接著是p2:它的作用是解析分隔符“-”(不包括單引號包裹內的“-”)后的下一個字符:
// 解析作為分隔符的“-”的下一個字符 p2 = ch => {if (ch == '-'){// 如果當前字符為“-”,說明一個token group結束了(因為前一個字符也是“-”),// 則將當前的token group加入結果集,并且準備新的token groupresult.Add(currentTokenGroup);currentTokenGroup = new List<string>();return p1;}else if (ch == '\''){// 如果當前字符為單引號,則說明新的token以單引號包裹// 則開始解析單引號內的token字符return p5;}else{// 如果是普通字符,則算作當前token的字符,// 并繼續解析單引號外的token字符currentToken.Append(ch);return p4;} };接著是p3:解析token內部或結尾的單引號的下一個字符:
// 解析token內部或結尾的單引號的下一個字符 p3 = ch => {if (ch == '\''){// 如果當前字符為單引號,則說明連續兩個單引號,// 所以表明token中出現了“單個”單引號,并且當前token一定包裹在單引號內,// 因此繼續解析單引號內的token字符currentToken.Append('\'');return p5;}else if (ch == '-'){// 如果當前字符為一個分隔符,則說明上一個token已經結束了// 于是將當前token加入當前token group,準備新的token,// 并解析分隔符后的下一個字符currentTokenGroup.Add(currentToken.ToString());currentToken = new StringBuilder();return p2;}else{// 單引號后面只可能是另一個單引號或者一個分隔符,// 否則說明輸入錯誤,則拋出異常throw new ArgumentException();} };最后則是p4和p5,分別用于處理普通的token以及被單引號包裹的token字符:
// 用于解析單引號外的token字符, // 即一個沒有特殊字符(分隔符或單引號)的token p4 = ch => {if (ch == '\''){// 如果token中出現了單引號,則拋出異常throw new ArgumentException();}if (ch == '-'){// 如果出現了分隔符,則表明當前token結束了,// 于是將當前token加入當前token group,準備新的token,// 并解析分隔符的下一個字符currentTokenGroup.Add(currentToken.ToString());currentToken = new StringBuilder();return p2;}else{// 對于其他字符,則當作token中的普通字符處理// 繼續解析單引號外的token字符currentToken.Append(ch);return p4;} };// 用于解析單引號內的token字符 p5 = ch => {if (ch == '\''){// 對于被單引號包裹的token內的第一個單引號,// 需要解析其下一個字符,才能得到其真實含義return p3;}else{// 對于普通字符,則添加到當前token內currentToken.Append(ch);return p5;} };這些狀態中的邏輯都有一個特點,它們都會通過C#編譯器形成的閉包來操作“外部”狀態——不過這個“外部”是指這些匿名函數的外部,但是它們統統屬于Parse方法本身!這意味著,雖然我們的狀態并非“純函數”,但是Parse方法是沒有任何副作用(Side Effect,即除了返回值外不會影響其他外部狀態,以及相同的返回值永遠相同的結果)。這個特點確保了Parse方法可以被任意多個線程同時調用。winter-cn的做法巧妙地使用了C#編譯器的特性,簡化了Parse方法的實現。
在定義完5種狀態之后,我們便要從p1開始依次處理字符串中的每個字符,并且隨著狀態的遷移改變處理每個字符的邏輯。當然,最后的“收尾”工作也是必不可少的:
static List<List<string>> Parse(string text) {...text.Aggregate(p1, (sp, ch) => sp(ch));currentTokenGroup.Add(currentToken.ToString());result.Add(currentTokenGroup);return result; }可以看出,這種做法的優勢之一是完全的“順序處理”,只遍歷一次。如果您使用字符串的分割或者正則表達式進行解析的話,一般總是會有“回溯”,以及拆分出更多的字符串。因此,根據推測,這個做法從性能上來講應該也有一定優勢,不過還是需要真實的性能比較才能得出確切的結果。本文全部代碼已經存放在http://gist.github.com/214427中,您可以復制、執行,調試等等。
這次的“趣味編程”是到目前為止最為熱鬧的一次,在上一篇文章的回復里您還可以發現許多朋友給出的大量解決方案,不過由于時間精力有限,我無法一一瀏覽了。此外,由于winter-cn已經給出了與我思路接近但實現更好的做法,后來我又用F#實現了另外一個思路不同的版本,您會發現F#有一些語言特性似乎非常適合進行字符串解析工作,它對于我們編寫C#代碼也有一定的借鑒意義。
from:?http://blog.zhaojie.me/2009/10/code-for-fun-tokenizer-answer-1.html
總結
以上是生活随笔為你收集整理的趣味编程:从字符串中提取信息(参考答案 - 上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 趣味编程:函数式链表的快速排序(参考答案
- 下一篇: 趣味编程:从字符串中提取信息(参考答案