# Regex 正则表达式

# 概述

MT 正则表达式库提供了一套与 Java 标准正则表达式兼容的 API,同时针对文本编辑器场景进行了优化。与系统的 java.util.regex 相比,MT 正则库可以直接在 BufferedText 等文本缓冲区上进行匹配,避免了 toString() 转换带来的性能开销。

主要特性

  • 性能优化:直接在文本缓冲区上匹配,避免字符串转换
  • 超时控制:支持设置匹配超时时间,防止复杂正则导致卡顿
  • 快照功能:可以保存匹配状态,避免重复计算
  • 扩展功能:提供 lookingAt(int) 等扩展方法,支持完整单词匹配标志
  • 替换增强:支持捕获组引用和大小写转换

# 核心类

# Regex

正则表达式工具类,提供静态方法用于编译正则表达式和验证替换模板。

# 编译正则表达式

// 编译正则表达式
Pattern pattern = Regex.compile("\\d+");

// 使用指定标志编译正则表达式
Pattern pattern = Regex.compile(
    "hello",
    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
);

参数说明

  • regex:正则表达式字符串
  • flags:匹配标志位,可使用 | 组合多个标志

# 验证替换模板

// 验证替换模板语法是否正确
try {
    Regex.checkReplacementTemplate(pattern, "替换为:$1");
} catch (PatternSyntaxException e) {
    // 替换模板语法错误
    Log.e("替换模板验证失败", e);
}

功能说明

checkReplacementTemplate() 方法用于验证替换模板的语法是否正确,包括:

  • 转义序列是否完整(如 \n\t\\ 等)
  • $ 符号后是否跟随了数字
  • ${} 语法格式是否正确
  • 命名捕获组是否存在(如果提供了 pattern 参数)

注意:该方法不会检查数字序号的捕获组是否真实存在。例如,即使正则表达式只有 2 个捕获组,$3 也能通过语法验证,但在实际替换时不会产生任何输出。

# Pattern

正则表达式模式,表示已编译的正则表达式。

# 匹配标志

Pattern 提供了以下匹配标志,可以通过 | 运算符组合使用:

标志 说明 嵌入标志
UNIX_LINES Unix 行模式,只识别 \n 作为行终止符 (?d)
CASE_INSENSITIVE 忽略大小写匹配 (?i)
COMMENTS 注释模式,忽略空白字符和 # 开头的注释 (?x)
MULTILINE 多行模式,^$ 匹配行的开始和结束 (?m)
LITERAL 字面量模式,将模式字符串视为普通字符 -
DOTALL 点号通配模式,使 . 匹配包括换行符在内的任意字符 (?s)
UNICODE_CASE Unicode 大小写折叠,与 CASE_INSENSITIVE 配合使用 (?u)
CANON_EQ 规范等价,按 Unicode 规范分解进行字符匹配 -
UNICODE_CHARACTER_CLASS Unicode 字符类,启用 Unicode 版本的预定义字符类 (?U)
MATCH_WHOLE_WORD 全词匹配(MT 扩展) -

标志说明

  • CASE_INSENSITIVE:默认仅对 US-ASCII 字符生效,配合 UNICODE_CASE 可支持 Unicode 字符,可能影响性能
  • DOTALL:启用后 . 可以匹配换行符,适用于跨行匹配场景
  • UNICODE_CASE:配合 CASE_INSENSITIVE 使用时按 Unicode 标准进行大小写匹配,可能影响性能
  • UNICODE_CHARACTER_CLASS:启用 Unicode 版本的 \d\w 等字符类,隐含启用 UNICODE_CASE,可能影响性能
  • MATCH_WHOLE_WORD:MT 正则自定义扩展标志,用于精确的完整单词匹配

# 创建匹配器

Pattern pattern = Regex.compile("\\w+");
Matcher matcher = pattern.matcher("Hello World");

# 获取模式信息

String regex = pattern.pattern();  // 获取正则表达式字符串
int flags = pattern.flags();        // 获取标志位

# Matcher

正则表达式匹配器,用于对文本序列执行匹配操作。

# 基本匹配方法

# matches() - 完全匹配

判断整个输入序列是否与正则表达式完全匹配。

