Regular Expressions Tutorial: A Beginner-Friendly Guide to Regex

· 10 min read

什么是正则表达式以及为什么要学习它

正则表达式(Regular Expression,简称 regex 或 regexp)是一种强大的文本模式匹配工具。它使用特殊的字符序列来定义搜索模式,可以用于查找、替换、验证和提取文本中的特定内容。

想象一下,你需要从一个包含数千行日志的文件中找出所有的电子邮件地址,或者验证用户输入的手机号码格式是否正确。如果使用传统的字符串操作方法,代码会变得冗长且难以维护。而正则表达式可以用一行简洁的模式来完成这些任务。

为什么要学习正则表达式?

正则表达式的应用场景非常广泛:

基础语法

正则表达式由普通字符和特殊字符(元字符)组成。让我们从最基础的概念开始。

字面字符(Literal Characters)

最简单的正则表达式就是普通字符本身。例如,正则表达式 cat 会匹配文本中的 "cat" 字符串。

文本: "The cat sat on the mat"
正则: cat
匹配: "The cat sat on the mat"

字面字符是区分大小写的,所以 cat 不会匹配 "Cat" 或 "CAT"。

点号(.)

点号 . 是一个特殊字符,它可以匹配除换行符外的任意单个字符。

文本: "cat", "cot", "cut", "c@t"
正则: c.t
匹配: 所有四个字符串都会被匹配

如果你想匹配字面意义上的点号,需要使用反斜杠进行转义:\.

文本: "file.txt"
正则: file\.txt
匹配: "file.txt" (而不是 "fileAtxt" 等)

锚点(Anchors)

锚点用于指定匹配的位置,而不是匹配具体的字符。

^ (脱字符) - 行首

^ 匹配字符串或行的开始位置。

文本: "cat\ndog\ncat"
正则: ^cat
匹配: 只匹配第一行的 "cat"

$ (美元符号) - 行尾

$ 匹配字符串或行的结束位置。

文本: "cat\ndog\ncat"
正则: cat$
匹配: 只匹配最后一行的 "cat"

组合使用

结合使用 ^$ 可以匹配整行内容:

正则: ^cat$
匹配: 只匹配完整的一行,且该行只包含 "cat"

转义字符

某些字符在正则表达式中有特殊含义,如果要匹配这些字符本身,需要使用反斜杠 \ 进行转义:

特殊字符: . * + ? ^ $ { } [ ] ( ) | \
转义方式: \. \* \+ \? \^ \$ \{ \} \[ \] \( \) \| \\

例如,匹配价格 "$5.99":

正则: \$5\.99

字符类

字符类允许你定义一组字符,匹配其中的任意一个。

方括号字符类 [abc]

方括号 [] 内的字符表示一个字符集,匹配其中的任意一个字符。

文本: "cat", "cot", "cut", "cit"
正则: c[aou]t
匹配: "cat", "cot", "cut" (不匹配 "cit")

范围 [a-z]

使用连字符 - 可以定义字符范围:

[a-z]   匹配任意小写字母
[A-Z]   匹配任意大写字母
[0-9]   匹配任意数字
[a-zA-Z] 匹配任意字母
[a-z0-9] 匹配任意字母或数字

示例:

文本: "a1", "b2", "c3", "d4"
正则: [a-c][1-3]
匹配: "a1", "b2", "c3" (不匹配 "d4")

否定字符类 [^abc]

在方括号内使用 ^ 表示否定,匹配不在集合中的任意字符:

正则: [^0-9]
含义: 匹配任意非数字字符
文本: "a1b2c3"
正则: [^0-9]+
匹配: "a", "b", "c" (所有非数字字符)

预定义字符类

正则表达式提供了一些常用的预定义字符类:

\d - 数字

\d 等价于 [0-9],匹配任意数字字符。

文本: "Room 123"
正则: Room \d+
匹配: "Room 123"

\D - 非数字

\D 等价于 [^0-9],匹配任意非数字字符。

\w - 单词字符

\w 等价于 [a-zA-Z0-9_],匹配字母、数字或下划线。

文本: "user_name123"
正则: \w+
匹配: "user_name123"

\W - 非单词字符

\W 等价于 [^a-zA-Z0-9_],匹配任意非单词字符。

\s - 空白字符

\s 匹配任意空白字符,包括空格、制表符、换行符等。

等价于: [ \t\n\r\f\v]
文本: "hello world"
正则: hello\sworld
匹配: "hello world"

\S - 非空白字符

\S 匹配任意非空白字符。

实际应用示例

匹配一个简单的用户名(只包含字母、数字和下划线,3-16个字符):

正则: ^[a-zA-Z0-9_]{3,16}$
或者: ^\w{3,16}$

