A Gentle Introduction to Regular Expressions
正则表达式入门
A regular expression is a tiny language for describing patterns in text. It is the most powerful find-and-replace tool ever invented, and it lives inside almost every editor, every programming language, and every command-line tool. Once you can read it, you stop fighting with your text and start shaping it. This guide assumes no prior knowledge and aims to make you comfortable with the 90 percent of regex you will actually use.
正则表达式是一种用于描述文本模式的小型语言。它是有史以来最强大的"查找替换"工具,几乎所有编辑器、编程语言和命令行工具里都能见到它。一旦你能读懂它,就不用再跟文本较劲,而是开始塑造它。本指南不要求任何前置知识,目标让你掌握实际会用到的 90% 的正则用法。
Where regex came from
正则的来源
The idea was introduced in 1956 by the mathematician Stephen Cole Kleene, who described "regular events" in the algebra of sets. In the 1960s and 1970s, Ken Thompson built the first practical regex engine into the text editor QED and later into grep, a tool that still ships with every Unix system today. The POSIX standard gave the language a formal grammar, and Perl in the late 1980s added most of the modern conveniences — character classes, non-greedy quantifiers, lookaheads — that have since spread to nearly every other engine.
这一概念由数学家 Stephen Cole Kleene 在 1956 年提出,他在集合代数中描述了"正则事件"。1960 至 1970 年代,Ken Thompson 把首个实用的正则引擎集成进文本编辑器 QED,后来又放进了 grep —— 这个工具至今仍是所有 Unix 系统的标配。POSIX 标准为这门语言给出了形式化语法,而 1980 年代末的 Perl 加入了大部分现代便利特性 —— 字符类、非贪婪量词、先行断言 —— 这些特性后来被几乎所有其他引擎借鉴。
A regex is written as a string of literal characters and metacharacters. The literal characters match themselves: cat matches the three letters "cat". The metacharacters describe structure: a single dot . matches any one character, a star * means "zero or more of the previous thing", and so on. The job of a regex engine is to find, in a given input text, every position where the pattern matches.
正则由字面字符和元字符组成。字面字符匹配它自身:cat 匹配三个字母 "cat"。元字符描述结构:单个点 . 匹配任意一个字符,星号 * 表示"前一个元素出现零次或多次",以此类推。正则引擎的工作就是:在给定的输入文本中,找出所有匹配该模式的位置。
Character classes and anchors
字符类与锚点
Square brackets define a character class — a set of characters, any one of which is allowed at this position. [aeiou] matches any vowel. [a-z] matches any lowercase letter. [A-Za-z0-9] matches any alphanumeric character. A caret inside the brackets, [^0-9], negates the set and means "any character except a digit".
方括号定义一个字符类 —— 在该位置可以是这一组字符中的任意一个。[aeiou] 匹配任意一个元音字母;[a-z] 匹配任意一个小写字母;[A-Za-z0-9] 匹配任意一个字母或数字。方括号里开头的尖括号 [^0-9] 表示"取反" —— 匹配"不是数字"的任意字符。
There are also pre-defined shortcuts: \d for a digit, \w for a word character (letter, digit, or underscore), \s for whitespace, and their uppercase counterparts \D, \W, \S for "anything except".
还有一些预定义的快捷方式:\d 表示数字,\w 表示单词字符(字母、数字或下划线),\s 表示空白字符;相应的大写形式 \D、\W、\S 则表示"除它们之外的一切"。
Anchors do not match characters, they match positions. ^ matches the start of the line (or string, depending on flags), and $ matches the end. So ^cat matches the word "cat" only at the start of a line, and cat$ matches it only at the end. To match a literal caret or dollar sign, escape it with a backslash: \^, \$.
锚点不匹配字符,而是匹配位置。^ 匹配行(或字符串,取决于标志位)的开头,$ 匹配结尾。因此 ^cat 只在一行开头匹配 "cat",cat$ 只在一行结尾匹配。要匹配字面上的尖括号或美元符,需要用反斜杠转义:\^、\$。
Quantifiers — how many, and how greedy
量词 —— 多少次,以及有多贪婪
A quantifier follows a single character or group and says how many times it is allowed to repeat. The most common are: * (zero or more), + (one or more), ? (zero or one), and {n,m} (between n and m times). For example, \d+ matches one or more digits, and [a-z]{3,5} matches a sequence of three to five lowercase letters.
量词跟在单个字符或一个分组之后,规定它可以重复出现多少次。最常见的量词有:*(零次或多次)、+(一次或多次)、?(零次或一次),以及 {n,m}(n 到 m 次)。例如,\d+ 匹配一个或多个数字,[a-z]{3,5} 匹配三到五个连续的小写字母。
Quantifiers are greedy by default: they consume as much as they can while still allowing the rest of the pattern to match. Given the string "<h1>Hello</h1>" and the pattern <.*>, the greedy engine matches the entire <h1>Hello</h1>. To make it lazy, append a question mark: <.*?> now matches only <h1>. The question mark is a one-character switch that says "as few as possible".
量词默认是贪婪的:会尽可能多地匹配,同时仍然让模式的剩余部分能够匹配。给定字符串 "<h1>Hello</h1>" 和模式 <.*>,贪婪引擎会匹配整个 <h1>Hello</h1>。要改为懒惰模式,在量词后加一个问号:<.*?> 就只会匹配 <h1>。这个问号就像一个开关,表示"尽可能少地匹配"。
Groups, capture, and alternation
分组、捕获与选择
Parentheses do two jobs. First, they group: (ab)+ matches "ab", "abab", "ababab". Second, they capture: whatever is matched inside the parentheses is saved and can be referenced later, either by number in the same pattern (\1, \2) or by index in code after the match.
圆括号有两项工作。第一,分组:(ab)+ 匹配 "ab"、"abab"、"ababab"。第二,捕获:括号内匹配到的内容会被保存,可以在同一模式后面通过编号(\1、\2)引用,也可以在代码里按索引取出。
Use parentheses to find repeated words: \b(\w+)\s+\1\b matches "the the" or "is is" but not "the cat". The \b is a word boundary, \w+ captures a word, and \1 demands that the next word be identical. Non-capturing groups (?:...) group without saving — use them when you only want the grouping effect and not the capture overhead.
用括号可以找出重复的词:\b(\w+)\s+\1\b 能匹配 "the the" 或 "is is",但不会匹配 "the cat"。\b 是单词边界,\w+ 捕获一个单词,\1 要求接下来的单词必须完全相同。非捕获分组 (?:...) 只起分组作用,不保存内容 —— 当你只需要分组而不需要捕获时,使用它可以避免不必要的开销。
A vertical bar | means "or". cat|dog matches either "cat" or "dog". gr(a|e)y matches "gray" or "grey". Be careful with precedence: alternation is the lowest priority, so ^I have a cat|dog$ parses as ^I have a cat or dog$ — almost certainly not what you want. When in doubt, add parentheses.
竖线 | 表示"或"。cat|dog 匹配 "cat" 或 "dog";gr(a|e)y 匹配 "gray" 或 "grey"。注意优先级:选择运算的优先级最低,所以 ^I have a cat|dog$ 会被解析为 ^I have a cat 或 dog$ —— 几乎肯定不是你想要的。拿不准时,加上括号。
Lookaheads: match without consuming
先行断言:匹配但不消耗
A lookahead is a zero-width assertion: it checks that something follows, but it does not consume those characters. (?=\d) is a positive lookahead — the next character must be a digit. (?!\d) is negative — the next character must not be a digit. A lookbehind works the same way but checks what comes before: (?<=\$)\d+ matches one or more digits only when they are preceded by a dollar sign.
先行断言是一种"零宽"断言:它检查后面是否出现某内容,但不会消耗这些字符。(?=\d) 是正向先行 —— 下一个字符必须是数字;(?!\d) 是负向先行 —— 下一个字符不能是数字。先行后行(lookbehind)原理相同,但检查的是之前的内容:(?<=\$)\d+ 只匹配紧跟在美元符号后的一个或多个数字。
A practical example: passwords must be 8+ characters and contain at least one digit. ^(?=.*\d).{8,}$ does the trick. The lookahead (?=.*\d) checks for at least one digit anywhere ahead, without consuming it, then .{8,}$ matches the rest of the line. A more common use is to find file names with a specific extension: .*\.(?![^.]*$) matches anything that contains a dot, used as a quick "is there an extension" check.
一个实际例子:密码要求至少 8 个字符,且至少含一个数字。^(?=.*\d).{8,}$ 就能搞定。先行断言 (?=.*\d) 检查前方某处是否含数字,但不消耗它;然后 .{8,}$ 匹配这一行剩余部分。更常见的用法是筛选含特定扩展名的文件名:.*\.(?![^.]*$) 用于快速检查"是否含点号"。
Flags that change everything
改变一切的标志位
Most engines attach single-letter flags to a pattern. The four you will use constantly are:
大多数引擎在模式后用单字母标志位控制行为。最常用的四个是:
i— case-insensitive./cat/imatches "cat", "Cat", "CAT".i—— 不区分大小写。/cat/i匹配 "cat"、"Cat"、"CAT"。g— global. Without it, only the first match is returned; with it, every match is returned.g—— 全局匹配。不加它时只返回第一个匹配;加上它则会返回所有匹配。m— multiline. Makes^and$match at every line boundary, not just at the start and end of the string.m—— 多行模式。让^和$匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。s— dotall. The dot.normally does not match newlines; withs, it does.s—— 单行(点号全部)模式。点号.默认不匹配换行符;加s后会匹配。
JavaScript writes flags after the closing slash: /cat/gi. Python passes them as the second argument: re.compile(r"cat", re.IGNORECASE). POSIX grep uses command-line options: grep -i cat file.txt. The flag names are roughly the same; the syntax is not.
JavaScript 把标志位放在结尾斜杠之后:/cat/gi。Python 把它们作为第二个参数传入:re.compile(r"cat", re.IGNORECASE)。POSIX grep 则用命令行选项:grep -i cat file.txt。标志名大致相同,写法各异。
Real recipes
实战配方
A few patterns you will reach for again and again. None of these is "perfect" by RFC standards — that takes a hundred-line regex with comments — but all of them are good enough to catch the obvious cases and reject the obvious junk.
下面这些模式你会反复用到。它们都没达到 RFC 标准 —— 那种完美的正则可能要上百行并配注释 —— 但都足以应付大多数情况:捕获明显有效的数据,挡掉明显无效的输入。
- Email (loose):
[\w.+-]+@[\w-]+\.[\w.-]+. Good enough to spot a typo like missing @ or space. - 邮箱(宽松):
[\w.+-]+@[\w-]+\.[\w.-]+。足以发现缺少 @ 或包含空格这类明显错误。 - Phone (international, loose):
\+?\d{1,3}?[ -]?\(?\d{1,4}\)?[ -]?\d{1,4}[ -]?\d{1,9}. Handles spaces, dashes, parentheses, and country codes. - 电话(国际,宽松):
\+?\d{1,3}?[ -]?\(?\d{1,4}\)?[ -]?\d{1,4}[ -]?\d{1,9}。可处理空格、短横线、括号以及国家代码。 - URL (http/https):
https?://[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=%]*. - URL(http/https):
https?://[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=%]*。 - IPv4 address:
\b(?:25[0-5]|2[0-4]\d|[01]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)){3}\b. Restricts each octet to 0–255. - IPv4 地址:
\b(?:25[0-5]|2[0-4]\d|[01]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)){3}\b。限制每个八位字节为 0–255。 - ISO date:
\d{4}-\d{2}-\d{2}. For real validation, parse and check that month 1–12 and day 1–31. - ISO 日期:
\d{4}-\d{2}-\d{2}。要做严格校验,应再解析月份是否 1–12、日期是否 1–31。
How to read a regex out loud
如何"读"出一个正则
When you see /[A-Z][a-z]+/g in code, do not panic. Read it piece by piece. The slashes are delimiters. [A-Z] is "one uppercase letter". [a-z]+ is "one or more lowercase letters". The g flag means "every match in the input, not just the first". So the whole pattern means: "every word that starts with a capital letter followed by one or more lowercase letters" — exactly the form of an English sentence's first word, or a CamelCase identifier in code.
在代码里看到 /[A-Z][a-z]+/g 时,不要慌。逐段读:斜杠是正则的分隔符;[A-Z] 是"一个大写字母";[a-z]+ 是"一个或多个小写字母";g 标志表示"找出所有匹配,不止第一个"。所以整段模式的意思是:"每个以大写字母开头、后跟一个或多个小写字母的词" —— 也就是英文句子的首词,或代码中的 CamelCase 标识符。
Practice this. Pick a regex you find in a real codebase, break it into pieces, and describe each piece in plain English. After a few rounds, a 200-character pattern starts to look like a sentence with foreign words rather than an alien inscription.
多加练习。挑一段在真实代码库里看到的正则,把它拆成片段,每段用平实的英语描述。几轮之后,一段 200 字符的模式看起来就像一句夹杂着生词的句子,而不再是天书。
When NOT to use regex
什么时候不该用正则
The famous Stack Overflow answer by bobince has been viewed millions of times: "RegEx matches HTML, but you should not parse HTML with regex." HTML is not a regular language — tags can nest, attributes contain escaped quotes, and CDATA sections can contain anything. The same is true of JSON, XML, and YAML. Use a proper parser. Regex is the right tool for finding patterns within text; it is the wrong tool for understanding text's structure.
Stack Overflow 上 bobince 那条被点击了数百万次的回答有句名言:"正则可以匹配 HTML,但你绝不应该用正则解析 HTML。"HTML 不是正则语言 —— 标签可以嵌套、属性里有转义引号、CDATA 段可以包含任意内容。JSON、XML、YAML 也是如此。一定要用真正的解析器。正则适合在文本中寻找模式,但不适合理解文本的结构。
A good rule of thumb: if your pattern fits comfortably in one line and you can describe it in one sentence, regex is the right tool. If your pattern needs comments, branches, and a flowchart, you are really trying to write a parser — and a parser is what you should use.
一个不错的经验法则:如果你的模式能在一行内轻松写完、并能用一句话描述,那正则就是合适工具。如果你的模式需要注释、分支甚至流程图,那你其实在尝试写一个解析器 —— 这时就应该真正去写一个解析器。
Try the tools
试试这些工具
Test your patterns live with the regex tester. Paste a pattern, paste some sample text, and watch the matches highlight in real time. The tester also explains each part of the pattern, which is a great way to learn by reading.
用 正则测试器 即时检验你的模式。粘贴一段模式,再粘贴示例文本,匹配结果会实时高亮显示。该测试器还会逐段解释模式的含义,是边读边学的绝佳方式。