Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
755 changes: 22 additions & 733 deletions README.md

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions docs/00-getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 入门:定位、引擎、验证

[目录](../README.md) · 下一章:[基础语法](01-basics.md)

本章目录:

- [这份笔记是什么](#这份笔记是什么)
- [默认引擎](#默认引擎)
- [怎么验证](#怎么验证)
- [阅读约定](#阅读约定)
- [从例子开始](#从例子开始)
- [本文修正过什么](#本文修正过什么)

---

先看仓库首页的定位和目录。这一章把「这本笔记是什么、用哪个引擎、怎么对拍」写完整,再用一个小例子开场。

---

## 这份笔记是什么

| 是 | 不是 |
| --- | --- |
| 速查 + 学习笔记 | 30 分钟读完的完整教材 |
| 可验证的例子(先写测试字符串,再写模式) | 只靠记忆背语法 |
| 在经典大纲上的勘误与补例 | 宣称独自发明了这一套章节 |
| 默认讲 **通用语法 / PCRE 风格** | 默认讲 .NET,或保证 JS / Python / Java 行为完全一样 |

适合:已经知道「正则能用来找文本规则」,想把元字符、转义、量词、字符类、分枝、分组、反向引用、环视、贪婪/懒惰对上可运行的例子;学完语法后想套几个日常模式,也想知道什么时候不该硬上正则。

不适合:第一次听说正则、只想读故事式长文——请先看鹿鸣(deerchao)的原文。

---

## 默认引擎

**默认按通用语法 / PCRE 风格讲解。** 在 [regex101](https://regex101.com/) 验证时,左栏 Flavor 请选 **PCRE** 或 **PCRE2**。

同一串模式在别的引擎里可能不同:

| 引擎 | 常见差异(举例) |
| --- | --- |
| JavaScript | `\w` 默认不含 Unicode 字母;部分旧环境没有后行断言;命名组写法较晚才有 |
| Python `re` | 后行断言通常要定长;命名组常用 `(?P<name>…)` |
| Java | 后行断言有限制;`\w` / Unicode 属性与 PCRE 不完全相同 |
| .NET | `\w` 更宽(常含汉字);**平衡组 / 捕获堆栈** 是专有语法 |

文中凡是 **.NET 才能用、PCRE 默认没有** 的特性,会标成:

`(.NET 专有 / 非 PCRE 默认)`

平衡组、命名组堆栈等较重的 .NET 内容放在 [附录 A](99-appendix-dotnet.md),主路径保持 PCRE 友好。更短的跨引擎对照见 [附录 B](05-advanced.md#附录-b-引擎差异速查)。

---

## 怎么验证

1. 打开 [https://regex101.com/](https://regex101.com/)
2. Flavor 选 **PCRE2**
3. **先在 TEST STRING 里写要匹配的文本**,再写 Regular Expression
4. 对照本文的 ✓ 匹配 / ✗ 不匹配;若结果对不上,先看「引擎」一行,再看是否勾了 `i` / `m` / `s` / `u` 等修饰符(对照见 [标志与选项对照表](02-groups-and-lookaround.md#标志与选项对照表))

不要只看模式「长得对」。正则难的是边界:多一个空格、少一个转义、引擎不同,结果都会变。

仓库里还有一份可自动跑的清单,见 [如何运行校验](../README.md#如何运行校验)。

---

## 阅读约定

核心语法尽量统一成下面四行:

- **模式** — 要写进引擎的表达式
- **含义** — 人话
- **✓ 匹配 / ✗ 不匹配** — 用来核对,不是文学描写
- **引擎** — 通用(PCRE);有差异就标出来

未特别说明时,匹配都是**区分大小写**、`.` 不匹配换行、`\w` 按 PCRE 默认理解为 `[A-Za-z0-9_]`(**不含汉字**)。

前几章把语法对上例子;[常用实战模式](03-cookbook.md#常用实战模式) 动手套常用模式;[什么时候不该用正则](04-caveats.md#什么时候不该用正则) 是刹车——学完「能写」之后,再记住「不该写」。改正文里的 ✓ / ✗ 时,请同步 [`examples.yml`](../examples.yml) 并看 [如何运行校验](../README.md#如何运行校验)。

---

## 从例子开始

正则表达式用来描述「文本要满足的规则」。它比文件名通配符 `*.doc` 更精确,也更难写对。

最简单的模式就是字面量:`hi` 会找到文本里连续的 `h` 和 `i`。很多单词里都含有 `hi`(`him`、`history`),如果只要「单词 hi」,用单词边界:

**模式:** `\bhi\b`
**含义:** 作为独立单词出现的 `hi`
**✓ 匹配:** `hi`、`say hi.`
**✗ 不匹配:** `him`、`history` 里的那一段(不是独立单词)
**引擎:** 通用(PCRE)。`\b` 是位置,不消耗字符。PCRE 默认按 ASCII `\w` 判断单词边界;`.NET` 下汉字常被当成「单词字符」,边界会不一样。

再远一点:`hi` 后面不远处有 `Lucy`:

**模式:** `\bhi\b.*\bLucy\b`
**含义:** 单词 `hi`,中间任意非换行字符,再碰到单词 `Lucy`
**✓ 匹配:** `hi Lucy`、`hi there, Lucy`
**✗ 不匹配:** 中间有换行时(默认 `.` 不匹配 `\n`);没有 `Lucy`
**引擎:** 通用(PCRE)。需要跨行时开单行模式(`.` 匹配换行,PCRE 修饰符 `s`)。

电话号码的入门形:

**模式:** `0\d{2}-\d{8}`
**含义:** `0` + 两位数字 + `-` + 八位数字(三位区号的一种写法)
**✓ 匹配:** `010-12345678`
**✗ 不匹配:** `0376-1234567`(区号四位、本地号七位,规则不同)
**引擎:** 通用(PCRE)

`\d` 是一位数字;`{2}` / `{8}` 是重复次数。后面 [重复(量词)](01-basics.md#重复量词) 会展开。

---

## 本文修正过什么

相对仓库里早期笔记,这一版主要做了:

1. 去掉所有 `<font>` / `</red>` 等 HTML 着色,改为纯 Markdown + 目录。
2. 修好损坏例子:IP(`(\d{1,3\ .}{3}\d{1.3})`)、电话里断裂的括号和空格转义、转义写成 `\ .` 这种中间有空格的形式。
3. 反向引用一节不再错标成「反义例子」;环视从混在一起的列表里拆出来。
4. 命名捕获由错误的 `(?(name)exp)` 改为 `(?<name>exp)`。
5. 字符类补上 `\w` 应有的 `_`;默认引擎从「跟着 .NET 走」改为 PCRE,并把平衡组放入附录。
6. 核心语法补上 ✓ 匹配 / ✗ 不匹配,方便在 regex101 上对拍。
7. 把「处理选项」扩成跨语言的 [标志与选项对照表](02-groups-and-lookaround.md#标志与选项对照表)(`g` 按「找出全部」来讲,不当成 PCRE 模式正文修饰符)。
8. [贪婪与懒惰](02-groups-and-lookaround.md#贪婪与懒惰) 改成同一测试字符串的 ✓ / ✗ 对照,并补了标签例子。
9. 语法后面接上 [常用实战模式](03-cookbook.md#常用实战模式) 和 [什么时候不该用正则](04-caveats.md#什么时候不该用正则)(含 ReDoS 的教学说明,不是吓唬人)。
10. 增加 [`examples.yml`](../examples.yml) + [`scripts/check_examples.py`](../scripts/check_examples.py) + GitHub Actions,把正文里能对上号的 ✓ / ✗ 自动跑一遍(见 [如何运行校验](../README.md#如何运行校验))。
11. 把长篇笔记拆到 [`docs/`](../README.md#文档目录),仓库首页只保留定位、目录和校验说明。

仍可能有引擎边角差异。发现问题请对照 [regex101](https://regex101.com/)(PCRE2)和你实际语言的文档,欢迎直接改笔记。

---

[目录](../README.md) · 下一章:[基础语法](01-basics.md)
193 changes: 193 additions & 0 deletions docs/01-basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# 基础语法:元字符到分枝

上一章:[入门](00-getting-started.md) · [目录](../README.md) · 下一章:[分组与环视](02-groups-and-lookaround.md)

本章目录:

- [元字符](#元字符)
- [字符转义](#字符转义)
- [重复(量词)](#重复量词)
- [字符类](#字符类)
- [分枝条件](#分枝条件)

---

## 元字符

元字符是「有特殊含义的符号」,不是普通文字。

| 模式 | 含义 |
| --- | --- |
| `.` | 除换行以外的任意一个字符 |
| `\w` | 单词字符。PCRE 默认:`[A-Za-z0-9_]` |
| `\s` | 空白。PCRE 默认:空格、Tab、换行等;**不一定**含中文全角空格 |
| `\d` | 一位数字 `0-9`(PCRE 默认;开 Unicode 后可能更宽) |
| `\b` | 单词开头或结尾的**位置** |
| `^` | 字符串开头(多行模式下是行首) |
| `$` | 字符串结尾(多行模式下是行尾) |

### `\ba\w*\b`

- **含义:** 以字母 `a` 开头的单词(一个或多个 `\w`)
- **✓ 匹配:** `apple`、`an`、`a`
- **✗ 不匹配:** `banana`(不是 `a` 开头)、`Apple`(默认区分大小写)
- **引擎:** 通用(PCRE)。`.NET` / Python 3 的 `\w` 常含汉字;PCRE 在 regex101 未开 Unicode 时不含。

### `\d+`

- **含义:** 一位或更多连续数字
- **✓ 匹配:** `1`、`007`、`2026`
- **✗ 不匹配:** 空字符串、`abc`
- **引擎:** 通用(PCRE)。`*` 可以是 0 次,`+` 至少 1 次。

### `\b\w{6}\b`

- **含义:** 刚好 6 个单词字符的「单词」
- **✓ 匹配:** `python`、`regexp`(6 个字符)
- **✗ 不匹配:** `regex`(5)、`regular`(7)
- **引擎:** 通用(PCRE)

### `^\d{5,12}$`

- **含义:** **整串**都是 5 到 12 位数字(常用来做 QQ 号这类校验)
- **✓ 匹配:** `12345`、`123456789012`
- **✗ 不匹配:** `1234`(太短)、`1234567890123`(太长)、`12345abc`(有字母)
- **引擎:** 通用(PCRE)。若去掉 `^` 和 `$`,只表示「里面有一段 5–12 位数字」,`abc12345` 也会被找出来。

`^` 和 `$` 匹配的是位置。多行模式(修饰符 `m`)下,它们改成匹配每一行的行首/行尾。

---

## 字符转义

元字符要匹配「它自己」时,前面加 `\`。写进文档或代码时,**不要在反斜杠和被转义字符中间插空格**。

### `\.`

- **含义:** 普通的英文句点,不是「任意字符」
- **✓ 匹配:** `deerchao.cn` 里的那个 `.`
- **✗ 不匹配:** 用 `.`(未转义)去「只想要句点」——它会匹配几乎任意字符
- **引擎:** 通用(PCRE)

### `\*`

- **含义:** 普通的星号
- **✓ 匹配:** `3*4` 里的 `*`
- **✗ 不匹配:** 把 `*` 当字面量却不转义——它会变成量词
- **引擎:** 通用(PCRE)

### `\\`

- **含义:** 普通的反斜杠(模式里要写两个 `\`)
- **✓ 匹配:** `C:\Windows` 里的 `\`
- **✗ 不匹配:** 模式里只写一个 `\` 且后面不构成合法转义
- **引擎:** 通用(PCRE)。在很多编程语言的**字符串字面量**里还要再转义一次,例如 Python 普通字符串写成 `"\\\\"`,或改用原始字符串 `r"\\"`。

常用例子:`deerchao\.cn` 匹配 `deerchao.cn`;`C:\\Windows` 匹配 `C:\Windows`。

---

## 重复(量词)

量词作用在「它前面的那一个元素」上:一个字符、一个转义、或一个分组。

| 模式 | 含义 |
| --- | --- |
| `*` | 重复 0 次或更多 |
| `+` | 重复 1 次或更多 |
| `?` | 重复 0 次或 1 次 |
| `{n}` | 刚好 n 次 |
| `{n,}` | 至少 n 次 |
| `{n,m}` | n 到 m 次(含两端) |

### `Windows\d+`

- **含义:** `Windows` 后面至少一位数字
- **✓ 匹配:** `Windows7`、`Windows11`
- **✗ 不匹配:** `Windows`(后面没有数字)、`windows11`(默认区分大小写)
- **引擎:** 通用(PCRE)

### `^\w+`

- **含义:** 从字符串开头(或开了多行后的行首)起的第一个单词
- **✓ 匹配:** `Hello world` 中的 `Hello`
- **✗ 不匹配:** 行首是空格时,开头对不上 `\w`(除非先写 `\s*`)
- **引擎:** 通用(PCRE)。具体是「整串第一个」还是「每行第一个」,取决于有没有修饰符 `m`。

默认量词是**贪婪**的:能多匹配就多匹配。需要尽量少时用懒惰量词,见 [贪婪与懒惰](02-groups-and-lookaround.md#贪婪与懒惰)。

---

## 字符类

方括号 `[…]` 列出「这里可以是哪些字符」。没有现成元字符的集合(例如元音)就自己列。

### `[aeiou]`

- **含义:** 任意一个英文小写元音
- **✓ 匹配:** `a`、`e`、`regex` 里的 `e`
- **✗ 不匹配:** `b`、`A`(默认区分大小写;要含大写写成 `[aeiouAEIOU]` 或加 `i`)
- **引擎:** 通用(PCRE)

### `[.?!]`

- **含义:** 句点、问号或感叹号(**在字符类里 `.` 就是句点**,不必写成 `\.`)
- **✓ 匹配:** `.`、`?`、`!`
- **✗ 不匹配:** `,`、空格
- **引擎:** 通用(PCRE)

### `[0-9]`

- **含义:** 一位数字,教学上常视作和 `\d` 相同
- **✓ 匹配:** `0`、`9`
- **✗ 不匹配:** `a`
- **引擎:** 通用(PCRE)。开 Unicode 后 `\d` 在部分引擎会匹配更宽的数字,`[0-9]` 仍是 ASCII 数字。

### `[A-Za-z0-9_]`

- **含义:** 只考虑英文时,约等于 PCRE 默认的 `\w`
- **✓ 匹配:** `A`、`z`、`0`、`_`
- **✗ 不匹配:** `-`、空格、汉字(PCRE 默认)
- **引擎:** 通用(PCRE)。旧笔记写成 `[a-z0-9A-Z]` 漏了下划线,**不等于** `\w`。`.NET` 的 `\w` 通常还包含汉字等字母。

### 电话号码:字符类版(方便,但偏松)

**模式:** `\(?0\d{2}[) -]?\d{8}`

- **含义:** 可选的左括号,`0` 和两位数字,然后可选的 `)` / 空格 / `-`,再八位数字
- **✓ 匹配:** `(010)88886666`、`022-22334455`、`02912345678`
- **✗ 不匹配(本意):** 本地号不是 8 位的号码
- **引擎:** 通用(PCRE)。`(` `)` 是元字符,字面量要写成 `\(` `\)`。

这个写法也会误收 **格式不配对** 的串,例如 `010)12345678`、`(022-87654321`。要收紧,用下一节的分枝,而不是再往字符类里堆符号。

---

## 分枝条件

`|` 表示「几种规则满足一种即可」。引擎**从左到右**试分枝,左边成功了,右边不再试。所以**更具体的规则写在左边**。

### `0\d{2}-\d{8}|0\d{3}-\d{7}`

- **含义:** 两种国内电话:三位区号 + 8 位本地号,或四位区号 + 7 位本地号(中间有 `-`)
- **✓ 匹配:** `010-12345678`、`0376-1234567`
- **✗ 不匹配:** `01012345678`(没有 `-`)、`010-1234567`(本地号位数不对)
- **引擎:** 通用(PCRE)

### `\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}`

- **含义:** 三位区号的电话:括号完整配对,或完全不用括号;区号和本地号之间可以是 `-`、空格或没有间隔
- **✓ 匹配:** `(010)88886666`、`010-22334455`、`02912345678`
- **✗ 不匹配(请用整串校验):** `010)12345678`、`(022-87654321` —— 模式写成 `^(?:\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8})$` 时这两串会失败。若**不加** `^$`,第二段分枝仍可能在 `(022-87654321` 里找到子串 `022-87654321`
- **引擎:** 通用(PCRE)。旧笔记把括号和 `?` 抄断了,写成 `\(?0\d{2})?[- ]?…` 这类损坏模式,不能直接用。

### `\d{5}-\d{4}|\d{5}`

- **含义:** 美国邮编:5 位,或 5+4(ZIP+4)
- **✓ 匹配:** `12345`、`12345-6789`
- **✗ 不匹配:** 若写成 `\d{5}|\d{5}-\d{4}`,在「查找」时 `12345-6789` 往往只吃到前 5 位——左边已经成功,右边不会再试
- **引擎:** 通用(PCRE)。校验整串时请加 `^…$`。

---

上一章:[入门](00-getting-started.md) · [目录](../README.md) · 下一章:[分组与环视](02-groups-and-lookaround.md)
Loading