Pattern pattern = Regex.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher matcher = pattern.matcher("2024-01-15");
if (matcher.matches()) {
    Log.i("完全匹配成功");
}
# find() - 查找匹配

从当前位置开始查找下一个匹配的子序列。

Pattern pattern = Regex.compile("\\d+");
Matcher matcher = pattern.matcher("价格:100元,折扣:20元");

while (matcher.find()) {
    Log.i("找到数字:" + matcher.group());
}
// 输出:100, 20

可以从指定位置开始查找:

if (matcher.find(10)) {
    Log.i("从位置10开始找到:" + matcher.group());
}
# lookingAt() - 前缀匹配

判断输入序列的开头是否与正则表达式匹配,不要求匹配整个序列。

Pattern pattern = Regex.compile("https?://");
Matcher matcher = pattern.matcher("http://example.com/path");

if (matcher.lookingAt()) {
    Log.i("以 http:// 或 https:// 开头");
}

MT 正则扩展了 lookingAt(int start) 方法,可以从指定位置开始匹配:

// 从位置 7 开始匹配
if (matcher.lookingAt(7)) {
    Log.i("从位置7开始匹配成功");
}

# 获取匹配结果

在调用 matches()find()lookingAt() 成功后,可以获取匹配的详细信息。

# 获取匹配内容
String matched = matcher.group();      // 获取整个匹配内容
String group1 = matcher.group(1);      // 获取第1个捕获组
# 获取匹配位置
int start = matcher.start();           // 获取匹配起始位置
int end = matcher.end();               // 获取匹配结束位置
int group1Start = matcher.start(1);   // 获取第1个捕获组起始位置
int group1End = matcher.end(1);       // 获取第1个捕获组结束位置
# 获取捕获组数量
int count = matcher.groupCount();  // 获取捕获组数量(不包括组0)

# 匹配状态快照(MT 扩展)

toSnapshot() 方法可以创建当前匹配状态的快照,用于保存匹配结果以便后续访问。

Pattern pattern = Regex.compile("(\\w+)@(\\w+\\.\\w+)");
Matcher matcher = pattern.matcher("user@example.com");

if (matcher.find()) {
    // 保存匹配快照
    MatcherSnapshot snapshot = matcher.toSnapshot();

    // 继续使用 matcher 进行其他操作...

    // 稍后可以从快照获取之前的匹配结果
    String email = snapshot.group(0);
    String username = snapshot.group(1);
    String domain = snapshot.group(2);
}

快照的优势

  • 避免重复调用 find()matches()
  • 可以保存多个匹配结果
  • 在异步场景中缓存匹配信息

# 替换操作

# replaceAll() - 替换所有匹配
Pattern pattern = Regex.compile("\\d+");
Matcher matcher = pattern.matcher("价格100元,优惠20元");
String result = matcher.replaceAll("**");
// 结果:价格**元,优惠**元
# replaceFirst() - 替换首个匹配
String result = matcher.replaceFirst("**");
// 结果:价格**元,优惠20元
# 使用捕获组引用

替换字符串支持引用捕获组:

Pattern pattern = Regex.compile("(\\w+)@(\\w+)");
Matcher matcher = pattern.matcher("user@example");
String result = matcher.replaceAll("$1 at $2");
// 结果:user at example

支持的捕获组引用格式:

  • $n:引用第 n 个捕获组
  • ${n}:引用第 n 个捕获组(更清晰)
  • ${name}:引用命名捕获组
# 转义和特殊字符

替换字符串支持以下转义序列:

转义序列 说明
\n 换行符
\r 回车符
\t 制表符
\$ 字面量 $ 符号
\\ 字面量 \ 符号
# 大小写转换

替换字符串支持对捕获组内容进行大小写转换:

转换符 说明
\l 将后续一个字符转为小写
\u 将后续一个字符转为大写
\L 将后续所有字符转为小写
\U 将后续所有字符转为大写

示例

Pattern pattern = Regex.compile("(\\w+)");
Matcher matcher = pattern.matcher("hello world");

// 将每个单词首字母大写
matcher.replaceAll("\\u\\L$1");  // 结果:Hello World

