對於包含自然語言文件輸入的應用程式,使用者期望它具備拼寫檢查功能。因為從頭開始建構一個拼寫檢查器不是一項簡單的任務,所以這篇文章為您提供一個使用 Jazzy 的工作區。Jazzy 是一個開放原始程式碼的 Java 拼寫檢查器 API。Java 開發人員 Tom White 對基於電腦拼寫檢查背後的主要演算法進行了深入探討,然後向您展示 Jazzy API 如何才能幫助您把它們整合到 Java 應用程式中。

電腦擅長執行快速搜索操作,可以根據指定的搜索詞,對大量儲存的資訊快速進行搜索。但是,拼寫檢查應用程式所要求的搜索能力,不僅僅是正確的字串匹配。在這篇文章中,我將介紹搜索演算法的一些歷史,包括語音匹配演算法(比如 Soundex 和 Metaphone ),字串相似性類型(例如動態程式設計演算法)。我會解釋這些演算法對於拼寫檢查來說各自的優勢與不足,然後介紹最後一個變種-- Aspell 演算法,這個演算法是專門為拼寫檢查應用程式撰寫的。

Aspell 演算法結合了前面搜索與匹配演算法的最佳特性,是 Jazzy 的底層框架,是 Java 平台的拼寫檢查器 API。在這篇文章的後半部分裡,您將看到 Jazzy 在 Java 框架裡是如何應用 Aspell 演算法的。我會向您展示 Jazzy 識別拼寫錯誤的單字並提供合適的修正 。在文章結尾,我會用一個實例,示範在 Jazzy 的幫助下,您可以很容易地把它的拼寫檢查特性合併到 Java 應用程式中。

語音匹配演算法

正確拼出姓氏可能是個挑戰。如果一個人的姓不太常見,那麼當他們透過電話訂購的時候,經常發現名字被弄錯。即使是常見的名字也可能因為拼寫上的微小差異而拼錯,例如 SmithSmyth是發音相同的常見名字的兩個變體。

特別是在名字拼寫上,變化更豐富,這樣就形成了一些有趣的拼寫檢查演算法。我要介紹的第一個演算法類型是語音匹配演算法,它的目標是解決“哪個名字和聽起來像 x的名字匹配”這樣的問題。在搜索資料庫和其他參考應用程式裡,這種演算法類型相當普遍。例如,在搜索家族歷史時,使用者應該既能檢索到正確匹配,也會得到相似的匹配,這樣就有可能找到家族姓氏淵源流傳中發生變化或者在某些記錄中被拼寫錯誤的歷史。

Soundex 演算法從 1920 年起就一直被用來為所有美國人口做索引,是家族軟體常用的演算法。原始 Soundex 演算法在 1918 年由 Margaret K. Odell 和 Robert C. Russell 申請專利(請參閱 參考資料),他們當初是想“提供一個索引,按照發音而不是按照名字的字母表輸入名字,對名字分組”。

從實際上說,Soundex 演算法的運作方式是把某個字母表中的每個字母對應成代表它的語音組的一個數位程式。在這個方案裡,像 dt)這樣的字母在同一個組裡,因為它們發音相近(實際上每個字母都是用類似的機制發出聲音的),而母音則一概忽略。透過對整體單字應用這種對應,就產生了單字的語音“鍵”。發音相近的單字通常會有相同的鍵。例如, SmithSmyth 的 Soundex 都是 S530

Soundex 最常見的一個變體透過 Donald E. Knuth 的 The Art of Computer Programming一書流行開來。您可以在清單 1 中看到這個演算法的 Java 實作。請注意該演算法使用了 Java 正規運算式,正規運算式只有在 Java 1.4 發行版之後才可用。

清單1. Knuth 的 Soundex


public class KnuthSoundex implements PhoneticEncoder {
//                                            ABCDEFGHIJKLMNOPQRSTUVWXYZ
private static final String SOUNDEX_DIGITS = "01230120022455012623010202";
public String calculateCode(String string) {
String word = string.toUpperCase();                                 // 01 ASHCROFT
word = word.replaceAll("[^A-Z]", "");                               // 02
if (word.length() == 0) {                                           // 03
return "";                                                        // 04
}                                                                   // 05
char first = word.charAt(0);                                        // 06
word = first + word.substring(1).replaceAll("[HW]", "");            // 07 ASCROFT
StringBuffer sndx = new StringBuffer();                             // 08
for (int i = 0; i < word.length(); i++) {                           // 09
sndx.append(SOUNDEX_DIGITS.charAt((int) (word.charAt(i) - 'A'))); // 10
}                                                                   // 11
word = sndx.toString().replaceAll("(.)//1+", "$1");                 // 12 026013
word = first + word.substring(1);                                   // 13 A26013
word = word.replaceAll("0", "");                                    // 14 A2613
return (word + "000").substring(0, 4);                              // 15 A261
}
}

程式說明

上面的程式相當簡潔,所以我逐行來說明它的功能:

  • 行 01 到 05 對輸入進行規範化,把輸入變成大寫字母,去掉其他字元。
  • 行 06 保證單字的第一個字母不變。
  • 行 07 去掉後續的 HW 字母。
  • 行 08 到 11 用字母的語音程式替換單字裡的每個字母。
  • 行 12 刪除相鄰的相同語音程式。(請注意:這意味著,與母音的處理方式不同,插在中間的字元 HW 不會對組合相同程式的字母形成障礙。)
  • 與行 06 類似,行 13 保證單字的第一個字母不變。
  • 行 14 消除所有母音。
  • 行 15 透過把單字裁剪成 4 個字母,形成 Soundex (可能要用字元 0 來填充)。

為了真正理解演算法,手動地逐行執行演算法會很有幫助。程式右手邊的列用於追蹤 word 變數的值,從輸入的名字 Ashcroft開始。對於演算法來說,這是個很好的測試使用案例,因為 sc組合,沒有理會插在中間的 h。(在相同類型的 Web 網站上可以找到的許多 Soundex 實作都沒有正確地實作這一規則。)

Soundex 用於拼寫檢查

不幸的是,Soundex 演算法是一個差勁的拼寫檢查備選方案。首先來說,發音不同的單字可能有相同的 soundex。例如, WhiteWood 的 soundex 碼相同,同為 W300 。這並不奇怪,因為 Soundex 演算法的設計,就是為了把發音 相似的名字組合在一起,而不是嚴格地按照發音 相同組合。雖然這個特性對於某些應用程式來說可能是理想的 -- 例如用來幫助電話操作員識別用不同重音說出的名字的應用程式 -- 但是它對拼寫檢查應用程式沒有用,因為它會產生太多的匹配。例如,拼錯的 algorithum一詞會與我的範例字典中的下列單字匹配:

alacritous, alacrity, alcheringa, alcoran, algeria, algerian, algerians, algiers, algor, algorism, algorithm, algorithmic, algorithmically, algorithms, alizarin, alizarine, alkoran, alleger, allegers, allegoric, allegorical, allegorically, allegories, allegorist, allegorists, allegorizes, allegory, allegretto, allegrettos, allegro, allegros, allocheiria, allochiria, allocortex, allograft, allograph, allographic, allographs

即使考慮到同一單字的變體( allegoric、 allegorical、 allegorically)造成的額外匹配,您通常也應該要求拼寫檢查演算法提供更加嚴格的匹配。 您應該還記得, Soundex 演算法也會把每個 soundex 程式裁剪成 4 個字元,這樣就疏忽了長單字的尾部,因此也就進一步增加了匹配的數量。而且麻煩還不止於此。

同音問題

正如發音不同的單字有可能有相同的 soundex,反過來的情況也有可能發生:發音相同的單字,叫做 同音詞(homophone),可能有不同的程式。這是由於某些字母可能不發音,例如在 ThompsonT512 )中的 p 造成它發音與 ThomsonT525 )相同,但程式不同,還有 LeighL200 )中的 ghLeeL000 ) ,也有同樣的問題。與此類似,單字的開始字母可能不同,但是不影響它的發音,例如 CarrC600 )中的 cKarrK600 )中的 k。Soundex 演算法本身造成了這個問題,因為它無法把每個單字中的原始字母對應成語音數位。

所謂同音的問題,實際上產生於這樣一個現實:英語語言有不規範拼寫(可能比其他語言更甚)。雖然 Soundex 演算法有許多小的變體,但是他們都缺少對英語拼寫規則的認識,更不用說這些規則的例外了。這種不規範的後果就是, Soundex 不太適合做英語中的拼寫檢查。例如, Soundex 對於拼寫錯誤的 lamL500 ),提供了一個正確拼寫形式 lambL510 )不同的語音撰寫程式。這樣,基於 Soundex 的拼寫檢查應用程式就無法把 lamb作為拼寫錯誤的 lam的修改建議。正是這個問題,引領著 Lawrence Phillips 找到了 Soundex 演算法的替代品,叫做 Metaphone

Metaphone 語音匹配演算法Soundex 演算法參考資料),他們當初是想“提供一個索引,按照發音而不是按照名字的字母表輸入名字,對名字分組”。 清單1. Knuth 的 Soundex


public class KnuthSoundex implements PhoneticEncoder {
//                                            ABCDEFGHIJKLMNOPQRSTUVWXYZ
private static final String SOUNDEX_DIGITS = "01230120022455012623010202";
public String calculateCode(String string) {
String word = string.toUpperCase();                                 // 01 ASHCROFT
word = word.replaceAll("[^A-Z]", "");                               // 02
if (word.length() == 0) {                                           // 03
return "";                                                        // 04
}                                                                   // 05
char first = word.charAt(0);                                        // 06
word = first + word.substring(1).replaceAll("[HW]", "");            // 07 ASCROFT
StringBuffer sndx = new StringBuffer();                             // 08
for (int i = 0; i < word.length(); i++) {                           // 09
sndx.append(SOUNDEX_DIGITS.charAt((int) (word.charAt(i) - 'A'))); // 10
}                                                                   // 11
word = sndx.toString().replaceAll("(.)//1+", "$1");                 // 12 026013
word = first + word.substring(1);                                   // 13 A26013
word = word.replaceAll("0", "");                                    // 14 A2613
return (word + "000").substring(0, 4);                              // 15 A261
}
}

