戰(zhàn):先行與后行斷言詳解及避坑指南)
正則表達(dá)式里的先行斷言和后行斷言是我在代碼評(píng)審里見(jiàn)得最多、也最容易被誤用的兩個(gè)語(yǔ)法點(diǎn)。很多人寫業(yè)務(wù)正則時(shí)一遇到“匹配但不消耗字符”的需求就卡殼要么繞一大圈用捕獲組加額外判斷要么干脆放棄正則改用字符串截取代碼又長(zhǎng)又難維護(hù)。這篇內(nèi)容就是把這個(gè)點(diǎn)徹底講透先行斷言lookahead和后行斷言lookbehind到底是什么、能解決什么問(wèn)題、在哪些場(chǎng)景下比捕獲組更合適、以及實(shí)際寫的時(shí)候有哪些坑。不管你是剛接觸正則的新手還是寫了幾年正則但一直沒(méi)系統(tǒng)搞明白斷言的老手都能從里面拿到可以直接抄作業(yè)的寫法和排查思路。1. 斷言到底在解決什么問(wèn)題1.1 從一次真實(shí)的代碼評(píng)審說(shuō)起前陣子幫一個(gè)團(tuán)隊(duì)看一段表單校驗(yàn)邏輯需求是“密碼必須包含至少一個(gè)大寫字母但不能以數(shù)字開(kāi)頭”。寫這段代碼的同學(xué)用了兩段正則加一個(gè)額外的字符串判斷代碼大概長(zhǎng)這樣先跑一遍/^[A-Za-z]/判斷首字符再跑一遍/[A-Z]/判斷有沒(méi)有大寫字母中間還夾了一堆 if-else。功能是能跑但可讀性很差而且一旦需求變成“不能以數(shù)字開(kāi)頭且必須包含大寫字母和特殊符號(hào)”這段邏輯就得推倒重寫。其實(shí)這個(gè)需求用斷言一行就能表達(dá)清楚。斷言的核心價(jià)值就在這里它允許你在某個(gè)位置“看一眼”前后文判斷條件是否成立但不把看過(guò)的字符算進(jìn)匹配結(jié)果里。這個(gè)“不消耗字符”的特性是它和普通捕獲組最本質(zhì)的區(qū)別也是很多場(chǎng)景下它比捕獲組更優(yōu)雅的原因。打個(gè)生活化的比方。假設(shè)你在一排書(shū)架上找書(shū)普通匹配就像“我把這本書(shū)抽出來(lái)看看不對(duì)再放回去”抽出來(lái)這個(gè)動(dòng)作會(huì)改變書(shū)架的狀態(tài)而斷言更像“我站在書(shū)架前掃一眼書(shū)脊確認(rèn)旁邊那本書(shū)是什么但我手不碰任何書(shū)”。位置沒(méi)動(dòng)判斷做完了后續(xù)的匹配還能從原地繼續(xù)。這個(gè)類比基本能概括斷言的全部行為特征。1.2 先行與后行的方向區(qū)分?jǐn)嘌苑謨蓚€(gè)方向理解方向是第一步也是最容易搞混的一步。先行斷言lookahead是往右看也就是看當(dāng)前位置后面的內(nèi)容。語(yǔ)法是(?...)表示“后面必須是這個(gè)”(?!...)表示“后面必須不是這個(gè)”。注意這里的“后面”指的是正則引擎前進(jìn)的方向也就是字符串的右側(cè)。后行斷言lookbehind是往左看看當(dāng)前位置前面的內(nèi)容。語(yǔ)法是(?...)表示“前面必須是這個(gè)”(?!...)表示“前面必須不是這個(gè)”。我用一個(gè)具體例子把方向釘死。字符串price: 100正則(?price: )\d。引擎先找到price:這個(gè)位置然后從它后面開(kāi)始匹配數(shù)字(?price: )這個(gè)后行斷言的意思是“我當(dāng)前位置的左邊得是price:”條件滿足于是匹配到100。最終匹配結(jié)果只有100price:不在結(jié)果里。這就是“看一眼但不消耗”的直接體現(xiàn)。反過(guò)來(lái)\d(? dollars)匹配100 dollars里的100先行斷言(? dollars)檢查數(shù)字右邊是不是dollars是的話數(shù)字就算匹配成功但dollars本身不進(jìn)結(jié)果。兩個(gè)方向一左一右記住“先行看右、后行看左”就不會(huì)亂。1.3 斷言與捕獲組的本質(zhì)差異很多人會(huì)問(wèn)我用捕獲組也能實(shí)現(xiàn)類似效果啊為什么要用斷言這個(gè)問(wèn)題值得掰開(kāi)說(shuō)因?yàn)檫x型選錯(cuò)了后面維護(hù)會(huì)很痛苦。捕獲組的思路是“把前后文也匹配進(jìn)來(lái)然后從結(jié)果里取需要的部分”。比如提取price: 100里的數(shù)字用price: (\d)然后取第一個(gè)捕獲組。斷言的思路是“前后文只做條件判斷不進(jìn)匹配結(jié)果”用(?price: )\d直接拿到的就是100。差異體現(xiàn)在三個(gè)地方。第一是結(jié)果干凈度斷言直接給出目標(biāo)不需要二次提取尤其在批量替換場(chǎng)景下差別巨大。第二是位置連續(xù)性斷言不消耗字符所以多個(gè)斷言可以疊加在同一個(gè)位置上連續(xù)判斷捕獲組做不到這一點(diǎn)。第三是替換時(shí)的可控性用斷言做的替換被“看一眼”的部分原樣保留不會(huì)因?yàn)樘鎿Q邏輯寫錯(cuò)而被誤傷。舉個(gè)替換場(chǎng)景就明白了。要把100 dollars改成100 USD用先行斷言\d(? dollars)替換成$ USD之類的方式dollars部分完全不受影響如果用捕獲組(\d) dollars替換成$1 USD一旦捕獲組編號(hào)寫錯(cuò)或者替換模板有誤整個(gè)dollars都可能被吃掉。斷言在這里相當(dāng)于給替換操作上了一道保險(xiǎn)。2. 四種斷言語(yǔ)法的逐個(gè)拆解2.1 正向先行斷言 (?...)正向先行斷言回答的問(wèn)題是“我當(dāng)前位置的右邊是不是以某段內(nèi)容開(kāi)頭”語(yǔ)法(?pattern)其中 pattern 是你要檢查的內(nèi)容。它的典型用途是給匹配加前置條件。比如校驗(yàn)密碼強(qiáng)度要求“至少 8 位且必須包含數(shù)字”可以寫成^(?.*\d).{8,}$。拆開(kāi)看^定位到開(kāi)頭(?.*\d)在開(kāi)頭這個(gè)位置往右掃一遍確認(rèn)整行里存在數(shù)字條件滿足后位置不動(dòng)繼續(xù)用.{8,}$匹配整行。這里的關(guān)鍵是(?.*\d)只做檢查.*\d匹配到的內(nèi)容不會(huì)被消耗所以后面的.{8,}$還能從頭開(kāi)始匹配。這個(gè)“零寬”特性是正向先行斷言最核心的行為。你可以把它理解成一個(gè)“探針”探一下右邊的情況探完縮回來(lái)位置不變。多個(gè)正向先行斷言可以串聯(lián)比如^(?.*\d)(?.*[A-Z])(?.*[!#]).{8,}$就是“同時(shí)包含數(shù)字、大寫字母、特殊符號(hào)且至少 8 位”三個(gè)探針依次在開(kāi)頭位置檢查互不干擾。這種寫法比用三個(gè)獨(dú)立正則分別判斷要緊湊得多。2.2 負(fù)向先行斷言 (?!...)負(fù)向先行斷言是正向的反面“我當(dāng)前位置的右邊不能以某段內(nèi)容開(kāi)頭”。語(yǔ)法(?!pattern)。它最常見(jiàn)的用途是排除特定模式。比如匹配所有不是.jpg結(jié)尾的文件名可以寫^\w\.(?!jpg$)\w$這種思路實(shí)際寫法要看具體需求調(diào)整。更典型的例子是“匹配不包含某個(gè)詞的行”比如排除包含TODO的代碼行用^(?!.*TODO).*$。這里(?!.*TODO)在行首檢查“整行里沒(méi)有 TODO”沒(méi)有則條件成立然后.*$匹配整行。負(fù)向先行斷言有個(gè)容易踩的坑否定范圍要寫清楚。(?!abc)是否定“右邊緊跟 abc”而不是否定“右邊任何位置出現(xiàn) abc”。要否定整行出現(xiàn)得寫(?!.*abc)。這個(gè)區(qū)別我在實(shí)際項(xiàng)目里見(jiàn)過(guò)不止一次搞混寫出來(lái)的正則“看起來(lái)對(duì)但就是匹配不對(duì)”排查半天發(fā)現(xiàn)是否定范圍寫窄了。2.3 正向后行斷言 (?...)正向后行斷言檢查“當(dāng)前位置的左邊是不是某段內(nèi)容”語(yǔ)法(?pattern)。它適合提取帶固定前綴的數(shù)據(jù)比如從日志里提取user_id后面的數(shù)字用(?user_id)\d直接拿到數(shù)字前綴不污染結(jié)果。后行斷言有一個(gè)硬性限制必須提前知道大多數(shù)正則引擎要求后行斷言里的 pattern 是定長(zhǎng)的。也就是說(shuō)(?abc)可以(?a{3})可以但(?a)或(?a{1,3})在很多引擎里會(huì)直接報(bào)錯(cuò)。原因是后行斷言需要引擎“往回退固定步數(shù)”去檢查變長(zhǎng)的話退多少步不確定實(shí)現(xiàn)起來(lái)代價(jià)太大。不同引擎對(duì)這個(gè)限制的寬松程度不一樣。JavaScript 在較新的規(guī)范里已經(jīng)支持變長(zhǎng)后行斷言但老版本瀏覽器和部分運(yùn)行時(shí)仍然受限Python 的re模塊明確要求定長(zhǎng)PCRE 系列相對(duì)寬松一些但也有邊界。寫跨語(yǔ)言的正則時(shí)后行斷言一律按定長(zhǎng)來(lái)寫是最穩(wěn)妥的策略需要變長(zhǎng)匹配時(shí)改用捕獲組或者調(diào)整整體思路。2.4 負(fù)向后行斷言 (?!...)負(fù)向后行斷言檢查“當(dāng)前位置的左邊不能是某段內(nèi)容”語(yǔ)法(?!pattern)。典型場(chǎng)景是排除特定前綴比如匹配不是以test_開(kāi)頭的變量名用(?!test_)\b\w\b這種思路。它同樣受定長(zhǎng)限制。另外要注意一個(gè)語(yǔ)義細(xì)節(jié)(?!abc)是“左邊緊鄰的位置不是 abc”而不是“左邊任何地方?jīng)]有 abc”。這個(gè)“緊鄰”的語(yǔ)義和先行斷言的“緊跟”是對(duì)稱的理解了一個(gè)另一個(gè)自然就通了。四種語(yǔ)法放在一起對(duì)比方向、正負(fù)、定長(zhǎng)要求三個(gè)維度可以整理成一張表寫正則時(shí)對(duì)著查很快語(yǔ)法方向語(yǔ)義定長(zhǎng)要求典型場(chǎng)景(?...)右右邊必須是無(wú)密碼強(qiáng)度、條件校驗(yàn)(?!...)右右邊不能是無(wú)排除模式、過(guò)濾行(?...)左左邊必須是多數(shù)引擎要求定長(zhǎng)提取帶前綴數(shù)據(jù)(?!...)左左邊不能是多數(shù)引擎要求定長(zhǎng)排除帶前綴數(shù)據(jù)3. 實(shí)操場(chǎng)景斷言在真實(shí)需求里的落地寫法3.1 密碼強(qiáng)度校驗(yàn)的組合斷言密碼校驗(yàn)是斷言最經(jīng)典的應(yīng)用場(chǎng)景因?yàn)樾枨筇烊皇恰岸鄠€(gè)條件同時(shí)滿足”而斷言可以零寬疊加正好對(duì)上。需求8 到 20 位必須包含大寫字母、小寫字母、數(shù)字且不能包含空格。寫法const pwdPattern /^(?.*[a-z])(?.*[A-Z])(?.*\d)(?!.*\s).{8,20}$/;逐段拆解。^錨定開(kāi)頭。(?.*[a-z])在開(kāi)頭往右掃確認(rèn)存在小寫字母。(?.*[A-Z])確認(rèn)存在大寫字母。(?.*\d)確認(rèn)存在數(shù)字。(?!.*\s)確認(rèn)整行沒(méi)有空白字符。四個(gè)斷言都在開(kāi)頭位置完成檢查位置始終不動(dòng)。最后.{8,20}$從開(kāi)頭匹配 8 到 20 個(gè)任意字符到結(jié)尾。這里有個(gè)細(xì)節(jié)值得說(shuō)為什么用.*而不是直接寫字符類因?yàn)閿嘌允窃凇爱?dāng)前位置”檢查而當(dāng)前位置是開(kāi)頭要檢查“整行里有沒(méi)有某類字符”必須用.*把視線推到行內(nèi)任意位置再判斷。如果寫成(?[a-z])那檢查的是“開(kāi)頭緊鄰位置是不是小寫字母”語(yǔ)義完全不同。這個(gè)區(qū)別是新手最容易寫錯(cuò)的地方我見(jiàn)過(guò)不少人把(?.*\d)寫成(?\d)然后困惑為什么“1abc”能過(guò)而“abc1”過(guò)不了。3.2 數(shù)據(jù)提取中的前后綴剝離日志分析里經(jīng)常要從keyvalue格式里提取 value且 key 是固定的幾個(gè)。比如從status200 latency35ms里提取200和35。提取status的值(?status)\d。提取latency的值(?latency)\d。兩個(gè)后行斷言都是定長(zhǎng)的status六個(gè)字符latency八個(gè)字符完全符合引擎限制跨語(yǔ)言都能跑。如果 key 是變長(zhǎng)的比如user_1_id100和user_22_id200都要提取后行斷言就不好寫了因?yàn)閡ser_\d_id是變長(zhǎng)的。這時(shí)候有兩個(gè)選擇一是改用捕獲組user_\d_id(\d)取第一組二是如果引擎支持變長(zhǎng)后行斷言比如較新的 JavaScript可以寫(?user_\d_id)\d但要清楚這段正則在 Python 里會(huì)直接拋錯(cuò)。我的建議是跨語(yǔ)言場(chǎng)景一律用捕獲組兜底斷言留給定長(zhǎng)前綴的場(chǎng)景這樣代碼的可移植性最好。3.3 批量替換時(shí)保護(hù)上下文斷言在替換場(chǎng)景里的價(jià)值比提取場(chǎng)景更明顯因?yàn)樘鎿Q最怕誤傷上下文。需求把文本里所有獨(dú)立的數(shù)字100替換成200但不能動(dòng)1100、1000這種嵌在更長(zhǎng)數(shù)字里的。用單詞邊界\b100\b是一種思路但如果數(shù)字前后是下劃線或者中文\b的行為可能不符合預(yù)期。用斷言更精確(?!\d)100(?!\d)意思是“左邊不是數(shù)字右邊也不是數(shù)字”。這樣1100里的100左邊是1被負(fù)向后行斷言排除1000里的100右邊是0被負(fù)向先行斷言排除只有獨(dú)立的100能匹配上。這個(gè)組合斷言在數(shù)據(jù)清洗里非常實(shí)用。我處理過(guò)一批從表格導(dǎo)出的文本數(shù)字和單位粘連嚴(yán)重用(?!\d)(\d)(?!\d)先把獨(dú)立數(shù)字圈出來(lái)再做單位換算比先分詞再處理要快得多而且不會(huì)破壞原始格式。3.4 排除特定前綴的標(biāo)識(shí)符匹配代碼靜態(tài)檢查里經(jīng)常要“匹配所有變量名但排除以tmp_開(kāi)頭的臨時(shí)變量”。用負(fù)向后行斷言(?!tmp_)\b[a-zA-Z_]\w*\b。這里要注意\b和斷言的位置關(guān)系。(?!tmp_)檢查的是“當(dāng)前位置左邊緊鄰不是tmp_”而\b是單詞邊界。如果變量名是tmp_count引擎在t的位置檢查左邊左邊是行首或空格不是tmp_斷言通過(guò)然后\b[a-zA-Z_]\w*\b會(huì)匹配整個(gè)tmp_count——這顯然不是我們想要的因?yàn)槲覀円懦氖钦麄€(gè)以tmp_開(kāi)頭的標(biāo)識(shí)符而不是“左邊緊鄰 tmp_ 的位置”。正確的思路是把斷言放在標(biāo)識(shí)符內(nèi)部或者換個(gè)角度用負(fù)向先行斷言配合錨點(diǎn)。更穩(wěn)妥的寫法是\b(?!tmp_)[a-zA-Z_]\w*\b在單詞邊界處用負(fù)向先行斷言檢查“右邊不是以 tmp_ 開(kāi)頭”這樣tmp_count在t位置檢查時(shí)右邊是tmp_斷言失敗整個(gè)標(biāo)識(shí)符被排除。這個(gè)例子說(shuō)明一個(gè)道理斷言的放置位置決定了它檢查的是“緊鄰”還是“整體”寫之前先想清楚要排除的粒度。4. 引擎差異與兼容性避坑4.1 各語(yǔ)言對(duì)后行斷言的支持現(xiàn)狀后行斷言的支持程度是跨語(yǔ)言正則最大的坑之一我整理了一張對(duì)照表實(shí)際項(xiàng)目里對(duì)著查能省不少調(diào)試時(shí)間語(yǔ)言/引擎正向先行負(fù)向先行正向后行負(fù)向后行后行變長(zhǎng)支持JavaScript (ES2018)支持支持支持支持支持Python re支持支持支持支持不支持定長(zhǎng)Java支持支持支持支持有限支持PCRE支持支持支持支持部分版本支持Go regexp支持支持不支持不支持不支持Go 的regexp包完全不支持后行斷言這是很多從其他語(yǔ)言轉(zhuǎn)過(guò)來(lái)的開(kāi)發(fā)者踩的第一個(gè)坑。在 Go 里要實(shí)現(xiàn)“提取帶前綴的數(shù)字”只能老老實(shí)實(shí)用捕獲組prefix(\d)然后取子匹配。寫跨語(yǔ)言庫(kù)的時(shí)候如果目標(biāo)包含 Go后行斷言基本可以放棄統(tǒng)一用捕獲組是最省心的方案。4.2 定長(zhǎng)限制的邊界與繞行方案Python 里寫(?a)b會(huì)直接拋error: look-behind requires fixed-width pattern。這個(gè)報(bào)錯(cuò)很明確但實(shí)際需求里“變長(zhǎng)前綴”的情況并不少見(jiàn)得有繞行方案。方案一改用捕獲組。(?:a)(b)取第一組雖然多一步提取但兼容性最好。方案二拆成兩步先用一個(gè)正則定位前綴再在子串上做二次匹配。方案三如果前綴的變長(zhǎng)范圍有限比如a{1,3}可以展開(kāi)成多個(gè)定長(zhǎng)斷言并聯(lián)(?a)(?aa)?這種寫法不優(yōu)雅但能跑實(shí)際項(xiàng)目里我一般不用可讀性太差。我的經(jīng)驗(yàn)是能用捕獲組解決的就別硬上后行斷言。后行斷言的收益主要在“結(jié)果干凈”和“替換安全”上如果這兩點(diǎn)不是剛需捕獲組的兼容性和可讀性都更好。斷言是錦上添花的工具不是非用不可的銀彈。4.3 性能考量斷言會(huì)不會(huì)拖慢匹配斷言本身是零寬的理論上不增加匹配的字符消耗但它會(huì)增加引擎的“檢查次數(shù)”。尤其是(?.*\d)這種內(nèi)部帶.*的斷言在長(zhǎng)字符串上會(huì)觸發(fā)回溯性能影響不能忽略。實(shí)測(cè)過(guò)一個(gè)場(chǎng)景對(duì) 10 萬(wàn)行的日志做密碼強(qiáng)度校驗(yàn)用四個(gè)(?.*xxx)串聯(lián)的正則比用四個(gè)獨(dú)立正則分別判斷再合并結(jié)果耗時(shí)高出約 30%。原因是每個(gè)斷言內(nèi)部的.*都要掃一遍整行四個(gè)斷言就是四遍掃描。如果性能敏感可以把斷言順序調(diào)整成“最容易失敗的放前面”讓引擎盡早短路或者干脆拆成多個(gè)簡(jiǎn)單正則用邏輯與組合犧牲一點(diǎn)代碼優(yōu)雅換性能。提示斷言里的.*是回溯大戶長(zhǎng)文本場(chǎng)景下優(yōu)先考慮用字符類限定范圍比如(?[^A-Z]*[A-Z])代替(?.*[A-Z])前者在遇到大寫字母時(shí)能更早停止掃描。5. 常見(jiàn)問(wèn)題排查與實(shí)操心得5.1 斷言寫對(duì)了但匹配結(jié)果不對(duì)的排查順序遇到“正則看起來(lái)沒(méi)問(wèn)題但結(jié)果不對(duì)”我一般按這個(gè)順序排查命中率很高。第一步確認(rèn)斷言的方向。先行看右、后行看左寫反了是最常見(jiàn)的低級(jí)錯(cuò)誤尤其是從別人的代碼里復(fù)制過(guò)來(lái)改的時(shí)候。第二步確認(rèn)斷言的粒度。(?!abc)是“緊鄰不是 abc”(?!.*abc)是“整行沒(méi)有 abc”兩者語(yǔ)義差很遠(yuǎn)。第三步確認(rèn)引擎是否支持。后行斷言在 Go 里直接不支持在 Python 里要求定長(zhǎng)報(bào)錯(cuò)信息通常很明確但如果是靜默失敗比如某些在線工具就得手動(dòng)驗(yàn)證。第四步用最小用例驗(yàn)證。把正則拆到只剩一個(gè)斷言用abc、xabc、abcx三個(gè)輸入分別測(cè)看行為是否符合預(yù)期比盯著復(fù)雜正則干想要快得多。5.2 斷言與錨點(diǎn)、邊界的組合陷阱斷言和^、$、\b組合時(shí)位置語(yǔ)義容易出問(wèn)題。比如^(?.*\d)里的^把位置釘在行首斷言在行首往右檢查這是對(duì)的。但如果寫成(?.*\d)^雖然邏輯上等價(jià)可讀性差很多而且某些引擎對(duì)錨點(diǎn)位置的優(yōu)化會(huì)受影響。再比如\b(?!tmp_)這種組合\b匹配的位置和斷言檢查的位置是同一個(gè)點(diǎn)語(yǔ)義上沒(méi)問(wèn)題但實(shí)際寫的時(shí)候容易把\b放在斷言里面或外面搞混。我的習(xí)慣是錨點(diǎn)和邊界放在斷言外面斷言只負(fù)責(zé)內(nèi)容條件這樣結(jié)構(gòu)清晰排查時(shí)也容易定位是哪一層出了問(wèn)題。5.3 可讀性維護(hù)給復(fù)雜斷言加注釋一個(gè)帶四五個(gè)斷言的正則過(guò)兩周自己都看不懂。我的做法是在代碼里用注釋把每個(gè)斷言的意圖寫清楚或者用命名捕獲組配合注釋。比如密碼校驗(yàn)?zāi)嵌螌懗啥嘈屑幼⑨尩男问絚onst pwdPattern new RegExp( ^ (?.*[a-z]) // 至少一個(gè)小寫字母 (?.*[A-Z]) // 至少一個(gè)大寫字母 (?.*\\d) // 至少一個(gè)數(shù)字 (?!.*\\s) // 不含空白字符 .{8,20} // 總長(zhǎng)度 8-20 $ );雖然拼接字符串不如字面量簡(jiǎn)潔但可維護(hù)性提升明顯尤其是團(tuán)隊(duì)協(xié)作時(shí)別人接手能快速理解每個(gè)條件的意圖。這個(gè)習(xí)慣我堅(jiān)持了好幾年代碼評(píng)審時(shí)因?yàn)檎齽t看不懂而產(chǎn)生的溝通成本降了很多。5.4 斷言不是萬(wàn)能的什么時(shí)候該放棄最后說(shuō)一個(gè)反向的經(jīng)驗(yàn)不是所有“看一眼”的需求都適合用斷言。如果斷言嵌套超過(guò)兩層或者需要配合多個(gè)捕獲組才能完成提取那這段正則的復(fù)雜度已經(jīng)超過(guò)收益了不如拆成多步字符串處理或者用專門的解析庫(kù)。我給自己定的紅線是單個(gè)正則里斷言不超過(guò)三個(gè)后行斷言不超過(guò)一個(gè)且不出現(xiàn)斷言嵌套斷言。超過(guò)這個(gè)復(fù)雜度代碼的可讀性和可調(diào)試性都會(huì)急劇下降維護(hù)成本遠(yuǎn)高于多寫幾行普通代碼。正則的定位是“處理有規(guī)律的模式”不是“用一行代碼解決所有問(wèn)題”認(rèn)清這個(gè)邊界寫出來(lái)的代碼才經(jīng)得起時(shí)間考驗(yàn)。