Swift 正規表示式完整教程

SunshineBrother發表於2019-07-21

NSRegularExpression

正規表示式,又稱正規表示法、常規表示法。(英語:Regular Expression,在程式碼中常簡寫為regex、regexp或RE),電腦科學的一個概念。正規表示式使用單個字串來描述、匹配一系列符合某個句法規則。在很多文字編輯器裡,正規表示式通常被用來檢索、替換那些符合某個模式的文字。

列舉型別

typedef NS_OPTIONS(NSUInteger, NSRegularExpressionOptions) {
     NSRegularExpressionCaseInsensitive          = 1 << 0,   // 不區分大小寫的
     NSRegularExpressionAllowCommentsAndWhitespace  = 1 << 1,   // 忽略空格和# (註釋符)
     NSRegularExpressionIgnoreMetacharacters        = 1 << 2,   // 整體化
     NSRegularExpressionDotMatchesLineSeparators    = 1 << 3,   // 匹配任何字元,包括行分隔符
     NSRegularExpressionAnchorsMatchLines          = 1 << 4,   // 允許^和$在匹配的開始和結束行
     NSRegularExpressionUseUnixLineSeparators      = 1 << 5,   // (查詢範圍為整個的話無效)
     NSRegularExpressionUseUnicodeWordBoundaries    = 1 << 6    // (查詢範圍為整個的話無效)
     };
複製程式碼
typedef NS_OPTIONS(NSUInteger, NSMatchingOptions)  {
   NSMatchingReportProgress         = 1 << 0, //找到最長的匹配字串後呼叫block回撥
   NSMatchingReportCompletion       = 1 << 1, //找到任何一個匹配串後都回撥一次block
   NSMatchingAnchored               = 1 << 2, //從匹配範圍的開始處進行匹配
   NSMatchingWithTransparentBounds  = 1 << 3, //允許匹配的範圍超出設定的範圍
   NSMatchingWithoutAnchoringBounds = 1 << 4  //禁止^和$自動匹配行還是和結束
     };
複製程式碼

此列舉值只在block方法中用到

 typedef NS_OPTIONS(NSUInteger, NSMatchingFlags) {
   NSMatchingProgress               = 1 << 0, //匹配到最長串是被設定     
   NSMatchingCompleted              = 1 << 1, //全部分配完成後被設定    
   NSMatchingHitEnd                 = 1 << 2, //匹配到設定範圍的末尾時被設定   
   NSMatchingRequiredEnd            = 1 << 3, //當前匹配到的字串在匹配範圍的末尾時被設定     
   NSMatchingInternalError          = 1 << 4  //由於錯誤導致的匹配失敗時被設定
     };
複製程式碼

方法

1. 返回所有匹配結果的集合(適合,從一段字串中提取我們想要匹配的所有資料)
 *  - (NSArray *)matchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range;
 2. 返回正確匹配的個數(通過等於0,來驗證郵箱,電話什麼的,代替NSPredicate)
 *  - (NSUInteger)numberOfMatchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range;
 3. 返回第一個匹配的結果。注意,匹配的結果儲存在  NSTextCheckingResult 型別中
 *  - (NSTextCheckingResult *)firstMatchInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range;
 4. 返回第一個正確匹配結果字串的NSRange
 *  - (NSRange)rangeOfFirstMatchInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range;
 5. block方法
 *  - (void)enumerateMatchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range usingBlock:(void (^)(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop))block;
複製程式碼

替換方法

- (NSString *)stringByReplacingMatchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range withTemplate:(NSString *)templ;
- (NSUInteger)replaceMatchesInString:(NSMutableString *)string options:(NSMatchingOptions)options range:(NSRange)range withTemplate:(NSString *)templ;
- (NSString *)replacementStringForResult:(NSTextCheckingResult *)result inString:(NSString *)string offset:(NSInteger)offset template:(NSString *)templ;
複製程式碼

使用案例

字串的替換