程式說明 Soundex 用於拼寫檢查 同音問題

對於包含自然語言文件輸入的應用程式,使用者期望它具備拼寫檢查功能。因為從頭開始建構一個拼寫檢查器不是一項簡單的任務,所以這篇文章為您提供一個使用 Jazzy 的工作區。Jazzy 是一個開放原始程式碼的 Java 拼寫檢查器 API。Java 開發人員 Tom White 對基於電腦拼寫檢查背後的主要演算法進行了深入探討,然後向您展示 Jazzy API 如何才能幫助您把它們整合到 Java 應用程式中。

電腦擅長執行快速搜索操作,可以根據指定的搜索詞,對大量儲存的資訊快速進行搜索。但是,拼寫檢查應用程式所要求的搜索能力,不僅僅是正確的字串匹配。在這篇文章中,我將介紹搜索演算法的一些歷史,包括語音匹配演算法(比如 Soundex 和 Metaphone ),字串相似性類型(例如動態程式設計演算法)。我會解釋這些演算法對於拼寫檢查來說各自的優勢與不足,然後介紹最後一個變種-- Aspell 演算法,這個演算法是專門為拼寫檢查應用程式撰寫的。

Aspell 演算法結合了前面搜索與匹配演算法的最佳特性,是 Jazzy 的底層框架,是 Java 平台的拼寫檢查器 API。在這篇文章的後半部分裡,您將看到 Jazzy 在 Java 框架裡是如何應用 Aspell 演算法的。我會向您展示 Jazzy 識別拼寫錯誤的單字並提供合適的修正 。在文章結尾,我會用一個實例,示範在 Jazzy 的幫助下,您可以很容易地把它的拼寫檢查特性合併到 Java 應用程式中。

正確拼出姓氏可能是個挑戰。如果一個人的姓不太常見,那麼當他們透過電話訂購的時候,經常發現名字被弄錯。即使是常見的名字也可能因為拼寫上的微小差異而拼錯,例如 SmithSmyth是發音相同的常見名字的兩個變體。

特別是在名字拼寫上,變化更豐富,這樣就形成了一些有趣的拼寫檢查演算法。我要介紹的第一個演算法類型是語音匹配演算法,它的目標是解決“哪個名字和聽起來像 x的名字匹配”這樣的問題。在搜索資料庫和其他參考應用程式裡,這種演算法類型相當普遍。例如,在搜索家族歷史時,使用者應該既能檢索到正確匹配,也會得到相似的匹配,這樣就有可能找到家族姓氏淵源流傳中發生變化或者在某些記錄中被拼寫錯誤的歷史。

回到頂端

Soundex 演算法從 1920 年起就一直被用來為所有美國人口做索引,是家族軟體常用的演算法。原始 Soundex 演算法在 1918 年由 Margaret K. Odell 和 Robert C. Russell 申請專利(請參閱

從實際上說,Soundex 演算法的運作方式是把某個字母表中的每個字母對應成代表它的語音組的一個數位程式。在這個方案裡,像 dt)這樣的字母在同一個組裡,因為它們發音相近(實際上每個字母都是用類似的機制發出聲音的),而母音則一概忽略。透過對整體單字應用這種對應,就產生了單字的語音“鍵”。發音相近的單字通常會有相同的鍵。例如, SmithSmyth 的 Soundex 都是 S530

Soundex 最常見的一個變體透過 Donald E. Knuth 的 The Art of Computer Programming一書流行開來。您可以在清單 1 中看到這個演算法的 Java 實作。請注意該演算法使用了 Java 正規運算式,正規運算式只有在 Java 1.4 發行版之後才可用。

上面的程式相當簡潔,所以我逐行來說明它的功能:

  • 行 01 到 05 對輸入進行規範化,把輸入變成大寫字母,去掉其他字元。
  • 行 06 保證單字的第一個字母不變。
  • 行 07 去掉後續的 HW 字母。
  • 行 08 到 11 用字母的語音程式替換單字裡的每個字母。
  • 行 12 刪除相鄰的相同語音程式。(請注意:這意味著,與母音的處理方式不同,插在中間的字元 HW 不會對組合相同程式的字母形成障礙。)
  • 與行 06 類似,行 13 保證單字的第一個字母不變。
  • 行 14 消除所有母音。
  • 行 15 透過把單字裁剪成 4 個字母,形成 Soundex (可能要用字元 0 來填充)。

