Skip to content
返回博客
教程

Regex 正则表达式速查:元字符、分组、Lookahead/Lookbehind 一页搞定

一份覆盖元字符、量词、锚点、分组与 Lookaround 的正则速查表;附 15+ 个常用模式,以及 JavaScript 与 Python 引擎的差异说明,帮你避开灾难性回溯。

12 分钟

Regex 正则表达式速查:元字符、分组、Lookahead/Lookbehind 一页搞定

正则表达式(regex)是一种小巧的模式语言,用来匹配文本。\d+ 表示「一个或多个数字」,^Error 表示「以 Error 开头的行」,仅此而已。本篇速查把语法压在一页里:元字符、量词、锚点、分组、Lookaround、Flag,再加上 15+ 个能直接粘进 JavaScript 或 Python 的常用模式。

文章面向已经知道字符串是什么、想要一份参考而不是入门导览的开发者。只想查符号?跳到「快速参考表」。想搞清楚 Lookaround 和踩过哪些坑?看对应章节,它们的失误能让一台服务器卡死。

1. 2026 年了,正则还值得学吗

正则表达式会被编译成状态机,扫描字符串后要么匹配、要么失败。语法很小,能干的事却不少。

AI 现在能帮你起草模式,但有几类活儿仍然属于手写正则:

  • 日志解析:一千万行 nginx access log,要从某个 User-Agent 里捞出所有 5xx 请求。一段 40 字符的正则配合 grep -E 几秒搞定;逐行调用 LLM 的方案是没法跑的。
  • 表单与字段校验:电话号码、邮编、ISO 时间戳、激活码。模式紧贴输入框,浏览器每次按键都跑一遍。
  • 批量查找替换:重构上千份文件,要捕获某个名字再重新注入。sedripgrep、编辑器的「全局替换」都原生支持正则。

如果想看正则在 JSON 工具链里的搭档,可以读我们的 jq 速查手册:30 个真实 JSON 命令行实战模式

1.1 如何读懂一段正则模式(5 秒钟正则表达式入门)

多数模式从左到右、一个 token 一个 token 地看反而更清晰。以 ^[A-Z]\w+\d{2,4}$ 为例:

  • ^ 是锚点(anchor),把匹配锁定在字符串起始位置,前面不能有任何字符。
  • [A-Z] 是字符类(character class),只匹配一个大写字母。仅此一个,后面还没有量词。
  • \w+ 匹配一个或多个单词字符(字母、数字、下划线)。
  • \d{2,4} 匹配两到四位数字。
  • $ 锚定到字符串末尾,后面不能再有任何字符。

所以整段模式能匹配 Order42Job1999,但匹配不了 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}nm
{n,}n 次或更多
*?, +?, ??, {n,m}?对应的懒惰版本

锚点(Anchors)

模式匹配
^字符串起始(开启 m 标志后为行起始)
$字符串末尾(开启 m 标志后为行末尾)
\b单词边界
\B非单词边界
\A字符串的绝对起始位置(Python)
\Z字符串的绝对末尾位置(Python)

字符类(Character classes)

模式匹配
[abc]abc 中任意一个
[^abc]abc 之外的任意字符
[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多行模式:^$ 按行匹配
sdotall:. 可匹配换行符
g全局匹配(JS),返回所有匹配
uUnicode 模式
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 -Esed -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 不存在。三种解法:

  1. 拍平模式/a+b/ 一样能用,且没有嵌套。
  2. 使用原子组(atomic group)(Python regex、PCRE、Java、Ruby 支持):(?>a+)+b,一旦 a+ 匹配成功,引擎就拒绝再回溯进去。
  3. 换引擎:Go 的 regexp、RE2、Rust 的 regex crate 用线性时间 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)。把 mg 搭配起来,就能拿到所有匹配行。

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 ms

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 行为而不是匹配本身。不带 gString.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.findallre.finditerre.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:netisIP() 或 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 -Esed -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,正则拒绝服务攻击)是灾难性回溯的生产级版本。修复方法:

  1. 静态分析safe-regexrecheck、ESLint 的 no-misleading-character-class 等工具能在危险模式上线前拦下来。
  2. 原子组(Python regex、PCRE、Ruby、Java 支持):(?>...) 阻止引擎在回溯时重新进入该组。
  3. 占有量词(possessive quantifiers)(PCRE/Java 中的 *+++?+):思路一样,写法更精简。
  4. 换成非回溯引擎:Go 的 regexp、RE2、Rust 的 regex crate、Python 的 re2 绑定都跑在线性时间内。ripgrep 是 RE2 在真实世界里最常见的部署案例。
  5. 先校验输入长度。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 这类棘手对应,有时会带来意料之外的匹配结果。