let test = "sdgreihen一個安靜的晚上jlosd一個"
let regex = "一個"
let RE = try NSRegularExpression(pattern: regex, options: .caseInsensitive)
let modified = RE.stringByReplacingMatches(in: test, options: .reportProgress, range: NSRange(location: 0, length: test.count), withTemplate: "是的")
複製程式碼

列印

sdgreihen是的安靜的晚上jlosd是的
複製程式碼

字串的匹配

let test = "sdgreihendfjbhiidfjdbjb"
let regex = "jb"
let RE = try NSRegularExpression(pattern: regex, options: .caseInsensitive)
let matchs = RE.matches(in: test, options: .reportProgress, range: NSRange(location: 0, length: test.count))
print(matchs.count)
複製程式碼

但是有的時候,我們需要匹配的不是準確的字串,是模糊匹配,像檢測手機號,郵箱等等

let test = "1832321108"
let regex = "^1[0-9]{10}$"
let RE = try NSRegularExpression(pattern: regex, options: .caseInsensitive)
let matchs = RE.matches(in: test, options: .reportProgress, range: NSRange(location: 0, length: test.count))
print(matchs.count)
複製程式碼

我們接下來學習一下正規表示式的規則

正規表示式

我們先來寫一個方便測試的工具

/// 正則匹配
///
/// - Parameters:
///   - regex: 匹配規則
///   - validateString: 匹配對test象
/// - Returns: 返回結果
func RegularExpression (regex:String,validateString:String) -> [String]{
    do {
        let regex: NSRegularExpression = try NSRegularExpression(pattern: regex, options: [])
        let matches = regex.matches(in: validateString, options: [], range: NSMakeRange(0, validateString.count))
        
        var data:[String] = Array()
        for item in matches {
            let string = (validateString as NSString).substring(with: item.range)
            data.append(string)
        }
        
        return data
    }
    catch {
        return []
    }
}


/// 字串的替換
///
/// - Parameters:
///   - validateString: 匹配物件
///   - regex: 匹配規則
///   - content: 替換內容
/// - Returns: 結果
func replace(validateString:String,regex:String,content:String) -> String {
    do {
        let RE = try NSRegularExpression(pattern: regex, options: .caseInsensitive)
        let modified = RE.stringByReplacingMatches(in: validateString, options: .reportProgress, range: NSRange(location: 0, length: validateString.count), withTemplate: content)
        return modified
    }
    catch {
        return validateString
    }
   
}

複製程式碼

本章節按照下面順序研究

  • 正規表示式字元匹配攻略
  • 正規表示式位置匹配攻略
  • 正規表示式括號的作用
  • 正規表示式回溯法原理
  • 正規表示式的拆分

第一章、正規表示式字元匹配攻略

正規表示式是匹配模式,要麼匹配字元,要麼匹配位置

  • 1、兩種模糊匹配
  • 2、字元組
  • 3、量詞
  • 4、分支結構

1、兩種模糊匹配

如果正則只有精確匹配是沒多大意義的,比如hello,也只能匹配字串中的hello這個子串

正規表示式之所以強大,是因為其能實現模糊匹配。

而模糊匹配,有兩個方向上的“模糊”:橫向模糊和縱向模糊。

1.1、橫向模糊匹配

橫向模糊指的是,一個正則可匹配的字串的長度不是固定的,可以是多種情況的。

其實現的方式是使用量詞。譬如{m,n},表示連續出現最少m次,最多n次。

比如ab{2,5}c表示匹配這樣一個字串:第一個字元是a,接下來是2到5個字元b,最後是字元c。測試如下:

let regex = "ab{2,5}c"
let validate = "abc abbc abbbc abbbbc abbbbbc abbbbbbc"
let result = RegularExpression(regex: regex, validateString: validate)

//列印結果
["abbc", "abbbc", "abbbbc", "abbbbbc"]
複製程式碼

1.2、縱向模糊匹配

縱向模糊指的是,一個正則匹配的字串,具體到某一位字元時,它可以不是某個確定的字元,可以有多種可能。

其實現的方式是使用字元組。譬如[abc],表示該字元是可以字元abc中的任何一個。