匹配十六进制颜色代码:

正则: #[0-9A-Fa-f]{6}
示例: "#FF5733", "#00ff00"

量词

量词用于指定前面的字符或组应该出现的次数。

* (星号) - 零次或多次

* 匹配前面的元素零次或多次。

文本: "ct", "cat", "caat", "caaat"
正则: ca*t
匹配: 所有字符串都匹配

+ (加号) - 一次或多次

+ 匹配前面的元素一次或多次。

文本: "ct", "cat", "caat", "caaat"
正则: ca+t
匹配: "cat", "caat", "caaat" (不匹配 "ct")

? (问号) - 零次或一次

? 匹配前面的元素零次或一次,使其成为可选的。

文本: "color", "colour"
正则: colou?r
匹配: 两个单词都匹配

另一个常见用法是匹配可选的协议:

正则: https?://
匹配: "http://" 和 "https://"

{n} - 精确次数

{n} 匹配前面的元素恰好 n 次。

文本: "123", "1234", "12345"
正则: \d{4}
匹配: "1234" 中的 "1234", "12345" 中的前四个数字

{n,} - 至少 n 次

{n,} 匹配前面的元素至少 n 次。

正则: \d{3,}
匹配: 至少三个连续的数字

{n,m} - n 到 m 次

{n,m} 匹配前面的元素至少 n 次,最多 m 次。

正则: \d{2,4}
匹配: 2到4个连续的数字

贪婪与非贪婪匹配

默认情况下,量词是贪婪的,会尽可能多地匹配字符。在量词后面加上 ? 可以使其变为非贪婪(懒惰)模式。

文本: "<div>content</div>"
正则(贪婪): <.+>
匹配: "<div>content</div>" (整个字符串)

正则(非贪婪): <.+?>
匹配: "<div>" 和 "</div>" (分别匹配)

实际应用示例

匹配中国大陆手机号码(11位数字,以1开头):

正则: ^1\d{10}$
解释: ^ 开始, 1 字面字符, \d{10} 十个数字, $ 结束

匹配 HTML 标签:

正则: <[^>]+>
解释: < 左尖括号, [^>]+ 一个或多个非右尖括号字符, > 右尖括号

匹配重复的单词:

正则: \b(\w+)\s+\1\b
示例: "the the" 会被匹配

分组与捕获

圆括号 () 用于创建分组,可以将多个字符作为一个单元处理,并且可以捕获匹配的内容供后续使用。

基本分组

使用圆括号可以将多个字符组合成一个单元,然后对整个单元应用量词:

文本: "ababab"
正则: (ab)+
匹配: "ababab" (整个字符串)

没有分组的话:

正则: ab+
匹配: "ab", "abb", "abbb" 等 (只有 b 重复)

捕获组

圆括号不仅用于分组,还会捕获匹配的内容。每个捕获组都有一个编号,从1开始。

文本: "John Doe"
正则: (\w+)\s(\w+)
捕获组1: "John"
捕获组2: "Doe"

捕获的内容可以在替换操作中使用,或者通过编程语言的 API 访问。

反向引用

反向引用允许你在正则表达式中引用之前捕获的内容。使用 \1, \2 等表示第1、第2个捕获组。

文本: "hello hello", "world world"
正则: (\w+)\s\1
匹配: 重复的单词

匹配 HTML 标签对:

正则: <(\w+)>.*?</\1>
示例: "<div>content</div>" 匹配
      "<div>content</span>" 不匹配

非捕获组

如果只想分组而不想捕获,可以使用 (?:...):

正则: (?:https?|ftp)://\w+
解释: 匹配 http, https 或 ftp 协议,但不捕获协议部分

非捕获组的优势:

命名捕获组

许多现代正则表达式引擎支持命名捕获组,使用 (?<name>...) 语法:

