1. 程式人生 > >不到40行程式碼構建正則表示式引擎

不到40行程式碼構建正則表示式引擎

譯者注:如何用不到40行的程式碼構建一個正則表示式引擎?作者在本文就將他本人的解決思路記錄了下來,如果你也想挑戰,不妨借鑑一下作者的思路,說不定你寫的程式碼可能不到30行。以下為譯文。

無意之間我發現了一篇文章,Rob Pike用C語言實現了一個正則表示式引擎的模型。於是我也嘗試用Javascript寫一個,並且增加了測試規範。測試規範和解決方案都放在了GitHub倉庫上面。本文將重點介紹解決方案。

問題描述

正則表示式引擎將支援以下語法:

最終目標是用最少的程式碼提供最強大的功能,從而滿足上述正則表示式用例。

單字元匹配

第一步是編寫一個函式,該函式有兩個入參,返回值是一個布林型別,表示匹配結果。.

表示通配模式,可以匹配任意字元。

簡單舉一些用例:

matchOne('a', 'a') -> true

matchOne('.', 'z') -> true

matchOne('', 'h') -> true

matchOne('a', 'b') -> false

matchOne('p', '') -> false

function matchOne(pattern, text) {
  if (!pattern) return true // Any text matches an empty pattern
  if (!text) return false   // If the pattern is defined but the text is empty, there cannot be a match
  if (pattern === ".") return true // Any inputted text matches the wildcard
  return pattern === text
}

相同長度的字串匹配

現在需要增加引數的長度,並且暫時只考慮pattern和string長度相同的情況。根據以前的經驗,我很自然的認為這種情況非常適合用遞迴來解決。 我們只需要反覆呼叫matchOne函式就可以了。

function match(pattern, text) {
  if (pattern === "") return true  // Our base case - if the pattern is empty, any inputted text is a match
  else return matchOne(pattern[0], text[0]) && match(pattern.slice(1), text.slice(1))
}

上面的程式碼首先將pattern[0]text[0]進行比較,然後將pattern[1]text[1]進行比較,並繼續將pattern[i]text[i]進行比較,直到i === pattern.length - 1。如果在某個地方沒有匹配成功,那麼最終返回的結果就是匹配失敗。

我們來舉個例子。假設呼叫match('a.c','abc'),實際上返回的就是matchOne('a','a')&& match('.c','bc')

如果繼續分析下去,其實最終的結果就是matchOne('a','a')&& matchOne('.','b')&& matchOne('c','c')&& match("","") ,這就相當於true && true && true && true,所以返回結果就是true!

$字元

接下來增加特殊字元$的支援,它可以匹配字串後面的所有字元。要想實現該功能,只需要在上一步的match函式中增加一個額外基本情況的判斷就可以了。

function match(pattern, text) {
  if (pattern === "") return true
  if (pattern === "$" && text === "") return true
  else return matchOne(pattern[0], text[0]) && match(pattern.slice(1), text.slice(1))
}

^字元

讓我們新增對特殊模式字元^的支援,它允許匹配字串的開頭。這裡我將介紹一個新的函式–search

function search(pattern, text) {
  if (pattern[0] === "^") {
    return match(pattern.slice(1), text)
  }
}

這個函式將成為程式碼的新入口。到目前為止只是在文字開始時才開始匹配。現在只是通過強迫使用者以^來開始。但是如何支援文字中出現的任何模式呢?

任意位置的匹配

截止到目前為止,下面的表示式將會返回true

search("^abc", "abc")

search("^abcd", "abcd")

但是search("bc", "abcd")返回的卻是undefined。我們期望讓它返回true

如果使用者沒有指明要從第一個字元開始就要匹配,那麼我們希望在文字內的每個可能的起始點進行搜尋。這是預設的處理規則,除非pattern是以^開始。

function search(pattern, text) {
  if (pattern[0] === "^") {
      return match(pattern.slice(1), text)
  } else {
      // This code will run match(pattern, text.slice(index)) on every index of the text.
      // This means that we test the pattern against every starting point of the text.
      return text.split("").some((_, index) => {
      return match(pattern, text.slice(index))
    })
  }
}

?字元

使用?的話,那麼在?前面的0個或者1個字元可以進行匹配。

這裡有一些範例:

search("ab?c", "ac") -> true

search("ab?c", "abc") -> true

search("a?b?c?", "abc") -> true

search("a?b?c?", "") -> true

第一步是修改match函式,當檢測到?字元出現以後就開始呼叫matchQuestion函式,matchQuestion函式的定義將會在下面的內容看到。

function match(pattern, text) {
  if (pattern === "") {
    return true
  } else if (pattern === "$" && text === "") {
    return true
  // Notice that we are looking at pattern[1] instead of pattern[0].
  // pattern[0] is the character to match 0 or 1 of.
  } else if (pattern[1] === "?") {
    return matchQuestion(pattern, text)
  } else {
    return matchOne(pattern[0], text[0]) && match(pattern.slice(1), text.slice(1))
  }
}

matchQuestion函式需要處理兩種情況:

  1. ?之前的字元沒有匹配成功,但是?後面所有的文字都和pattern的剩餘部分匹配成功了;
  2. ?之前的字元都匹配成功了,並且其餘的文字(這裡應該減去一個字元)也和pattern的剩餘部分匹配成功了;

上面兩種情況中只要滿足一種,那麼matchQuestion函式就會返回true

讓我們先考慮第一種情況。如果pattern中的文字除了_?不一樣以外,其它都能匹配成功,這種情況我們怎麼去檢查呢?換句話說,如果?前面的字元只出現了0次,這種情況我們怎麼去檢查呢?我們從pattern剔除掉2個字元(第一個字元就是?前面的哪個,第二個字元就是?本身),然後再呼叫match函式。

function matchQuestion(pattern, text) {
  return match(pattern.slice(2), text);
}

第二種情況更具挑戰性,但是和上面介紹的一樣,它還是重用了之前已經寫好的函式。

function matchQuestion(pattern, text) {
  if (matchOne(pattern[0], text[0]) && match(pattern.slice(2), text.slice(1))) {
    return true;
  } else {
    return match(pattern.slice(2), text);
  }
}

如果text[0]pattern[0]匹配上了,而且其它的文字和pattern中剩餘的也能匹配上,那麼我們就成功了。注意,程式碼我們也可以這麼寫:

function matchQuestion(pattern, text) {
  return (matchOne(pattern[0], text[0]) && match(pattern.slice(2), text.slice(1))) || match(pattern.slice(2), text);
}

我更喜歡後面一個方法的原因是因為它明確地指出了有兩種情況,只要滿足其中一種,那麼返回的結果就是true

*字元

我們希望能夠匹配*前面0個或多個字元。

下面這些表示式的返回結果都應該是true

search("a*", "")

search("a*", "aaaaaaa")

search("a*b", "aaaaaaab")

這個跟?的情況很相似,我們在match函式裡面再增加一個matchStar方法。

function match(pattern, text) {
  if (pattern === "") {
    return true
  } else if (pattern === "$" && text === "") {
    return true
  } else if (pattern[1] === "?") {
    return matchQuestion(pattern, text)
  } else if (pattern[1] === "*") {
    return matchStar(pattern, text)
  } else {
    return matchOne(pattern[0], text[0]) && match(pattern.slice(1), text.slice(1))
  }
}

matchStarmatchQuestion一樣,也要處理兩種情況:

  1. *前面的部分沒有匹配成功,但是其它文字和pattern中*後面的都匹配成功了;
  2. *前面的部分匹配成功了,並且其它文字和pattern中*後面的也都匹配成功了;

由於這兩種情況都能決定匹配的結果,因此我們知道matchStar可以用布林型別OR來實現。此外,matchStar的情況1與matchQuestion的情況1完全相同,因此同樣也可以使用match(pattern.slice(2),text)進行實現。這意味著我們只需要制定滿足情況2的表示式。

function matchStar(pattern, text) {
  return (matchOne(pattern[0], text[0]) && match(pattern, text.slice(1))) || match(pattern.slice(2), text);
}

重構

現在我們可以回過頭來,對search函式進行簡化,而且正好可以將我從Peter Norvig寫的裡面學到的一個技巧應用上。

function search(pattern, text) {
  if (pattern[0] === "^") {
    return match(pattern.slice(1), text)
  } else {
    return match(".*" + pattern, text)
  }
}

我們使用*字元本身來允許pattern中字串可以出現在任何地方。前面的.*表示在pattern前面出現了任何數量的任何字元,我們也希望能匹配成功。

結論

功能如此強大,但是程式碼卻如此簡潔明瞭,這真是一件很了不起的事情。完整的原始碼可以再GitHub倉庫中找到。

相關推薦

40程式碼構建表示式引擎

譯者注:如何用不到40行的程式碼構建一個正則表示式引擎?作者在本文就將他本人的解決思路記錄了下來,如果你也想挑戰,不妨借鑑一下作者的思路,說不定你寫的程式碼可能不到30行。以下為譯文。 無意之間我發現了一篇文章,Rob Pike用C語言實現了一個正則表示式引擎的模型。於是我

1000程式碼徒手寫表示式引擎【1】--JAVA中表示式的使用

