学习正则表达式:基础入门篇


学习正则表达式:基础入门篇

前言:什么是正则表达式,为什么它如此重要?

在浩瀚的数字世界里,我们每天都在与海量的文本数据打交道。无论是编写代码、处理日志文件、验证用户输入、网页抓取,还是简单地在文档中查找特定模式,我们都面临着一个共同的挑战:如何高效、精确地识别和操作符合特定规则的文本?

想象一下,你需要从一篇数千字的文章中找出所有的电子邮件地址,或者需要验证用户输入的手机号码是否符合中国大陆的格式,又或者你想从复杂的服务器日志中提取所有报错信息。手动完成这些任务不仅耗时耗力,而且极易出错。这时,正则表达式(Regular Expression,常简写为 regex 或 regexp) 就如同一把瑞士军刀,为我们提供了强大而灵活的解决方案。

正则表达式,本质上是一种描述文本模式(pattern)的微型语言。 它使用一系列特殊字符(称为“元字符”)和普通字符组合起来,定义了一个搜索规则。通过这个规则,我们可以在文本中进行复杂的查找、替换、提取和验证操作。

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

  1. 效率提升: 对于重复性的文本处理任务,正则表达式可以极大地提高工作效率,将原本需要数小时甚至数天手动完成的工作,缩短到几秒钟或几分钟。
  2. 功能强大: 它可以处理非常复杂的文本模式匹配,这是简单的字符串查找功能无法比拟的。
  3. 应用广泛: 几乎所有的编程语言(如 Python, Java, JavaScript, PHP, Ruby, C#, Go 等)都内置了对正则表达式的支持。此外,许多文本编辑器(如 VS Code, Sublime Text, Notepad++)、数据库(如 MySQL, PostgreSQL)、命令行工具(如 grep, sed, awk)以及各种网络服务和应用都广泛使用正则表达式。掌握它意味着你在众多领域都拥有了一项核心技能。
  4. 数据处理的核心: 在数据清洗、数据提取、数据验证等数据科学和数据分析任务中,正则表达式是不可或缺的工具。
  5. 编程必备技能: 对于软件开发者、系统管理员、数据分析师、测试工程师等 IT 从业者来说,正则表达式几乎是一项必备的基础技能。

然而,正则表达式的语法初看起来可能有些晦涩难懂,充满了各种特殊符号,让许多初学者望而却步。但这就像学习任何一门新语言一样,只要掌握了基本的语法规则和核心概念,循序渐进地练习,你会发现它并没有想象中那么可怕,反而其强大的功能会让你爱不释手。

本篇文章《学习正则表达式:基础入门篇》旨在为初学者铺设一条平坦的学习路径。我们将从最基础的概念讲起,逐步深入,通过大量的示例和清晰的解释,帮助你理解正则表达式的核心原理,掌握常用的元字符和语法规则,并学会如何在实际场景中应用它们。无论你是否有编程基础,只要你对高效处理文本感兴趣,这篇文章都将是你入门正则表达式的理想起点。

让我们一起揭开正则表达式的神秘面纱,探索这个强大工具的魅力吧!

第一章:正则表达式的基石——字面量与元字符

正则表达式的核心在于其独特的语法,它由两类字符构成:

  1. 字面量(Literal Characters): 指的是那些在正则表达式中代表其自身含义的普通字符,比如字母 azAZ,数字 09,以及一些没有特殊含义的符号(如下划线 _)。
  2. 元字符(Metacharacters): 指的是那些在正则表达式中具有特殊含义、不代表其字面意思的字符。它们是正则表达式强大功能的关键所在。

1.1 字面量匹配:所见即所得

最简单的正则表达式就是由纯粹的字面量构成的。这种正则表达式的匹配规则非常直观:精确匹配(Exact Match)

例如,正则表达式 cat 会在文本中精确查找并匹配字符串 “cat”。

  • 在文本 “The cat sat on the mat.” 中,cat 会匹配到第一个 “cat”。
  • 在文本 “scatter” 中,cat 也会匹配到中间的 “cat”。
  • 在文本 “Catch the catalog” 中,cat 会匹配 “Catch” 中的 “cat” 和 “catalog” 中的 “cat”。
  • 在文本 “Dog” 中,cat 无法找到任何匹配。

字面量匹配是正则表达式的基础,虽然简单,但在需要精确查找固定字符串时非常有用。

1.2 元字符:赋予正则表达式魔力

元字符是正则表达式的灵魂。它们赋予了我们描述更复杂、更灵活模式的能力。不同的正则表达式流派(flavor,我们稍后会讨论)支持的元字符可能略有差异,但核心的元字符是共通的。下面我们将逐一介绍最常用、最重要的元字符。

1.2.1 点号 .:匹配任意单个字符(除换行符外)

点号 . 是一个非常常用的元字符,它代表除了换行符(\n)之外的任何单个字符

  • 正则表达式 c.t 可以匹配:
    • cat (点号匹配 a)
    • cot (点号匹配 o)
    • c_t (点号匹配 _)
    • c8t (点号匹配 8)
    • c#t (点号匹配 #)
  • 但它不能匹配 ct (因为 . 必须代表一个字符)或 c\nt (如果点号的默认行为不包含换行符)。

注意: 在某些正则表达式的模式(mode)或特定实现中,可以设置让 . 也匹配换行符,但这通常不是默认行为。

1.2.2 字符集 [...]:匹配方括号内的任意一个字符

如果我们想匹配特定几个字符中的任意一个,可以使用方括号 [...] 来定义一个字符集(Character Set)。方括号内的任何一个字符都可以被匹配。

  • 正则表达式 gr[ae]y 可以匹配:
    • gray (a 在字符集 [ae] 中)
    • grey (e 在字符集 [ae] 中)
  • 但它不能匹配 grygraey

  • 正则表达式 [0123456789] 可以匹配任意一个阿拉伯数字字符。

字符集范围 [ - ]
为了方便表示连续的字符范围,可以在字符集内部使用连字符 -

  • [0-9] 等同于 [0123456789],匹配任意一个数字。
  • [a-z] 匹配任意一个小写字母。
  • [A-Z] 匹配任意一个大写字母。
  • [a-zA-Z] 匹配任意一个大写或小写字母。
  • [a-zA-Z0-9] 匹配任意一个字母或数字。
  • [0-9a-fA-F] 匹配任意一个十六进制数字符。

注意:
* 连字符 - 只有在两个字符之间且有意义时(如 a-z, 0-9)才表示范围。如果它出现在字符集的开头、结尾,或者紧跟在另一个范围之后(如 [a-z-][-a-z][a-z0-9-]),它通常只代表普通的连字符 - 本身。
* 在字符集内部,大部分元字符(如 .*+? 等)会失去其特殊含义,变成普通的字面量字符。例如,[.?*] 只会匹配点号 .、问号 ? 或星号 * 本身。但是,有几个例外:
* ]:如果想在字符集里匹配右方括号 ],通常需要将其放在字符集的最前面 []...](某些引擎)或使用反斜杠转义 [...\]...]
* ^:如果出现在字符集的开头 [^...],具有特殊含义(见下文)。
* -:如上所述,用于表示范围。如果想匹配 - 本身,最好将其放在开头 [-...]、结尾 [...-] 或紧跟范围之后 [a-z-...],或使用反斜杠转义 [\-]
* \:仍然用于转义,例如 [\d] 在某些引擎里可能依然代表数字(虽然通常直接写 [0-9] 更清晰),而 [\^] 则明确匹配 ^ 字符本身。

