怎樣寫一個能夠校驗複雜規則的正則表達式?

寫在前面

每當提到正則表達式,身邊很多朋友的反應是這樣的:

“那玩意兒最好不要用,沒法維護!”,

“太複雜,不常用,學不來” ....

不可否認,正則的表達形式,讓人看起來的確有些難以理解,如果某天你接手維護一段代碼,當你滿懷好奇地打開代碼文件,發現裡面很多代碼長成這個樣子:

let funCallRegExp = /(^(\s+)?(((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\(((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?)(,((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?))*\)))(\s+)?(,(\s+)?(((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\(((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?)(,((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?))*\)))(\s+)?)*)$/gm;
怎樣寫一個能夠校驗複雜規則的正則表達式?

顯然,這樣的代碼是無法維護的。但是我可以從中揣測出來,它一定是做了一個比較複雜的驗證。其實對於這種使用正則做複雜校驗的情況,並不等同於洪水猛獸,也是有跡可循的,下面我就給出一種情況,並給出解決方案,希望可以拋磚引玉。

問題來了

現在有一種類似於函數調用的語法,這種調用大概是這樣的:

sum(param1, param2), avg(score)

下面列出了接近30多個case來描述對這個語法的各種限制:

describe("validate", () => {
 it("不同方法的調用用逗號分隔,逗號兩側可用空格分隔,也可不分隔", () => {
 expect(validate("sum(param1),avg(score)")).toBe(true);
 expect(validate("sum(param1) ,avg(score)")).toBe(true);
 expect(validate("sum(param1) , avg(score)")).toBe(true);
 expect(validate("sum(param1) avg(score)")).toBe(false);
 expect(validate("sum(param1)avg(score)")).toBe(false);
 });
 it("整個語句開頭或末尾不能出現逗號或無關文字(空格除外)", () => {
 expect(validate(" sum(param1),avg(b)")).toBe(true);
 expect(validate(" sum(param1),avg(b) ")).toBe(true);
 expect(validate("sum(param1),avg(b) ")).toBe(true);
 expect(validate("a sum(test),avg(a)")).toBe(false);
 expect(validate(" sum(param1),avg(b)12")).toBe(false);
 expect(validate("sdf sum(param1),avg(b) sdf")).toBe(false);
 });
 it("函數名&參數:字母或者下劃線打頭,可由字母數字下劃線組成", () => {
 expect(validate("sum(a)")).toBe(true);
 expect(validate("sum(a0)")).toBe(true);
 expect(validate("_sum(a0)")).toBe(true);
 expect(validate("_sum0(_a0)")).toBe(true);
 expect(validate("_sum0(_)")).toBe(true);
 expect(validate("_sum0(1)")).toBe(false);
 expect(validate("_sum0(1a)")).toBe(false);
 expect(validate("2aa(a)")).toBe(false);
 });
 it("函數名&參數:不能包含保留字 class || function", () => {
 expect(validate("afunctiona(a,b)")).toBe(true);
 expect(validate("functiona(a,b)")).toBe(true);
 expect(validate("class(a), a(a,b)")).toBe(false);
 expect(validate("function(a,b)")).toBe(false);
 expect(validate("function(a,b)")).toBe(false);
 expect(validate("sum(a,function)")).toBe(false);
 expect(validate("sum(class,b)")).toBe(false);
 expect(validate("class1(a,b)")).toBe(true);
 expect(validate("function1(a,b)")).toBe(true);
 });
 it("參數間可以存在空格", () => {
 expect(validate("sum(a ,b ,c)")).toBe(true);
 expect(validate("sum( a , b ,c)")).toBe(true);
 expect(validate("sum( a , b ,c) , avg(d,e , fff)")).toBe(true);
 });
 it("函數調用可以不傳參數", () => {
 expect(validate("sum()")).toBe(true);
 expect(validate("sum( )")).toBe(true);
 });
});

解決方案

越是複雜的問題,越需要對這個問題進行拆解,將它轉換為一系列簡單的子問題,這些簡單的子問題,我們可以很容易的使用正則給出解決方案,然後將這些方案逐一組合起來,也就形成了最終方案。

在這裡,可以把我們需要驗證的內容拆成一下的部分:

0 . 空格 = 可以出現0個或多個空格

1 . 參數 = 由字母或下劃線開頭,字母數字或下劃線組成

2 . 無保留字參數 = 參數 && 不包含關鍵詞

3 . 可包含空格參數 = 無保留字參數 && 參數頭尾可以出現0個或多個空格

4 . 參數集 = 多個參數的組合,參數可以是 1 個或多個

5 . 參數塊 = 參數集 + 左右括號 ,可以沒有參數集,此時括號內為空,但可以出現0個或多個空格 eg . sum() or sum( )

6 . 函數名 = 無保留字參數

7 . 函數調用 = 函數名+參數塊

8 . 可包含空格的函數調用 = 函數調用 && 函數調用頭尾可以出現0個或多個空格

9 . 多函數調用 = 多個函數調用的組合,函數調用可出現1個或多個

10. 最終結果 = 多函數調用 && 頭尾不包含除空格外的其它內容

有了以上的的思路,驗證方案就已經呼之欲出了,貼出代碼:

function validate(inputStr) {
 // 0. 空格 = 可以出現0個或多個空格
 const spaceOrEmpty = ` *`; // 匹配多個空格或空
 // 1. 參數 = 由字母或下劃線開頭,字母數字或下劃線組成
 const param = `[a-zA-Z_][a-zA-Z0-9_]*`;
 // 2. 無保留字參數 = 參數 && 不包含關鍵詞
 const paramWithoutReservedWords = `(?!\\bclass\\b|\\bfunction\\b)(${param})`;
 // 3. 可包含空格參數 = 無保留字參數 && 參數頭尾可以出現0個或多個空格
 const paramWithSpaceWithoutReservedWords = `${spaceOrEmpty}${paramWithoutReservedWords}${spaceOrEmpty}`;
 // 4. 參數集 = 多個參數的組合,參數可以是 1 個或多個
 const params = `(${paramWithSpaceWithoutReservedWords}(,${paramWithSpaceWithoutReservedWords})*)`;
 // 5. 參數塊 = 參數集 + 左右括號 ,可以沒有參數集,此時括號內為空,但可以出現0個或多個空格 eg . sum() or sum( )
 const paramsBlock = `\\(${params}?${spaceOrEmpty}\\)`;
 // 6. 函數名 = 無保留字參數
 const funName = paramWithSpaceWithoutReservedWords;
 // 7. 函數調用 = 函數名+參數塊
 const funCall = `${funName}${paramsBlock}`;
 // 8. 可包含空格的函數調用 = 函數調用 && 函數調用頭尾可以出現0個或多個空格
 const funCallWithSpace = `${spaceOrEmpty}${funCall}${spaceOrEmpty}`;
 // 9. 多函數調用 = 多個函數調用的組合,函數調用可出現1個或多個
 const multipleFunCall = `${funCallWithSpace}(,${funCallWithSpace})*`;
 // 10.最終結果 = 多函數調用 && 頭尾不包含除空格外的其它內容
 const finalFunCall = `^${multipleFunCall}怎樣寫一個能夠校驗複雜規則的正則表達式?


分享到:


相關文章: