qinghuan
← 回到知识探索

后端现场 / 可下载小项目

搜索词已经转义,为什么替换成 $1 仍然变了?

把搜索框内容和替换框内容分开追踪,用点号、美元符号与反斜线验证两套解释规则。

展开本文目录
  1. 下载完整练习包
  2. 第一步:先只改变搜索位置
  3. 第二步:搜索正确,也要检查替换位置
  4. 第三步:把两个输入接回同一个方法
  5. 第四步:把产品规则留在 API 之外
  6. 运行单文件,按实际输出逐项核对
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
开始前,会这些就够了
  • 会读 Java 字符串字面量;知道源码里的双反斜线表示运行时一个反斜线。
  • 理解方法参数与返回值;了解正则分组的基本用途。
  • 先读 把模糊需求写成可测试的提示:已有单遍模板实验说明替换值按字面写入;接着对比用户搜索词与替换词各自的解释规则。
遇到不熟悉的词,先看这 2 个解释
字面量搜索
把输入按原样字符查找,不把点号或括号当成规则。
字面量替换
把替换框里的美元符号和反斜线按原样写进结果。
打开完整术语小抄 ↗

后台有一个“批量替换商品说明”的工具,用户输入搜索词 a.b,希望只改这三个字符。随后又输入替换内容 $1\archive,希望把它原样保存。两个输入都是普通文本,但只给搜索词转义,仍不能保证替换内容按原文写回。

本实验将同一段文本分别送进搜索规则和替换规则。你会看到点号使搜索范围扩大,而 $1 在替换位置会引用已经匹配到的分组。它们发生在不同阶段,所以需要分别确定输入的含义。

把例子带到自己的电脑上

下载完整练习包

需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • RegexLiteralDemo.java:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

java -version
javac -version
javac -encoding UTF-8 --release 17 RegexLiteralDemo.java
java -cp . RegexLiteralDemo

解压到独立练习文件夹,在含 RegexLiteralDemo.java 的目录打开终端运行。Windows 内存固定字符串;搜索/替换分别对照、特殊字符、未命中及空搜索规则;JDK17/21各9项断言。未测试文件写回、任意正则性能或其他语境转义。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

第二步:搜索正确,也要检查替换位置

代码用一个明确的捕获组匹配 a.b,再分别传入原始 $1 和经过 Matcher.quoteReplacement 处理的 $1。前者输出 a.b,后者输出 $1。这组对照把搜索固定住,只改变替换解释。

捕获组可以理解为匹配时暂存的一段文字;替换规则里的 $1 表示取第一段。因此原始 $1 并不是随意丢失,而是被执行成了替换指令。

Pattern.quote 处理要交给 Pattern 的文本;Matcher.quoteReplacement 处理要交给替换步骤的文本。两者不能因为名字都有 quote 就互换,也不能将同一份已处理字符串不断重复处理。

第三步:把两个输入接回同一个方法

literalReplace 先拒绝空搜索词,再分别处理 needle 和 replacement。完整输出保留了替换文本中的美元符号和反斜线,同时没有改动中间的 axb。

源码用 String.replace(CharSequence, CharSequence) 做一项独立对照:如果需求只是原样查找、原样替换,可以直接使用这个更短的入口。它的名字也叫 replace,但不能与接收正则的 replaceAll 混为一谈。

另一项输入故意带有反斜线加 E。它检查使用 API 处理复杂文本仍能找到原样片段;不要自己在任意输入两端拼接转义标记,以为中间永远不会包含结束标记。

第四步:把产品规则留在 API 之外

本例拒绝空搜索词,是工具自己的规则。空内容究竟表示“没有填写”还是“每个位置都匹配”,应由产品约定,不能交给某个库默认行为替你决定。

实跑只覆盖内存中的固定字符串、字面量搜索与替换、一个捕获组和未命中场景。没有读取用户文件、执行批量保存或测试正则性能,也不提供 SQL、HTML 等其他语境的转义。

带回项目时,分别记录输入来源、允许的搜索模式、替换是否支持分组、空输入规则,再写回归样本。这样排错时能知道问题发生在查找、替换还是写回。

运行单文件,按实际输出逐项核对

