正则表达式与字符串处理
2026/7/9大约 7 分钟
正则表达式与字符串处理
Go 开发者已知
Go 的 regexp 包使用 RE2 语法,不支持捕获组命名、Lookahead/Lookbehind 等高级特性。Go 的字符串格式化使用 fmt.Sprintf。
// Go 正则表达式
import "regexp"
// 编译正则为 regexp.Regexp 对象
re := regexp.MustCompile(`\d+`)
matched := re.MatchString("hello123") // true
result := re.FindString("abc123def456") // "123"
results := re.FindAllString("abc123def456", -1) // ["123", "456"]
// Go 正则不支持:
// 1. 命名捕获组 (?P<name>...)
// 2. Lookahead (?=...) / (?!...)
// 3. Lookbehind (?<=...) / (?<!...)
// 4. 反向引用 \1
// Go 字符串格式化
name := "World"
greeting := fmt.Sprintf("Hello %s", name) // "Hello World"
// strings 包常用操作
import "strings"
strings.HasPrefix(s, "http") // 检查前缀
strings.HasSuffix(s, ".go") // 检查后缀
strings.Split(s, ",") // 分割
strings.Join(arr, ",") // 合并
strings.TrimSpace(s) // 去除空白
strings.ReplaceAll(s, "o", "0") // 替换所有TS 怎么做
RegExp 类型与修饰符
// RegExp 类型
let re: RegExp = /\d+/;
// 或使用构造函数
re = new RegExp("\\d+", "gi"); // 转义需要双反斜杠
// 常用修饰符
// i — 忽略大小写
// g — 全局匹配(查找所有匹配而非第一个)
// m — 多行模式(^ 和 $ 匹配每行)
// s — dotAll(. 匹配换行符)
// u — Unicode 模式
// y — 粘滞模式(从 lastIndex 开始匹配)
const text = "Hello HELLO hello";
console.log(text.match(/hello/i)); // ["Hello"](第一个匹配,忽略大小写)
console.log(text.match(/hello/gi)); // ["Hello", "HELLO", "hello"](全局)
// dotAll 模式(ES2018+)
console.log(/hello.world/.test("hello\nworld")); // false
console.log(/hello.world/s.test("hello\nworld")); // trueString 正则方法
const text = "My email is alice@example.com and bob@test.org";
// String.match —— 返回匹配结果
const single = text.match(/\w+@\w+\.\w+/);
// ["alice@example.com", index: 12, ...]
const all = text.match(/\w+@\w+\.\w+/g);
// ["alice@example.com", "bob@test.org"]
// String.matchAll —— 返回迭代器(g 模式下获取捕获组)
const matches = text.matchAll(/(\w+)@(\w+)\.(\w+)/g);
for (const match of matches) {
console.log(match[0]); // 完整匹配
console.log(match[1]); // 用户名
console.log(match[2]); // 域名
console.log(match[3]); // TLD
}
// String.replace —— 替换
console.log(text.replace(/\w+@\w+\.\w+/g, "[REDACTED]"));
// "My email is [REDACTED] and [REDACTED]"
// replace 支持回调函数
const censored = text.replace(/(\w+)@(\w+)\.(\w+)/g,
(match, username, domain, tld) => `${username[0]}***@${domain}.${tld}`
);
// "My email is a***@example.com and b***@test.org"
// String.search —— 返回索引(不支持 g 模式)
const index = text.search(/@/); // 12
// String.split —— 正则可用于分割
const parts = "a,b;c:d".split(/[,;:]/); // ["a", "b", "c", "d"]命名捕获组
// 命名捕获组 —— Go 不支持!
const dateStr = "2026-07-09";
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = dateStr.match(dateRegex);
if (match?.groups) {
console.log(match.groups.year); // "2026"
console.log(match.groups.month); // "07"
console.log(match.groups.day); // "09"
}
// replace 中使用命名引用
const formatted = dateStr.replace(
dateRegex,
"$<day>/$<month>/$<year>"
); // "09/07/2026"Lookahead / Lookbehind(ES2018+)
// Lookahead —— 前瞻断言
// Go 的 regexp 不支持!
// 正向前瞻 (?=pattern) —— 后面跟着某个模式
const prices = "Price: $10, Discount: $5";
const dollarAmounts = prices.match(/\$\d+(?=\s|,)/g);
// ["$10", "$5"]
// 负向前瞻 (?!pattern) —— 后面不跟着某个模式
const noDiscount = prices.match(/\$\d+(?!\s*Discount)/g);
// ["$10"]
// Lookbehind —— 后顾断言
// 正向后顾 (?<=pattern) —— 前面有某个模式
const afterPrice = prices.match(/(?<=Price:\s)\$\d+/);
// ["$10"]
// 负向后顾 (?<!pattern) —— 前面没有某个模式
const notAfterDiscount = prices.match(/(?<!Discount:\s)\$\d+/);
// ["$10"]模板字面量
// 模板字面量 —— TS 原生字符串插值
const name = "World";
const greeting = `Hello ${name}!`; // "Hello World!"
// 支持表达式
const sum = `1 + 2 = ${1 + 2}`; // "1 + 2 = 3"
// 多行字符串
const multiLine = `
Line 1
Line 2
Line 3
`.trim();
// 标签模板(Tagged Template)—— 模板字面量的高级用法
function highlight(strings: TemplateStringsArray, ...values: string[]) {
return strings.reduce((result, str, i) => {
const value = values[i] ? `<strong>${values[i]}</strong>` : "";
return result + str + value;
}, "");
}
const user = "Alice";
const role = "admin";
const html = highlight`User ${user} has role ${role}`;
// "User <strong>Alice</strong> has role <strong>admin</strong>"TypeScript
// TS 模板字面量类型(编译期)
type EventName = `on${Capitalize<string>}`;
// 可以匹配 "onClick" | "onChange" | "onSubmit" 等
// 模板字面量类型做字符串解析
type Route = `/api/${string}/v${number}`;
const route1: Route = "/api/users/v1"; // OK
const route2: Route = "/api/users/v2"; // OK
// const route3: Route = "/api/users/abc"; // 类型错误Go
// Go 等价操作
name := "World"
greeting := fmt.Sprintf("Hello %s", name)
// Go 没有模板字面量类型的概念
// Go 的多行字符串用反引号
multiLine := `
Line 1
Line 2
Line 3
`差异分析
| 维度 | Go (regexp) | TypeScript (RegExp) |
|---|---|---|
| 引擎 | RE2 (保证线性时间) | 各浏览器实现不同 (V8/SpiderMonkey) |
| 命名捕获组 | 不支持 | (?<name>...) + match.groups |
| Lookahead | 不支持 | (?=...) / (?!...) |
| Lookbehind | 不支持 | (?<=...) / (?<!...) |
| 反向引用 | 不支持 | \1, \2 等 |
| 回溯 | 无回溯(性能稳定) | 可能回溯(极端情况 DoS) |
| Unicode 支持 | 好 | 好(u 修饰符) |
| 字符串插值 | fmt.Sprintf | 模板字面量 `Hello ${name}` |
| 多行字符串 | `raw string` 或 + 拼接 | `template literal` 原生支持 |
| 编译期字符串类型 | 无 | 模板字面量类型 |
Bad Practice
1. 用正则解析 HTML/XML
// Bad: 用正则解析 HTML —— 几乎永远会出错
const html = '<div class="content">Hello <b>World</b></div>';
const matches = html.match(/<div.*?>(.*?)<\/div>/); // 可能匹配错误
// 嵌套标签、属性变化、注释等都会导致正则失败
// Best: 使用 DOMParser
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const content = doc.querySelector(".content")?.textContent;
// 不要用正则解析 HTML —— Zalgo 名言:
// "When you try to parse HTML with regex, you will be visited by the nine lords of chaos."2. 不加 g 修饰符调用多次 match
// Bad: match 不加 g 只返回第一个
const text = "abc123def456ghi789";
let match;
while ((match = /\d+/.exec(text)) !== null) {
// 如果不加 g,exec 会无限循环(总是从开头匹配)
console.log(match[0]);
}
// Best: 使用 matchAll
for (const match of text.matchAll(/\d+/g)) {
console.log(match[0]);
}3. 正则搜索中的 ReDoS 风险
// Bad: 可能导致 ReDoS(正则拒绝服务攻击)
const vulnerable = /^(a+)+b$/;
"a".repeat(30) + "c"; // 灾难性回溯!指数级时间
// 避免嵌套量词
// (a+)+ → a+
// (a|b)* → [ab]*
// Best: 保持正则简单,避免嵌套重复
const safe = /^a+b$/;4. 忘记转义特殊字符
// Bad: 忘记转义正则中的特殊字符
function searchLiteral(text: string, search: string) {
return text.match(new RegExp(search)); // search 中的 . * ? 等会被解释为正则语法
}
// Best: 转义用户输入
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function searchLiteralSafe(text: string, search: string) {
return text.match(new RegExp(escapeRegExp(search), "g"));
}Best Practice
1. 预编译正则为模块顶层常量
// Best: 模块顶层预编译 —— 避免重复编译开销
// 类似于 Go 的 var re = regexp.MustCompile(...)
// validators.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const URL_REGEX = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
const PHONE_REGEX = /^1[3-9]\d{9}$/;
export function isValidEmail(email: string): boolean {
return EMAIL_REGEX.test(email);
}
export function isValidUrl(url: string): boolean {
return URL_REGEX.test(url);
}
export function isValidPhone(phone: string): boolean {
return PHONE_REGEX.test(phone);
}2. 封装命名捕获组解析函数
// Best: 封装命名捕获组返回类型安全的结果
interface ParsedDate {
year: number;
month: number;
day: number;
}
const DATE_REGEX = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
function parseDate(dateStr: string): ParsedDate | null {
const match = dateStr.match(DATE_REGEX);
if (!match?.groups) return null;
return {
year: parseInt(match.groups.year, 10),
month: parseInt(match.groups.month, 10),
day: parseInt(match.groups.day, 10),
};
}
// 使用
const date = parseDate("2026-07-09");
if (date) {
console.log(`${date.year}年${date.month}月${date.day}日`);
}3. 模板字面量代替字符串拼接
// Bad: 字符串拼接
const path = "/api/" + resource + "/" + id + "?lang=" + lang + "&v=" + version;
// Best: 模板字面量
const path2 = `/api/${resource}/${id}?lang=${lang}&v=${version}`;
// Best: 模板字面量做 URL 构建
function buildUrl(base: string, path: string, params: Record<string, string>): string {
const query = Object.entries(params)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&");
return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}${query ? `?${query}` : ""}`;
}4. 使用正则验证时封装类型守卫
// Best: 正则验证 + 类型守卫
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function isUUID(value: string): value is `${string}-${string}-${string}-${string}-${string}` {
return UUID_REGEX.test(value);
}
function processId(id: string) {
if (isUUID(id)) {
// 这里 id 被缩小为 UUID 字面量类型
console.log("Valid UUID:", id);
}
}
// 结合 Zod 更强大(见第8篇)5. 正则调试技巧:拆分复杂正则
// Best: 拆分复杂正则为多个简单正则
interface ParsedLogEntry {
timestamp: string;
level: "INFO" | "WARN" | "ERROR";
message: string;
}
// 拆分为子正则
const TIMESTAMP = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;
const LEVEL = /(INFO|WARN|ERROR)/;
const MESSAGE = /(.+)$/;
function parseLogLine(line: string): ParsedLogEntry | null {
const tsMatch = line.match(TIMESTAMP);
if (!tsMatch) return null;
const restAfterTs = line.slice(tsMatch[0].length).trim();
const levelMatch = restAfterTs.match(LEVEL);
if (!levelMatch) return null;
const message = restAfterTs.slice(levelMatch[0].length).trim();
if (!message) return null;
return {
timestamp: tsMatch[0],
level: levelMatch[1] as ParsedLogEntry["level"],
message,
};
}
// 日志行: "2026-07-09 10:30:00 ERROR Failed to connect to database"
const entry = parseLogLine("2026-07-09 10:30:00 ERROR Failed to connect to database");总结
- Go 的
regexp基于 RE2(安全但功能有限),TS 的RegExp功能更丰富但需警惕 ReDoS - 命名捕获组和 Lookahead/Lookbehind 是 TS 正则的杀手级特性
- 模板字面量比
fmt.Sprintf更直观,且 TS 有编译期模板字面量类型 - 正则一定要预编译到顶层常量,不要在循环中重复编译
- 绝不用正则解析 HTML/XML