1.2.3 排除型字符集 [^...]:匹配不在方括号内的任意一个字符

如果在字符集的开头使用脱字符 ^,它表示否定(Negation)[^...] 会匹配任何一个没有^ 后面列出的字符。

  • 正则表达式 [^0-9] 匹配任何一个非数字字符。
  • 正则表达式 [^aeiou] 匹配任何一个非小写元音字母的字符。
  • 正则表达式 q[^u] 匹配一个 q 后面紧跟着一个不是 u 的字符。例如,它可以匹配 “Iraq” 中的 “q” 后面的空格,或者 “cinq” 中的 “q”,但不能匹配 “quit” 或 “queen” 中的 “qu”。

重要提示: ^ 只有紧跟在左方括号 [ 后面时才表示否定。在其他位置(我们稍后会看到),^ 有不同的含义(表示字符串或行的开始)。

1.2.4 预定义字符集(常用字符类的简写)

为了方便书写常用的字符集,正则表达式提供了一些预定义的字符集(或称字符类 Character Classes)作为简写形式。它们通常以反斜杠 \ 开头。

  • \d:匹配任意一个数字字符。等同于 [0-9]
  • \D:匹配任意一个非数字字符。等同于 [^0-9]
  • \w:匹配任意一个单词字符(Word Character)。通常包括大小写字母、数字和下划线 _。等同于 [a-zA-Z0-9_]
  • \W:匹配任意一个非单词字符。等同于 [^a-zA-Z0-9_]。它会匹配空格、标点符号等。
  • \s:匹配任意一个空白字符(Whitespace Character)。通常包括空格 、制表符 \t、换行符 \n、回车符 \r、换页符 \f、垂直制表符 \v 等。
  • \S:匹配任意一个非空白字符。等同于 [^\s]

这些预定义字符集大大简化了正则表达式的书写。例如,要匹配一个或多个数字,我们可以写 \d+ 而不是 [0-9]++ 的含义稍后介绍)。

注意: \w 的具体定义可能因正则表达式引擎和区域设置(locale)而略有不同,有时可能包含其他 Unicode 字符。但 [a-zA-Z0-9_] 是最普遍的定义。

1.2.5 锚点(Anchors):定位匹配位置

