Regex 正则表达式速查:元字符、分组、Lookahead/Lookbehind 一页搞定
正则表达式(regex)是一种小巧的模式语言,用来匹配文本。\d+ 表示「一个或多个数字」,^Error 表示「以 Error 开头的行」,仅此而已。本篇速查把语法压在一页里:元字符、量词、锚点、分组、Lookaround、Flag,再加上 15+ 个能直接粘进 JavaScript 或 Python 的常用模式。
文章面向已经知道字符串是什么、想要一份参考而不是入门导览的开发者。只想查符号?跳到「快速参考表」。想搞清楚 Lookaround 和踩过哪些坑?看对应章节,它们的失误能让一台服务器卡死。
1. 2026 年了,正则还值得学吗
正则表达式会被编译成状态机,扫描字符串后要么匹配、要么失败。语法很小,能干的事却不少。
AI 现在能帮你起草模式,但有几类活儿仍然属于手写正则:
- 日志解析:一千万行
nginxaccess log,要从某个 User-Agent 里捞出所有 5xx 请求。一段 40 字符的正则配合grep -E几秒搞定;逐行调用 LLM 的方案是没法跑的。 - 表单与字段校验:电话号码、邮编、ISO 时间戳、激活码。模式紧贴输入框,浏览器每次按键都跑一遍。
- 批量查找替换:重构上千份文件,要捕获某个名字再重新注入。
sed、ripgrep、编辑器的「全局替换」都原生支持正则。
如果想看正则在 JSON 工具链里的搭档,可以读我们的 jq 速查手册:30 个真实 JSON 命令行实战模式。
1.1 如何读懂一段正则模式(5 秒钟正则表达式入门)
多数模式从左到右、一个 token 一个 token 地看反而更清晰。以 ^[A-Z]\w+\d{2,4}$ 为例:
^是锚点(anchor),把匹配锁定在字符串起始位置,前面不能有任何字符。[A-Z]是字符类(character class),只匹配一个大写字母。仅此一个,后面还没有量词。\w+匹配一个或多个单词字符(字母、数字、下划线)。\d{2,4}匹配两到四位数字。$锚定到字符串末尾,后面不能再有任何字符。
所以整段模式能匹配 Order42、Job1999,但匹配不了 X07(等等——这要求 X 后面至少有两个单词字符)。诀窍是先看锚点,再看字符类,接着看量词(quantifier),最后看边界。本文余下的每一段模式都按这个顺序拆解。
2. 快速参考表
很多读者就是为这一节来的,需要哪段抄哪段。
元字符(Metacharacters)
| 模式 | 匹配 |
|---|---|
. | 除换行符外的任意字符(开启 s/dotall 标志后包含换行) |
\d | 一个数字([0-9],开启 u 标志后包含全部 Unicode 数字) |
\D | 非数字 |
\w | 单词字符([A-Za-z0-9_]) |
\W | 非单词字符 |
\s | 任意空白(空格、Tab、换行……) |
\S | 任意非空白 |
量词(Quantifiers)
| 模式 | 匹配 |
|---|---|
* | 0 次或多次(贪婪) |
+ | 1 次或多次(贪婪) |
? | 0 次或 1 次(贪婪) |
{n} | 恰好 n 次 |
{n,m} | n 到 m 次 |
{n,} | n 次或更多 |
*?, +?, ??, {n,m}? | 对应的懒惰版本 |
锚点(Anchors)
| 模式 | 匹配 |
|---|---|
^ | 字符串起始(开启 m 标志后为行起始) |
$ | 字符串末尾(开启 m 标志后为行末尾) |
\b | 单词边界 |
\B | 非单词边界 |
\A | 字符串的绝对起始位置(Python) |
\Z | 字符串的绝对末尾位置(Python) |
字符类(Character classes)
| 模式 | 匹配 |
|---|---|
[abc] | a、b、c 中任意一个 |
[^abc] | a、b、c 之外的任意字符 |
[a-z] | 任意小写字母 |
[0-9] | 任意数字 |
\p{L} | 任意 Unicode 字母(JS 中需开 u 标志,Python re 默认即为 Unicode) |
分组(Groups)
| 模式 | 匹配 |
|---|---|
(...) | 捕获组 |
(?:...) | 非捕获组 |
(?<name>...) | 命名捕获(JS ES2018+);Python 写作 (?P<name>...) |
\1, \2 | 对第 1、2 组的反向引用 |
Lookaround
| 模式 | 匹配 |
|---|---|
(?=...) | 正向先行(positive lookahead) |
(?!...) | 负向先行(negative lookahead) |
(?<=...) | 正向后行(positive lookbehind) |
(?<!...) | 负向后行(negative lookbehind) |
标志(Flags)
| 标志 | 作用 |
|---|---|
i | 大小写不敏感 |
m | 多行模式:^ 与 $ 按行匹配 |
s | dotall:. 可匹配换行符 |
g | 全局匹配(JS),返回所有匹配 |
u | Unicode 模式 |
y | 粘附匹配(JS),锚定到 lastIndex |
3. 元字符与字符类
3.1 字面字符与特殊字符
绝大多数字符都是字面值。需要当成字面字符使用时必须转义的 12 个元字符是:
. ^ $ * + ? ( ) [ ] { } | \
忘记转义 . 是最常见的正则 Bug。\. 匹配字面的点号。在字符类内部,[.] 同样匹配字面点号,大部分元字符进了 [...] 都会失去特殊含义,例外是 ]、\、^(出现在第一位时)和 -(出现在中间时)。
3.2 简写字符类
简写类看起来人畜无害,直到遇见 Unicode:
// JavaScript — without the u flag, \d is ASCII only
/\d/.test('5'); // true
/\d/.test('٥'); // false (Arabic-Indic digit)
/\d/u.test('٥'); // false — even with u, \d stays ASCII in JS
/\p{N}/u.test('٥'); // true — \p{N} is the Unicode-aware digit class
# Python — re module treats \d as Unicode by default
import re
re.match(r'\d', '٥') # <Match span=(0, 1)>
re.match(r'(?a)\d', '٥') # None — (?a) forces ASCII
如果只处理英文 ASCII 输入,\d 与 [0-9] 等价。一旦用户粘贴一个带重音符号的名字,就该用 \p{L} 替换 \w。
3.3 自定义字符类
// JavaScript
/[A-Za-z][A-Za-z0-9_-]{2,29}/.test('valid_handle-1'); // true
// Negation and ranges combined
/[^aeiou\s]/g // any non-vowel, non-whitespace character
谈到 Unicode 类别,\p{L} 是「任意字母」,\p{N} 是「任意数字」,\p{Script=Han} 是「任意汉字」。JavaScript 需要带 u 标志;Python 仅在 PyPI 的 regex 包里支持 \p{...},标准库 re 不支持。
在命令行里干活,还会经常遇到 POSIX 字符类:
| POSIX 类 | 匹配 | ASCII 等价 |
|---|---|---|
[[:alpha:]] | 字母 | [A-Za-z] |
[[:digit:]] | 数字 | [0-9](JS/Python 中的 \d) |
[[:alnum:]] | 字母和数字 | [A-Za-z0-9] |
[[:space:]] | 空白字符(whitespace) | \s |
[[:upper:]] | 大写字母 | [A-Z] |
[[:lower:]] | 小写字母 | [a-z] |
POSIX 字符类在 grep -E、sed -E 以及其它遵循 POSIX ERE 的工具里有效。它们在 JavaScript 和 Python 的 re 中不可用,对应位置改用简写(\d、\s、\w)即可。
4. 量词与贪婪 vs 懒惰
4.1 基础量词
/a*/.exec('aaab') // ['aaa'] — 0 or more
/a+/.exec('aaab') // ['aaa'] — 1 or more
/a?/.exec('aaab') // ['a'] — 0 or 1
/a{2,3}/.exec('aaaab') // ['aaa'] — 2 to 3
4.2 贪婪与懒惰
量词默认贪婪:先尽量多吃字符,再回退让整个模式匹配成立。加 ? 就翻成懒惰版本。
const html = '<p>one</p><p>two</p>';
html.match(/<p>.*<\/p>/)[0]; // '<p>one</p><p>two</p>' (greedy eats both)
html.match(/<p>.*?<\/p>/)[0]; // '<p>one</p>' (lazy stops at first)
抽取标签或带引号字符串时,懒惰版本通常就是你要的。再进一步:能不用 . 就别用,换成排除类。<p>[^<]*</p> 比 <p>.*?</p> 更快,因为没东西可回溯。
4.3 灾难性回溯(Catastrophic backtracking)
服务器卡死常常出在这里。让一个量词嵌套在另一个量词里、并且两者匹配范围有歧义重叠,引擎就会在放弃前探索指数级数量的路径。
// Don't do this
/(a+)+b/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // takes seconds
对于 41 个 a 后跟 ! 的字符串,引擎要试约 2^41 种切分方式才确认 b 不存在。三种解法:
- 拍平模式:
/a+b/一样能用,且没有嵌套。 - 使用原子组(atomic group)(Python
regex、PCRE、Java、Ruby 支持):(?>a+)+b,一旦a+匹配成功,引擎就拒绝再回溯进去。 - 换引擎:Go 的
regexp、RE2、Rust 的regexcrate 用线性时间 NFA,从设计上就不会发生灾难性回溯。
JavaScript 和 Python re 都会回溯,标准库都不提供原子组(Python 的 PyPI regex 包补上了)。输入长度可控时这没问题;一旦输入来自用户,就要么先做长度校验、要么把验证压到 RE2 上。
5. 锚点与单词边界
5.1 ^ 与 $
默认情况下,^ 是整个输入的起始,$ 是输入末尾。加上 m(多行)标志后,它们变成每一行的起始与末尾:
const log = 'INFO start\nERROR boom\nINFO done';
log.match(/^ERROR.*/); // null — single-line mode, ^ only matches index 0
log.match(/^ERROR.*/m); // ['ERROR boom']
5.2 \b 与 \B
\b 是零宽断言:匹配单词字符(\w)与非单词字符之间的位置,常用于整词搜索:
/\bcat\b/.test('the cat sat'); // true
/\bcat\b/.test('concatenate'); // false
单词边界是基于 \w 定义的,而 \w 默认只覆盖 ASCII。中文、日文、韩文文本词之间没有空格,所以 \b 在这里识别不出词的边界。你需要在正则之前先过一遍分词器(jieba、MeCab),而不是想着用正则替代它们。
5.3 多行模式
import re
text = "INFO ok\nERROR fail\nINFO done\n"
re.findall(r'^ERROR.*$', text) # []
re.findall(r'^ERROR.*$', text, re.MULTILINE) # ['ERROR fail']
JavaScript 里同样的写法是 text.match(/^ERROR.*$/gm)。把 m 和 g 搭配起来,就能拿到所有匹配行。
6. 分组、捕获与反向引用
6.1 捕获组
括号有两个职责:把子模式打包给量词使用,以及把匹配结果捕获下来供之后取用。
'2026-05-13'.match(/(\d{4})-(\d{2})-(\d{2})/);
// ['2026-05-13', '2026', '05', '13', index: 0, ...]
分组按左括号从左到右编号,从 1 开始。
6.2 非捕获组
只想分组、不想捕获时,用 (?:...)。它更快,也能让编号组保持整洁:
/(?:https?):\/\/(\S+)/.exec('see https://go-tools.org');
// ['https://go-tools.org', 'go-tools.org']
// — the protocol is grouped but not captured; group 1 is the host
6.3 命名分组
给分组起名能让模式更可读,重构时也更安全。
// JavaScript (ES2018+)
const m = '2026-05-13'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year; // '2026'
# Python — note the (?P<...>) syntax
import re
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-05-13')
m.group('year') # '2026'
6.4 反向引用
反向引用让模式后段重复前一个捕获匹配到的内容。
// Find any character that repeats consecutively
'bookkeeper'.match(/(\w)\1/g); // ['oo', 'kk', 'ee']
// Match paired HTML tags by name
const tag = /<(\w+)>(.*?)<\/\1>/;
'<b>bold</b>'.match(tag);
// ['<b>bold</b>', 'b', 'bold']
Python 中 \1 在模式与替换字符串里都能用;命名引用在模式里写作 (?P=name),在 re.sub 替换字符串里写作 \g<name>。
7. Lookaround:先行与后行断言
Lookaround 是零宽断言。它们检查条件但不消耗字符,所以可以串联使用。
7.1 Lookahead(先行)
// Password: at least 8 chars, one digit, one uppercase, one lowercase
const strong = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$/;
strong.test('Hunter2!'); // true
strong.test('hunter2!'); // false — no uppercase
// Negative lookahead — file names that are not .tmp
/^[\w-]+(?!\.tmp$)\.[a-z]+$/.test('report.csv'); // true
7.2 Lookbehind(后行)
Lookbehind 是 Lookahead 的镜像,它断言当前位置之前的内容。
// Extract a price after a currency symbol — keep the number, drop the $
'price: $42.50'.match(/(?<=\$)\d+(\.\d+)?/); // ['42.50', '.50']
// Negative lookbehind — match Bond but not James Bond
'Mr. Bond'.match(/(?<!James )Bond/); // ['Bond']
'James Bond'.match(/(?<!James )Bond/); // null
7.3 JavaScript 与 Python 的 Lookbehind 差异
这是两个引擎里少数会让跨语言移植出错的点。
| 引擎 | Lookbehind 长度 |
|---|---|
| JavaScript(V8、SpiderMonkey、JSC 16.4+) | 自 ES2018 起支持变长。(?<=\d+) 合法。 |
Python 标准库 re | 仅支持定长。(?<=\d+) 会抛出 error: look-behind requires fixed-width pattern。 |
Python regex PyPI 包 | 支持变长。import regex; regex.search(r'(?<=\d+)abc', '12abc')。 |
Python 的绕路办法:用已知重复次数改写 Lookbehind(如 (?<=\d{3})),或者把前缀也捕获下来、匹配完再切掉。
8. 标志与修饰符
8.1 i:大小写不敏感
/error/i.test('FATAL ERROR'); // true
re.search(r'error', 'FATAL ERROR', re.IGNORECASE) # <Match span=(6, 11)>
8.2 m 与 s
m 把 ^ 和 $ 翻成按行锚定。s(dotall)让 . 能匹配换行符。两者相互独立,需要哪种行为就开哪个,全要就一起开。
/<script>(.*?)<\/script>/s.exec('<script>\nalert(1)\n</script>')[1];
// '\nalert(1)\n' — without s, the . would refuse the newlines
8.3 g:全局匹配
在 JavaScript 中,g 改变的是 API 行为而不是匹配本身。不带 g 时 String.match 返回捕获组;带 g 时返回所有匹配到的字符串。要在全部匹配中都保留捕获组,用 matchAll。
const text = 'a=1 b=2 c=3';
text.match(/(\w)=(\d)/); // first match with groups
text.match(/(\w)=(\d)/g); // ['a=1', 'b=2', 'c=3'] — no groups
[...text.matchAll(/(\w)=(\d)/g)]; // every match, with groups
Python 没有 g 标志。re.findall、re.finditer、re.sub 就是它对应的「全局」版本。
8.4 u:Unicode 与 \p{...}
// Match any Han character (Chinese, Japanese kanji)
/\p{Script=Han}+/gu.test('Hello 世界'); // true
// Match emoji (extended pictographic)
/\p{Extended_Pictographic}/u.test('👋'); // true
Python 默认开启 Unicode;re.findall(r'[一-鿿]+', text) 是匹配汉字区段的等价写法。要用完整的 Unicode 属性转义,请装 PyPI 的 regex 包:regex.findall(r'\p{Script=Han}+', text)。
9. 每天都会用到的常用模式
9.1 邮箱校验
先说实话,按需要选版本。
// The 95% pattern — what most form validators use
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
email.test('a@b.co'); // true
// The "I really want to be RFC 5322-ish" pattern
const rfc = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
在纯正则里完整实现 RFC 5322 邮箱校验要约 6000 字符,而且仍然有边界情况搞错。用上面的 95% 模式,然后发一封验证邮件,这才是唯一真正有效的验证方式。
9.2 URL 抽取
const urlPattern = /https?:\/\/[^\s<>"]+/g;
const found = 'See https://example.com/a?b=1 and http://x.io'.match(urlPattern);
// ['https://example.com/a?b=1', 'http://x.io']
抽出 URL 后通常要查看它的查询字符串。把它粘进我们的 URL 编码解码工具,百分号编码参数一眼能读清楚。想全面理解什么时候该编码、什么时候该解码,请读 URL 编码与解码:百分号编码开发实战指南。
9.3 电话号码
// E.164 — international, optional + and 1-3 digit country code
const e164 = /^\+?[1-9]\d{1,14}$/;
e164.test('+14155551234'); // true
// North American Number Plan with separators
const nanp = /^(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
nanp.test('(415) 555-1234'); // true
校验比「这个格式像不像号码」更严的需求,请用 libphonenumber。正则没法判断某个区号是否真的存在。
9.4 IPv4 与 IPv6
// IPv4 — strict 0-255 per octet
const ipv4 = /^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$/;
ipv4.test('192.168.1.1'); // true
ipv4.test('999.0.0.1'); // false
// IPv6 — the simplified form. The full RFC 4291 pattern is ~600 chars.
const ipv6simple = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
ipv6simple.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); // true
要处理带 :: 缩写、内嵌 IPv4、Zone 标识的真实 IPv6,请用 node:net 的 isIP() 或 Python 的 ipaddress.ip_address()。用纯正则硬刚做一次就够了,之后只会变成维护负担。
9.5 ISO 8601 日期与时间戳
// Date only — YYYY-MM-DD
const isoDate = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
isoDate.test('2026-05-13'); // true
// Date + time + optional fractional seconds + Z or offset
const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
iso.test('2026-05-13T09:30:00.123Z'); // true
ISO 8601 看起来简单,坑却不少:闰秒、周日期(2026-W19)、序数日期(2026-133)。关于 epoch 秒、毫秒和时区偏移的全貌,请看 Unix 时间戳完全指南。
10. 用正则做查找替换
10.1 JavaScript:String.replace 配合 $1
// Reformat US dates: MM/DD/YYYY -> YYYY-MM-DD
'05/13/2026'.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2');
// '2026-05-13'
// Use a callback when the replacement is conditional
'price 42 dollars'.replace(/(\d+) dollars/, (_, n) => `$${n}`);
// 'price $42'
$1、$2 等引用编号组,$<name> 引用命名组,$& 是整个匹配,$$ 是字面的 $。
10.2 Python:re.sub 配合 \1 与回调
import re
# Same date reformat as above
re.sub(r'(\d{2})/(\d{2})/(\d{4})', r'\3-\1-\2', '05/13/2026')
# '2026-05-13'
# Callback — uppercase every email address in a string
def upper_email(m):
return m.group(0).upper()
re.sub(r'[\w.-]+@[\w.-]+', upper_email, 'mail me at hi@go-tools.org')
# 'mail me at HI@GO-TOOLS.ORG'
Python 替换串里用 \1 或 \g<name>。前缀 r'...'(raw string)很关键,少了它 \1 会变成一个字面字符。
10.3 命令行:sed、grep、ripgrep、jq
命令行里做批量重构时,正则就从脚本走进了 Shell:
# ripgrep — find every TODO with a name attached
rg -n '\bTODO\(([^)]+)\)' --replace 'TODO(\1)'
# grep -E with anchors — failed login lines from auth.log
grep -E '^[A-Z][a-z]{2} +[0-9]+ .*Failed password' /var/log/auth.log
# sed — strip trailing whitespace, in-place, across a tree
find . -name '*.md' -print0 | xargs -0 sed -i -E 's/[[:space:]]+$//'
ripgrep 使用 Rust 的 regex crate(RE2 风格,线性时间,不支持 Lookbehind)。grep -E 和 sed -E 走的是 POSIX 扩展正则,没有 \d,用 [0-9] 或 [[:digit:]] 代替。数据是 JSON 时,把正则换成 jq,可以参考 jq 速查手册:30 个真实 JSON 命令行实战模式作为对照参考卡。
11. 常见坑
11.1 忘记转义 .
线上真实发生过的 Bug:一个日志脱敏器本来要遮蔽 IP 地址。
// Wrong — matches '192a168b1c1' too
/(\d+).(\d+).(\d+).(\d+)/.test('192a168b1c1'); // true
// Right
/(\d+)\.(\d+)\.(\d+)\.(\d+)/.test('192a168b1c1'); // false
在字符类内部,. 已经是字面值,所以 [.] 和 \. 都行。其它位置必须转义。
11.2 贪婪的 .* 吃过头
'<a href="x"><b>bold</b></a>'.match(/<(.*)>/)[1];
// 'a href="x"><b>bold</b></a' — the whole thing!
贪婪的 .* 一路扫到字符串末尾,再回退到找到 >,也就是输入里最后一个 >。改用懒惰版(.*?),或者更快更清晰:用排除类([^>]*)。
11.3 多行锚点的误解
^ 和 $ 默认不匹配换行符,它们匹配的是整个输入的起始与末尾位置。加 m 标志才会把它们变成按行锚定;加 s 标志才让 . 跨越换行符。两个标志彼此正交,做日志解析时通常两个都要开。
11.4 ReDoS:怎么发生、怎么拆弹
ReDoS(regex denial of service,正则拒绝服务攻击)是灾难性回溯的生产级版本。修复方法:
- 静态分析:
safe-regex、recheck、ESLint 的no-misleading-character-class等工具能在危险模式上线前拦下来。 - 原子组(Python
regex、PCRE、Ruby、Java 支持):(?>...)阻止引擎在回溯时重新进入该组。 - 占有量词(possessive quantifiers)(PCRE/Java 中的
*+、++、?+):思路一样,写法更精简。 - 换成非回溯引擎:Go 的
regexp、RE2、Rust 的regexcrate、Python 的re2绑定都跑在线性时间内。ripgrep 是 RE2 在真实世界里最常见的部署案例。 - 先校验输入长度。10 KB 的正则炸弹是 Bug;给输入加 10 字节上限只是一行代码。
想看与正则搭配的日常工具(格式化、解码器、转换器等)有哪些,请读 新手指南:如何使用开发者工具提高工作效率。
把复杂模式推到线上之前,先交互式地测一遍。regex101.com 能在 PCRE、JavaScript、Python、Go 等方言之间切换,把每个 token 用大白话讲清楚,还能一步步展示回溯过程,让你在生产环境之前先发现灾难性模式。
12. FAQ
Regex 里 * 和 + 有什么区别?
* 匹配零次或多次出现(可以匹配空字符串);+ 匹配一次或多次(至少要一次)。a* 能匹配 ''、'a'、'aaaa';a+ 能匹配 'a' 和 'aaaa' 但不能匹配 ''。
怎么用正则跨多行匹配?
打开多行标志,JavaScript 里写 /.../m,Python 里写 re.MULTILINE,这样 ^ 和 $ 就按行锚定。如果还要让 . 跨越换行符,再加上 dotall 标志(JavaScript 的 s、Python 的 re.DOTALL)。
JavaScript 和 Python 的正则一样吗?
核心语法(量词、锚点、字符类、基础分组)90% 相同。两点真正的差异:JavaScript(ES2018+)支持变长 Lookbehind,命名组写作 (?<name>...);Python 标准库 re 只支持定长 Lookbehind,且命名组用 (?P<name>...)。要在 Python 里用变长 Lookbehind,从 PyPI 安装 regex 包即可。
我的正则为什么会触发灾难性回溯?
你写了带重叠匹配的嵌套量词,例如 (a+)+ 或 (a|a)*。当输入「几乎匹配但末尾不通过」时,引擎会尝试内层量词的每一种切分方式,搜索空间呈指数级膨胀。修法:用原子组 (?>a+)+、占有量词 a++,或者改用 RE2、Go regexp 这类非回溯引擎。
JavaScript 能用 Lookbehind 吗?
能。正向 (?<=...) 与负向 (?<!...) Lookbehind 自 ES2018 起就进入了 V8(Chrome、Node.js)、SpiderMonkey(Firefox)和 JavaScriptCore(Safari 16.4+),且支持变长。要兼容更老的 Safari,用 Babel 转译,或者用 try/catch 包住 new RegExp 做特性检测。
怎么在正则里匹配一个字面的点号 .?
用反斜杠转义:\. 匹配字面点号。在字符类内部,点号本来就是字面值,[.] 和 [\.] 都能用。在字符类外,未转义的 . 是元字符,含义是「除换行符外的任意字符」(开启 dotall 标志后则是「任意字符」)。
正则里的 \s 是什么意思?
\s 匹配任意空白字符(whitespace)——空格、Tab(\t)、换行(\n)、回车(\r)、垂直 Tab、换页符。在 Unicode 模式下(JavaScript 加 u 标志,Python 3.x 默认即是),它还能匹配 NBSP( )以及其它 Unicode 空白码点。反向类 \S 匹配任意非空白字符。
正则表达式区分大小写吗?
默认区分。/cat/ 匹配不了 Cat。打开大小写不敏感(case-insensitive)标志即可忽略大小写:JavaScript 加 i(写作 /cat/i),Python 用 re.IGNORECASE 或内联 (?i) 分组。在 Unicode 模式下,大小写折叠还会覆盖 ß↔SS、土耳其语的带点/不带点 I 这类棘手对应,有时会带来意料之外的匹配结果。