正则: (?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
文本: "2024-03-15"
捕获: year="2024", month="03", day="15"

实际应用示例

提取日期并重新格式化:

文本: "2024-03-15"
正则: (\d{4})-(\d{2})-(\d{2})
替换: $2/$3/$1
结果: "03/15/2024"

验证并提取邮箱地址的用户名和域名:

正则: ^([a-zA-Z0-9._%-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$
文本: "[email protected]"
捕获组1: "user"
捕获组2: "example.com"

选择符

管道符 | 用于表示"或"的关系,匹配多个可能的模式之一。

基本用法

正则: cat|dog
匹配: "cat" 或 "dog"
文本: "I have a cat and a dog"
正则: cat|dog
匹配: "cat" 和 "dog"

与分组结合

选择符通常与分组一起使用,以限制其作用范围:

正则: (cat|dog)s?
匹配: "cat", "cats", "dog", "dogs"

没有分组的话:

正则: cat|dogs
匹配: "cat" 或 "dogs" (注意 s 只应用于 dog)

多个选择

可以有多个选择项:

正则: red|green|blue
匹配: "red", "green" 或 "blue"

实际应用示例

匹配常见的图片文件扩展名:

正则: \.(jpg|jpeg|png|gif|bmp|svg)$
示例: "image.jpg", "photo.png"

匹配不同的日期格式:

正则: \d{4}-\d{2}-\d{2}|\d{2}/\d{2}/\d{4}
匹配: "2024-03-15" 或 "03/15/2024"

匹配常见的编程语言关键字:

正则: \b(if|else|while|for|return|function|class)\b

前瞻与后顾

前瞻(lookahead)和后顾(lookbehind)是零宽度断言,它们匹配位置而不是字符,不会消耗字符。

正向前瞻 (?=...)

正向前瞻断言某个位置后面跟着特定的模式,但不包括该模式在匹配结果中。

文本: "abc123"
正则: \w+(?=\d)
匹配: "abc" (后面跟着数字,但数字不包括在匹配中)

实际应用 - 匹配后面跟着 "px" 的数字:

文本: "width: 100px, height: 200px"
正则: \d+(?=px)
匹配: "100" 和 "200"

负向前瞻 (?!...)

负向前瞻断言某个位置后面不跟着特定的模式。

文本: "abc123 def456"
正则: \w+(?!\d)
匹配: "abc12" 和 "def45" (后面不跟数字的字母数字序列)

实际应用 - 匹配不以 ".jpg" 结尾的文件名:

正则: \w+\.(?!jpg)\w+
匹配: "file.png", "doc.pdf" (不匹配 "image.jpg")

正向后顾 (?<=...)

正向后顾断言某个位置前面是特定的模式。

文本: "$100, €200, £300"
正则: (?<=\$)\d+
匹配: "100" (前面是美元符号的数字)

实际应用 - 提取价格数字(前面有货币符号):

正则: (?<=\$|€|£)\d+
匹配: 所有货币符号后的数字

负向后顾 (?<!...)

负向后顾断言某个位置前面不是特定的模式。

文本: "abc123 xyz789"
正则: (?<![a-z])\d+
匹配: 前面不是字母的数字序列

组合使用

前瞻和后顾可以组合使用,创建复杂的匹配条件:

正则: (?<=@)\w+(?=\.)
文本: "[email protected]"
匹配: "example" (@ 和 . 之间的内容)

实际应用示例

密码强度验证(至少8个字符,包含大写字母、小写字母和数字):

正则: ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
解释:
  (?=.*[a-z]) 确保至少有一个小写字母
  (?=.*[A-Z]) 确保至少有一个大写字母
  (?=.*\d)    确保至少有一个数字
  .{8,}       至少8个字符

提取 HTML 标签之间的内容:

正则: (?<=>)[^<]+(?=<)
文本: "<div>Hello World</div>"
匹配: "Hello World"

常用正则表达式模式表

以下是一些常用的正则表达式模式,可以直接在项目中使用或根据需要进行调整。

类型 正则表达式 说明 示例
电子邮件 ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ 基本的邮箱验证 [email protected]
电子邮件(严格) ^[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 标准 [email protected]
中国手机号 ^1[3-9]\d{9}$ 11位,以1开头 13812345678
美国电话 ^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$ 支持多种格式 (123) 456-7890
URL ^https?://[^\s/$.?#].[^\s]*$ 基本 URL 验证 https://example.com
URL(完整) ^(https?|ftp)://[^\s/$.?#].[^\s]*$ 包括 FTP 协议 ftp://files.example.com
IPv4 地址 ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ 验证 IPv4 地址 192.168.1.1
IPv6 地址 ^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::)$ 简化的 IPv6 验证

Frequently Asked Questions

What does the regex .* mean?

The dot (.) matches any character except newline, and the asterisk (*) means zero or more times. So .* matches any sequence of characters, including an empty string. It is the most greedy pattern.

How do I match an email address with regex?

A practical pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ This matches most valid emails. Note that fully RFC-compliant email validation is extremely complex.

What is the difference between * and + in regex?

* matches zero or more occurrences (optional, can match nothing). + matches one or more occurrences (requires at least one match). For example, a* matches empty string, but a+ requires at least one "a".

How do I make regex case-insensitive?

Add the i flag: /pattern/i in JavaScript, re.IGNORECASE in Python, or -i flag in grep. This makes [a-z] also match uppercase letters.

What is catastrophic backtracking in regex?

When a regex engine tries exponentially many paths to match a pattern, causing extreme slowness. Common cause: nested quantifiers like (a+)+. Avoid by using atomic groups, possessive quantifiers, or restructuring the pattern.