锚点用于指定匹配必须发生在字符串中的特定位置,它们本身不匹配任何字符,只匹配位置。

  • ^:匹配字符串的开头

    • 正则表达式 ^cat 只会匹配以 “cat” 开头的字符串,如 “catastrophe”,但不会匹配 “The cat”。
    • 多行模式(Multiline Mode): 在某些模式下(通常通过标记 flag 启用),^ 也可以匹配每一行的开头(紧跟在换行符之后的位置)。
  • $:匹配字符串的结尾

    • 正则表达式 cat$ 只会匹配以 “cat” 结尾的字符串,如 “The cat”,但不会匹配 “catastrophe”。
    • 多行模式: 在多行模式下,$ 也可以匹配每一行的结尾(紧挨在换行符之前的位置)。
  • \b:匹配单词边界(Word Boundary)。单词边界是指 \w(单词字符)和 \W(非单词字符)之间的位置,或者 \w 与字符串开头/结尾之间的位置。

    • 正则表达式 \bcat\b 会匹配作为一个独立单词出现的 “cat”。它可以匹配 “The cat sat”,但不会匹配 “catastrophe” 或 “scatter”。
    • \b 的判断逻辑是:当前位置的一边是 \w,另一边是 \W(或者是字符串的开头/结尾)。
    • 例如,在 “cat!” 中,t! 之间有一个单词边界,c 和字符串开头之间也有一个。但在 “cat” 内部,ca 之间、at 之间没有单词边界。
  • \B:匹配非单词边界(Non-word Boundary)。它是 \b 的反义,匹配任何不是单词边界的位置。

    • 正则表达式 \Bcat\B 会匹配被其他单词字符包围的 “cat”,例如在 “concatenate” 中匹配 “cat”,但不会匹配 “The cat sat”。
    • 正则表达式 \bcat\B 会匹配以 “cat” 开头但不以 “cat” 结尾的单词,如 “catastrophe”。
    • 正则表达式 \Bcat\b 会匹配以 “cat” 结尾但不以 “cat” 开头的单词,如 “scatter”。

锚点对于确保匹配发生在预期位置至关重要,尤其是在进行验证或精确提取时。

1.2.6 量词(Quantifiers):指定重复次数

量词用于指定前面的元素(可以是一个字面量字符、一个字符集、一个预定义字符集,或者一个分组)必须重复出现的次数。

  • *(星号):匹配前面的元素零次或多次(0 or more times)。

    • colou*r 匹配 “color” (u 出现 0 次) 和 “colour” (u 出现 1 次),甚至 “colouuuuur” (u 出现 5 次)。
    • ab*c 匹配 “ac”, “abc”, “abbc”, “abbbc” 等。
  • +(加号):匹配前面的元素一次或多次(1 or more times)。

    • go+gle 匹配 “gogle”, “google”, “gooogle” 等,但不匹配 “ggle”。
    • \d+ 匹配一个或多个数字,如 “1”, “123”, “98765”。
  • ?(问号):匹配前面的元素零次或一次(0 or 1 time)。这表示前面的元素是可选的

    • colou?r 匹配 “color” (u 出现 0 次) 和 “colour” (u 出现 1 次),但不匹配 “colouur”。
    • https?:// 匹配 “http://” 和 “https://”。
  • {n}:匹配前面的元素恰好 n

    • \d{3} 匹配恰好 3 个数字,如 “123”,但不匹配 “12” 或 “1234”。
    • a{5} 匹配 “aaaaa”。
  • {n,}:匹配前面的元素至少 n(n or more times)。

    • \d{3,} 匹配至少 3 个数字,如 “123”, “1234”, “12345” 等,但不匹配 “12”。
  • {n,m}:匹配前面的元素至少 n 次,但不超过 m(between n and m times, inclusive)。

    • \d{3,5} 匹配 3 到 5 个数字,如 “123”, “1234”, “12345”,但不匹配 “12” 或 “123456”。
    • a{2,4} 匹配 “aa”, “aaa”, “aaaa”。

贪婪模式 vs. 惰性模式 (Greedy vs. Lazy)

默认情况下,量词 *, +, {n,}, {n,m} 都是贪婪的(Greedy)。这意味着它们会尽可能多地匹配字符,同时仍然允许整个正则表达式匹配成功。

  • 考虑文本 “

    Title

    “。

  • 正则表达式 <.+> 使用贪婪的 + 量词。. 匹配除换行符外的任何字符。贪婪的 + 会一直匹配到最后一个 >,因此整个匹配结果是 “

    Title

    “。

有时,我们希望量词尽可能少地匹配字符,这就是惰性模式(Lazy,也叫非贪婪模式 Non-Greedy)。通过在贪婪量词后面加上一个问号 ? 来启用惰性模式。

  • *?:匹配零次或多次,但尽可能少地匹配。
  • +?:匹配一次或多次,但尽可能少地匹配。
  • ??:匹配零次或一次,但尽可能少地匹配(通常倾向于匹配 0 次)。
  • {n,}?:匹配至少 n 次,但尽可能少地匹配。
  • {n,m}?:匹配 nm 次,但尽可能少地匹配(倾向于匹配 n 次)。

  • 还是文本 “

    Title

    “。

  • 正则表达式 <.+?> 使用惰性的 +? 量词。当它遇到第一个 > 时(即 <h1> 中的 >),它发现已经满足了 .+?(匹配了 h1)并且整个模式(后面还有一个 >)也能匹配成功,于是它就停止匹配。因此,第一个匹配结果是 “

    “。如果允许查找所有匹配项,它会继续查找,找到第二个匹配 “

    “。