為了真正理解演算法,手動地逐行執行演算法會很有幫助。程式右手邊的列用於追蹤 word 變數的值,從輸入的名字 Ashcroft開始。對於演算法來說,這是個很好的測試使用案例,因為 sc組合,沒有理會插在中間的 h。(在相同類型的 Web 網站上可以找到的許多 Soundex 實作都沒有正確地實作這一規則。)

不幸的是,Soundex 演算法是一個差勁的拼寫檢查備選方案。首先來說,發音不同的單字可能有相同的 soundex。例如, WhiteWood 的 soundex 碼相同,同為 W300 。這並不奇怪,因為 Soundex 演算法的設計,就是為了把發音 相似的名字組合在一起,而不是嚴格地按照發音 相同組合。雖然這個特性對於某些應用程式來說可能是理想的 -- 例如用來幫助電話操作員識別用不同重音說出的名字的應用程式 -- 但是它對拼寫檢查應用程式沒有用,因為它會產生太多的匹配。例如,拼錯的 algorithum一詞會與我的範例字典中的下列單字匹配:

alacritous, alacrity, alcheringa, alcoran, algeria, algerian, algerians, algiers, algor, algorism, algorithm, algorithmic, algorithmically, algorithms, alizarin, alizarine, alkoran, alleger, allegers, allegoric, allegorical, allegorically, allegories, allegorist, allegorists, allegorizes, allegory, allegretto, allegrettos, allegro, allegros, allocheiria, allochiria, allocortex, allograft, allograph, allographic, allographs

即使考慮到同一單字的變體( allegoric、 allegorical、 allegorically)造成的額外匹配,您通常也應該要求拼寫檢查演算法提供更加嚴格的匹配。 您應該還記得, Soundex 演算法也會把每個 soundex 程式裁剪成 4 個字元,這樣就疏忽了長單字的尾部,因此也就進一步增加了匹配的數量。而且麻煩還不止於此。

正如發音不同的單字有可能有相同的 soundex,反過來的情況也有可能發生:發音相同的單字,叫做 同音詞(homophone),可能有不同的程式。這是由於某些字母可能不發音,例如在 ThompsonT512 )中的 p 造成它發音與 ThomsonT525 )相同,但程式不同,還有 LeighL200 )中的 ghLeeL000 ) ,也有同樣的問題。與此類似,單字的開始字母可能不同,但是不影響它的發音,例如 CarrC600 )中的 cKarrK600 )中的 k。Soundex 演算法本身造成了這個問題,因為它無法把每個單字中的原始字母對應成語音數位。

所謂同音的問題,實際上產生於這樣一個現實:英語語言有不規範拼寫(可能比其他語言更甚)。雖然 Soundex 演算法有許多小的變體,但是他們都缺少對英語拼寫規則的認識,更不用說這些規則的例外了。這種不規範的後果就是, Soundex 不太適合做英語中的拼寫檢查。例如, Soundex 對於拼寫錯誤的 lamL500 ),提供了一個正確拼寫形式 lambL510 )不同的語音撰寫程式。這樣,基於 Soundex 的拼寫檢查應用程式就無法把 lamb作為拼寫錯誤的 lam的修改建議。正是這個問題,引領著 Lawrence Phillips 找到了 Soundex 演算法的替代品,叫做 Metaphone。 演算法

Metaphone 演算法背後的想法,首先發表在 1990 年的 Computer Language雜誌上(請參閱 參考資料),這個演算法明確地對英語發音的公共規則進行了撰寫程式,而這正是 Soundex 沒有解決的問題。例如, Metaphone 演算法包含一個明確的規則:在字母 b在單字末尾出現在字母 m後面時,就刪除它。這個規則保證了 lamlamb 會有相同的撰寫程式( LM ),這樣就使拼寫檢查應用程式能夠為 lam提供正確的替換。

Metaphone 演算法使用了 16 個輔音類別,由下列字元代表:

B X S K J T F H L M N P R 0 W Y

字元 0 是零,用來代表 th 的聲音。就像在 Soundex 演算法裡一樣,第一個字母被保留,最後的程式被裁剪成四個字元,但是如果短於四個字元,也並不填充。迭代的字母和母音通常被刪除,與母音的處理一樣。Metaphone 演算法整體上是一套規則集,可以把字母組合對應成輔音類別。這個演算法的 Java 實作需要幾百行程式,具體可以參閱 Apache Jakarta Commons Codec 專案中的 Metaphone 程式(請參閱 參考資料)。在清單 2 中,您可以看到當您把 Apache 的 Metaphone 類別用作 JUnit 的測試使用案例,檢查單字 lamb的程式時發生的情況:

清單2. 使用 Apache Metaphone 類別


import junit.framework.TestCase;
import org.apache.commons.codec.language.Metaphone;
public class ApacheMetaphoneTest extends TestCase {
public void test() {
Metaphone metaphone = new Metaphone();
assertEquals("LM", metaphone.encode("lam"));
assertEquals("LM", metaphone.metaphone("lam"));
assertEquals(metaphone.encode("lamb"), metaphone.encode("lam"));
assertTrue(metaphone.isMetaphoneEqual("lamb", "lam"));
}
}