保存为 RegexLiteralDemo.java,在源码目录执行下方命令;需要完整 JDK 17 或更新版本。无需 Maven、网络或第三方依赖。

2026 年 10 月 18 日在 Windows 上分别以 Microsoft OpenJDK 17.0.20.1 与 21.0.12、--release 17 编译运行,输出一致。每项断言失败都会终止程序;下方是本次真实输出。

展开收起代码 · 小实验java · 36 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · java
import java.util.regex.*;
public class RegexLiteralDemo {
    static int checks;
    static void check(boolean ok) { if (!ok) throw new AssertionError(); checks++; }
    static String literalReplace(String input, String needle, String replacement) {
        if (needle.isEmpty()) throw new IllegalArgumentException("empty search is undefined here");
        return Pattern.compile(Pattern.quote(needle)).matcher(input)
                .replaceAll(Matcher.quoteReplacement(replacement));
    }
    public static void main(String[] args) {
        String input = "a.b axb a.b";
        String unquoted = Pattern.compile("a.b").matcher(input).replaceAll("X");
        String searchOnly = Pattern.compile(Pattern.quote("a.b")).matcher(input).replaceAll("X");
        check(unquoted.equals("X X X")); check(searchOnly.equals("X axb X"));
        System.out.println("unquoted_search=" + unquoted);
        System.out.println("quoted_search=" + searchOnly);
        Pattern grouped = Pattern.compile("(a\\.b)");
        String interpreted = grouped.matcher("a.b").replaceAll("$1");
        String literal = grouped.matcher("a.b").replaceAll(Matcher.quoteReplacement("$1"));
        check(interpreted.equals("a.b")); check(literal.equals("$1"));
        System.out.println("replacement_$1: interpreted=" + interpreted + "; literal=" + literal);
        String replacement = "$1\\archive";
        String result = literalReplace(input, "a.b", replacement);
        check(result.equals("$1\\archive axb $1\\archive"));
        check(result.equals(input.replace("a.b", replacement)));
        System.out.println("both_literal=" + result);
        String odd = "left\\E.right";
        check(literalReplace("<" + odd + ">", odd, "$0").equals("<$0>"));
        check(literalReplace("no match", "a.b", "$1").equals("no match"));
        boolean emptyRejected=false;
        try { literalReplace("abc", "", "X"); } catch (IllegalArgumentException e) { emptyRejected=true; }
        check(emptyRejected);
        System.out.println("embedded_quote_end=preserved; absent_match=unchanged; empty_search=rejected");
        System.out.println("PASS checks=" + checks);
    }
}

怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 RegexLiteralDemo.java;编译成功后,再执行 java -cp . RegexLiteralDemo。两条命令分别回车。2026-10-18 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Windows 内存固定字符串;搜索/替换分别对照、特殊字符、未命中及空搜索规则;JDK17/21各9项断言。未测试文件写回、任意正则性能或其他语境转义。

运行后对照结果
unquoted_search=X X X
quoted_search=X axb X
replacement_$1: interpreted=a.b; literal=$1
both_literal=$1\archive axb $1\archive
embedded_quote_end=preserved; absent_match=unchanged; empty_search=rejected
PASS checks=9

记住这三点

  • 搜索规则与替换规则是两个入口,各自确定是否按原文处理。
  • 原样查找使用 Pattern.quote;原样替换使用 Matcher.quoteReplacement。
  • 只做字面量替换时,可直接评估 String.replace。

别把例子用过头

  • 只转义搜索词,却把用户替换内容直接交给 replaceAll。
  • 把源码转义、正则转义和替换转义当成同一件事。
  • 没有定义空搜索词的行为。

自己试一小步

加入“只替换第一个命中”和“有意支持 $1”的两种模式,怎样避免含义混淆?

  1. 先列出每种模式对搜索和替换的承诺。
  2. 选择至少六组包含美元符号、反斜线、括号、空输入及未命中的数据,先预测结果。
  3. 保留普通文本模式,再新增明确命名的方法,不修改用户输入。
  4. 提交模式表、断言、完整输出与 Java 版本。

做到这些,就算完成

  • 字面量模式中的特殊字符保持原样。
  • 启用分组的模式说明缺失分组如何处理。
  • 没有把本例能力宣称为其他文本语境的转义工具。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。