理解贪婪和惰性模式对于精确控制匹配范围非常重要,尤其是在处理嵌套结构或重复模式时。

1.2.7 转义字符 \:还原元字符的字面意义

如果我们需要匹配一个本身就是元字符的字符(如 .*+?^$()[]{}\ 本身),我们需要使用反斜杠 \ 来进行转义(Escape),告诉正则表达式引擎将其视为普通的字面量字符,而不是具有特殊含义的元字符。

  • 要匹配点号 . 本身,使用 \.。例如,192\.168\.1\.1 匹配 IP 地址 “192.168.1.1”。如果写成 192.168.1.1. 会匹配任何字符,可能会错误地匹配 “192a168b1c1″。
  • 要匹配星号 * 本身,使用 \*
  • 要匹配问号 ? 本身,使用 \?
  • 要匹配反斜杠 \ 本身,使用 \\
  • 要匹配左方括号 [ 本身,使用 \[
  • 要匹配左花括号 { 本身,使用 \{
  • 等等…

注意: 在字符集 [...] 内部,如前所述,大部分元字符(除了 ], ^, -, \)通常不需要转义,它们默认就是字面量。但在字符集外部,它们都需要转义才能匹配其字面含义。

反斜杠 \ 的另一个作用是用于引入预定义字符集(如 \d, \s, \w)和锚点(如 \b)。

1.2.8 分组与捕获 (...):合并与提取

圆括号 (...) 在正则表达式中有两个主要作用:

  1. 分组(Grouping): 将多个字符或子模式视为一个整体单元。这允许我们将量词应用于整个组,或者将 alternation(见下文)限制在组内。

    • 正则表达式 (abc)+ 匹配一个或多个连续的 “abc” 序列,如 “abc”, “abcabc”, “abcabcabc”。如果没有括号,abc+ 只会匹配 “ab” 后面跟着一个或多个 “c”,如 “abc”, “abcc”, “abccc”。
    • ^(http|https):// 匹配以 “http://” 或 “https://” 开头的字符串。这里的括号将 httphttps 以及 |(表示或)组合在一起。
  2. 捕获(Capturing): 默认情况下,圆括号括起来的部分会被正则表达式引擎捕获(Capture)并存储起来,以便后续引用或提取。这些被捕获的部分称为捕获组(Capturing Groups)。捕获组按照它们在正则表达式中出现的左括号 ( 的顺序从 1 开始编号。

    • 正则表达式 (\d{4})-(\d{2})-(\d{2}) 用于匹配 YYYY-MM-DD 格式的日期。
      • 当它匹配 “2023-10-26” 时:
      • 捕获组 1 ( (\d{4}) ) 会捕获 “2023”。
      • 捕获组 2 ( (\d{2}) ) 会捕获 “10”。
      • 捕获组 3 ( (\d{2}) ) 会捕获 “26”。
    • 许多编程语言和工具允许你访问这些捕获组的内容。例如,在 Python 中使用 re 模块,你可以通过 match.group(1), match.group(2), match.group(3) 来分别获取年、月、日。
    • 整个正则表达式匹配的内容通常可以通过 group(0) 或类似方式获得(即 “2023-10-26″)。

非捕获组 (?:...)

有时我们只需要分组的功能(比如应用量词或限制 alternation),但并不需要捕获该部分的内容,或者不希望它干扰捕获组的编号。这时可以使用非捕获组(Non-capturing Group),语法是在左括号后紧跟 ?:

  • 正则表达式 (?:abc)+ 仍然匹配一个或多个 “abc” 序列,但 “abc” 本身不会被捕获。
  • 考虑从 <h1>Title</h1> 中提取标签名(h1)和内容(Title)。
    • 使用 (<(h1|h2|h3)>)(.*?)(</\2>)(这里用到了反向引用 \2,稍后简单提及,表示匹配与第二个捕获组相同的内容)。
      • 组 1: <h1> (因为 (<(h1|h2|h3)>) )
      • 组 2: h1 (因为 (h1|h2|h3) )
      • 组 3: Title (因为 (.*?) )
      • 组 4: </h1> (因为 (</\2>) )
    • 如果我们只想捕获标签名和内容,可以改写为:<([hH][1-6])>(.*?)</\1> (更简洁常用方式)
      • 组 1: h1H1H6
      • 组 2: Title
    • 或者,如果我们明确不希望捕获整个开始标签,可以使用非捕获组:(?:<([hH][1-6])>)(.*?)(?:</\1>)
      • 组 1: h1H1H6
      • 组 2: Title
      • 这样捕获组编号更清晰,只包含我们关心的部分。

使用非捕获组通常是好的实践,除非你确实需要捕获该组的内容。它可以提高效率(引擎不需要存储捕获内容)并使捕获组的管理更简单。

1.2.9 选择(Alternation) |:匹配多个可能选项之一

竖线 | 在正则表达式中表示或(OR) 的关系,允许匹配 | 两边的任意一个模式。

  • cat|dog 匹配 “cat” 或者 “dog”。
  • gr(a|e)y 等同于 gr[ae]y,匹配 “gray” 或 “grey”。括号 () 在这里用于限制 | 的作用范围,只在 ae 之间选择。如果没有括号,gra|ey 会匹配 “gra” 或者 “ey”。
  • ^(GET|POST|PUT|DELETE)$ 匹配恰好是 “GET”, “POST”, “PUT”, “DELETE” 其中之一的完整字符串。

注意 | 的优先级: | 的优先级相对较低。例如,abc|def 匹配 “abc” 或 “def”。如果想匹配 “ab” 后面跟着 “c” 或 “d” 再跟着 “ef”,需要使用括号:ab(c|d)ef。它会先尝试匹配 ab,然后匹配 cd,最后匹配 ef

第二章:整合运用——构建实用正则表达式

掌握了基本的元字符后,我们就可以开始组合它们来构建解决实际问题的正则表达式了。编写正则表达式往往是一个逐步求精的过程:从一个简单的模式开始,根据需求不断添加约束和细节。

2.1 示例:验证简单的电子邮件地址

电子邮件地址的格式非常复杂,完美的验证 regex 极其冗长且难以维护(甚至有争议说不可能完全用 regex 完美验证所有符合 RFC 标准的地址)。但我们可以构建一个用于常见场景的、相对简化的验证 regex。

目标: 验证一个基本的 [email protected] 格式。
* 用户名部分(username):允许字母、数字、下划线 _、点号 .、连字符 -。至少一个字符。点号和连字符不能连续出现,也不能出现在开头或结尾(简化起见,暂时忽略此复杂约束)。
* @ 符号:必须存在。
* 域名部分(domain):允许字母、数字、连字符 -。至少一个字符。连字符不能出现在开头或结尾(简化起见,暂时忽略)。
* 点号 .:必须存在。
* 顶级域名(tld):只允许字母,至少两个字符。

逐步构建:

  1. 基本结构: ^.+@.+\..+$

    • ^: 字符串开头。
    • .+: 匹配至少一个任意字符(用户名)。
    • @: 匹配 @ 符号。
    • .+: 匹配至少一个任意字符(域名)。
    • \.: 匹配点号 . 本身(注意转义)。
    • .+: 匹配至少一个任意字符(顶级域名)。
    • $: 字符串结尾。
    • 问题: 这个 regex 太宽松了,!@#$%^&*().! 也能匹配。
  2. 限制字符集:

    • 用户名:[\w.-]+ (允许单词字符、点、连字符,至少一个)
    • 域名:[a-zA-Z0-9-]+ (允许字母、数字、连字符,至少一个)
    • 顶级域名:[a-zA-Z]{2,} (只允许字母,至少两个)
    • 组合起来: ^[\w.-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$
    • 改进: 这个版本好多了,限制了允许的字符。
  3. 处理域名中的点: 域名部分可能包含子域名,如 sub.domain.tld。我们的 [a-zA-Z0-9-]+ 只处理了一级域名。

    • 域名部分可以看作是 [a-zA-Z0-9-]+ 后面跟着零个或多个 \.[a-zA-Z0-9-]+ 的组合。
    • 改进域名部分: [a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*
      • [a-zA-Z0-9-]+: 匹配第一部分(如 domainsub)。
      • (\.[a-zA-Z0-9-]+)*: 匹配零个或多个(由 * 控制). 加上更多域名部分的组合(如 .com 中的 ., 或 .co.uk 中的 .co)。这里使用了非捕获组 (?:...) 可能更好,如果我们不关心捕获子域名结构: [a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*
    • 更新整个 regex (使用非捕获组): ^[\w.-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$
    • 问题: 这个 regex 仍然允许用户名或域名部分以 .- 开头/结尾,例如 [email protected] 也能匹配。它也允许 @. 这种无效情况。
  4. 进一步精化(更复杂,超出基础入门范畴): 解决开头/结尾的连字符/点号问题,以及不允许连续点号/连字符等,需要更复杂的结构,可能用到前后查找(Lookarounds)等高级特性。对于入门,上面的版本通常作为一个不错的起点。

要点:
* 从简单模式开始,逐步添加约束。
* 使用字符集 [...] 限制允许的字符。
* 使用量词 +, *, {n,m} 控制重复次数。
* 使用 ^$ 锚定整个字符串,确保完全匹配。
* 记得转义元字符,如 \.
* 考虑使用非捕获组 (?:...) 避免不必要的捕获。

2.2 示例:匹配中国大陆手机号码

中国大陆手机号码目前主要是 11 位数字,通常以 13, 14, 15, 16, 17, 18, 19 开头。

目标: 匹配 11 位数字,且以特定数字开头。

逐步构建:

  1. 匹配 11 位数字: ^\d{11}$

    • ^: 开头。
    • \d{11}: 恰好 11 个数字。
    • $: 结尾。
    • 问题: 任何 11 位数字都能匹配,如 01234567890
  2. 限制第一位数字: 手机号第一位是 1

    • ^1\d{10}$
    • 1: 匹配数字 1。
    • \d{10}: 后面跟着 10 个数字。
    • 问题: 仍然太宽泛,10000000000 也能匹配。
  3. 限制第二位数字(号段): 第二位目前主要是 3, 4, 5, 6, 7, 8, 9。

    • ^1[3-9]\d{9}$ (使用字符集 [3-9])
    • [3-9]: 匹配 3 到 9 之间的任意一个数字。
    • \d{9}: 后面跟着 9 个数字。
    • 改进: 这个 regex 已经相当不错,可以覆盖大部分常见手机号格式。
  4. 更精确的号段(如果需要): 如果需要匹配更精确的已知号段组合(如 130-139, 145, 147, 150-153, 155-159 等),regex 会变得更复杂,通常使用 | 来组合多个可能的开头。

    • 例如,一个更严格(但可能需要更新)的版本: ^1(3[0-9]|4[579]|5[0-35-9]|6[6]|7[0-8]|8[0-9]|9[189])\d{8}$
      • 1: 开头是 1。
      • ( ... ): 一个分组,包含所有可能的第二、三位组合。
      • 3[0-9]: 匹配 130-139。
      • |: 或。
      • 4[579]: 匹配 145, 147, 149。
      • … 其他号段 …
      • \d{8}: 最后是 8 位数字。
      • $: 结尾。
    • 注意: 这种非常具体的 regex 需要随着新号段的放出而维护更新。对于一般验证,^1[3-9]\d{9}$ 通常足够。

2.3 示例:从文本中提取 URL

目标: 从一段文本中找出所有看起来像 URL 的字符串(以 http:// 或 https:// 开头)。

文本示例:
“Visit our site at http://www.example.com. Also check https://sub.example.org/path?query=1#fragment and ftp://invalid.link.”

构建 Regex:

  1. 匹配协议头: https?://

    • http: 匹配 “http”。
    • s?: 匹配 s 零次或一次(可选)。
    • ://: 匹配 “://”。
  2. 匹配域名/IP 及路径等: URL 的后面部分结构复杂,可以包含字母、数字、-, ., _, ~, :, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, = 等字符。一个简单(但可能不完美)的方式是匹配一系列非空白字符 \S

    • \S+: 匹配一个或多个非空白字符。
  3. 组合: https?://\S+

    • 这个 regex 会匹配 “http://” 或 “https://” 开头,直到遇到第一个空白字符(空格、换行等)为止。
    • 在示例文本中,它会匹配:
      • http://www.example.com. (注意末尾的点号也被匹配了,因为 . 不是空白符)
      • https://sub.example.org/path?query=1#fragment
    • 问题:
      • 它错误地包含了 URL 末尾可能存在的标点符号(如句号)。
      • 它没有捕获 ftp 等其他协议。
      • \S+ 可能匹配过多或过少,不够精确。
  4. 改进(尝试排除结尾标点): 我们可以尝试更精确地定义 URL 中允许的字符,或者使用更复杂的技巧(如前后查找)来避免捕获结尾的标点。一个稍微改进的版本可能尝试匹配到路径、查询、片段部分结束的位置,但这会显著增加复杂度。对于入门,https?://\S+ 是一个常用且相对有效的起点,但要知道它的局限性。

提取捕获组:
如果我们想分别提取协议、域名等,可以使用捕获组。
* ^(https?):\/\/([^\s/]+)(\/\S*)?$ (这是一个简化的例子,尝试捕获协议、域名和路径)
* ^(https?): 捕获组 1: 协议 (“http” or “https”)
* :\/\/: 匹配 ://
* ([^\s/]+): 捕获组 2: 域名部分 (匹配一个或多个非空白、非斜杠的字符)
* (\/\S*)?: 可选的捕获组 3: 路径及之后 (匹配一个斜杠 / 后跟零个或多个非空白字符)
* 这个 regex 仍然非常简化,例如没有很好地处理端口号、用户名密码等情况。

要点:
* 提取任务通常不需要 ^$ 锚定整个字符串,因为我们是在文本中查找匹配项。
* 使用捕获组 (...) 来提取感兴趣的部分。
* \S+ 是一个快速匹配非空白序列的简便方法,但要注意其可能匹配过多或不足的局限性。

第三章:正则表达式引擎与流派 (Flavors)

虽然我们学习了通用的正则表达式概念和语法,但需要注意,不同的软件和编程语言在实现正则表达式时,其具体的引擎(Engine)和支持的流派(Flavor)可能存在细微差异。

正则表达式引擎是实际执行模式匹配的代码库。常见的引擎有:
* Perl 引擎 (被 PCRE 广泛模仿)
* PCRE (Perl Compatible Regular Expressions):非常流行,被 PHP, R, Nginx, Apache (httpd) 等使用。
* Python 的 re 模块引擎
* JavaScript 的引擎 (ECMAScript 标准定义)
* Java 的 java.util.regex 引擎
* .NET 的 System.Text.RegularExpressions 引擎
* Ruby 的 Onigmo/Oniguruma 引擎
* POSIX BRE (Basic Regular Expressions) 和 ERE (Extended Regular Expressions):在一些 Unix/Linux 工具(如 grep, sed 的不同模式)中使用,语法相对老旧和受限。

流派(Flavor)通常指的是特定引擎支持的语法和特性的集合。虽然核心功能(如字面量、., [], ^, $, *, +, ?, {}, |, (), \)在大多数现代流派中是相似的,但在以下方面可能存在差异:

  • 预定义字符集的具体含义: \w\s 可能包含或排除哪些 Unicode 字符。
  • 支持的简写: 是否支持 \d, \D, \w, \W, \s, \S。POSIX BRE 可能不支持。
  • 元字符的转义: 在 POSIX BRE 中,+, ?, |, () 可能需要转义 \+, \?, \|, \(\) 才能获得特殊含义,而在 ERE 和大多数现代流派中它们本身就是元字符。
  • 高级特性:前后查找(Lookarounds)反向引用(Backreferences)命名捕获组(Named Capture Groups)原子组(Atomic Grouping)递归模式(Recursion)条件判断(Conditionals)等的支持程度不同。
  • 模式修饰符(Flags/Modifiers): 如忽略大小写 (i)、多行模式 (m)、点号匹配换行符 (s. 的 dotall 模式) 等的可用性和表示方式。

对初学者意味着什么?

  1. 关注核心通用语法: 本文介绍的核心元字符和概念在绝大多数现代正则表达式流派中都是通用的。掌握它们是基础。
  2. 了解你使用的环境: 当你在特定的编程语言、编辑器或工具中使用正则表达式时,最好查阅其官方文档,了解它使用的是哪种引擎/流派,以及是否有特殊的语法或行为。
  3. 使用在线测试工具: 利用在线正则表达式测试工具(如 Regex101, RegExr 等)是学习和调试的好方法。这些工具通常允许你选择不同的流派进行测试,帮助你理解差异。

虽然存在差异,但不必过分担心。你学习的基础知识将在 90% 以上的情况下适用。遇到问题时,意识到可能是流派差异,并去查阅文档,通常就能解决。

第四章:工具与资源推荐

学习正则表达式,理论结合实践至关重要。以下是一些推荐的工具和资源:

4.1 在线正则表达式测试器

这些网站允许你输入正则表达式和测试文本,实时查看匹配结果、捕获组、匹配步骤解释等,是学习和调试 regex 的利器。

  • Regex101 (https://regex101.com/): 非常强大和流行。
    • 支持多种流派 (PCRE, Python, JavaScript, Go, Java, .NET)。
    • 提供详细的匹配解释 (Explanation)。
    • 高亮显示匹配和捕获组。
    • 内置 Quick Reference (快速参考)。
    • 可以保存和分享你的 regex 和测试用例。
  • RegExr (https://regexr.com/): 界面友好,功能也很全面。
    • 支持 PCRE 和 JavaScript 流派。
    • 实时匹配高亮和解释。
    • 包含社区分享的 regex 示例。
    • 提供 Cheatsheet (备忘单)。
  • 其他工具: 还有许多其他类似的在线工具,可以根据个人喜好选择。

4.2 编程语言/工具的官方文档

如果你主要在某种特定环境中使用 regex(如 Python, JavaScript, Java, grep 等),那么该环境的官方文档是了解其具体实现、支持的特性和 API 用法的最权威来源。

  • Python: re 模块文档
  • JavaScript: MDN Web Docs on Regular Expressions
  • Java: java.util.regex.Pattern Javadoc
  • PHP: PCRE Functions documentation
  • grep/sed/awk: man pages (e.g., man grep)

4.3 教程与书籍

  • 本篇文章: 希望能为你打下坚实的基础。
  • 网上教程:
    • Regular-Expressions.info (https://www.regular-expressions.info/): 非常全面和深入的 regex 教程和参考网站。
    • MDN Web Docs (JavaScript): 对 JavaScript regex 有很好的教程。
    • 许多编程语言的官方教程或社区教程中都有 regex 的章节。
  • 书籍:
    • 《精通正则表达式》(Mastering Regular Expressions) by Jeffrey E. F. Friedl: 这是 regex 领域的经典之作,内容深入全面,适合进阶学习。

4.4 练习平台

  • RegexOne (https://regexone.com/): 提供一系列互动式的 regex 练习,从易到难。
  • HackerRank, LeetCode 等: 这些编程挑战平台也包含一些涉及 regex 的问题。

第五章:给初学者的建议与最佳实践

学习正则表达式需要耐心和练习。以下是一些建议,希望能帮助你更顺利地掌握它:

  1. 从简单开始,逐步深入: 不要试图一次性掌握所有元字符和高级特性。先理解最核心的概念(字面量, ., [], ^, $, *, +, ?, {}, |, (), \),并通过简单示例练习。
  2. 多动手实践: 理论学习后,一定要动手编写和测试 regex。使用在线测试工具,尝试匹配不同的文本,观察结果,找出错误。
  3. 分解复杂问题: 面对一个复杂的匹配需求时,尝试将其分解成几个小的、更容易解决的子问题。分别构建子模式的 regex,然后再组合起来。
  4. 明确匹配目标: 在写 regex 之前,清晰地定义你想要匹配什么,以及不想要匹配什么。考虑各种边界情况和可能的反例。
  5. 使用 ^$ 进行精确匹配: 如果你需要确保整个字符串都符合模式(例如在验证输入时),记得使用 ^$ 锚定开头和结尾。
  6. 注意贪婪与惰性: 理解量词默认的贪婪行为,并在需要时使用惰性量词 *?, +?, ??, {n,m}? 来获得期望的最短匹配。
  7. 转义特殊字符: 匹配元字符本身时,切记使用反斜杠 \ 进行转义。这是初学者常见的错误来源。
  8. 优先使用非捕获组 (?:...) 如果你只需要分组功能而不需要捕获内容,使用非捕获组可以提高效率并简化捕获组管理。
  9. 测试,测试,再测试: 用各种不同的有效和无效输入来测试你的 regex,确保它能正确处理所有预期情况,并且不会错误地匹配非预期内容。
  10. 可读性也很重要(在可能的情况下): 虽然 regex 语法紧凑,但对于复杂的 regex,可以考虑使用:
    • 注释: 某些流派支持在 regex 中添加注释(例如 PCRE 中的 (?# comment) 或使用 x (verbose) 模式)。
    • 冗长/自由间隔模式(Verbose/Free-spacing mode): 某些流派(如 Python, Perl, PCRE)支持一种模式(通常用 x 标记),允许你在 regex 中加入空白符(空格、换行)和 # 开头的注释,以提高可读性。例如:
      “`regex
      # Regex to match YYYY-MM-DD date format
      ^
      (\d{4}) # Capture group 1: Year (4 digits)

      • Literal hyphen

        (\d{2}) # Capture group 2: Month (2 digits)

      • Literal hyphen

        (\d{2}) # Capture group 3: Day (2 digits)
        $
        “`

  11. 不要过度优化(初期): 对于初学者,首先关注正确性。性能优化通常在 regex 成为瓶颈时才需要深入考虑。
  12. 寻求帮助与参考: 遇到困难时,不要害怕查阅文档、在线资源或寻求社区帮助。

结语:正则表达式——值得掌握的强大技能

正则表达式初看起来可能像是一堆神秘的符号,但正如我们在这篇入门文章中所探讨的,一旦你理解了其核心组件——字面量、元字符(如点号、字符集、锚点、量词、分组、选择)以及它们如何组合工作——你就能开始释放它强大的威力。

我们通过实例学习了如何构建用于验证(如邮箱、手机号)和提取(如 URL)的正则表达式,了解了不同引擎/流派可能存在的差异,并推荐了一些学习工具和资源。最后,我们分享了一些给初学者的建议和最佳实践。

掌握正则表达式无疑会为你打开一扇新的大门,无论是在日常的文本编辑、系统管理,还是在编程开发、数据处理等领域,它都将成为你工具箱中一把锋利而高效的瑞士军刀。虽然本文只是基础入门,正则表达式的世界还有更多高级特性等待探索,例如:

  • 前后查找(Lookarounds): (?=...), (?!...), (?<=...), (?<!...),用于匹配基于其前后内容的位置,但不消耗字符。
  • 反向引用(Backreferences): \1, \2 等,引用之前捕获组匹配到的内容。
  • 命名捕获组(Named Capture Groups): (?<name>...),用名称代替数字来引用捕获组。
  • 原子组(Atomic Grouping): (?>...),阻止引擎在匹配失败时回溯。
  • 条件判断(Conditionals): (?(condition)yes-pattern|no-pattern),根据条件选择不同的匹配模式。
  • Unicode 支持: \p{Property}\P{Property},基于 Unicode 属性进行匹配。

但饭要一口一口吃,路要一步一步走。扎实掌握本文介绍的基础知识,是你通往精通正则表达式之路的关键第一步。不断练习,不断应用,你会逐渐体会到它的精妙与强大。

希望这篇《学习正则表达式:基础入门篇》能为你点亮前行的道路,祝你在学习正则表达式的旅程中收获满满!


Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top