杂记 › 技术笔记 › 替换次数报 5 还是报 1:批量查找替换的三个开关与零宽匹配实测

替换次数报 5 还是报 1:批量查找替换的三个开关与零宽匹配实测

批量查找替换的三个开关各改了什么:字面档转义表点名14个元字符,摘掉g后替换次数改看整串变没变,零宽匹配统计4次而输出可以一字不差;空查找词整条丢弃,坏正则静默跳过。

同一条规则,「替换次数」这一格有两套量法

输入 cat cat cat cat cat,丢一条把 cat 换成 dog 的规则,统计区报 5;勾上「仅替换第一个匹配」再跑,报 1,输出里也确实只改掉最前面那一处。把替换词写回 cat 本身,两个数字变成 5 和 0——整串一个字没动,全量档照报 5。这页的「替换次数」不是一把尺子,是三套算法共用的一格。

这一篇只测 批量查找替换 的三个开关各自改了哪一段:模板实测 6240 字节、158 行,三个 checkbox 都没有 checked 属性,默认全不勾。规则行排队的先后、替换串里美元符号那一类展开,另一篇已经量过,本篇不碰。

字面档那张转义表点名 14 个元字符

「使用正则表达式」不勾时,查找词先过一遍字符类 /[.*+?^${}()|[\]\\]/g:命中的每一个前面插一根反斜杠,再交给 new RegExp。类里逐字点名的元字符共 14 个——. * + ? ^ $ { } ( ) | [ ] \。{ 与 } 都在表内,实测 a{2} 出来是 a\{2\};而 -、/、空格三样不在表内,a-b、a/b、a b 进去都原样出来。

替换词统一填 X、另两个开关关着,同一串查找词两档各跑一次:

查找词输入文本字面档正则档
a.ba.b aXb Y.b a.b23
a(b)a(b) a(b)x ab21
a{2}aa a{2} aaa12
a|ba|b a b c14
a?ba?b ab b13

字面档只认逐字符相同的那两处 a.b,输出 X aXb Y.b X;正则档的点吃任意字符,连 aXb 也算命中。差得最狠的是 a|b,1 对 4:竖线在字面档只是串里的一个符号,在正则档成了或运算。

区分大小写只增删 flags 里的一个字母

模板逐字写的是 caseSensitive ? 'g' : 'gi':不勾(默认)拿到带 i 的 gi,勾上才是 g。输入 Hello hello HELLO heLLo、查找 hello、替换 X:不勾报 4,原始字符 23 变结果字符 7;勾上报 1,只有纯小写那个被换掉,23 变 19,两档差 3 次。

「仅替换第一个匹配」把 g 摘掉,顺手换了尺子

这一档做两件事:先用 flags = flags.replace('g', '') 把 g 摘掉,让替换只落在第一处;计数分支则从「命中几条」换成比较替换前后的整串——if (before !== result) 才加一,于是每条规则在这一档最多贡献 1,加不加看的是这串变没变。

深色是这一格报的次数,浅色是真实改动的处数 cat→dog 全量档 报 5 / 改 5 cat→dog 仅第一个匹配 报 1 / 改 1 cat→cat 自等替换 全量报 5 / 仅第一个报 0 真实改动 0 处

图 1:同一批样本下「替换次数」的读数与真实改动处数

零宽匹配:统计那一格与落地那一格分家

非 firstOnly 档的那个数不来自替换的返回值:先用 pattern.source 带 g 重建一个正则去 match() 取长度,再另跑一次替换落地,两个对象互不携带状态。命中长度为零的正则,从这里开始和「改了几处」分道:

  • x* 于 abc(串里没有 x)统计 4 次;替换词填 X 得 XaXbXcX,结果字符从 3 涨到 7;填空串则输出 abc 一字不差,那一格仍报 4。
  • \b 于 a bc 统计 4 次,得 XaX XbcX,4 涨到 8。
  • (?=a) 于 aba 统计 2 次,得 XabXa,3 涨到 5。
  • 锚点 ^ 与 $ 于 abc 各统计 1 次,各只多插 1 个字符。
  • a* 于 baa 统计 3 次,替换词 A 得 AbAA,3 变 4。