雖然在規則裡仍然有一些缺陷,但 Metaphone 演算法在 Soundex 上有了提高。例如,Metaphone 的作者 Phillips 指出, BryanBRYN )和 BrianBRN )應該有相同的程式。 Phillips 在 2000 年 6 月出版的 C/C++ Users Journal 上發表了他對 Metaphone 的模糊匹配(是這麼叫的)改進的嘗試。 DoubleMetaphone 演算法對原來的輔音類別做了一些修正,它把所有的開始母音都撰寫程式成 A ,所以不再使用 Soundex 演算法。更加根本的變化是,DoubleMetaphone 被撰寫成可以為多音詞回傳不同的程式。例如, hegemony中的 g 可以發輕聲,也可以發重音,所以演算法既回傳 HJMN ,也可以回傳 HKMN 。除了這些例子之外,Metaphone 演算法中的多數單字還是回傳單一鍵。您可以參見清單 3 中摘錄的 Apache 的 DoubleMetaphone 類別的程式。

清單3. 使用 Apache DoubleMetaphone 類別


import junit.framework.TestCase;
import org.apache.commons.codec.language.DoubleMetaphone;
public class ApacheDoubleMetaphoneTest extends TestCase {
public void test() {
DoubleMetaphone metaphone = new DoubleMetaphone();
assertEquals("HJMN", metaphone.encode("hegemony"));
assertEquals("HJMN", metaphone.doubleMetaphone("hegemony"));
assertEquals("HJMN", metaphone.doubleMetaphone("hegemony", false));
assertEquals("HKMN", metaphone.doubleMetaphone("hegemony_u44 ? true));
}
}

雖然 Soundex 和 Metaphone 演算法都解決了語音模糊的匹配問題,但是如果不能糾正打字錯誤,那麼拼寫檢查應用程式是不完整的。當您的手指在鍵盤上滑過,打的是 labmLBM )而不是 lambLM ), 打字錯誤就出現了。語音匹配演算法不能用它的替換來匹配這種拼寫錯誤,因為兩個單字聽起來是不同的。為了解決這類問題,您的拼寫檢查應用程式必須包括字串相似性演算法。

回到頂端

字串相似性演算法

您還記得這樣的字謎嗎--每次只允許修改單字的一個字母,就能把它變換成另外一個單字?例如, ship可以透過逐步修改變成 crow,透過中間單字 shopchopcrop。這種遊戲為您提供了一條路,可以清楚地理解兩個單字之間的距離這一概念。 距離是從一個單字變換成另外一個單字所需要的步數,要求是每次只能改變一個字母,而且每步都要使用字典中實際存在的單字。我把這叫做 字謎距離(puzzle distance)。在這個範例裡, shipcrow之間的字謎距離是 4。

雖然我們經常把距離當作是空間中二點之間的物理度量,但是數學家則用更具一般性的概念把它定義為 度量(metric)。這個定義讓您可以在不同的應用程式中使用距離的概念;在這裡,您感興趣的是兩個字串或兩個單字之間的距離。它的意義在於,對於拼寫錯誤的單字,您應該尋找和它“接近”(這就使用了距離的定義)的單字。距離度量的任何定義都必須滿足一些可以度量的屬性;例如,距離永遠不可能為負。

雖然順序比較有許多方面(請參閱 參考資料),但是您的目的是找到距離的定義,使距離有助於實作良好的拼寫校正。前面定義的字謎距離至少有一個理由不適合做這項工作:拼寫錯誤的單字比起正確拼寫的單字來說,通常不止錯了一個字母。例如,對於拼錯的 puzzel,找不到“路碑”可以到達拼寫正確的英文單字。幸運的是,已經設計了大量適用於拼寫檢查的度量方式。

回到頂端

動態程式設計演算法

動態程式設計演算法從本質上看是一種窮舉方法,它會考慮到把原本的單字轉換成目標單字的所有不同方法,進而找到成本最小、或者單字間距離最短的方法。 Levenshtein 距離演算法是動態程式設計演算法的一個具體實作,它允許進行三類操作,把原本的單字 x轉換成目標單字 y

  • 把單字 x中的一個字元 替換成單字 y中的一個字元
  • 把單字 x中的一個字元 刪除
  • 在單字 y插入一個字元

每個操作都會有一定的成本,而總距離就是從單字 x變換到單字 y 的最小成本。從直觀上看,基於這些操作的演算法應該可以很好地進行拼寫校正,因為打字錯誤無外乎是這些操作所涉及的鍵入錯誤。(實際上, Levenshtein 距離也稱作 編輯距離。)例如,當我把單字 wrong打成 wromg(按了 m鍵,而不是 n 鍵)的時候,就是一個替換錯誤;當我打成 wromng(按了 m鍵,還有 n鍵)的時候,就是一個刪除錯誤;而當我打成 wrog(遺漏了 n 鍵),就是一個插入錯誤。

