一、兩條規則丟進去,滿屏都是 Class
替一份拖了幾年的文件做術語統一,我在取代頁裡排了兩條規則:第一條把 Type 換成 Kind,第二條把 Kind 換成 Class。預期是兩個詞各歸各位,實際跑完輸出 "Class Class"、取代次數 3——第二條規則連第一條剛造出來的 Kind 也沒放過。
問題不在規則本身,而在一個沒人寫進交接文件的前提:規則不是各自獨立作用於原文,它們排隊依序執行,第 2 條看到的是第 1 條的輸出。勾選區分大小寫也救不了這條鏈,依舊 3 次。
二、順序既決定輸出,也決定計數
把鏈縮到最短,差額就看得清了。輸入 "a1 b2",規則依序 [1→2, 2→3] 跑完得 "a3 b3"、取代次數 3;同一批規則反向 [2→3, 1→2] 得 "a2 b3"、取代次數 2。兩條規則各自單跑是基線:[1→2] 得 "a2 b2"、1 次,[2→3] 得 "a1 b3"、1 次,獨立相加本來只有 2 次。多出來的那 1 次,是 b2 本來不含 1、被第一條規則造出一個 2、又被第二條抓走。
鏈再長一位就更誇張。輸入 "abc" 配 [a→b, b→c, c→d],輸出是 "ddd"、取代次數 6;這三條規則單獨跑只能得到 "bbc"、"acc"、"abd",各 1 次。同一批規則、同一份輸入,只是讓它們在頁面上排了個隊,計數就從 1+1+1 變成 6。
圖 1:同一批規則,單跑與串跑的命中差
頁面上只有新增規則、清空規則和複製三個按鈕,規則列不提供上移下移。要修正順序只能刪掉重排,或分兩遍跑:把第一遍的輸出複製出來,當第二遍的輸入。
三、取代字串裡的 $,不認「勾了正則」這個前提
取代那一側的坑更容易踩,因為它和正則那個勾選框無關。規則 [Hello→$&] 在純字面模式下跑完,輸出和輸入一字不差,取代次數卻照常顯示 1——$& 是命中內容本身,於是這一趟什麼都沒改成,計數卻不認帳。
想替命中的詞包一層標記:[Hello→<b>$&</b>],跑出來的就是 <b>Hello</b>,勾不勾正則都一樣。
體量也會跳。預設那三行文字是 43 個字元,只加一條 [World→$`|$'] 就膨脹成 77 個字元,命中只有 1 處:$` 取命中之前的全部文字,$' 取之後的全部文字,取代字串裡塞進去的是這兩段拼接。
正則檔裡的 $1 更薄。[(Hello)|(World)→$1] 把 11 個字元的 "Hello World" 削成 6 個字元——命中來自第二個分支時,$1 對應的那個群組沒有參與比對,取代結果變成空字串。要輸出字面的 $1,得寫 $$1。
四、「取代次數」量的不是改了多少處
預設檔按處累加:同一份預設文字計 3 次。勾選「僅取代第一個符合」,尺子換成每條規則最多 +1,同一份輸入只剩 1 次。兩檔都沒有錯,但把它當成「改動量」的憑證就會誤判:[Hello→Hello] 這種一個字都沒動的取代,預設檔同樣計 3 次。
「原始字元 / 結果字元」兩格也不能當成位元組看:數的是 UTF-16 碼元,不是位元組也不是字數——"中文測試" 頁面顯示 4,UTF-8 編碼是 12 位元組。
五、正則寫錯的時候,頁面一聲不響
這一頁交給 JS 引擎,只拼 g 和 i 兩個 flag,不認 (?i)、(?m) 這類其他方言裡的前置寫法。1000 行、4889 個字元的文字,想用正則 (?m)^ 給每一行加個前綴,跑完是 0 命中、4889 個字元原樣,而「規則數量」那一格照常顯示 1。只寫 ^ 而不帶 (?m) 時只算 1 處,那份輸入實際只替第一行加上前綴,4889 變 4891。
樣板裡的 catch 是空的,壞規則被跳過卻不報錯,唯一的線索是命中數比預期少。「尋找」裡只寫一個反斜線、又勾著正則,同樣會被當壞規則丟掉,規則數量照報。想確認寫法本身,先在正則測試那篇裡單獨驗一遍。
六、大文字很穩,巢狀量詞會拖死整頁
規模分兩頭看。字面取代很穩:100000 個字元的輸入配一條規則實測 8.8 ms;可規則一鏈起來,5 條 [a→b, b→c, c→d, d→e, e→f] 就把取代次數推到 500000。另一頭是新增規則這個動作本身,500 行規則實測 1068.7 ms——頁面每加一行就全量重跑一遍。
真正會卡死的是回溯。正則 (a+)+b:20 個 a 是 52.1 ms,24 個 a 是 758.2 ms,26 個 a 到 3211.7 ms,每多 2 個字元約 4.2 倍。它同步跑在主執行緒上,這幾秒整頁不動,也不會有任何提示。
圖 2:巢狀量詞下輸入長度與耗時
七、我現在動手前會核對的五件事
| 現象 | 先看什麼 | 實測依據 |
|---|---|---|
| 結果比預期多改了一遍 | 規則列順序,後一條的尋找是否撞上前一條的輸出 | "abc" 三條鏈式 6 次,單跑各 1 次 |
| 輸出與輸入一字不差 | 取代字串裡的 $&,它等於命中內容本身 | 字面 [Hello→$&] 計 1 次 |
| 文字突然變長或變短 | $` 與 $' 各抄一遍前後文,$1 可能是空字串 | 43 個字元撐成 77,11 個字元削成 6 |
| 命中 0 也不報錯 | (?m)、(?i) 與裸反斜線 | 4889 個字元原樣,規則數量仍顯示 1 |
| 整頁幾秒不動 | 正則裡的巢狀量詞 | (a+)+b 在 26 個字元上 3211.7 ms |
說到底是一句話:把規則當流水線看它的中間態,而不是當對照表。我先只放一條規則跑一遍、記下取代次數,再加第二條看數字怎麼變;跑完把原始輸入與最終輸出各貼一份到兩段文字對比裡看差額落在哪幾行,確認互不吃掉的那幾條再分兩批丟進文字批次取代的規則框。
Q:為什麼第二條規則把第一條的結果又改了一遍?
因為規則是依序覆蓋執行的,第 2 條作用在第 1 條的輸出上。"a1 b2" 配 [1→2, 2→3] 得 "a3 b3"、3 次,反向 [2→3, 1→2] 得 "a2 b3"、2 次。避開的辦法是讓後一條的尋找內容不出現前一條的取代結果裡,或乾脆分兩遍跑。
Q:取代字串裡的 $ 要怎麼打才算字面?
要字面的 $1 就寫 $$1。$&、$`、$' 在不勾正則時同樣被解析:[Hello→$&] 輸出與輸入一字不差卻計 1 次,[World→$`|$'] 把 43 個字元撐成 77 個字元。
Q:頁面卡住不動,是跑崩了嗎?
不一定是崩,也可能是回溯。100000 個字元的純字面取代只要 8.8 ms,而 (a+)+b 在 26 個字元上要 3211.7 ms,且同步跑在主執行緒、零提示。先查正則裡有重量詞的巢狀,再談文字規模。