比如a[123]b可以匹配如下三種字串:a1ba2ba3b。測試如下

let regex = "a[123]b"
let validate = "a0b a1b a2b a3b a4b"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)


//列印結果
["a1b", "a2b", "a3b"]
複製程式碼

2、字元組

需要強調的是,雖叫字元組(字元類),但只是其中一個字元。例如[abc],表示匹配一個字元,它可以是abc之一。

  • 1、範圍表示法:如果字元組裡的字元特別多的話,可以使用範圍表示法。比如[123456abcdefGHIJKLM],可以寫成[1-6a-fG-M]。用連字元-來省略和簡寫

  • 2、 排除字元組:縱向模糊匹配,還有一種情形就是,某位字元可以是任何東西,但就不能是"a"、"b"、"c"。此時就是排除字元組(反義字元組)的概念。例如[^abc],表示是一個除"a"、"b"、"c"之外的任意一個字元。字元組的第一位放^(脫字元),表示求反的概念。

2.1、常見的簡寫形式

有了字元組的概念後,一些常見的符號我們也就理解了。因為它們都是系統自帶的簡寫形式

正規表示式 匹配區間 記憶方式
\d [0-9]表示是一位數字 其英文是digit(數字)
\D [^0-9]表示除數字外的任意字元
\w [0-9a-zA-Z_]表示數字、大小寫字母和下劃線 w是word的簡寫,也稱單詞字元
\W [^0-9a-zA-Z_] 非單詞字元
\s [ \t\v\n\r\f]表示空白符,包括空格、水平製表符、垂直製表符、換行符、回車符、換頁符 s是space character的首字母
\S [^ \t\v\n\r\f] 非空白符
. [^\n\r\u2028\u2029]萬用字元,表示幾乎任意字元。換行符、回車符、行分隔符和段分隔符除外

特殊字元

2.2、量詞

量詞也稱重複。掌握{m,n}的準確含義後,只需要記住一些簡寫形式。

  • {m,} 表示至少出現m次
  • {m} 等價於{m,m},表示出現m次
  • ? 等價於{0,1},表示出現或者不出現。記憶方式:問號的意思表示,有嗎?
  • + 等價於{1,},表示出現至少一次。記憶方式:加號是追加的意思,得先有一個,然後才考慮追加。
  • * 等價於{0,},表示出現任意次,有可能不出現。記憶方式:看看天上的星星,可能一顆沒有,可能零散有幾顆,可能數也數不過來。

貪婪匹配:它會盡可能多的匹配。你能給我6個,我就要5個。你能給我3個,我就3要個。反正只要在能力範圍內,越多越好。

惰性匹配:就是儘可能少的匹配:

let regex = "\\d{2,5}"
let validate = "123 1234 12345 123456"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//列印結果
["123", "1234", "12345", "12345"]


---------------------------------
let regex = "\\d{2,5}?"
let validate = "123 1234 12345 123456"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)

//列印結果
["12", "12", "34", "12", "34", "12", "34", "56"]
複製程式碼

通過在量詞後面加個問號就能實現惰性匹配,因此所有惰性匹配情形如下:

{m,n}? {m,}? ?? +? *?

2.3、多選分支

一個模式可以實現橫向和縱向模糊匹配。而多選分支可以支援多個子模式任選其一。

具體形式如下:(p1|p2|p3),其中p1、p2和p3是子模式,用|(管道符)分隔,表示其中任何之一

例如要匹配goodnice可以使用good|nice。測試如下:

let regex = "good|nice"
let validate = "good idea, nice try."
let result = RegularExpression(regex: regex, validateString: validate)
print(result)

//列印結果
["good", "nice"]
複製程式碼

但有個事實我們應該注意,比如我用 good|goodbye,去匹配goodbye字串時,結果是good

let regex = "good|goodbye"
let validate = "goodbye"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)

//列印結果
["good"]

複製程式碼

而把正則改成goodbye|good,結果是

let regex = "goodbye|good"
let validate = "goodbye"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)

