小記:為開源專案增加一個新功能的開發歷程

jxlwqq發表於2018-08-23

本篇文章不是為了記開發流水賬,而是想把開發過程的遇到的問題以及解決思路和大家進行交流和學習。我是一名普普通通的 PHP 工程師,希望對初級開發同學有所幫助。具體的心得體會見文末的總結

本月初,我在 GitHub 上開源了一個自己的小專案:chinese-typesetting。這是一個糾正中文文案排版的 Composer 包。

chinese-typesetting 包含以下功能:

  • 在中文與英文字母/用於數學、科學和工程的希臘字母/數字之間新增空格;
  • 有限度的全形轉半形(英文、數字、空格以及一些特殊字元等使用半形字元);
  • 修復錯誤的標點符號;
  • 清除 HTML 標籤的樣式;
  • 清除空的 HTML 標籤;
  • 清除段首縮排;

本週,公司開發事務不多,無加班,於是開始構思新功能糾正英語專有名詞大小寫的實現。

英語專有名詞的資料來源

首先,面臨的第一個問題是:

英語專有名詞的資料從哪來?

我最先想到的是 Python 有一個自然語言處理的包 NLTK,這個包有個名為 pos_tag 的函式,可以用來識別並標註每個單詞的詞性,其中被標註為 NNP 或 NNPS 的單詞就是專有名詞(Proper Noun)。我猜想,NLTK 資料包裡應該有一個對應的專有名詞資料集,但是,苦於能力有限,我一直沒有找到。

上述的路徑走不通後,我又通過 Google 搜尋,發現通過網路字典來獲取資料是一條可行的方案。通過這一方法,終於在 Wiktionary 找到了英語專有名詞列表。於是,利用 Python 寫了一個爬蟲小指令碼,爬取了對應的資料。

最後,就是對爬取到的資料進行了一些整理和篩選。

篩選方案如下:

  • 使用 is_numeric() 方法,剔除諸如 007 等詞彙;
  • 使用 '/\W/' 正則,剔除諸如 ǃXóõ 等詞彙;
  • 剔除 strlen 方法,剔除 A 等單位元組詞彙;
  • 剔除跟 HTML、CSS、JavaScript 保留字衝突的詞彙;

如何讓使用者定製專有名詞資料

最初的程式碼如下:

/**
 * 專有名詞使用正確的大小寫
 * Correct English proper nouns.
 *
 * @param $text
 *
 * @return null|string|string[]
 */
public function properNoun($text)
{
    $dict = include __DIR__ . '/../data/dict.php';
    foreach ($dict as $noun) {
        $text = preg_replace("/\b{$noun}\b/i", $noun, $text);
    }
    return $text;
}

之後想到,如果使用這個方法的開發者想擴充套件或者忽略某些專有名詞,那該怎麼辦呢?
於是,我又將 properNoun() 方法改造如下:

/**
 * 專有名詞使用正確的大小寫
 * Correct English proper nouns.
 *
 * @param $text
 * @param array $extend
 * @param array $ignore
 *
 * @return null|string|string[]
 */
public function properNoun($text, array $extend = [], array $ignore = [])
{
    $dict = include __DIR__ . '/../data/dict.php';
    if ($extend) {
        $dict = array_merge($dict, $extend);
    }
    if ($ignore) {
        $dict = array_diff($dict, $ignore);
    }

    foreach ($dict as $noun) {
        $text = preg_replace("/\b{$noun}\b/i", $noun, $text);
    }
    return $text;
}

如何改進和優化程式碼邏輯

我在寫這個功能的時候,也在研究和參考一些現有開源專案的實現邏輯。在看到開源專案 auto-correct 的一個 commit 上後(PS:這個 PR 是社群大神 overtrue 提交的。),我又將 properNoun() 方法改造如下:

public function properNoun($text, array $extend = [], array $ignore = [])
{
    $dict = include __DIR__ . '/../data/dict.php';
    if ($extend) {
        $dict = array_merge($dict, $extend);
    }
    if ($ignore) {
        $dict = array_diff($dict, $ignore);
    }
    foreach ($dict as $noun) {
        $text = preg_replace("/(?<!\.|[a-z]){$noun}(?!\.|[a-z])/i", $noun, $text);
    }
    return $text;
}

如何避免過度替換

在我以為就要大功告成的時候,我用之前寫好的 PHPUnit 單元測試程式碼進行了測試,結果報出了錯誤,在上述方法中,如果傳入的引數是包含 HTML 標籤的富文字,那麼 HTML 的元素、元素屬性以及值都有可能會被替換。

如何避免過度替換這個問題呢?也就是說:

只替換文字,而忽略 HTML 標籤及標籤內部的內容?

我嘗試寫了好幾套匹配方案,都失敗了。最後還是請出了 Google 大神來幫忙。這裡,搜尋的關鍵字很重要,最好想把你要搜尋的關鍵詞翻譯成對應的英文單詞,這樣搜尋出的結果會令你更滿意。結果我找到了解決方案:Matching A Word / Characters Outside Of Html Tags

通過上面這部文章的提示,我又將 properNoun() 方法改造如下:

public function properNoun($text, array $extend = [], array $ignore = [])
{
    $dict = include __DIR__ . '/../data/dict.php';
    if ($extend) {
        $dict = array_merge($dict, $extend);
    }
    if ($ignore) {
        $dict = array_diff($dict, $ignore);
    }
    foreach ($dict as $noun) {
        // Matching proper nouns Outside Of Html Tags
        $text = preg_replace("/(?<!\.|[a-z]){$noun}(?!\.|[a-z])(?!([^<]+)?>)/i", $noun, $text);
    }
    return $text;
}

開發總結

  • 學會科學上網;
  • 善用 Google、Github 和 StackOverflow,這三樣“神器”會幫你解決掉開發過程中遇到的絕大部分(或者說所有)問題;
  • 學會一些 Google 搜尋小技巧。例如將搜尋關鍵字翻譯成英語單詞,這樣的搜尋結果會令你更滿意;
  • 英語真的很重要。最起碼你應該在 Chrome 瀏覽器上安裝一個 Google 翻譯 的外掛;
  • PHPUnit 真的很有用,特別是在頻繁增改功能或者需要程式碼重構的專案中;
  • 不要讓自己僅限於一個程式語言,學習另外一門或多門語言作為輔助,有益於擴充思路和開拓眼界;
  • 多逛逛 Laravel China 這樣的高品質社群;

最後的話

歡迎 Star,專案地址:https://github.com/jxlwqq/chinese-typesetting

相關文章