// 全部转为大写
matcher.replaceAll("\\U$1");  // 结果:HELLO WORLD

// 全部转为小写
matcher.replaceAll("\\L$1");  // 结果:hello world
# 高级替换

使用 appendReplacement()appendTail() 进行逐步替换:

Pattern pattern = Regex.compile("\\d+");
Matcher matcher = pattern.matcher("价格100元,优惠20元");
StringBuilder sb = new StringBuilder();

while (matcher.find()) {
    int num = Integer.parseInt(matcher.group());
    // 将数字翻倍
    matcher.appendReplacement(sb, String.valueOf(num * 2));
}
matcher.appendTail(sb);

Log.i(sb.toString());  // 价格200元,优惠40元

使用 expandReplacement() 展开替换模板:

if (matcher.find()) {
    String replacement = matcher.expandReplacement("匹配到:$0");
    Log.i(replacement);
}

# 超时控制

为了防止复杂正则表达式导致性能问题,MT 正则支持设置匹配超时时间。

Matcher matcher = pattern.matcher(text);
matcher.setTimeoutMillis(3000);  // 设置超时时间为3秒

if (matcher.find()) {
    // 匹配成功
} else {
    // 匹配失败或超时
}

默认超时时间:2000 毫秒(2 秒)

超时行为:超时后匹配方法会直接返回 false,不会抛出异常。

# 区域控制

可以限制匹配只在文本的指定范围内进行:

Matcher matcher = pattern.matcher("Hello World");
matcher.region(0, 5);  // 只匹配 "Hello" 部分

if (matcher.find()) {
    Log.i("在区域内找到:" + matcher.group());
}

int start = matcher.regionStart();  // 获取区域起始位置
int end = matcher.regionEnd();      // 获取区域结束位置

# 边界控制

# 透明边界模式
// 默认使用不透明边界
matcher.useTransparentBounds(false);

// 启用透明边界,前瞻、后顾等可以看到区域外的内容
matcher.useTransparentBounds(true);

boolean isTransparent = matcher.hasTransparentBounds();
# 锚定边界模式
// 默认启用锚定边界,^ 和 $ 匹配区域边界
matcher.useAnchoringBounds(true);

// 禁用锚定边界,^ 和 $ 只匹配整个输入序列的边界
matcher.useAnchoringBounds(false);

boolean isAnchoring = matcher.hasAnchoringBounds();

# 匹配状态检查

# hitEnd() - 检查是否到达输入末尾

判断匹配器在搜索过程中是否读取到了输入序列的最后一个字符。

matcher.find();
if (matcher.hitEnd()) {
    Log.i("匹配过程中到达了输入末尾");
}
# requireEnd() - 检查匹配的稳定性

判断更多输入是否可能影响当前的匹配结果。

matcher.find();
if (matcher.requireEnd()) {
    Log.i("更多输入可能导致当前匹配失效");
} else {
    Log.i("更多输入不会导致当前匹配失效");
}

说明

  • 如果此方法返回 true 且找到了匹配,则更多输入可能导致匹配丢失
  • 如果此方法返回 false 且找到了匹配,则更多输入可能改变匹配但不会丢失匹配
  • 如果没有找到匹配,则此方法的返回值无意义

典型应用场景:在增量输入场景(如实时搜索)中,通过 requireEnd() 判断是否需要等待更多输入后再确定匹配结果。

# 重置匹配器

// 重置匹配器状态
matcher.reset();

// 重置并设置新的输入文本
matcher.reset("新的文本内容");

# 获取关联对象

Pattern pattern = matcher.pattern();      // 获取关联的 Pattern
CharSequence text = matcher.getText();    // 获取关联的文本序列

# 使用示例

# 基本匹配和查找

import bin.mt.plugin.api.regex.Matcher;
import bin.mt.plugin.api.regex.Pattern;
import bin.mt.plugin.api.regex.Regex;

// 编译正则表达式
Pattern pattern = Regex.compile("\\b\\w+@\\w+\\.\\w+\\b");
Matcher matcher = pattern.matcher("联系方式:admin@example.com 或 user@test.org");