//列印結果
["goodbye"]
複製程式碼

也就是說,分支結構也是惰性的,即當前面的匹配上了,後面的就不再嘗試了。

第二章、正規表示式位置匹配攻略

匹配攻略主要是從以下幾個方面介紹

  • 1、什麼是位置?
  • 2、如何匹配位置?

1. 什麼是位置呢

位置是相鄰字元之間的位置。比如,下圖中箭頭所指的地方

Swift 正規表示式完整教程

2. 如何匹配位置呢?

2.1、^$

  • ^(脫字元)匹配開頭,在多行匹配中匹配行開頭
  • $(美元符號)匹配結尾,在多行匹配中匹配行結尾。

比如我們把字串的開頭和結尾用"#"替換

let regex = "^|$"
let validate = "hello"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)

//列印結果
#hello#
複製程式碼

2.2、 \b\B

\b是單詞邊界,具體就是\w\W之間的位置,也包括\w^之間的位置,也包括\w$之間的位置。

let regex = "\\b"
let validate = "[JS] Lesson_01.mp4"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)
//[#JS#] #Lesson_01#.#mp4#
複製程式碼

首先,我們知道,\w是字元組[0-9a-zA-Z_]的簡寫形式,即\w是字母數字或者下劃線的中任何一個字元。而\W是排除字元組[^0-9a-zA-Z_]的簡寫形式,即\W\w以外的任何一個字元。

此時我們可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一個"#",是怎麼來的。

  • 第一個"#",兩邊是"["與"J",是\W和\w之間的位置。
  • 第二個"#",兩邊是"S"與"]",也就是\w和\W之間的位置。
  • 第三個"#",兩邊是空格與"L",也就是\W和\w之間的位置。
  • 第四個"#",兩邊是"1"與".",也就是\w和\W之間的位置。
  • 第五個"#",兩邊是"."與"m",也就是\W和\w之間的位置。
  • 第六個"#",其對應的位置是結尾,但其前面的字元"4"是\w,即\w和$之間的位置。

\B就是\b的反面的意思,非單詞邊界。例如在字串中所有位置中,扣掉\b,剩下的都是\B的。

let regex = "\\B"
let validate = "[JS] Lesson_01.mp4"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)
//#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4
複製程式碼

2.3、(?=p)(?!p)

(?=p),其中p是一個子模式,即p前面的位置

比如(?=l),表示l字元前面的位置,例如:

let regex = "(?=l)"
let validate = "hello"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)

//he#l#lo
複製程式碼

(?!p)就是(?=p)的反面意思

let regex = "(?!l)"
let validate = "hello"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)
複製程式碼

3、案例

數字的千位分隔符表示法

比如把"12345678",變成"12 345 678"。

let regex = "(?=(\\d{3})+$)"
let validate = "12345678"
let result = replace(validateString: validate, regex: regex, content: " ")
print(result)
//12 345 678
複製程式碼

思路:

  • 1、 先把後三位弄出一個空格,使用(?=\d{3}$)
  • 2、因為每三位出現一次空格,所有可以使用量詞+,最終就是(?=(\\d{3})+$)

但是當我們在對123456789切分時,發現最前面多一個空格,此時我們需要不設定開頭,可以使用(?!^)。為了看出來效果,我們使用#來代替空格

let regex = "(?=(\\d{3})+$)"
let validate = "123456789"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)
//#123#456#789

let regex = "(?!^)(?=(\\d{3})+$)"
let validate = "123456789"
let result = replace(validateString: validate, regex: regex, content: "#")
print(result)
//123#456#789
複製程式碼

驗證密碼問題

密碼長度6-12位,由數字、小寫字元和大寫字母組成,但必須至少包括2種字元。