計算距離

為了更容易理解動態程式設計演算法,可以畫一個表格,它的行對應原本的單字的字母,它的列對應目標單字的字母。處在 (i, j)位置的單格代表從原本的單字的 i字母到目標單字的 j字母的最小距離。

對於 Levenshtein 距離,刪除和插入的成本為 1。如果字元有差異,那麼替換的成本為 1,否則為 0。開始演算法的時候,您先填充第一行,第一行對應著空的原本的單字,這樣它就是插入 0,1,..., j個字母的成本。同樣,第一列對應著空的目標單字,所以它就是刪除 0, 1, ..., i個字母的成本。如果您以 pzzelpuzzle 的轉換為例,那麼您會得到如 圖 1 所示的網格。

圖1. Levenshtein 演算法的第一階段

接下來,您要計算餘下的每個單格的值,透過考慮它的三個鄰居來計算:上、左、對角上和左。圖 2 顯示了這個計算方案。

圖2:如何計算單格的成本

對角
Min(
對角+ 替換成本,
+ 刪除成本,
+ 插入成本
)

例子結果網格如圖 3 如示。右下角單格的成本是 3,是 pzzelpuzzle之間的 Levenshtein 成本。

圖3. Levenshtein 演算法的最後階段

Levenshtein 演算法的屬性

作為額外優點, Levenshtein 演算法還為您提供了一系列操作,也叫做 校準(alignment),它構成了轉換。一對單字通常有不止一次校準。校準對應著沿圖表的箭頭從左上角單格到右下角單格的最小成本路徑。例如, 清單 4表示的校準(在 圖 3中以紅色箭頭表示),可以按照下面的操作順序,一個字母一個字母地讀成:

  • p替換成 p(成本為 0)
  • 插入 u(成本為 1)
  • z替換成 z(成本為 0)
  • z替換成 z(成本為 0)
  • 插入 l(成本為 1)
  • e替換成 e(成本為 0)
  • 刪除 l(成本為 1)

清單4. pzzel 和 puzzle 之間的校準


p-zz-el
puzzle-

Levenshtein 演算法的 Java 實作

清單 5 列出了 Levenshtein 演算法的一個簡單而直觀的 Java 實作。 LevenshteinDistanceMetric 類別有些類似於 Apache Jakarta Commons 專案的 StringUtils 類別。這些實作的限制是:它們不能處理大型字串,因為它們的儲存需求為 O(mn), 其中 mn 分別是原本的單字和目標單字的長度。如果您只需要計算距離,不需要校準,就像通常情況那樣,那麼可以很容易地把空間需求降到 O(n),因為計算下一行只需要前面一行。針對 Apache 版本已經提出了一個修正建議(請參閱 參考資料),但是它在本文寫作的時候還沒有被併進來(2.0版)。

請注意: Levenshtein 演算法的執行時間總是 O(mn)。所以,如果在非常大的字典裡尋找拼寫錯誤的最相近匹配,這個演算法就太慢了。

清單 5. Levenshtein 距離演算法的實作


public class LevenshteinDistanceMetric implements SequenceMetric {
/**
* Calculates the distance between Strings x and y using the
* <b>Dynamic Programming</b> algorithm.
*/
public final int distance(String x, String y) {
int m = x.length();
int n = y.length();
int[][] T = new int[m + 1][n + 1];
T[0][0] = 0;
for (int j = 0; j < n; j++) {
T[0][j + 1] = T[0][j] + ins(y, j);
}
for (int i = 0; i < m; i++) {
T[i + 1][0] = T[i][0] + del(x, i);
for (int j = 0; j < n; j++) {
T[i + 1][j + 1] =  min(
T[i][j] + sub(x, i, y, j),
T[i][j + 1] + del(x, i),
T[i + 1][j] + ins(y, j)
);
}
}
return T[m][n];
}
private int sub(String x, int xi, String y, int yi) {
return x.charAt(xi) == y.charAt(yi) ? 0 : 1;
}
private int ins(String x, int xi) {
return 1;
}
private int del(String x, int xi) {
return 1;
}
private int min(int a, int b, int c) {
return Math.min(Math.min(a, b), c);
}
}
回到頂端

介紹 Jazzy

迄今為止,我已經介紹了兩種拼寫檢查方法:語音匹配和順序比較。由於它們各自都沒有提供完整的解決方案,所以撰寫了一個把它們組合起來的演算法。下面是從 GNU Aspell 手冊中引用的內容:

[Aspell] 背後的秘密來自於整合了 Lawrence Philips 優秀的 metaphone 演算法和 Ispell 的靠近遺漏(near miss)策略,它會插入空格或連字元,交換兩個相鄰字母,改變一個字母,刪除一個字母,或者增加一個字母。

Jazzy 是 GPL/LGPL 協議下的基於 Java 的拼寫檢查器 API,它基於 Aspell 演算法,該演算法最初是用 C++ 撰寫的。