// 查找所有邮箱地址
while (matcher.find()) {
    String email = matcher.group();
    int start = matcher.start();
    int end = matcher.end();
    Log.i("找到邮箱:" + email + " 位置:[" + start + ", " + end + ")");
}

# 捕获组提取

// 提取日期的年月日
Pattern pattern = Regex.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("今天是2024-01-15");

if (matcher.find()) {
    String year = matcher.group(1);
    String month = matcher.group(2);
    String day = matcher.group(3);
    Log.i("年:" + year + ", 月:" + month + ", 日:" + day);
}

# 内容替换和转换

// 将 Markdown 链接转换为 HTML
Pattern pattern = Regex.compile("\\[([^\\]]+)\\]\\(([^)]+)\\)");
Matcher matcher = pattern.matcher("查看[文档](http://example.com)");
String html = matcher.replaceAll("<a href=\"$2\">$1</a>");
// 结果:查看<a href="http://example.com">文档</a>

// 驼峰命名转下划线
Pattern camelPattern = Regex.compile("([a-z])([A-Z])");
Matcher camelMatcher = camelPattern.matcher("userName");
String snakeCase = camelMatcher.replaceAll("$1_\\L$2");
// 结果:user_name

# 文本编辑器中的应用

在文本编辑器中使用 MT 正则库处理 BufferedText

import bin.mt.plugin.api.text.BufferedText;

void searchInEditor(TextEditor editor) {
    BufferedText text = editor.getText();

    // 方式1:使用 BufferedText.matcher() (推荐)
    Pattern pattern = Regex.compile("TODO:.*");
    Matcher matcher = text.matcher(pattern);

    // 方式2:使用 Pattern.matcher()
    // Matcher matcher = pattern.matcher(text);

    while (matcher.find()) {
        String todo = matcher.group();
        Log.i("找到 TODO:" + todo);
    }
}

性能提示

对于 BufferedText 文本,推荐使用 MT 正则库的 BufferedText.matcher() 方法创建匹配器。MT 正则库直接在文本缓冲区上进行匹配,避免了 toString() 转换。

错误示范

// ❌ 不要使用系统正则库处理 BufferedText
java.util.regex.Pattern sysPattern = java.util.regex.Pattern.compile("pattern");
sysPattern.matcher(bufferedText);  // 这会调用 toString(),导致性能问题

正确做法

// ✅ 使用 MT 正则库
Pattern mtPattern = Regex.compile("pattern");
bufferedText.matcher(mtPattern);  // 或 mtPattern.matcher(bufferedText)

# 高级匹配控制

Pattern pattern = Regex.compile("test");
Matcher matcher = pattern.matcher("This is a test in a test file");

// 设置超时时间
matcher.setTimeoutMillis(5000);

// 设置匹配区域
matcher.region(10, 20);

// 启用透明边界
matcher.useTransparentBounds(true);

// 查找匹配
if (matcher.find()) {
    MatcherSnapshot snapshot = matcher.toSnapshot();
    // 保存匹配快照供后续使用
}

# 注意事项

  1. 性能考虑

    • 复杂的正则表达式可能导致性能问题,建议设置合理的超时时间
    • 对于大文本,考虑使用区域控制限制匹配范围
    • 在文本编辑器中处理 BufferedText 时,务必使用 MT 正则库而非系统库
  2. 标志组合

    • UNICODE_CASE 需要与 CASE_INSENSITIVE 配合使用
    • UNICODE_CHARACTER_CLASS 隐含启用 UNICODE_CASE
    • Unicode 相关标志可能影响性能,仅在必要时使用
  3. 捕获组引用

    • 捕获组编号从 1 开始,0 表示整个匹配
    • 如果引用的捕获组不存在或未参与匹配,group() 返回 null
    • 替换模板语法验证不检查数字捕获组是否存在
  4. 边界控制

    • 默认使用不透明边界和锚定边界
    • 修改边界设置会影响 ^$\b 等边界匹配符的行为
    • 在使用 region() 时需要特别注意边界设置
  5. 线程安全

    • Pattern 对象是线程安全的,可以在多个线程间共享
    • Matcher 对象不是线程安全的,每个线程应使用独立的匹配器实例

# 相关接口