产品改名、术语统一、域名迁移、接口字段重命名——这类活儿的工作量看起来只是「查找替换」,实际出事的地方从来不在替换本身,而在规则之间的互相干扰。
下面五类问题,每一类都能让一次改名变成「改了一半」或者「改坏了一片」。
一、咬尾:A→B 和 B→C 同时存在
对照表里有这两条:
旧名 → 新名
新名 → 更新的名
依次执行会这样跑:
第 1 遍:全文的「旧名」→「新名」 (此时原文的旧名和原文的新名混在一起了)
第 2 遍:全文的「新名」→「更新的名」 (两批一起被改了)
结果:原文的「旧名」变成了「更新的名」 ✗
这在改名场景里极其常见:分两批改名、循环重命名(甲→乙、乙→丙)、或者对照表是从两个来源合并来的。
正确的模式是「同时生效」:把原文扫一遍,每个位置只被一条规则命中,命中之后跳过去继续往后扫,产出的新文本不再参与匹配。
怎么提前发现自己会踩:
把对照表的左列和右列分别排序,看有没有同一个词既出现在左列又出现在右列。
有,就说明规则会咬尾。这个检查花不了一分钟,但能省掉一次全量回滚。
自己用 sed -i 逐条跑脚本时默认就是依次执行,这条坑踩中的概率非常高。
二、长词被短词切开
规则里同时有:
张三 → 李四
张三丰 → 王五
遇到「张三丰」时,如果「张三」这条先匹配上,结果是「李四丰」——长词被切掉了前半截。
英文同理:user 和 username、id 和 idcard、api 和 apiKey。
原因:把多条规则合并成一个正则时,| 取的是最先匹配的分支,不是最长的那个。所以必须按查找词长度降序排列再合并,和你在表里写的顺序无关。
这条对自己写脚本的人尤其重要——多数人是按对照表的原始顺序生成命令的,而对照表通常按字母序或业务含义排列,长短是乱的。
验证方法:造一个同时包含短词和长词的测试串跑一遍,看长词有没有被切开。
三、中文没有词边界
英文可以靠全词匹配解决误伤:
cat 加全词 → 不会命中 category ✓
中文不行。 正则的 \b 定义在「单词字符和非单词字符的交界」上,而汉字在正则引擎眼里通常整体算一类,字与字之间没有边界。
所以规则「张三」会命中:
我张三说 ← 误伤
张三丰 ← 误伤(这个可以靠长词优先解决)
张三的书 ← 正确
三种应对:
- 把上下文写进查找词 —— 用「张三:」「张三说」这类带标点或固定搭配的写法,而不是光一个词
- 用否定环视 ——
张三(?!丰),排除已知的误伤 - 先跑一遍看命中行,把误伤挑出来再补规则
这也是中文术语改名比英文标识符改名危险得多的根本原因,值得多留一道人工确认。
四、一个名字有六种写法
把 userProfile 改成 memberProfile,需要覆盖的变体:
| 形式 | 样子 | 出现在 |
|---|---|---|
| 小驼峰 | userProfile | JS/Java 变量、字段 |
| 大驼峰 | UserProfile | 类名、组件名 |
| 蛇形 | user_profile | 数据库字段、Python |
| 短横线 | user-profile | CSS 类、URL、文件名 |
| 全大写 | USER_PROFILE | 常量、环境变量 |
| 空格分隔 | User Profile | 界面文案、文档标题 |
漏掉任何一种,结果就是「改了一半」——代码能跑,但某个接口字段、某个 CSS 类、某个环境变量还是旧名,而且往往几个月后才被发现。
做法是:改名前先把六种变体各搜一遍,看哪些真实存在,只为存在的写规则。
不存在的不要写——多一条规则就多一份误伤的机会,而且命中数为零的规则会稀释你对「命中数」这个信号的敏感度。
五、越界:改到了不该改的地方
三类地方几乎永远不该改:
- 依赖目录和构建产物 ——
node_modules、dist、build、vendor - 锁文件 ——
package-lock.json、pnpm-lock.yaml - 二进制和第三方源码
改到锁文件或依赖里,表现是构建失败或者行为诡异,而且不会有人想到是改名造成的,排查很花时间。
还有一类更隐蔽的越界:同一个词在不同上下文里含义不同。比如要改的词恰好也出现在 URL 里、注释里的历史说明里、或者某个字符串字面量里(那个字符串可能是对外协议的一部分,改了就不兼容了)。
这一类没有自动化的解法,只能靠执行前逐条看命中行。
把命中数当成校验手段
这是整套流程里最有价值的一个习惯:
改之前,每个词先全局搜一遍,记下命中多少处。 这个数字有三个用途:
- 摸清规模 —— 是 20 处还是 2000 处,决定了要不要分批
- 发现意外的变体 —— 你以为不存在的写法,搜一下才发现有 30 处
- 作为事后校验的基准 —— 改完再搜一次旧词,应该是 0
执行时再看每条规则的实际命中:
| 现象 | 通常意味着 |
|---|---|
| 命中 0 | 词写错了、多了空格、原文是全角标点、大小写没对上,或者这个词根本不在这批文件里 |
| 命中数远超预期 | 匹配范围太宽,多半是漏了全词匹配,或者中文词太短 |
| 整批文件都是 0 | 先确认编码读对了——按错的编码读进来,中文一个也匹配不上 |
批量文本替换 会把命中 0 的规则标成橙色,就是为了让这类问题在执行前被看见,而不是执行后。
执行策略
第一遍不要原地覆盖。 先导出到新目录做对比,确认无误再覆盖。原地覆盖不可撤销、不进回收站。
有版本控制的话事情简单得多:先提交一次干净的状态,改完用 diff 逐文件过一眼,不对就 git checkout 回去。这比任何备份机制都可靠。
顺带一提,改完发现某些文件被误改想回滚时的几条路径,见 git reset —hard 之后怎么捞回来。
分工
三个相邻的工具容易混:
| 需求 | 用哪个 |
|---|---|
| 把一条正则调对 | Regex Pro —— 单文本、单条正则的测试台 |
| 多文件、多规则改内容 | 批量文本替换 |
| 改文件名不动内容 | 文件批量重命名 |
先在 Regex Pro 里把正则调对,再拿到批量替换里跑多文件——这个顺序能避免「几百个文件跑完才发现正则写错了」。正则本身的常见写法见 10 个最常用的正则,捕获组和替换模板的用法见 正则替换模板与跨语言导出。