簡介: 本文是系列部落格的第一篇,主要講解和分析正則表示式規則以及JAVA中原生正則表示式引擎的使用。在後續的文章中會涉及基於NFA的正則表示式引擎內部的工作原理,並在此基礎上用1000行左右的JAVA程式碼,實現一個支援常用功能的正則表示式引擎。它支援貪婪匹配和懶惰匹配;支援零寬度字元(如“\b”, “\B

驗證URL連結和IP有效性的JS程式碼表示式

#js驗證一個URl字串是否有效 function isValidURL(url){ var urlRegExp=/^((https|http|ftp|rtsp|mms)?:\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+

表示式引擎構建——基於編譯原理DFA(龍書第三章)——2 構造抽象語法樹

簡要介紹     構造抽象語法樹是構造基於DFA的正則表示式引擎的第一步。目前在我實現的這個正則表示式的雛形中,正則表示式的運算子有3種,表示選擇的|運算子,表示星號運算的*運算子,表示連線的運算子cat(在實際正則表示式中被省去)。 例如對於正則表示式a*b|c,在a*

C#後臺程式碼使用表示式判斷是否符合要求

C#後臺使用正則表示式: string pattern = @"^[0-9a-zA-Z_]{1,10}$";//字母數字下劃線,1到10位 bool result = false; if (!string.IsNullO

批量快速修改程式碼表示式替換

[\W]*?X 跨行匹配任意字元到X字元結束 $1 為匹配到的(xxx)變數 " /** * XXXX */ private" 替換為: " @ApiModelPropertyvalue = "XXXX" p

Linux表示式引擎(BRE ERE)支援的一些表達形式(Part.I BRE)

BRE(basic regular expression):以sed為例 純文字 :echo "Happy New Year" | sed -n '/Happy/p' 錨字元 : 匹配在行首 :echo "Happy New Year" | sed -n '/^

表示式引擎測試筆記

判斷是否是傳統型NFA 用nfa|nfa not 來匹配 “nfa not” 字串 1. 如果只有 nfa 匹配 ,則是傳統型nfa。 2. 如果整個nfa not 都能匹配,則要麼是POSIX NFA ,要麼是 DFA。 是DFA還是POSIX NFA

re2表示式引擎學習(五)

改寫為DFA匹配時的執行過程。 首先打印出來的是NFA的結構,然後將NFA的結構轉化為DFA的結構,構建對應的DFA轉移矩陣。然後根據轉移矩陣進行匹配 執行時,正則表示式為ab*c|d,匹配的字串為d ab*c|d 9. alt -> 6 | 8 6. alt -&

實現一個表示式引擎in Python(一)

前言 專案地址:Regex in Python 開學摸魚了幾個禮拜,最近幾天用Python造了一個正則表示式引擎的輪子,在這裡記錄分享一下。 實現目標 實現了所有基本語法 st = 'AS342abcdefg234aaaaabccccczczxczcasdzxc' pattern = '

實現一個表示式引擎in Python(二)

專案地址:Regex in Python 在看一下之前正則的語法的 BNF 正規化 group ::= ("(" expr ")")* expr ::= factor_conn ("|" factor_conn)* factor_conn ::= f

實現一個表示式引擎in Python(三)

專案地址:Regex in Python 前兩篇已經完成的寫了一個基於NFA的正則表示式引擎了,下面要做的就是更近一步,把NFA轉換為DFA,並對DFA最小化 DFA的定義 對於NFA轉換為DFA的演算法,主要就是將NFA中可以狀態節點進行合併,進而讓狀態節點對於一個輸入字元都有唯一的一個跳轉節點 所以對於D

到1000表示式程式碼分析07

不到1000行的正則表示式程式碼分析07 早晨先翻開ruby0.49下的regex.c,發現還是頭大,因為太長了,而且邏輯太複雜,比oz的複雜了不止一個數量級。於是仍舊回到oz的grep.c下的正則引擎原始碼。 昨天在睡覺時,一直在想,grep.c的正則引警是NFA,因為匹配時是正則表示式作主導,而

python表示式大作業之模擬計算器(29程式碼)

今天很開心,完成了一項艱鉅的作業,剛開始見到這個作業時我是有些懵逼的,一心想著用findall精準匹配,但是發現匹配後無法處理資料,後來看了點兒老師的思路——用search一個一個地匹配然後替換,然後

表示式(十五)——統計程式碼中的程式碼、註釋和空白行

package com.wy.regular; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFo

MyEclipse去除網上覆制下來的程式碼帶有的號(使用表示式

一、正則表示式去除程式碼行號 作為開發人員,我們經常從網上覆制一些程式碼,有些時候複製的程式碼前面是帶有行號,如: MyEclipse本身自帶有查詢替換功能,並且支援正則表示式替換,使用正則替換就可以很容易去除這些行號 使用快捷鍵“ctrl+F”開啟MyEclipse的查詢替換功能,如

這20個表示式,讓你少寫1,000程式碼

正則表示式——古老而又強大的文字處理工具。僅用一段簡短的表示式語句,就能快速地實現一個複雜的業務邏輯。掌握正則表示式,讓你的開發效率有一個質的飛躍。 正則表示式經常被用於欄位或任意字串的校驗,比如下面這段校驗基本日期格式的JavaScript程式碼:

VS2013 用表示式統計程式碼

公司 軟體申請 著作權 這鬼東西,所以在網上找了下統計方法,網上的正則表示式 (^:b*[^:b#/]+.*$)似乎不行,查了原因 去掉了了 : 號的匹配可行,具體如下: ^b*[^:b#/]+.*$

到1000表示式原始碼分析06

不到1000行的正則表示式原始碼分析06 今天想談NFA與DFA的區別,對程式碼來說。 我喜歡購書,只要是經典的書,就買了。但當時也許看不懂。記得,《設計模式》一出來時,就購買了。可總是看不懂。直到看了程傑寫的《大話設計模式》才基本看懂。 其實,學正則表示式,有本經典書《精通正則表示式》也是經典,買