針對這個問題我們可以分步實現

  • 1、密碼長度6-12位,由數字、小寫字元和大寫字母組成。正規表示式為^[0-9A-Za-z]{6,12}$

  • 2、判斷是否包含有某一種字元。要求的必須包含數字,正規表示式為(?=.*[0-9])(?=.*[0-9])表示該位置後面的字元匹配.*[0-9],有任何多個任意字元,後面再跟個數字。翻譯成大白話,就是接下來的字元,必須包含個數字。

  • 3、同時包含具體兩種字元,比如同時包含數字和小寫字母,正規表示式為(?=.*[0-9])(?=.*[a-z])

  • 4、完整的正規表示式為(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$

第三章、正規表示式括號的作用

不管哪門語言中都有括號。正規表示式也是一門語言,而括號的存在使這門語言更為強大。

內容包括:

  • 1、分組和分支結構
  • 2、引用分組
  • 3、反向引用
  • 4、非捕獲分組

1、分組和分支結構

分組

我們知道a+匹配連續出現的“a”,而要匹配連續出現的“ab”時,需要使用(ab)+

其中括號是提供分組功能,使量詞+作用於ab這個整體,測試如下

let regex = "(ab)+"
let validate = "ababa abbb ababab"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["abab", "ab", "ababab"]
複製程式碼

分支結構 而在多選分支結構(p1|p2)中,此處括號的作用也是不言而喻的,提供了子表示式的所有可能。

要匹配如下的字串

I love Swift I love Regular Expression

測試如下

let regex = "^I love (Swift|Regular Expression)$"
let validate = "I love Swift"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["I love Swift"]
複製程式碼

2、引用分組

這個功能好像swift不支援,有可能我沒找到相應方法,有找到相關支援方法的歡迎提出來。

這是括號一個重要的作用,有了它,我們就可以進行資料提取,以及更強大的替換操作。

而要使用它帶來的好處,必須配合使用實現環境的API。

以日期為例。假設格式是yyyy-mm-dd的,我們可以先寫一個簡單的正則

var regex = /\d{4}-\d{2}-\d{2}/;
複製程式碼

然後再修改成括號版的

var regex = /(\d{4})-(\d{2})-(\d{2})/;
複製程式碼

比如提取出年、月、日,可以這麼做:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) ); 
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
複製程式碼

match返回的一個陣列,第一個元素是整體匹配結果,然後是各個分組(括號裡)匹配的內容,然後是匹配下標,最後是輸入的文字。(注意:如果正則是否有修飾符g,match返回的陣列格式是不一樣的)。

另外也可以使用正則物件的exec方法

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( regex.exec(string) ); 
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
複製程式碼

同時,也可以使用建構函式的全域性屬性$1$9來獲取:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";

regex.test(string); // 正則操作即可,例如
//regex.exec(string);
//string.match(regex);

console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
複製程式碼

比如,想把yyyy-mm-dd格式,替換成mm/dd/yyyy怎麼做?

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); 
// => "06/12/2017"
複製程式碼

3、反向引用

除了使用相應API來引用分組,也可以在正則本身裡引用分組。但只能引用之前出現的分組,即反向引用。

還是以日期為例。

比如要寫一個正則支援匹配如下三種格式

2016-06-12 2016/06/12 2016.06.12

最先可能想到的正則是:

var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true
複製程式碼

其中/和.需要轉義。雖然匹配了要求的情況,但也匹配"2016-06/12"這樣的資料。

假設我們想要求分割符前後一致怎麼辦?此時需要使用反向引用:

var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
複製程式碼

注意裡面的\1,表示的引用之前的那個分組(-|\/|\.)。不管它匹配到什麼(比如-),\1都匹配那個同樣的具體某個字元。

我們知道了\1的含義後,那麼\2\3的概念也就理解了,即分別指代第二個和第三個分組

括號巢狀怎麼辦

以左括號(開括號)為準。比如:

var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
複製程式碼

我們可以看看這個正則匹配模式:

  • 第一個字元是數字,比如說1,
  • 第二個字元是數字,比如說2,
  • 第三個字元是數字,比如說3,
  • 接下來的是\1,是第一個分組內容,那麼看第一個開括號對應的分組是什麼,是123,
  • 接下來的是\2,找到第2個開括號,對應的分組,匹配的內容是1,
  • 接下來的是\3,找到第3個開括號,對應的分組,匹配的內容是23,
  • 最後的是\4,找到第3個開括號,對應的分組,匹配的內容是3。

