前言
最近工作中涉及到一些使用正則表達(dá)式的地方,雖然以前也遇到過這種情況,但是我的處理方式一般都是百度一下...看一下網(wǎng)上有成型的解決方式就拿來直接用了雪情。從來沒有認(rèn)真的系統(tǒng)的了解正則表達(dá)式的語法結(jié)構(gòu)和相關(guān)知識状婶,這次在百度查詢的時候偶然間看到 deerchao 大神寫的關(guān)于正則表達(dá)式的一系列內(nèi)容,讓自己對這部分內(nèi)容有了一個相對比較全面的了解伏蚊,希望用一篇筆記整理一下自己的理解立轧,并供以后查閱,當(dāng)然如果能夠幫助到看到這篇文章的你躏吊,我會感到不勝榮幸氛改。鑒于個人水平有限,文中若有錯誤比伏,請您諒解并不吝指正胜卤。
正則表達(dá)式是什么玩意?
其實當(dāng)我們初步開始接觸一門語言或者開發(fā)技術(shù)的時候赁项,我們都無可避免的了解一些關(guān)于字符串處理的相關(guān)方法或者函數(shù)葛躏,比如 OC 中的 NSString 相關(guān)的方法:
NSString* str = [NSString stringWithFormat:@"%@", userId];
以及 Java 中的 string 相關(guān)的方法:
str.trim();
一般情況下,當(dāng)然我是說我 =悠菜。= 舰攒,這個時候我們就會認(rèn)為關(guān)于字符串處理的東西我們了解的夠多了,系統(tǒng)提供的 API 已經(jīng)足夠應(yīng)付我們遇到的所有問題了悔醋,然而在實際的開發(fā)工作中我們會經(jīng)常遇到的場景是去查找符合某些復(fù)雜規(guī)則的字符串的需要摩窃,或者從一個字符串中提取出符合某些 規(guī)則 的子串,這個時候僅僅依靠系統(tǒng)的 API 是很難完成這個任務(wù)(最起碼付出的時間成本很高)篙顺,需要配合使用的 規(guī)則 便是我們今天要聊的 正則表達(dá)式 偶芍。
在編寫處理字符串的程序或網(wǎng)頁時,經(jīng)常會有查找符合某些復(fù)雜規(guī)則的字符串的需要德玫。正則表達(dá)式就是用于描述這些規(guī)則的工具匪蟀。換句話說,正則表達(dá)式就是記錄文本規(guī)則的代碼宰僧。
正則表達(dá)式的語法
其實最簡單的正則匹配我們都會材彪,笨蛋其實就是‘相等’嘛,比如說:我們想要在一個長長的字符串中將 hello 提取出來,第一種方法不需要使用正則段化,我們可以遍歷字符串嘁捷,截取這個 range 下的子串,并一一與"hello"進(jìn)行對比显熏,相同的就取出來雄嚣;還有另外的方法就是使用正則表達(dá)式進(jìn)行匹配截取,當(dāng)然這個時候的正則表達(dá)式就是最簡單的正則喘蟆,使用hello
就可以了缓升。
當(dāng)然,我們在開發(fā)中遇到的字符串匹配的場景并不會永遠(yuǎn)這樣簡單蕴轨,比如說:還是剛剛的那個場景港谊,只不過我需要提取的是 hello 這個單詞,那么剛剛的使用正則的匹配就會將 ahellob 當(dāng)中的 hello 也會匹配出來橙弱,而這并不是我所希望歧寺,這個時候我們就需要學(xué)習(xí)以下的一些內(nèi)容。
元字符
元字符是幫助我們在書寫正則表達(dá)式的時候棘脐,代表一個位置或者一個(或多個)字符而存在的斜筐,比如回到上面的例子,我需要精確的獲得 hello 這個單詞荆残,并且保證我獲得的 hello 不是在其他的單詞當(dāng)中奴艾,不難分析的是,我需要一個玩意來讓我告訴正則匹配内斯,h 的前面是單詞的開始并且 o 的后面是單詞的結(jié)束蕴潦,這個時候我們需要用到\\b
,相應(yīng)的正則表達(dá)式變?yōu)?code>\\bhello\\b俘闯,\\b
是代表 匹配單詞的開始或結(jié)束 的元字符潭苞。
我們常見的元字符主要包括:
代碼 | 元字符說明 |
---|---|
. | 匹配除換行符以外的任意字符 |
\w | 匹配字母或數(shù)字或下劃線或漢字 |
\s | 匹配任意的空白符 |
\d | 匹配數(shù)字 |
\b | 匹配單詞的開始或結(jié)束 |
^ | 匹配字符串的開始 |
$ | 匹配字符串的結(jié)束 |
重復(fù)限定符
有的時候需要表示有多個同一類型的字符出現(xiàn)的時候就需要使用限定符來進(jìn)行限定,比如需要匹配兩個數(shù)字真朗,就需要使用\\d{2}
此疹,以下是常見的限定符號:
代碼 | 限定符說明 |
---|---|
* | 重復(fù)零次或更多次 |
+ | 重復(fù)一次或更多次 |
? | 重復(fù)零次或一次 |
{n} | 重復(fù) n 次 |
{n,} | 重復(fù) n 次或更多次 |
{n,m} | 重復(fù) n 次到 m 次 |
反義
反義的理解比較簡單,其實就是元字符表示的反義遮婶,舉個例子就很好理解了:比如說想要查找查找除了數(shù)字以外蝗碎,其它任意字符都行的情況。
代碼 | 反義符說明 |
---|---|
\W | 匹配任意不是字母或數(shù)字或下劃線或漢字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意不是數(shù)字的字符 |
\B | 匹配不是單詞的開始或結(jié)束的位置 |
[^x] | 匹配x以外的任意字符 |
[^aeiou] | 匹配除了aeiou這幾個字母以外的任意字符 |
字符類
當(dāng)我學(xué)習(xí)到上面的那些的時候旗扑,我已經(jīng)覺得自己無所不能了蹦骑。
好吧,我承認(rèn)我就是一個牛皮匠=臀防。=
對于數(shù)字眠菇、字母或數(shù)字边败、空白,這些已經(jīng)有對應(yīng)的元字符表示這些字符集合捎废,書寫正則進(jìn)行匹配的時候還是比較簡單的笑窜,但是如果說我們想要匹配的事 10以內(nèi)的奇數(shù) ,那么原來的提供的那些元字符就不那么容易能夠完成任務(wù)了登疗。
其實正則表達(dá)式的語法中給我們提供了可以自定義字符集的方法排截,在方括號里面將你需要的字符列出來就行了,比如上面的例子中只要使用[13579]
就可以了谜叹。
分支條件
分支條件我的理解簡單來說就是編程語言中的 邏輯或 匾寝,下面是一段引用的正式解釋:
正則表達(dá)式里的分枝條件指的是有幾種規(guī)則,如果滿足其中任意一種規(guī)則都應(yīng)該當(dāng)成匹配荷腊,具體方法是用 | 把不同的規(guī)則分隔開。
比如美國郵編的規(guī)則是5位數(shù)字急凰,或者用連字號間隔的9位數(shù)字女仰,那么我們可以使用分支條件處理:\\d{5}-\\d{4}|\\d{5}
需要注意的是:使用分支條件的時候需要特別注意條件的先后順序,原因是匹配分枝條件時抡锈,將會從左到右地測試每個條件疾忍,如果滿足了某個分枝的話,就不會去再管其它的條件了床三。
例如上述例子一罩,如果改成\\d{5}|\\d{5}-\\d{4}
的話,就只能夠匹配5位的郵編(以及9位郵編的前5位)撇簿。
分組
前面提到的重復(fù)限定符已經(jīng)讓我們知道了如何對單個字符進(jìn)行重復(fù)表達(dá)(在字符后面加上重復(fù)限定符聂渊,如\\d+
),但是考慮到現(xiàn)實應(yīng)用中我們會遇到對多個字符進(jìn)行重復(fù)表達(dá)四瘫,比如:常見的 IP 地址汉嗽,IP 地址的大致樣子為 3位數(shù)字加上一個點 重復(fù)三次,再加上一個三位數(shù)字找蜜,這里需要用到分組表達(dá)饼暑,用小括號來指定子表達(dá)式(也叫做分組),然后你就可以指定這個子表達(dá)式的重復(fù)次數(shù)了洗做,正則表達(dá)式為:(\\d{1,3}\\.){3}\\d{1,3}
當(dāng)然弓叛,上面的表達(dá)式最終可能會匹配出不符合 IP 地址規(guī)范的地址,如:999.999.999.999诚纸,只能使用冗長的分組撰筷,選擇,字符類來描述一個正確的IP地址:
((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)
關(guān)于分組咬清,還會引申出更多的一些細(xì)節(jié)之處:
后向引用
當(dāng)我們在正則表達(dá)式中使用分組的時候闭专,如果分組匹配到內(nèi)容(即小括號內(nèi)的子表達(dá)式匹配的內(nèi)容)奴潘,正則匹配引擎會記錄這段內(nèi)容,并分配組號影钉,這個過程我們可以形象的稱之為 捕獲 画髓。
書寫復(fù)雜的正則的時候可能會需要對分組捕獲的內(nèi)容進(jìn)行進(jìn)一步的處理,后向引用就是對前面某個分組匹配的內(nèi)容進(jìn)行進(jìn)一步的處理平委。
在這里最簡單的例子就是重復(fù)的兩個單詞放在一起奈虾,比如:hello hello 或者 hi hi ,注意兩個單詞之間有若干空格廉赔,配這種字符串的正則表達(dá)式為:\\b(\\w+)\\b\\s+\\1\\b
肉微,其中\\1
就代表分組1匹配到的內(nèi)容。
關(guān)于分組組號的分配這里有一些補充:
1蜡塌、分組0對應(yīng)整個正則表達(dá)式碉纳;
2、實際上組號分配過程是要從左向右掃描兩遍的:第一遍只給未命名組分配馏艾,第二遍只給命名組分配--因此所有命名組的組號都大于未命名的組號劳曹;
3、可以使用(?:exp)這樣的語法來剝奪一個分組對組號分配的參與權(quán)琅摩;
4铁孵、可以給分組添加一個自定義的組號:(?<name>exp)
,name 就是這個分組的組號房资,其中的<name>
可以替換成'name'
分組中捕獲相關(guān)的常用語法:
代碼 | 反義符說明 |
---|---|
(exp) | 匹配exp,并捕獲文本到自動命名的組里 |
(?<name>exp) | 匹配exp,并捕獲文本到名稱為name的組里蜕劝,也可以寫成(?'name'exp) |
(?:exp) | 匹配exp,不捕獲匹配的文本,也不給此分組分配組號 |
零寬斷言和負(fù)向零寬斷言
這兩個概念的名稱表面上看是非常難以揣測意思的轰异,并且非常拗口岖沛。
其實無論是零寬斷言還是負(fù)向零寬斷言都是代表的一個位置,就像前面介紹過的\\b
溉浙、^
和 $
烫止,零寬 的含義就是零寬度的一個位置,并且 斷言 這個位置滿足一定的條件戳稽,零寬斷言是用來查找在 某些內(nèi)容 之前或者之后的 一些東西馆蠕,并且不包括 這些內(nèi)容 ;而負(fù)向零寬斷言與零寬斷言是相反的關(guān)系惊奇,它用來匹配 一些東西 的前面或之后不是 某些內(nèi)容互躬,并且不包括 這些內(nèi)容。
相關(guān)的常用語法:
代碼/語法 | 反義符說明 |
---|---|
(?=exp) | 匹配exp前面的位置 |
(?<=exp) | 匹配exp后面的位置 |
(?!exp) | 匹配后面內(nèi)容不是exp的位置 |
(?<!exp) | 匹配前面內(nèi)容不是exp的位置 |
下面我們通過幾個例子逐個對上面的語法進(jìn)行一下應(yīng)用:
-
(?=exp)
即零寬度正預(yù)測先行斷言颂郎,它斷言自身出現(xiàn)的位置的后面能匹配表達(dá)式exp吼渡。比如\\b\\w+(?=ing\\b)
,匹配以ing結(jié)尾的單詞的前面部分(除了ing以外的部分)乓序,如查找I'm singing while you're dancing.時寺酪,它會匹配sing和danc坎背; -
(?<=exp)
即零寬度正回顧后發(fā)斷言,它斷言自身出現(xiàn)的位置的前面能匹配表達(dá)式exp寄雀。比如(?<=\\bre)\\w+\\b
會匹配以re開頭的單詞的后半部分(除了re以外的部分)得滤,例如在查找reading a book時,它匹配ading盒犹; -
(?!exp)
即零寬度負(fù)預(yù)測先行斷言懂更,斷言此位置的后面不能匹配表達(dá)式exp。例如:\\d{3}(?!\\d)
匹配三位數(shù)字急膀,而且這三位數(shù)字的后面不能是數(shù)字沮协;\\b((?!abc)\\w)+\\b
匹配不包含連續(xù)字符串a(chǎn)bc的單詞。 -
(?<!exp)
即零寬度負(fù)回顧后發(fā)斷言卓嫂,斷言此位置的前面不能匹配表達(dá)式exp慷暂。(?<![a-z])\\d{7}
匹配前面不是小寫字母的七位數(shù)字。
貪婪和懶惰原則
當(dāng)正則表達(dá)式中包含接受重復(fù)的限定符時命黔,匹配的過程中默認(rèn)會在滿足表達(dá)式整體得到匹配的前提下匹配盡可能多的字符呜呐,這種行為被稱作 貪婪匹配。
比如對于字符串 aabab 悍募,如果使用表達(dá)式a.*b
進(jìn)行匹配,匹配的結(jié)果將會是以a開始洋机,以b結(jié)束的最長的字符串坠宴,即 aabab 本身。
如果我們需要匹配盡可能少的字符绷旗,那么我們就需要懶惰匹配喜鼓,在重復(fù)限定符的后面加上?
,就代表進(jìn)行懶惰匹配衔肢,剛剛的例子中庄岖,如果我們使用a.*?
進(jìn)行匹配的話,匹配結(jié)果會是aab(第一到第三個字符)和ab(第四到第五個字符)角骤。
在這里補充一下:為什么第一個匹配是aab(第一到第三個字符)而不是ab(第二到第三個字符)隅忿?簡單地說,因為正則表達(dá)式有另一條規(guī)則邦尊,比懶惰/貪婪規(guī)則的優(yōu)先級更高:最先開始的匹配擁有最高的優(yōu)先權(quán)——The match that begins earliest wins背桐。
懶惰限定符的常見語法:
代碼 | 說明 |
---|---|
*? | 重復(fù)任意次,但盡可能少重復(fù) |
+? | 重復(fù)1次或更多次蝉揍,但盡可能少重復(fù) |
?? | 重復(fù)0次或1次链峭,但盡可能少重復(fù) |
{n,m}? | 重復(fù)n到m次,但盡可能少重復(fù) |
{n,}? | 重復(fù)n次以上又沾,但盡可能少重復(fù) |
在 iOS 開發(fā)中使用正則表達(dá)式
正則的基本語法在所有的正則引擎中基本差不多弊仪,貌似看有的語言使用的正則引擎對零寬斷言和負(fù)向零寬斷言不支持熙卡,使用 Swift 測試了一下,應(yīng)該 iOS 還是支持零寬斷言和負(fù)向零寬斷言的正則語法励饵。
在 iOS 中常見的正則表達(dá)式的使用方式有三種:
- 在 NSPredicate 中使用正則表達(dá)式:
- (BOOL)validateNumber:(NSString *) textString
{
NSString* number=@"^[0-9]+$";
NSPredicate *numberPre = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",number];
return [numberPre evaluateWithObject:textString];
}
- NSString 方法中使用:
NSString *searchText = @"rangeOfString";
NSRange range = [searchText rangeOfString:@"^[0-9]+$" options:NSRegularExpressionSearch];
if (range.location != NSNotFound) {
NSLog(@"range :%@", [searchText substringWithRange:range]);
}
- NSRegularExpression:
//1. 要匹配的字符串
NSString *str = @"I'm singing while you're dancing";
//Pattern: 正則表達(dá)式語句
NSString *pattern = @"\\\\b\\\\w+(?=ing\\\\b)";
//2. 創(chuàng)建正則表達(dá)式
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
//3. 匹配結(jié)果
NSArray *result = [regex matchesInString:str options:NSMatchingReportCompletion range:NSMakeRange(0, str.length)];
// 打印結(jié)果
if (result.count == 0) {
NSLog(@"匹配出錯");
} else {
NSLog(@"匹配成功: %lu",(unsigned long)result.count);
NSLog(@"%@", result);
}
同樣的代碼也用 Swift 進(jìn)行了測試驳癌,結(jié)果一致:
// 定義正則表達(dá)式語句
let pattern = "\\\\b\\\\w+(?=ing\\\\b)"
// 定義正則表達(dá)式對象
let regex = try! NSRegularExpression(pattern: pattern, options: .CaseInsensitive)
// 待匹配的字符串
let str = "I'm singing while you're dancing"
// 進(jìn)行匹配
let arr: [NSTextCheckingResult] = regex.matchesInString(str , options: NSMatchingOptions.ReportCompletion, range: NSMakeRange(0, str.characters.count))
print(arr)
在我個人的日常使用中,還是第三種用的稍微多一些曲横,NSPredicate 基本用的機會很少喂柒,也不是太了解,后續(xù)有機會了解的話會補充到本篇博客中禾嫉。
結(jié)尾
- 本篇博客中只是羅列了正則表達(dá)式中較為基礎(chǔ)的部分語法灾杰,其實有許多東西我也是邊學(xué)習(xí)邊總結(jié),很多語法也不是特別清楚熙参,后面有補充的會一點一點補充到這篇博客中艳吠;
- 正則所涵蓋的語法點非常多也比較瑣碎,正則表達(dá)式書寫出來也比較的繞而且亂七八糟的符號也比較亂孽椰,所以需要不斷的練習(xí)和使用才能靈活的解決開發(fā)中的實際問題昭娩,就我個人而言也是需要加強練習(xí),改掉工作中依賴搜索的懶毛彩蜇摇(前提是開發(fā)進(jìn)度允許=栏渺。=);
最后锐涯,祝大家玩的愉快磕诊!