Aspell 演算法和 Jazzy

如果進行拼寫檢查的單字不在字典裡,那麼 Aspell 演算法就會假定它是拼寫錯誤的。在這種情況下,演算法用以下步驟來建立一個經過排序的修正建議列表:

  1. 加入拼寫錯誤靠近的語音匹配:加入字典中所有與拼寫錯誤單字語音撰寫程式相同的單字, 以及與拼寫錯誤單字的編輯距離小於指定閾值的所有單字。
  2. 加入與拼寫錯誤單字的“靠近遺漏”(near miss)接近的語音匹配:加入與拼寫錯誤單字只差一個編輯操作的所有單字的語音程式。對於這些程式,加入字典中所有與拼寫錯誤單字語音撰寫程式相同的單字, 以及 與拼寫錯誤單字的編輯距離小於指定閾值的單字。
  3. 最佳猜測:如果沒有找到建議,就加入字典中所有與拼寫錯誤的單字的語音程式相同的單字, 以及與拼寫錯誤的單字編輯距離最小的單字。
  4. 排序:按照編輯距離排序單字,把每一步驟中找到的單字放在一起。

Aspell 演算法的優勢在於它利用編輯距離的方式,它在單字級別上和語音程式級別上都使用編輯距離。在實務中,這可以形成足夠的模糊匹配,進而為拼寫錯誤單字形成良好的修正建議。

編輯距離說明

在 Jazzy 中使用的編輯距離與以前在 Levenshtein 距離中的定義不同。除了替代、刪除、插入之外,Jazzy 還包括了交換相鄰字母、改變字母大小寫的操作。操作的成本是可設定的。預設的語音撰寫程式方式是 Metaphone,但是也可以使用一個語音轉換規則檔(請參閱 參考資料),檔以表格的方式定義了像 Metaphone 這樣的轉換規則。表格驅動的方式使得可以很容易地把基於 Jazzy 的檢查器設定為支援其他語言。

回到頂端

建立拼寫檢查器

從現在開始,我要把精力放在描述用 Jazzy API 實際建立一個拼寫檢查器上。清單 6 示範了如何用 Jazzy 撰寫一個 Java 拼寫檢查器。

清單6. 一個簡單的拼寫檢查器


import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.List;
import com.swabunga.spell.engine.SpellDictionary;
import com.swabunga.spell.engine.SpellDictionaryHashMap;
import com.swabunga.spell.event.SpellCheckEvent;
import com.swabunga.spell.event.SpellCheckListener;
import com.swabunga.spell.event.SpellChecker;
import com.swabunga.spell.event.StringWordTokenizer;
public class Suggest {
public static class SuggestionListener implements SpellCheckListener {
public void spellingError(SpellCheckEvent event) {
System.out.println("Misspelling: " + event.getInvalidWord());
List suggestions = event.getSuggestions();
if (suggestions.isEmpty()) {
System.out.println("No suggestions found.");
} else {
System.out.print("Suggestions: ");
for (Iterator i = suggestions.iterator(); i.hasNext();) {
System.out.print(i.next());
if (i.hasNext()) {
System.out.print(", ");
}
}
System.out.println();
}
}
}
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.err.println("Usage: Suggest <dictionary file>");
System.exit(1);
}
SpellDictionary dictionary = new SpellDictionaryHashMap(new File(args[0]));
SpellChecker spellChecker = new SpellChecker(dictionary);
spellChecker.addSpellCheckListener(new SuggestionListener());
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.print("Enter line to spell check (return to_u101 ?xit): ");
String line = in.readLine();
if (line.length() == 0) {
break;
}
spellChecker.checkSpelling(new StringWordTokenizer(line));
}
}
}

main() 方法用命令列指定的檔建立了一個 SpellDictionarySpellDictionaryHashMap 實作在記憶體中保存單字,這樣比較快,但是對於大型字典不適合。 (對於容易引起記憶體不足的應用程式,還提供了基於磁片的實作。) SpellDictionary 被用來構造 SpellChecker 物件,在用標準輸入填充之前,先用它註冊 SpellCheckListener 。拼寫檢查器通常內嵌在使用者驅動的應用程式裡,而事件驅動的設計本身就適合這類程式。在這個例子裡,偵聽器( SuggestionListener )只是在接收到 SpellCheckEvent 事件時,向標準輸出寫出拼寫錯誤和建議列表。清單 7 顯示了一個執行範例。

清單7. 用 Jazzy 進行拼寫檢查


Enter line to spell check (return to exit): choklut biskit
Misspelling: choklut
Suggestions: chocolate
Misspelling: biskit
Suggestions: biscuit
Enter line to spell check (return to exit):

這個例子非常簡單,更複雜的應用程式可以利用 Jazzy 對使用者字典管理的支援,執行向字典增加單字、忽略單字、用選中的修正自動替換迭代錯誤拼寫等任務。要獲得詳細資訊,請參閱 SpellCheckEvent (在 參考資料中)的 API 文件。