4、 非捕獲分組

之前文中出現的分組,都會捕獲它們匹配到的資料,以便後續引用,因此也稱他們是捕獲型分組。

如果只想要括號最原始的功能,但不會引用它,即,既不在API裡引用,也不在正則裡反向引用。此時可以使用非捕獲分組(?:p),例如本文第一個例子可以修改為:

var regex = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); 
// => ["abab", "ab", "ababab"]
複製程式碼

第四章、正規表示式回溯法原理

學習正規表示式,是需要懂點兒匹配原理的。

而研究匹配原理時,有兩個字出現的頻率比較高:“回溯”。

聽起來挺高大上,確實還有很多人對此不明不白的。

因此,本章就簡單扼要地說清楚回溯到底是什麼東西。

內容包括:

  • 1、沒有回溯的匹配
  • 2、有回溯的匹配
  • 3、常見的回溯形式

1、沒有回溯的匹配

假設我們的正則是ab{1,3}c,其視覺化形式是:

Swift 正規表示式完整教程

而當目標字串是abbbc時,就沒有所謂的“回溯”。其匹配過程是:

Swift 正規表示式完整教程

其中子表示式b{1,3}表示“b”字元連續出現1到3次

2、有回溯的匹配

如果目標字串是"abbc",中間就有回溯。

Swift 正規表示式完整教程

圖中第5步有紅顏色,表示匹配不成功。此時b{1,3}已經匹配到了2個字元“b”,準備嘗試第三個時,結果發現接下來的字元是“c”。那麼就認為b{1,3}就已經匹配完畢。然後狀態又回到之前的狀態(即第6步,與第4步一樣),最後再用子表示式c,去匹配字元“c”。當然,此時整個表示式匹配成功了。圖中的第6步,就是“回溯”。

3、常見的回溯形式

正規表示式匹配字串的這種方式,有個學名,叫回溯法。回溯法也稱試探法,它的基本思想是:從問題的某一種狀態(初始狀態)出發,搜尋從這種狀態出發所能達到的所有“狀態”,當一條路走到“盡頭”的時候(不能再前進),再後退一步或若干步,從另一種可能“狀態”出發,繼續搜尋,直到所有的“路徑”(狀態)都試探過。這種不斷“前進”、不斷“回溯”尋找解的方法,就稱作“回溯法”

本質上就是深度優先搜尋演算法。其中退到之前的某一步這一過程,我們稱為“回溯”。從上面的描述過程中,可以看出,路走不通時,就會發生“回溯”。即,嘗試匹配失敗時,接下來的一步通常就是回溯。

貪婪量詞

之前的例子都是貪婪量詞相關的。比如b{1,3},因為其是貪婪的,嘗試可能的順序是從多往少的方向去嘗試。首先會嘗試"bbb",然後再看整個正則是否能匹配。不能匹配時,吐出一個"b",即在"bb"的基礎上,再繼續嘗試。如果還不行,再吐出一個,再試。如果還不行呢?只能說明匹配失敗了

let regex = "\\d{1,3}"
let validate = "12345"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["123", "45"]
複製程式碼

惰性量詞

惰性量詞就是在貪婪量詞後面加個問號。表示儘可能少的匹配,比如:

let regex = "\\d{1,3}?"
let validate = "12345"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["1", "2", "3", "4", "5"]
複製程式碼

分支結構

我們知道分支也是惰性的,比如/can|candy/,去匹配字串"candy",得到的結果是"can",因為分支會一個一個嘗試,如果前面的滿足了,後面就不會再試驗了。分支結構,可能前面的子模式會形成了區域性匹配,如果接下來表示式整體不匹配時,仍會繼續嘗試剩下的分支。這種嘗試也可以看成一種回溯。比如正則

Swift 正規表示式完整教程

第五章、正規表示式的拆分

