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]
,表示該字元是可以字元a
、b
、c
中的任何一個。
比如a[123]b
可以匹配如下三種字串:a1b
、a2b
、a3b
。測試如下
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]
,表示匹配一個字元,它可以是a
、b
、c
之一。
-
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是子模式,用|
(管道符)分隔,表示其中任何之一
例如要匹配good
和nice
可以使用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. 什麼是位置呢
位置是相鄰字元之間的位置。比如,下圖中箭頭所指的地方
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
,其視覺化形式是:
而當目標字串是abbbc
時,就沒有所謂的“回溯”。其匹配過程是:
其中子表示式b{1,3}
表示“b”字元連續出現1到3次
2、有回溯的匹配
如果目標字串是"abbc",中間就有回溯。
圖中第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",因為分支會一個一個嘗試,如果前面的滿足了,後面就不會再試驗了。分支結構,可能前面的子模式會形成了區域性匹配,如果接下來表示式整體不匹配時,仍會繼續嘗試剩下的分支。這種嘗試也可以看成一種回溯。比如正則
第五章、正規表示式的拆分
對於一門語言的掌握程度怎麼樣,可以有兩個角度來衡量:讀和寫。
不僅要求自己能解決問題,還要看懂別人的解決方案。程式碼是這樣,正規表示式也是這樣。正則這門語言跟其他語言有一點不同,它通常就是一大堆字元,而沒有所謂“語句”的概念。如何能正確地把一大串正則拆分成一塊一塊的,成為了破解“天書”的關鍵。
本章就解決這一問題,內容包括:
- 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)$
量詞連綴問題
假設,要匹配這樣的字串:
- 每個字元為a、b、c任選其一
- 字串的長度是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正規表示式完整教程(略長)