空位为什么这么多:带 g 的匹配遇到零宽命中时靠 lastIndex 加一强制推进,/x*/g 打在 abx 上能收出 4 项(三个空串加一个 x)。这一档里统计次数与替换实际走的编辑次数逐例相等,真正对不上的是它与结果字符增减。

空查找词整条丢弃,坏正则一声不响

规则收集那行是 if (inputs[0].value):查找框留空的规则在收集阶段就被整条丢掉,「规则数量」显示的是过滤之后的条数,不等于屏幕上的行数。三行里第 2 行查找留空,规则数量报 2、与行数差 1;查找填 0 属真值字符串照样保留;三行查找全空则规则数量 0、替换次数 0、输出原样。

编译不过的正则走的是另一条:new RegExp 抛错被 catch (e) {} 这个空分支吞掉,没有提示文案,那条规则进了列表却没执行。三条规则 a→X、b→Y、一个左括号→Z 打在 a b c 上,第三条抛错,输出 X Y c 保留前两条结果、替换次数 2、规则数量照报 3;同样这三串在字面档会被转义成字面量、不抛错,次数变 3。唯一的线索就是次数比预期少。

「原始字符 / 结果字符」数的是 UTF-16 码元

那两格取的是 raw.length 与 result.length,既不是码点也不是字节:一枚 emoji 报 2,一个汉字「中」报 1,一个增平面汉字(U+20000)报 2;中文测试 报 4,UTF-8 编码是 12 字节。首屏预置的三行 Hello 是纯 ASCII,43 码元、43 字节,init() 塞的那条 Hello→Hi 跑完输出三行 Hi、结果字符 34——每处省 3 码元,三处共省 9。

规则行那个属性值是怎么拼出来的

addRule(find, replace) 生成规则行时把查找词直接拼进 HTML 属性,第 71 行逐字是 'value="' + (find || '') + '" oninput="process()"',两侧没有做引号处理。字符串桩里逐字比对拼出的片段与属性读回的值:查找词 Hello 拼出 value="Hello"、原样读回;含单引号的 a'b 拼出 value="a'b",单引号在双引号属性里不需要转义,也原样读回;含两个裸双引号的 say "hi" now 拼出的片段逐字是 value="say "hi" now",而属性读取从 value=" 起读到下一个裸双引号为止,实际读回停在 say ,后面那截落到属性之外。替换词那一格用的是同一行写法。

FAQ

Q:替换次数能当成「改了多少处」吗?

不能直接用。非 firstOnly 档它等于各条规则命中数的累加,零宽空位也进账;firstOnly 档换成整串变没变,每条最多 1。同一份输入两档读数是 5 和 1,自等替换时一个报 5、一个报 0。

Q:勾上正则后命中变多,是哪里坏了?

不是。字面档走那张点名 14 个元字符的转义表,正则档把同一串按语法解释,撞上的位置本来就不同。想先确认写法本身,另一篇记过单验的路子,这里不重复。

Q:统计区整块不见了,是不是没跑?

输入为空时四格连标签一起被置空,那是分支本身的行为;输入一串空格则照常显示。数字看着不对,先分辨它属于哪一档口径,再看有没有哪一行的查找框留空。

📎 相关文章

空一个框就 0 张卡:分数通分页的静默失败、跳号的「分数N」与 990 行不约分 2026-11-07 删右侧给空串、截右侧还整行:text-line-ops 的码元口径与越界不对称实测 2026-11-07 120 个词槽、15 条整句、1 句写死的开头:lorem-ipsum 的词表清点与重复片段实测 2026-11-05 SVG 预览的尺寸听谁的:显示框恒等于容器,删掉 viewBox 后 r=1 实测 2 像素 2026-11-05 文本分列后空字段没丢:a,,b 照留、列号越界填空、填 0 或 abc 退回全部列实测 2026-11-05