指南

15 道正则面试题,附详细解答

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

Regex Hunter 标志

· 阅读约 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 个字符;只能是小写字母、数字和下划线;以字母开头,不以下划线结尾,且不能出现两个连续的下划线。

匹配9/9 符合预期
/^(?!.*__)[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

题目: 改写一段文本中的所有日期,其余内容保持不变。

匹配5/5 符合预期
/\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、时间、方法、路径、状态码和大小。

匹配3/3 符合预期
/^(?<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 的行。

匹配4/4 符合预期
/^(?!.*\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。

匹配4/4 符合预期
/\[[^\]]*\]/

应当匹配2/2

  • [error] disk full [code 28]
  • []

不应匹配2/2

  • no brackets here
  • [unclosed
在沙盒里试试

陷阱: \[(.*)\] 是贪婪的。它会一直匹配到最后一个 ],返回 error] disk full [code 28。懒惰的 \[(.*?)\] 可以用,但 [^\]]* 准确表达了你的意思,而且从不回溯。更多内容见贪婪与懒惰量词。

06查找注明负责人的 TODO 注释

题目: 列出 // TODO(name): note 形式的注释,以及负责人和备注。

匹配5/5 符合预期
///\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。

匹配4/4 符合预期
/\d(?=\d{4})/

应当匹配2/2

  • 4111111111111111
  • card 5500000000000004

不应匹配2/2

  • 1234
  • no digits
在沙盒里试试
'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。

正则47 字符
/(?<=[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,但不接受空字符串。

匹配7/7 符合预期
/^(?=\d)(?:(\d+)h)?(?:(\d+)m)?$/

应当匹配3/3

  • 1h30m
  • 45m
  • 2h

不应匹配4/4

  • h
  • 30m1h
  • 1h 30m
  • 90
在沙盒里试试

原理: 两部分都是可选的,所以没有先行断言的话,模式也会接受空字符串。(?=\d) 表示“这里必须有内容,并且以数字开头”。只要一个模式的各部分全是可选的,这都是个常用技巧。

边界情况: 1h90m 也能通过。如果分钟必须小于 60,就用 ([0-5]?\d)m,或者在代码里检查数字,这样更易读。

10查找 1 到 3 级 Markdown 标题

题目: 在 Markdown 文件中找出 #、## 和 ### 标题,并捕获标题文本。

匹配5/5 符合预期
/^(#{1,3})\s+(\S.*)$/m

应当匹配2/2

  • # Title
  • ### Install

不应匹配3/3

  • #### Too deep
  • #hashtag
  • Not a # heading
在沙盒里试试

原理: #{1,3} 不会延伸到四个,因为后面必须紧跟 \s+。m 修饰符让 ^ 和 $ 在整个文档中按行生效(参见正则锚点)。

边界情况: 代码块里的 # 行不是标题,而正则看不到代码块。这时就该提一下 Markdown 解析器了。

11拆分含引号字段的 CSV 行

题目: 把 widget,"Sizes 2, 4 and 6",9.99 拆成三个字段,保留引号里的逗号。

正则29 字符
/(?:^|,)("(?:[^"]|"")*"|[^,]*)/
在沙盒里试试
每次匹配就是一个字段:带引号的字段(其中 "" 表示转义的引号),或一直到下一个逗号的普通文本。

使用 g 修饰符时,每次匹配的第 1 组就是一个字段:widget、"Sizes 2, 4 and 6" 和 9.99。之后去掉外层引号,并把 "" 换成 "。

接下来可以说: 带引号的字段可以包含换行,而逐行处理的正则应付不了。处理真实的 CSV,请用解析器(Python 的 csv 模块,JavaScript 的 Papa Parse)。好的面试答案既给出正则,也会说明这一点。

12查找 ASCII 以外的字符

题目: 标记出含有带重音字母、€ 之类符号或 emoji 的文本,比如在写入只接受 ASCII 的系统之前。

匹配5/5 符合预期
/[^\x00-\x7F]/

应当匹配3/3

  • café
  • naïve résumé
  • price: 5 €

不应匹配2/2

  • plain ASCII text
  • 100% ok!
在沙盒里试试

边界情况: 不加 u 修饰符时,一个 emoji 是两个 UTF-16 码元,两个都会匹配;加上 u 就是一个字符。如果只允许可打印 ASCII,就反过来写:^[\x20-\x7E]*$。

13校验主机名中的一个标签

题目: 一个标签(两个点之间的部分)由 1 到 63 个字母、数字或连字符组成,且不能以连字符开头或结尾。

匹配7/7 符合预期
/^(?!-)[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 秒。这就是灾难性回溯。

修正: 确保每个字符只能以一种方式被匹配。把分隔符改为必需,并允许末尾有一个可选空格:

匹配4/4 符合预期
/^\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。

匹配3/3 符合预期
/<[^>]+>/
  • <p>Hello <b>world</b></p>
  • <br/>
  • 3 < 5 and 7 > 2
  • plain text
在沙盒里试试

text.replace(/<[^>]+>/g, '') 对简单标记有效。第三行展示了它在哪里失效:含有 < 和 > 的纯文本会丢掉 < 5 and 7 >。像 title="x > y" 这样的属性也会让它出错。

面试官想听的答案: HTML 是嵌套的,而嵌套正是正则表达式无法计数的。要从 HTML 中提取文本,请用浏览器自带的解析器(new DOMParser() 和 textContent);要让 HTML 变得安全,请用清理(sanitizer)库。正则可以用来快速查看可信的标记,但绝不能用于安全防护。

16为面试做练习

读解答有帮助;在一点压力下亲手写模式帮助更大。

  • 练习包每个包专练一种技巧,大多数关卡都是你工作中会写的模式:格式、日志行和代码中的标识符。
  • 每日狩猎只需五分钟,能在两次面试之间保持你的正则手感。
  • 如果面试用 Python,读读JavaScript 与 Python 正则表达式,了解那些容易让人踩坑的差异。