自动拼写检查---jazzy简介相关推荐

  1. Word如何取消自动拼写检查、自动句首字母大写

    Word如何取消自动拼写检查.自动句首字母大写 用了Word这么久,最讨厌的两个功能就是自动拼写检查和自动句首字母大写.以前是懒去把这功能关了,今天实在忍不住了,下面贴解决方案: 1.Word取消自动 ...

  2. WPS for MacOS如何设置自动拼写检查

    在顶部菜单栏,点击[文件]–>[文字偏好设置]或者点击左上角的[文件]–>[文字偏好设置]:

  3. texstudio自动拼写检测_学用系列|能够自动检查英语拼写的希沃白板5

    希沃白板5更新了,实验室功能更新了"英语拼写/语法纠错",英语老师们可以来尝试哦. 实验室功能哪里找? 希沃白板5最新版提供了"英语拼写/语法纠错"的实验室功能 ...

  4. word@tips官方文档和教程@软件界面介绍@功能区自定义@拼写检查@AI润色改进@ 图片顶部上方插入文字

    文章目录 word 文档和教程 word软件界面元素 字符和标记 格式标记 段落标记(paragraph marks) 自定义功能区(Ribbon) 自定义功能区要点@层次关系 添加自定义选项卡(ta ...

  5. 火狐浏览器使用拼写检查

    1.使用拼写检查 Firefox 可以自动检查您在文本框中输入的包含超过一行的的单词的拼写错误. 只要您输入一个单词,就会和您安装的字典里的单词进行对照,如果字典里没有找到这个单词,这个单词就会标有红 ...

  6. 介绍 Java 平台的 Jazzy:一种新的拼写检查器 API

    计算机擅长执行快速搜索操作,可以根据给定的搜索词,对大量存储的信息快速进行搜索.但是,拼写检查应用程序所要求的搜索能力,不仅仅是正确的字符串匹配.在这篇文章中,我将介绍搜索算法的一些历史,包括语音匹配 ...

  7. Jazzy--一种新的拼写检查器API(Java平台)

    计算机擅长执行快速搜索操作,可以根据给定的搜索词,对大量存储的信息快速进行搜索.但是,拼写检查应用程序所要求的搜索能力,不仅仅是正确的字符串匹配. 在这篇文章中,我将介绍搜索算法的一些历史,包括语音匹 ...

  8. python代码大全p-21行Python代码实现拼写检查器

    引入 大家在使用谷歌或者百度搜索时,输入搜索内容时,谷歌总是能提供非常好的拼写检查,比如你输入 speling,谷歌会马上返回 spelling. 下面是用21行python代码实现的一个简易但是具备 ...

  9. 一起学shell之(九-2)拼写检查、进程

    #----------------------------------------------------------# # ====> 红色字体 -特指煮酒个人所见.加粗则为需要重点注意. # ...

最新文章

  1. 英特尔公布新技术路线图,将为 AWS、高通代工芯片
  2. vue 手写一个时间选择器
  3. sklearn中各算法类的fit,fit_transform和transform函数
  4. [GKCTF 2021]Random
  5. OpenGL球体的Phong渲染
  6. Jsoup消除不受信任的HTML(用于防止XSS的攻击)
  7. 9 PP配置-生产主数据-工作中心相关-定义工序控制码
  8. PHP单次数据库查询实现无限级分类
  9. 自动驾驶——标注工具的开发笔记(legacy)
  10. 苹果软件App上架问题
  11. 推荐两个BAT编辑器
  12. ESP32 开发笔记(三)源码示例 12_IR_Rev_RMT 使用RMT实现红外遥控接收解码(NEC编码)
  13. 贾俊平统计学思维导图- 第十三章 时间序列分析和预测
  14. 【详解】MD5加密真的安全吗?不安全有什么解决办法?
  15. 经验收藏:做网站的一些定律
  16. 基于Xilinx的FPGA下载配置详解及几种电路参考设计
  17. 去除Ubuntu 20桌面图标的快捷方式箭头
  18. 小众APP分享,有兴趣的朋友快来挖宝
  19. 关于麦克风的参数介绍 - 驻极体麦克风(ECM)和硅麦(MEMS)
  20. 台式计算机鼠标不动,台式电脑鼠标不动了怎么办 鼠标不动了怎么用键盘修复 鼠标不亮了也动不了了怎么办...

热门文章

  1. python爬取网易藏宝阁_Python爬虫:爬取阴阳师的式神立绘
  2. 优秀论文教育技术学与现代教育
  3. 在ArcMap中制作点阵地图
  4. 数组与指针的区别与联系
  5. 提升图纸管理规范,图纸管理快速跟踪追溯解决方案
  6. 生产图纸的权限管理,彩虹EDM图纸管理软件:权限管理功能
  7. WebFlux 和 Spring Security 会碰出哪些火花?
  8. kotlin入门学习文档
  9. PS修改透明图片上的文字
  10. PHP 文心千帆API接口对接