對於一門語言的掌握程度怎麼樣,可以有兩個角度來衡量:讀和寫。

不僅要求自己能解決問題,還要看懂別人的解決方案。程式碼是這樣,正規表示式也是這樣。正則這門語言跟其他語言有一點不同,它通常就是一大堆字元,而沒有所謂“語句”的概念。如何能正確地把一大串正則拆分成一塊一塊的,成為了破解“天書”的關鍵。

本章就解決這一問題,內容包括:

  • 1、結構和操作符
  • 2、注意要點
  • 3、案例分析

1、結構和操作符

  • 字面量,匹配一個具體字元,包括不用轉義的和需要轉義的。比如a匹配字元"a"
  • 字元組,匹配一個字元,可以是多種可能之一,比如[0-9],表示匹配一個數字。也有\d的簡寫形式。另外還有反義字元組,表示可以是除了特定字元之外任何一個字元,比如[^0-9],表示一個非數字字元,也有\D的簡寫形式。
  • 量詞,表示一個字元連續出現,比如a{1,3}表示“a”字元連續出現3次。另外還有常見的簡寫形式,比如a+表示“a”字元連續出現至少一次
  • 錨點,匹配一個位置,而不是字元。比如^匹配字串的開頭,又比如\b匹配單詞邊界,又比如(?=\d)表示數字前面的位置。
  • 分組,用括號表示一個整體,比如(ab)+,表示"ab"兩個字元連續出現多次,也可以使用非捕獲分組(?:ab)+
  • 分支,多個子表示式多選一,比如abc|bcd,表示式匹配"abc"或者"bcd"字元子串

這裡,我們來分析一個正則:

ab?(c|de*)+|fg

  • 1、由於括號的存在,所以,(c|de*)是一個整體結構。
  • 2、在(c|de*)中,注意其中的量詞*,因此e*是一個整體結構
  • 3、因為分支結構 |優先順序最低,因此c是一個整體、而de*是另一個整體
  • 4、同理,整個正則分成了 a、b?、(...)+、f、g。而由於分支的原因,又可以分成ab?(c|de*)+fg這兩部分。

2、注意要點

匹配字串整體問題

因為是要匹配整個字串,我們經常會在正則前後中加上錨字元 ^$

比如要匹配目標字串"abc"或者"bcd"時,如果一不小心,就會寫成^abc|bcd$

而位置字元和字元序列優先順序要比豎槓高,這句正則的意思是開始匹配abc或者結尾匹配bcd

let regex = "^abc|bcd$"
let validate = "abc123456"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["abc"]
複製程式碼

正確的寫法應該是^(abc|bcd)$

量詞連綴問題

假設,要匹配這樣的字串:

  1. 每個字元為a、b、c任選其一
  2. 字串的長度是3的倍數

此時正則不能想當然地寫成^[abc]{3}+$

let regex = "^[abc]{3}+$"
let validate = "abcaaa"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//[]
複製程式碼

正確的應該寫成^([abc]{3})+$

let regex = "^([abc]{3})+$"
let validate = "abcaaa"
let result = RegularExpression(regex: regex, validateString: validate)
print(result) 
//["abcaaa"]
複製程式碼

元字元轉義問題

^ $ . * + ? | \ / ( ) [ ] { } = ! : - ,

let regex = "\\^\\$\\.\\*\\+\\?\\|\\\\\\/\\[\\]\\{\\}\\=\\!\\:\\-\\,"
let validate = "^$.*+?|\\/[]{}=!:-,"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["^$.*+?|\\/[]{}=!:-,"]
複製程式碼

需要用\\轉義

匹配“[abc]”和“{3,5}”

let regex = "\\[abc]"
let validate = "[abc]"
let result = RegularExpression(regex: regex, validateString: validate)
print(result)
//["[abc]"]
複製程式碼

只需要在第一個方括號轉義即可,因為後面的方括號構不成字元組,正則不會引發歧義,自然不需要轉義。

文章轉載:JS正規表示式完整教程(略長)

相關文章