15 道正则面试题,附详细解答
十五道面试官常问的正则练习,从校验用户名到解释某个模式为何会让服务器卡死,每道都附解答、面试官会追问的边界情况和性能说明。

· 阅读约 9 分钟
/^(?!.*__)[a-z][a-z0-9_]{1,14}[a-z0-9]$/
抓住
- night_owl
- dev42
- x_ray
放过
- _admin
- 9lives
- owl_
好的正则面试题考察的是你能否写出正确、易读、面对异常输入也安全的模式:校验用户名、转换日期格式、从日志行中提取字段、遮盖卡号,以及解释某个模式为何会让服务器卡死。下面是 15 道这样的练习,附详细解答、面试官会追问的边界情况,以及必要时的性能说明。
这里的所有模式都是 JavaScript,所有示例都可实时交互:阅读时匹配结果会高亮显示。Python 或 Java 行为不同的地方,解答中会注明。
01校验用户名
题目: 3 到 16 个字符;只能是小写字母、数字和下划线;以字母开头,不以下划线结尾,且不能出现两个连续的下划线。
/^(?!.*__)[a-z][a-z0-9_]{1,14}[a-z0-9]$/应当匹配3/3
- night_owl
- dev42
- x_ray
不应匹配6/6
- _admin
- 9lives
- night__owl
- owl_
- ab
- Night_owl
原理: 首尾字符各有自己的字符类,中间部分取 1 到 14 个,所以总长是 3 到 16。否定先行断言 (?!.*__) 在消耗任何字符之前,先把整个字符串检查一遍有没有双下划线。
边界情况: 没有 ^ 和 $ 的话,!!night_owl!! 也能通过。这里不要用 \w:它允许大写字母,而且在 Python 3 中会匹配任何文字的字母。
02把日期从 DD/MM/YYYY 转成 YYYY-MM-DD
题目: 改写一段文本中的所有日期,其余内容保持不变。
/\b(\d{2})\/(\d{2})\/(\d{4})\b/应当匹配2/2
- Due 04/11/2026
- From 05/01/2027 to 12/01/2027
不应匹配3/3
- Already ISO: 2026-11-04
- 4/11/2026
- Order 104/11/20261
替换时把各分组按新顺序放回:
text.replace(/\b(\d{2})\/(\d{2})\/(\d{4})\b/g, '$3-$2-$1')
// "Due 04/11/2026" → "Due 2026-11-04"
用命名分组,替换式一目了然:(?<d>\d{2})\/(?<m>\d{2})\/(?<y>\d{4}) 配 '$<y>-$<m>-$<d>'。在 Python 中对应写法是 \g<y>-\g<m>-\g<d>,分组写成 (?P<y>…)。
边界情况: 两侧的 \b 防止在更长的数字内部匹配。正则只检查格式,不懂日历:31/02/2026 也能通过,所以之后要用日期库校验真实日期。详见日期正则。
03从访问日志行中提取字段
题目: 从 Web 服务器的一行日志中取出 IP、时间、方法、路径、状态码和大小。
/^(?<ip>\S+) \S+ \S+ \[(?<time>[^\]]+)\] "(?<method>[A-Z]+) (?<path>\S+) [^"]*" (?<status>\d{3}) (?<bytes>\d+|-)$/应当匹配2/2
- 203.0.113.9 - - [04/Nov/2026:10:15:32 +0000] "GET /blog/regex-games HTTP/1.1" 200 5123
- 198.51.100.4 - - [04/Nov/2026:10:16:01 +0000] "POST /api/login HTTP/2.0" 401 -
不应匹配1/1
- 203.0.113.9 - - [04/Nov/2026:10:15:32 +0000] "GET / HTTP/1.1"
原理: 每个字段都靠它不能包含的内容来匹配:\S+ 匹配不含空格的字段,[^\]]+ 一直到右方括号,[^"]* 一直到右引号。命名分组让结果自带说明:line.match(re).groups.status 就是 "200"。
性能: 否定字符类永远不会越界,所以不合格的行会很快失败。在字段之间用 .* 的诱人写法,可能会在放弃之前尝试多种方式拆分一条长行。
边界情况: 格式错误的请求会被记录为 "-",而这个模式会拒绝它;你要决定是跳过这些行,还是为它们加上 |-。
04只保留不含 DEBUG 的行
题目: 过滤日志,只留下不含单词 DEBUG 的行。
/^(?!.*\bDEBUG\b).*$/m应当匹配2/2
- INFO server started
- DEBUGGER attached
不应匹配2/2
- DEBUG cache miss
- warn: DEBUG off
原理: 否定先行断言先在整行中查找这个单词;只有找不到时,.* 才会拿下这一行。两侧的 \b 让 DEBUGGER 得以保留。
接下来可以说: 在实际代码中,lines.filter(l => !/\bDEBUG\b/.test(l)) 或 grep -v -w DEBUG 更清晰。两种都会,正是这道题想考的。
05提取方括号中的文本
题目: 从 [error] disk full [code 28] 中取出 error 和 code 28。
陷阱: \[(.*)\] 是贪婪的。它会一直匹配到最后一个 ],返回 error] disk full [code 28。懒惰的 \[(.*?)\] 可以用,但 [^\]]* 准确表达了你的意思,而且从不回溯。更多内容见贪婪与懒惰量词。
06查找注明负责人的 TODO 注释
题目: 列出 // TODO(name): note 形式的注释,以及负责人和备注。
///\s*TODO\((\w+)\):\s*(.+)/应当匹配2/2
- // TODO(ola): retry on timeout
- x = 1 //TODO(dev_ops): remove
不应匹配3/3
- // TODO: no owner
- // todo(ola): lowercase
- # TODO(ola): python comment
边界情况: 如果你的团队写 todo,就加上 i 修饰符。要同时覆盖 # 注释,就以 (?://|#) 开头。还要明确说出来:正则分不清注释和包含 // 的字符串,所以 lint 规则才是稳妥的答案。
07遮盖卡号,只保留最后四位
题目: 把 4111111111111111 变成 ************1111。
'4111111111111111'.replace(/\d(?=\d{4})/g, '*') // "************1111"
原理: 先行断言 (?=\d{4}) 只在一个数字后面还跟着四个数字时才匹配它,且不消耗后面的数字,所以最后四位永远不会被替换。
边界情况: 如果带空格,比如 4111 1111 1111 1111,就什么都不会被遮盖,因为没有哪个数字后面直接跟着四个数字。\d(?=(?:\D*\d){4}) 允许中间有分隔符,结果是 **** **** **** 1111。
08把 camelCase 转成 snake_case
题目: getUserId → get_user_id,parseHTTPResponse → parse_http_response。
/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/s.replace(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g, '_').toLowerCase()
原理: 这个模式不匹配任何字符,只匹配位置:小写字母或数字之后、大写字母之前,或者一串大写字母中最后一个之前(HTTPResponse 中的 P)。只用前半部分会得到 parse_httpresponse,这是常见的错误答案。
边界情况: 所有现行浏览器、Node 和 Python 都支持后行断言(Python 的 re 要求定长,这里正是定长)。如果不能用后行断言,就改为捕获字母:([a-z0-9])([A-Z]) → '$1_$2'。
09校验 1h30m 这样的时长
题目: 接受 1h30m、45m 和 2h,但不接受空字符串。
原理: 两部分都是可选的,所以没有先行断言的话,模式也会接受空字符串。(?=\d) 表示“这里必须有内容,并且以数字开头”。只要一个模式的各部分全是可选的,这都是个常用技巧。
边界情况: 1h90m 也能通过。如果分钟必须小于 60,就用 ([0-5]?\d)m,或者在代码里检查数字,这样更易读。
10查找 1 到 3 级 Markdown 标题
题目: 在 Markdown 文件中找出 #、## 和 ### 标题,并捕获标题文本。
原理: #{1,3} 不会延伸到四个,因为后面必须紧跟 \s+。m 修饰符让 ^ 和 $ 在整个文档中按行生效(参见正则锚点)。
边界情况: 代码块里的 # 行不是标题,而正则看不到代码块。这时就该提一下 Markdown 解析器了。
11拆分含引号字段的 CSV 行
题目: 把 widget,"Sizes 2, 4 and 6",9.99 拆成三个字段,保留引号里的逗号。
/(?:^|,)("(?:[^"]|"")*"|[^,]*)/使用 g 修饰符时,每次匹配的第 1 组就是一个字段:widget、"Sizes 2, 4 and 6" 和 9.99。之后去掉外层引号,并把 "" 换成 "。
接下来可以说: 带引号的字段可以包含换行,而逐行处理的正则应付不了。处理真实的 CSV,请用解析器(Python 的 csv 模块,JavaScript 的 Papa Parse)。好的面试答案既给出正则,也会说明这一点。
12查找 ASCII 以外的字符
题目: 标记出含有带重音字母、€ 之类符号或 emoji 的文本,比如在写入只接受 ASCII 的系统之前。
边界情况: 不加 u 修饰符时,一个 emoji 是两个 UTF-16 码元,两个都会匹配;加上 u 就是一个字符。如果只允许可打印 ASCII,就反过来写:^[\x20-\x7E]*$。
13校验主机名中的一个标签
题目: 一个标签(两个点之间的部分)由 1 到 63 个字母、数字或连字符组成,且不能以连字符开头或结尾。
/^(?!-)[a-z0-9-]{1,63}(?<!-)$/i应当匹配3/3
- regexhunter
- my-site-2
- a
不应匹配4/4
- -start
- end-
- under_score
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
原理: 先行断言和后行断言守住两端,所以长度限制只写在一处:{1,63}。最后一行有 64 个字符,多了一个。
边界情况: 不用环视的写法是 ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$,正确但更难核对。完整的主机名是用点连接的多个标签,最多 253 个字符,这个限制在代码里检查更简单。
14解释某个模式为何让服务器卡死
题目: 这个检查“用单个空格分隔的单词”的模式在测试中没问题,但某些输入会让服务器卡死。为什么?怎么修?
^(\w+\s?)+$
答案: 空格是可选的,所以像 aaaaaaaa 这样的一串字母,可以用极多种方式分配给分组的各次重复(a|aaaaaaa、aa|aaaaaa、a|a|aaaaaa…)。当字符串以无法匹配的字符结尾,比如 !,引擎会试遍所有拆分方式才放弃。每多一个字母,工作量就翻倍:在 Node 中,24 个字母加一个 ! 花了 8 秒。这就是灾难性回溯。
修正: 确保每个字符只能以一种方式被匹配。把分隔符改为必需,并允许末尾有一个可选空格:
/^\w+(?:\s\w+)*\s?$/应当匹配2/2
- hello world
- one two three
不应匹配2/2
- hello world
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
修正后的模式拒绝 3 万个字母加一个 ! 只需约 1 毫秒。其他好答案:在引擎支持时使用原子分组或占有量词(Java、PCRE、Python 3.11+),或对不可信输入使用 RE2 这样的线性时间引擎。完整内容见灾难性回溯。
15去除 HTML 标签,并知道何时打住
题目: 把 <p>Hello <b>world</b></p> 变成 Hello world。
text.replace(/<[^>]+>/g, '') 对简单标记有效。第三行展示了它在哪里失效:含有 < 和 > 的纯文本会丢掉 < 5 and 7 >。像 title="x > y" 这样的属性也会让它出错。
面试官想听的答案: HTML 是嵌套的,而嵌套正是正则表达式无法计数的。要从 HTML 中提取文本,请用浏览器自带的解析器(new DOMParser() 和 textContent);要让 HTML 变得安全,请用清理(sanitizer)库。正则可以用来快速查看可信的标记,但绝不能用于安全防护。
16为面试做练习
读解答有帮助;在一点压力下亲手写模式帮助更大。
- 练习包每个包专练一种技巧,大多数关卡都是你工作中会写的模式:格式、日志行和代码中的标识符。
- 每日狩猎只需五分钟,能在两次面试之间保持你的正则手感。
- 如果面试用 Python,读读JavaScript 与 Python 正则表达式,了解那些容易让人踩坑的差异。