From d0d9d7fe44ac4dd5cc0764ffeabd432ab443cfb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 11:24:40 +0000 Subject: [PATCH] Split long README into docs/ with a short homepage hub. Move teaching chapters into numbered docs/*.md files, keep examples.yml and the PCRE2 checker at the repo root, and leave LICENSE unchanged. Co-authored-by: Elven_xu <799835984@qq.com> --- README.md | 755 +------------------------------ docs/00-getting-started.md | 136 ++++++ docs/01-basics.md | 193 ++++++++ docs/02-groups-and-lookaround.md | 297 ++++++++++++ docs/03-cookbook.md | 80 ++++ docs/04-caveats.md | 29 ++ docs/05-advanced.md | 52 +++ docs/99-appendix-dotnet.md | 45 ++ examples.yml | 2 +- 9 files changed, 855 insertions(+), 734 deletions(-) create mode 100644 docs/00-getting-started.md create mode 100644 docs/01-basics.md create mode 100644 docs/02-groups-and-lookaround.md create mode 100644 docs/03-cookbook.md create mode 100644 docs/04-caveats.md create mode 100644 docs/05-advanced.md create mode 100644 docs/99-appendix-dotnet.md diff --git a/README.md b/README.md index 9340f82..9bc5fdc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Verifiable regex cheat sheet and study notes (Chinese). Not a full textbook. 章节骨架高度接近经典中文教程《正则表达式30分钟入门教程》。本仓库做的是:**整理、勘误、补上能对上号的匹配/不匹配例子**。完整叙述请读原文(见 [参考与致谢](#参考与致谢))。本仓库以 **MIT** 协议开源,见 [许可证](#许可证) 与根目录 [`LICENSE`](LICENSE)。 +教学正文在 [`docs/`](docs/);本页只做导航:定位、默认引擎、目录、怎么跑校验。 + --- ## 这份笔记是什么 @@ -17,9 +19,9 @@ Verifiable regex cheat sheet and study notes (Chinese). Not a full textbook. | 在经典大纲上的勘误与补例 | 宣称独自发明了这一套章节 | | 默认讲 **通用语法 / PCRE 风格** | 默认讲 .NET,或保证 JS / Python / Java 行为完全一样 | -适合:已经知道「正则能用来找文本规则」,想把元字符、转义、量词、字符类、分枝、分组、反向引用、环视、贪婪/懒惰对上可运行的例子;学完语法后想套几个日常模式,也想知道什么时候不该硬上正则。 +适合:已经知道「正则能用来找文本规则」,想把语法对上可运行的例子,也想知道什么时候不该硬上正则。不适合第一次听说正则——请先看鹿鸣(deerchao)的原文。 -不适合:第一次听说正则、只想读故事式长文——请先看鹿鸣(deerchao)的原文。 +展开(含阅读约定和开场例子):[入门:定位、引擎、验证](docs/00-getting-started.md)。 --- @@ -27,20 +29,25 @@ Verifiable regex cheat sheet and study notes (Chinese). Not a full textbook. **默认按通用语法 / PCRE 风格讲解。** 在 [regex101](https://regex101.com/) 验证时,左栏 Flavor 请选 **PCRE** 或 **PCRE2**。 -同一串模式在别的引擎里可能不同: +同一串模式在 JS / Python `re` / Java / .NET 里可能不同(`\w` 宽窄、后行是否定长、命名组写法)。文中凡是 **.NET 才能用、PCRE 默认没有** 的特性,标成 `(.NET 专有 / 非 PCRE 默认)`。平衡组等较重的 .NET 内容在 [附录 A](docs/99-appendix-dotnet.md);对照表见 [附录 B](docs/05-advanced.md#附录-b-引擎差异速查)。 -| 引擎 | 常见差异(举例) | -| --- | --- | -| JavaScript | `\w` 默认不含 Unicode 字母;部分旧环境没有后行断言;命名组写法较晚才有 | -| Python `re` | 后行断言通常要定长;命名组常用 `(?P…)` | -| Java | 后行断言有限制;`\w` / Unicode 属性与 PCRE 不完全相同 | -| .NET | `\w` 更宽(常含汉字);**平衡组 / 捕获堆栈** 是专有语法 | +未特别说明时:区分大小写、`.` 不匹配换行、`\w` 按 PCRE 默认是 `[A-Za-z0-9_]`(**不含汉字**)。 + +--- -文中凡是 **.NET 才能用、PCRE 默认没有** 的特性,会标成: +## 文档目录 -`(.NET 专有 / 非 PCRE 默认)` +前几章把语法对上例子;实战模式动手;然后是刹车——学完「能写」之后,再记住「不该写」。改正文里的 ✓ / ✗ 时,请同步 [`examples.yml`](examples.yml) 并看 [如何运行校验](#如何运行校验)。 -平衡组、命名组堆栈等较重的 .NET 内容放在文末 [附录 A](#附录-a-net-专有平衡组与递归匹配),主路径保持 PCRE 友好。 +| 文件 | 内容 | +| --- | --- | +| [docs/00-getting-started.md](docs/00-getting-started.md) | 定位、引擎、regex101 怎么对拍、阅读约定、[从例子开始](docs/00-getting-started.md#从例子开始)、[本文修正过什么](docs/00-getting-started.md#本文修正过什么) | +| [docs/01-basics.md](docs/01-basics.md) | [元字符](docs/01-basics.md#元字符)、[字符转义](docs/01-basics.md#字符转义)、[重复(量词)](docs/01-basics.md#重复量词)、[字符类](docs/01-basics.md#字符类)、[分枝条件](docs/01-basics.md#分枝条件) | +| [docs/02-groups-and-lookaround.md](docs/02-groups-and-lookaround.md) | [分组](docs/02-groups-and-lookaround.md#分组)、[反义](docs/02-groups-and-lookaround.md#反义)、[反向引用](docs/02-groups-and-lookaround.md#反向引用)、[环视](docs/02-groups-and-lookaround.md#环视零宽断言)、[贪婪与懒惰](docs/02-groups-and-lookaround.md#贪婪与懒惰)、[注释](docs/02-groups-and-lookaround.md#注释)、[标志与选项对照表](docs/02-groups-and-lookaround.md#标志与选项对照表) | +| [docs/03-cookbook.md](docs/03-cookbook.md) | [常用实战模式](docs/03-cookbook.md#常用实战模式)(邮箱 / 手机号 / 日期 / URL 味道 / 整数小数) | +| [docs/04-caveats.md](docs/04-caveats.md) | [什么时候不该用正则](docs/04-caveats.md#什么时候不该用正则)(含 ReDoS 说明) | +| [docs/05-advanced.md](docs/05-advanced.md) | [进阶索引](docs/05-advanced.md#进阶索引)、[附录 B 引擎差异速查](docs/05-advanced.md#附录-b-引擎差异速查) | +| [docs/99-appendix-dotnet.md](docs/99-appendix-dotnet.md) | [附录 A NET 专有平衡组与递归匹配](docs/99-appendix-dotnet.md#附录-a-net-专有平衡组与递归匹配) | --- @@ -49,17 +56,15 @@ Verifiable regex cheat sheet and study notes (Chinese). Not a full textbook. 1. 打开 [https://regex101.com/](https://regex101.com/) 2. Flavor 选 **PCRE2** 3. **先在 TEST STRING 里写要匹配的文本**,再写 Regular Expression -4. 对照本文的 ✓ 匹配 / ✗ 不匹配;若结果对不上,先看「引擎」一行,再看是否勾了 `i` / `m` / `s` / `u` 等修饰符(对照见 [标志与选项对照表](#标志与选项对照表)) - -不要只看模式「长得对」。正则难的是边界:多一个空格、少一个转义、引擎不同,结果都会变。 +4. 对照 `docs/` 里的 ✓ 匹配 / ✗ 不匹配;若结果对不上,先看「引擎」一行,再看是否勾了 `i` / `m` / `s` / `u` 等修饰符(对照见 [标志与选项对照表](docs/02-groups-and-lookaround.md#标志与选项对照表)) -仓库里还有一份可自动跑的清单,见 [如何运行校验](#如何运行校验)。 +不要只看模式「长得对」。正则难的是边界:多一个空格、少一个转义、引擎不同,结果都会变。更完整的步骤见 [怎么验证](docs/00-getting-started.md#怎么验证)。 --- ## 如何运行校验 -正文里的 ✓ / ✗ 抽了一份到 [`examples.yml`](examples.yml),用脚本自动跑,避免笔记和真实引擎各说各话。 +`docs/` 里的 ✓ / ✗ 抽了一份到 [`examples.yml`](examples.yml),用脚本自动跑,避免笔记和真实引擎各说各话。 **CI 用的引擎接近 PCRE2,但不是 regex101 的完整复刻。** GitHub Actions(`ubuntu-latest`)跑的是 **Python 3 + [`pcre2`](https://pypi.org/project/pcre2/) 包**(捆绑 libpcre2,比标准库 `re`、也比 PyPI 上的 `regex` 库更接近本文默认引擎),并默认加上 `ASCII`,让 `\w` / `\d` / `\b` 接近文中说的 PCRE 默认(**不含汉字**)。这和 JavaScript `RegExp`、Python `re`、以及 regex101 上每一个勾选项都可能有边角差别。递归 `(?R)`、.NET 平衡组不会放进这份会执行的清单。 @@ -87,722 +92,6 @@ python3 scripts/check_examples.py --- -## 目录 - -1. [从例子开始](#从例子开始) -2. [元字符](#元字符) -3. [字符转义](#字符转义) -4. [重复(量词)](#重复量词) -5. [字符类](#字符类) -6. [分枝条件](#分枝条件) -7. [分组](#分组) -8. [反义](#反义) -9. [反向引用](#反向引用) -10. [环视(零宽断言)](#环视零宽断言) -11. [贪婪与懒惰](#贪婪与懒惰) -12. [注释](#注释) -13. [标志与选项对照表](#标志与选项对照表) -14. [常用实战模式](#常用实战模式) -15. [什么时候不该用正则](#什么时候不该用正则) -16. [进阶索引](#进阶索引) -17. [附录 A NET 专有平衡组与递归匹配](#附录-a-net-专有平衡组与递归匹配) -18. [附录 B 引擎差异速查](#附录-b-引擎差异速查) -19. [本文修正过什么](#本文修正过什么) -20. [许可证](#许可证) - -前 13 节把语法对上例子;14 节动手套常用模式;15 节是刹车——学完「能写」之后,再记住「不该写」。改正文里的 ✓ / ✗ 时,请同步 [`examples.yml`](examples.yml) 并看 [如何运行校验](#如何运行校验)。 - -**阅读约定(核心语法尽量统一成下面四行):** - -- **模式** — 要写进引擎的表达式 -- **含义** — 人话 -- **✓ 匹配 / ✗ 不匹配** — 用来核对,不是文学描写 -- **引擎** — 通用(PCRE);有差异就标出来 - -未特别说明时,匹配都是**区分大小写**、`.` 不匹配换行、`\w` 按 PCRE 默认理解为 `[A-Za-z0-9_]`(**不含汉字**)。 - ---- - -## 从例子开始 - -正则表达式用来描述「文本要满足的规则」。它比文件名通配符 `*.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}` 是重复次数。后面 [重复(量词)](#重复量词) 会展开。 - ---- - -## 元字符 - -元字符是「有特殊含义的符号」,不是普通文字。 - -| 模式 | 含义 | -| --- | --- | -| `.` | 除换行以外的任意一个字符 | -| `\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`。 - -默认量词是**贪婪**的:能多匹配就多匹配。需要尽量少时用懒惰量词,见 [贪婪与懒惰](#贪婪与懒惰)。 - ---- - -## 字符类 - -方括号 `[…]` 列出「这里可以是哪些字符」。没有现成元字符的集合(例如元音)就自己列。 - -### `[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)。校验整串时请加 `^…$`。 - ---- - -## 分组 - -量词只作用于前一个元素。要重复「好几样东西组成的一小段」,用括号把它们包成**子表达式(分组)**。 - -### 教学简化版 IPv4(能讲清结构,但会放过非法地址) - -**模式:** `(\d{1,3}\.){3}\d{1,3}` - -- **含义:** 「1–3 位数字 + 点」重复 3 次,再跟 1–3 位数字 -- **✓ 匹配:** `192.168.1.1`、`0.0.0.0` -- **✗ 不匹配:** `192.168.1`(只有三段)、`abc.def.ghi.jkl` -- **引擎:** 通用(PCRE) - -它也会匹配 `256.300.888.999`——每位可以是 1–3 位数字,**没有**「必须 ≤ 255」的算术。正则不会做数学比较,只能把合法范围写成更啰嗦的分枝。 - -旧笔记里的 `(\d{1,3\ .}{3}\d{1.3})` 是损坏写法:`{1,3` 没有闭合、`\ .` 中间有空格、`{1.3}` 不是合法量词。正确简化形是 **`(\d{1,3}\.){3}\d{1,3}`**。 - -### 更严的教学版 IPv4(每位 0–255) - -**一段(0–255):** `2[0-4]\d|25[0-5]|[01]?\d\d?` - -| 分枝 | 覆盖 | -| --- | --- | -| `2[0-4]\d` | 200–249 | -| `25[0-5]` | 250–255 | -| `[01]?\d\d?` | 0–199(允许前导 0,如 `01`、`001`) | - -**整串校验模式:** `^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$` - -- **✓ 匹配:** `192.168.0.1`、`255.255.255.255`、`01.02.03.04`(前导 0 在 IPv4 文本里可以合法出现) -- **✗ 不匹配:** `256.1.1.1`、`1.1.1.999`、`1.1.1` -- **引擎:** 通用(PCRE)。这是**教学用** IPv4,不是生产级校验(不含 IPv6)。 - -查找(不加 `^$`)时,`256.1.1.1` 里仍会找到子串 `56.1.1.1`,`1.1.1.999` 里会找到 `1.1.1.99`。所以「每位不超过 255」只有在**锚定整串**(或前后都不是数字/点)时才站得住。原文给的不带锚点的「正确 IP」写法,只适合当结构演示。 - ---- - -## 反义 - -「除了这些以外」用大写的反义元字符,或字符类里的 `^`。 - -| 模式 | 含义 | -| --- | --- | -| `\W` | 不是 `\w` 的字符 | -| `\S` | 不是空白 | -| `\D` | 不是数字 | -| `\B` | 不是单词边界的位置 | -| `[^x]` | 不是 `x` 的一个字符 | -| `[^aeiou]` | 不是这些元音的一个字符 | - -`[^…]` 里的 `^` 只有紧跟在开方括号 `[` 后面时才表示取反;写在别的位置就是普通字符。 - -### `\S+` - -- **含义:** 连续的非空白 -- **✓ 匹配:** `hello`、`a_b-1` -- **✗ 不匹配:** 只有空格 / Tab 的片段(作为这一段 `\S+` 本身) -- **引擎:** 通用(PCRE) - -### `]+>` - -- **含义:** 以 `a` 开头、用尖括号包起来的标签粗模(`` 的字符,直到 `>`) -- **✓ 匹配:** ``、`` -- **✗ 不匹配:** ``(`+` 要求 `a` 和 `>` 之间至少还有一个字符;若也要匹配 `` 请改成 `]*>`)、`` -- **引擎:** 通用(PCRE)。这是教学例子,不是完整 HTML 解析。 - ---- - -## 反向引用 - -括号默认会**捕获**。从左到右,第一个捕获组是 `\1`,第二个是 `\2`。反向引用表示「这里必须再出现**当初捕获到的那串文本**」,不是再匹配一次同样的模式。 - -| 分类 | 模式 | 含义 | -| --- | --- | --- | -| 捕获 | `(exp)` | 匹配并捕获,自动编号 | -| 命名捕获 | `(?exp)` | 捕获到名字 `name`;PCRE / .NET 也可写成 `(?'name'exp)` `(部分引擎写法不同)` | -| 非捕获 | `(?:exp)` | 只分组,不占组号 | -| 命名反向引用 | `\k` | 引用名为 `name` 的捕获 `(JS / PCRE / .NET;Python 常用 `(?P=name)`)` | - -旧笔记把命名捕获写成 `(?(name)exp)`——那是**条件分组**,不是命名捕获。条件写法见 [进阶索引](#进阶索引)。 - -`.NET` 给未命名组和命名组分配组号的规则更绕(会扫两遍)。PCRE 主路径请优先用 `\1`、`\2` 或 `\k`,不要依赖「命名组一定排在未命名组后面」。 - -### `\b(\w+)\b\s+\1\b` - -- **含义:** 同一个单词连续出现两次(中间有空白),如笔误重复 -- **✓ 匹配:** `go go`、`kitty kitty`(多个空格也可以,因为中间是 `\s+`) -- **✗ 不匹配:** `go to`、`go Go`(默认区分大小写) -- **引擎:** 通用(PCRE) - -### `\b(?\w+)\b\s+\k\b` - -- **含义:** 同上,组名叫 `Word` -- **✓ 匹配:** `go go` -- **✗ 不匹配:** `go to` -- **引擎:** PCRE / .NET / 现代 JavaScript。Python 请写成 `\b(?P\w+)\b\s+(?P=Word)\b`。`(?'Word'…)` 是 PCRE 与 .NET 都认识的另一种命名组写法。 - ---- - -## 环视(零宽断言) - -环视只检查「这个位置的前面/后面像不像」,**不把检查到的字符吃进匹配结果**。所以叫零宽。 - -| 模式 | 含义 | -| --- | --- | -| `(?=exp)` | 后面能匹配 `exp`(正先行) | -| `(?!exp)` | 后面不能匹配 `exp`(负先行) | -| `(?<=exp)` | 前面能匹配 `exp`(正后行) | -| `(?).*?(?=)` - -- **含义:** 简单、无属性的成对标签中间的内容(不含标签本身) -- **✓ 匹配:** `bold` 中的 `bold` -- **✗ 不匹配:** `bold`(开头结尾标签名不同)、带属性的 `
…
`(这个简化模式要求 `<` 后立刻是标签名再 `>`) -- **引擎:** **PCRE2 / .NET**(后行里有变长的 `\w+`)。Python `re` 通常要求后行定长;Perl 也不接受无上界的 `\w+` 后行。regex101 请选 PCRE2。 - -更通用的写法,不依赖后行,内容在第 2 组: - -`<(\w+)>(.*?)` - ---- - -## 贪婪与懒惰 - -能使整个表达式成功的前提下: - -- **贪婪**(默认):尽量多吃 -- **懒惰**(量词后面加 `?`):尽量少吃 - -还有一条更优先的规则:**更早开始的匹配赢**(The match that begins earliest wins)。所以懒惰不是「从最短的子串里随便挑一段」,而是「从左往右,在当前位置用最少的重复把整句配上」。 - -| 模式 | 含义 | -| --- | --- | -| `*?` | 0 次或更多,尽量少 | -| `+?` | 1 次或更多,尽量少 | -| `??` | 0 次或 1 次,尽量少 | -| `{n,m}?` | n 到 m 次,尽量少 | -| `{n,}?` | 至少 n 次,尽量少 | - -下面两组对照**共用同一句测试字符串**。差别只来自量词贪不贪婪,方便在 regex101 里改一个 `?` 就看出结果。 - -### 对照一:`aabab` - -**测试字符串(两行模式都用它):** `aabab` - -| | 贪婪 | 懒惰 | -| --- | --- | --- | -| **模式** | `a.*b` | `a.*?b` | -| **含义** | `a` 与 `b` 之间尽量长 | `a` 与 `b` 之间尽量短 | -| **✓ 第一次匹配** | 整串 `aabab` | 左边起最短成功:`aab`(第 1–3 个字符) | -| **✗ 这一次不会是** | 只吃到 `aab` 或中间的 `ab`(还能更长时贪婪不收手) | 整串 `aabab`(还能更短时懒惰不撑满) | -| **勾上 Global 再找** | 已经吃完整串,没有第二次 | 还会再找到 `ab`(第 4–5 个字符) | -| **引擎** | 通用(PCRE) | 通用(PCRE) | - -懒惰的第一次是 `aab`,**不是**更短的第 2–3 个字符 `ab`:因为匹配从更左边的 `a` 开始。regex101 请勾 Global 才能看到第二次;这就是 [标志与选项对照表](#标志与选项对照表) 里的 `g`——「找出全部」,不是写进模式正文的 PCRE 修饰符。 - -### 对照二:成对标签(还是同一串) - -**测试字符串(两行模式都用它):** `onetwo` - -| | 贪婪 | 懒惰 | -| --- | --- | --- | -| **模式** | `.*` | `.*?` | -| **含义** | 从第一个 `` 撑到**最后一个** `` | 从第一个 `` 撑到**最近一个** `` | -| **✓ 第一次匹配** | 整段 `onetwo` | 第一对 `one` | -| **✗ 这一次不会是** | 只拿到第一对(默认还能更长) | 一次吞掉两对 | -| **勾上 Global 再找** | 没有剩余 | 再找到 `two` | -| **引擎** | 通用(PCRE)。`.` 默认不匹配换行 | 同上 | - -这只是为了把贪婪/懒惰看清楚。真要解析 HTML,请看 [什么时候不该用正则](#什么时候不该用正则)。 - ---- - -## 注释 - -`(?#comment)` 是内嵌注释,不参与匹配。 - -**模式:** `2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)` -**含义:** 与上面 IPv4「一段」相同,只是加了人读的注释 -**引擎:** 通用(PCRE)。JavaScript **没有** `(?#…)`。 - -需要多行把表达式拆开写时,开 **扩展 / 忽略空白** 模式(PCRE 修饰符 `x`,.NET 的 `IgnorePatternWhitespace`,详见 [标志与选项对照表](#标志与选项对照表))。此时未转义的空白被忽略,`#` 可以当到行尾的注释: - -```regex -(?<= # 前缀:简单标签 - <(\w+)> -) -.*? # 标签里的内容(懒惰) -(?= # 后缀:对应的闭合标签 - -) -``` - -(同样依赖变长后行,见上一节引擎说明。) - ---- - -## 标志与选项对照表 - -原文把这一节叫「处理选项」,对应 .NET 的 `RegexOptions`。主路径改记 **PCRE / regex101 左侧那一排字母**。下面只列**各语言都常碰到**的几个;没写进表里的(例如 PCRE 的 `A` 锚定、`U` 反转贪婪、Java 的 `UNIX_LINES`)请去该引擎手册查,本文不编造。 - -regex101 验证时:Flavor 选 PCRE2,需要哪个就勾哪个。`m` 和 `s` 可以同时开——一个改 `^$`,一个改 `.`,名字像反义词,实际互不影响。 - -| 常见叫法 | PCRE / regex101 | 做什么 | JS / Python / Java 别当成同一个旋钮 | -| --- | --- | --- | --- | -| `i` | `i` / `(?i)` | 忽略大小写 | 三家都有:JS `i`,Python `re.I`,Java `CASE_INSENSITIVE` / `(?i)` | -| `m` | `m` / `(?m)` | `^` / `$` 变成**行**首行尾,不只整串首尾 | 三家都有。**它不让 `.` 匹配换行** | -| `s` / `dotall` | `s` / `(?s)` | `.` 也匹配换行 | JS 功能名常叫 `dotAll`,字母仍是 `s`(较新的环境才有);Python `re.S` / `re.DOTALL`;Java `DOTALL` / `(?s)` | -| `x` / `extended` | `x` / `(?x)` | 忽略模式里未转义的空白,`#` 当到行尾的注释 | Python `re.X` / `re.VERBOSE`;Java `COMMENTS` / `(?x)`;**JavaScript 没有 `x`**,也不能用 `(?#…)` | -| `u` / `unicode` | PHP/PCRE 的 `u`:按 UTF-8 解释模式和主语。`\w` 会不会匹配汉字还要看是否启用 UCP,不要默认画等号 | 让引擎按 Unicode 文本来读,不是「字符串里有中文就自动打开」 | **不要把各语言的 `u` 画等号。** JS 的 `u` 是 Unicode 模式(代理对、部分转义更严;`\w` 仍多是 ASCII);Python 3 **默认就是 Unicode**,要 ASCII 语义的 `\w` 反而加 `re.A`;Java 的 `(?u)` 是 `UNICODE_CASE`(配合 `i` 做 Unicode 大小写折叠),Unicode 字符类是 **`(?U)`** | -| `g`(找出全部) | regex101 的 **Global**:列出所有匹配。**不是**写进 PCRE 模式正文的标准修饰符 | 「调用引擎时要不要继续往后找」 | JS 有标志 `g`;Python 用 `findall` / `finditer`,**没有** `g`;Java 用 `Matcher.find()` 循环;PHP 是 `preg_match_all`,**不能**在修饰符串里写 `g` | - -`.NET` 名称对照(方便读原文):`i` → IgnoreCase,`m` → Multiline,`s` → Singleline,`x` → IgnorePatternWhitespace。另外还有 `n`(ExplicitCapture):只捕获命名组,普通 `(…)` 变成非捕获。PCRE2 也有类似的「不要自动捕获」选项,日常教学少用。 - -内联写法(PCRE 通用):`(?i)`、`(?m)`、`(?s)`、`(?x)`,或局部 `(?i:exp)`。Java 内联字母和上表不完全同一套,尤其 `u` / `U`。 - -**模式:** `(?i)windows\d+` -**含义:** 忽略大小写的 `Windows` + 数字 -**✓ 匹配:** `Windows11`、`windows11`、`WINDOWS11` -**✗ 不匹配:** `window11`(少了 `s`) -**引擎:** 通用(PCRE) - ---- - -## 常用实战模式 - -前面都在拆零件。下面几条是**教学用**的整串校验(带 `^$`),方便抄去 regex101 对拍。它们**不是**国家标准,也**不是**完整 RFC——能挡住明显乱码,挡不住所有边角。生产环境请再看 [什么时候不该用正则](#什么时候不该用正则)。 - -座机、IPv4 已经在 [分枝条件](#分枝条件)、[分组](#分组) 里,这里不重复。 - -### 邮箱(教学简化,不是完整 RFC) - -**模式:** `^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$` - -- **含义:** 本地部分(字母数字和 `._%+-`)+ `@` + 域名标签 + `.` + 至少两位字母的「后缀」 -- **✓ 匹配:** `user@example.com`、`a.b-c@mail.co` -- **✗ 不匹配:** `user@`、`@example.com`、`user@.com`、`user@example`(没有点后缀) -- **引擎:** 通用(PCRE)。**不是完整 RFC 5322**:带引号的本地部分、注释、IP 当域名、中文域名都不覆盖。真正要收邮件,发一封确认信通常比把正则写成百科全书有用。 - -### 中国大陆手机号(基本) - -**模式:** `^1[3-9]\d{9}$` - -- **含义:** `1` + 第二位 `3–9` + 再 9 位数字,一共 11 位 -- **✓ 匹配:** `13812345678`、`19900001111` -- **✗ 不匹配:** `12812345678`(第二位是 `2`)、`1381234567`(10 位)、`138123456789`(12 位)、`138-1234-5678`(有分隔符) -- **引擎:** 通用(PCRE)。号段会变,物联网 / 虚拟号更乱。这是课堂用的「长得像 11 位手机号」,不是运营商数据库。 - -### 日期 `YYYY-MM-DD`(基本) - -**模式:** `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$` - -- **含义:** 四位年 + `-` + 01–12 月 + `-` + 01–31 日 -- **✓ 匹配:** `2026-09-15`、`2026-01-01`、`2026-12-31` -- **✗ 不匹配:** `2026-13-01`、`2026-09-32`、`26-09-15`、`2026/09/15` -- **引擎:** 通用(PCRE)。**不管闰年,也不管大月小月**:`2026-02-31` 也会过。要真日历请用语言自带的日期库。 - -### URL 味道 / `http(s)` 前缀(基本) - -**模式:** `^https?://[A-Za-z0-9.-]+(?::\d{1,5})?(?:/[^\s]*)?$` - -- **含义:** `http://` 或 `https://`,后面一段 ASCII 主机名(可含 `.` `-`),可选端口,可选后面的路径(路径里不出现空白) -- **✓ 匹配:** `https://example.com`、`http://example.com/path`、`https://example.com:8080/a?x=1` -- **✗ 不匹配:** `ftp://example.com`、`example.com`(没有协议)、`https://`(没有主机)、`https://example.com/has space` -- **引擎:** 通用(PCRE)。不处理用户名、IPv6 括号、中文域名、校验端口必须 ≤ 65535。只想判断「是不是 http(s) 开头」时,`^https?://` 这一小段往往就够了。 - -### 整数 / 小数 - -**整数模式:** `^-?\d+$` - -- **含义:** 可选负号,后面全是数字(允许前导 0,如 `007`) -- **✓ 匹配:** `0`、`-42`、`2026`、`007` -- **✗ 不匹配:** `3.14`、`01a`、`+3`(这个简化式不含正号)、空字符串 -- **引擎:** 通用(PCRE)。不要前导 0 时可用 `^-?(?:0|[1-9]\d*)$`。 - -**必须带小数点:** `^-?\d+\.\d+$` - -- **含义:** 整数部分至少一位,点,小数部分至少一位 -- **✓ 匹配:** `3.14`、`-0.5`、`0.0` -- **✗ 不匹配:** `.5`(点前没有数字)、`3.`(点后没有数字)、`42`(没有点)、`3.14.15` -- **引擎:** 通用(PCRE) - -**整数或小数(点可有可无):** `^-?\d+(?:\.\d+)?$` - -- **含义:** 在「必须带小数点」上,小数段改成可选 -- **✓ 匹配:** `42`、`3.14`、`-0.5` -- **✗ 不匹配:** `.5`、`3.` -- **引擎:** 通用(PCRE)。科学计数法、千分位逗号都不覆盖。 - ---- - -## 什么时候不该用正则 - -正则很会「在一段文本里找规则」,很不会「理解结构」。能用固定子串或一次 `split` 解决的,就别上正则——模式会过期,后来读的人也更累。 - -- **不要用正则解析 HTML / XML。** 标签会嵌套、会有注释、属性里会出现 `>`、还会有 CDATA。上面 `onetwo` 只为了讲贪婪/懒惰。生产环境请用 HTML / XML 解析器。 -- **带嵌套、转义、引号规则的格式,优先用现成解析器**:JSON、字段里带逗号的 CSV、编程语言源码、要拆 host / query / fragment 的完整 URL。 -- **校验「长得像」可以正则;校验「绝对对」往往不够。** 教学用的邮箱、日期、手机号能挡明显乱码,挡不住 RFC 边角、2 月 31 日、已经停用的号段。要准,用专用库,或再走一步服务端确认。 - -### 灾难性回溯(ReDoS)——知道即可,不必吓自己 - -有些模式在**匹配失败**时,会把「量词怎么切分」的组合穷举一遍。输入稍长,就会慢得像死机。这叫灾难性回溯;若有人故意喂长串,概念上就是 [ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)(OWASP 的说明页,了解即可)。 - -教学演示(**不要**拿去当校验规则,也不要拿去压测别人的服务): - -**模式:** `^(a+)+$` -**还算快:** `aaaaaaa`(能匹配,切法虽然多,很快就能成功) -**会明显变慢:** `aaaaaaaaaaaaaaaaaaaaX`(末尾的 `X` 让整句失败,引擎回头试每一种把 `a` 分给内层/外层 `+` 的方法) - -直觉:同一段字符被**两层都能重复的量词**套住(`(a+)+`、`(.*a)+` 这类),又允许失败后再试,就危险。教学上写成 `^a+$` 就没有这层嵌套。生产上:不要把不可信输入直接丢进自己拼的复杂模式;需要稳可以看原子组 `(?>…)`、超时,或干脆不用正则。 - -PCRE 一类引擎往往还有回溯上限,但「写成更朴素的模式」仍然是更好的习惯。 - ---- - -## 进阶索引 - -主路径用不到时,不必先记熟。需要再查手册。下列按 PCRE 默认理解;.NET 专有已标出。 - -| 模式 | 含义 | 引擎 | -| --- | --- | --- | -| `\t` `\n` `\r` `\f` `\v` | Tab / 换行 / 回车 / 换页 / 垂直 Tab | 通用(PCRE) | -| `\a` | BEL(响铃) | 通用(PCRE) | -| `\e` | Escape | 通用(PCRE);JS 字符串里含义不同 | -| `\b` | 在字符类 **外面**是单词边界;写在 `[]` **里面**是退格 | 通用(PCRE) | -| `\xnn` | 十六进制字节 | 通用(PCRE) | -| `\unnnn` | Unicode 码位(四位十六进制) | **.NET / JS**;PCRE 更常用 `\x{…}` | -| `\cN` | 控制字符,如 `\cC` 表示 Ctrl+C | 通用(PCRE) | -| `\A` | 整串开头,不受 `m` 影响 | 通用(PCRE);JS 无 `\A` | -| `\Z` | 整串结尾或最后那个换行前 | 通用(PCRE) | -| `\z` | 真正的整串结尾 | 通用(PCRE) | -| `\G` | 上一次匹配结束的位置 | PCRE / .NET / Java;JS 无 | -| `\p{L}` `\p{N}` `\p{Han}` | Unicode 属性 | PCRE 开 Unicode 后常用。`.NET` 示例里的 `\p{IsGreek}` 是 **.NET 命名**;PCRE 写 `\p{Greek}` | -| `(?>exp)` | 原子组:这一段匹配后不回溯 | PCRE / .NET / Java;JS 无此语法。教学见 [什么时候不该用正则](#什么时候不该用正则) | -| `(?imnsx:exp)` / `(?imnsx)` | 局部或之后改变修饰符 | 通用(PCRE),字母集合因引擎略有出入 | -| `(?(cond)yes\|no)` | 条件:成立走 `yes`,否则 `no` | PCRE / .NET;`cond` 可以是组号、组名或断言 | -| `(?(name)yes)` | 同上,失败分支为空 | 通用(PCRE) 条件语法;**拿它当「堆栈空了没」检查是 .NET 平衡组用法** | -| `(?R)` / `(?1)` | 递归整式或某个捕获组 | **PCRE** 嵌套括号常用这个,而不是平衡组 | -| `(?-exp)` | 平衡组 | `(.NET 专有 / 非 PCRE 默认)` 见附录 A | - ---- - -## 附录 A NET 专有平衡组与递归匹配 - -`(.NET 专有 / 非 PCRE 默认)` - -嵌套括号、嵌套标签这类「层层配对」,不能靠贪婪的 `\(.+\)` 保证左右数量相等。`.NET` 提供**命名捕获堆栈**(平衡组)来计数: - -| 模式 | 含义 | -| --- | --- | -| `(?'group'…)` / `(?…)` | 捕获并压栈 | -| `(?'-group'…)` / `(?<-group>…)` | 弹出名为 `group` 的最后一次捕获;栈空则失败 | -| `(?(group)yes\|no)` | 栈上还有 `group` 则走 `yes`,否则走 `no` | -| `(?!)` | 永远失败的负先行(用来在「栈还没空」时让整次匹配失败) | - -用尖括号代替圆括号,避免和分组括号缠在一起。教学结构(匹配最长的配对 `<…>`)大致是: - -```regex -< # 最外层左括号 -[^<>]* -( - ( - (?'Open'<) # 左:压入 Open - [^<>]* - )+ - ( - (?'-Open'>) # 右:弹出 Open - [^<>]* - )+ -)* -(?(Open)(?!)) # 还有没配对的 Open 就失败 -> -``` - -- **✓ 匹配(.NET):** `xx aa> yy` 里最长的那对尖括号及其中内容 -- **✗ 不匹配:** 左右数量对不上、且引擎无法通过回溯缩成配对结构时 -- **引擎:** `(.NET 专有 / 非 PCRE 默认)`。在 regex101 请改 Flavor 为 **.NET** 再试。PCRE 请用递归,例如匹配一层圆括号:`\((?:[^()]|(?R))*\)`。 - -把 `(?'name'exp)` 只当作「另一种命名组写法」时,PCRE 也认识;**把同一套语法当成堆栈计数**,才是 .NET 平衡组。 - ---- - -## 附录 B 引擎差异速查 - -| 话题 | PCRE / PCRE2(本文默认) | 其他 | -| --- | --- | --- | -| `\w` `\b` | 默认 ASCII | .NET、Python 3 更偏 Unicode;JS 默认 ASCII | -| 汉字 | 不要默认 `\w` 能匹配汉字 | .NET 常常可以 | -| 命名组 | `(?…)` / `(?'n'…)`,`\k` | Python:`(?P…)` / `(?P=n)` | -| 后行断言 | PCRE2 允许变长 | Python `re` 多要求定长;旧 JS 没有 | -| 嵌套配对 | 递归 `(?R)` | .NET:平衡组 | -| 试模式 | regex101 选 PCRE2;修饰符见 [标志与选项对照表](#标志与选项对照表) | 最终仍要以你代码里的引擎为准 | - ---- - -## 本文修正过什么 - -相对仓库里早期笔记,这一版主要做了: - -1. 去掉所有 `` / `` 等 HTML 着色,改为纯 Markdown + 目录。 -2. 修好损坏例子:IP(`(\d{1,3\ .}{3}\d{1.3})`)、电话里断裂的括号和空格转义、转义写成 `\ .` 这种中间有空格的形式。 -3. 反向引用一节不再错标成「反义例子」;环视从混在一起的列表里拆出来。 -4. 命名捕获由错误的 `(?(name)exp)` 改为 `(?exp)`。 -5. 字符类补上 `\w` 应有的 `_`;默认引擎从「跟着 .NET 走」改为 PCRE,并把平衡组放入附录。 -6. 核心语法补上 ✓ 匹配 / ✗ 不匹配,方便在 regex101 上对拍。 -7. 把「处理选项」扩成跨语言的 [标志与选项对照表](#标志与选项对照表)(`g` 按「找出全部」来讲,不当成 PCRE 模式正文修饰符)。 -8. [贪婪与懒惰](#贪婪与懒惰) 改成同一测试字符串的 ✓ / ✗ 对照,并补了标签例子。 -9. 语法后面接上 [常用实战模式](#常用实战模式) 和 [什么时候不该用正则](#什么时候不该用正则)(含 ReDoS 的教学说明,不是吓唬人)。 -10. 增加 [`examples.yml`](examples.yml) + [`scripts/check_examples.py`](scripts/check_examples.py) + GitHub Actions,把正文里能对上号的 ✓ / ✗ 自动跑一遍(见 [如何运行校验](#如何运行校验))。 - -仍可能有引擎边角差异。发现问题请对照 [regex101](https://regex101.com/)(PCRE2)和你实际语言的文档,欢迎直接改笔记。 - ---- - ## 许可证 本仓库以 [MIT License](LICENSE) 发布。你可以自由使用、复制、修改和分发这些笔记,只需保留版权声明和许可文本。完整条款见根目录 [`LICENSE`](LICENSE)。 diff --git a/docs/00-getting-started.md b/docs/00-getting-started.md new file mode 100644 index 0000000..f498dcf --- /dev/null +++ b/docs/00-getting-started.md @@ -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…)` | +| 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. 去掉所有 `` / `` 等 HTML 着色,改为纯 Markdown + 目录。 +2. 修好损坏例子:IP(`(\d{1,3\ .}{3}\d{1.3})`)、电话里断裂的括号和空格转义、转义写成 `\ .` 这种中间有空格的形式。 +3. 反向引用一节不再错标成「反义例子」;环视从混在一起的列表里拆出来。 +4. 命名捕获由错误的 `(?(name)exp)` 改为 `(?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) diff --git a/docs/01-basics.md b/docs/01-basics.md new file mode 100644 index 0000000..89fca09 --- /dev/null +++ b/docs/01-basics.md @@ -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) diff --git a/docs/02-groups-and-lookaround.md b/docs/02-groups-and-lookaround.md new file mode 100644 index 0000000..6753f80 --- /dev/null +++ b/docs/02-groups-and-lookaround.md @@ -0,0 +1,297 @@ +# 分组、反义、反向引用、环视 + +上一章:[基础语法](01-basics.md) · [目录](../README.md) · 下一章:[常用实战模式](03-cookbook.md) + +本章目录: + +- [分组](#分组) +- [反义](#反义) +- [反向引用](#反向引用) +- [环视(零宽断言)](#环视零宽断言) +- [贪婪与懒惰](#贪婪与懒惰) +- [注释](#注释) +- [标志与选项对照表](#标志与选项对照表) + +--- + +## 分组 + +量词只作用于前一个元素。要重复「好几样东西组成的一小段」,用括号把它们包成**子表达式(分组)**。 + +### 教学简化版 IPv4(能讲清结构,但会放过非法地址) + +**模式:** `(\d{1,3}\.){3}\d{1,3}` + +- **含义:** 「1–3 位数字 + 点」重复 3 次,再跟 1–3 位数字 +- **✓ 匹配:** `192.168.1.1`、`0.0.0.0` +- **✗ 不匹配:** `192.168.1`(只有三段)、`abc.def.ghi.jkl` +- **引擎:** 通用(PCRE) + +它也会匹配 `256.300.888.999`——每位可以是 1–3 位数字,**没有**「必须 ≤ 255」的算术。正则不会做数学比较,只能把合法范围写成更啰嗦的分枝。 + +旧笔记里的 `(\d{1,3\ .}{3}\d{1.3})` 是损坏写法:`{1,3` 没有闭合、`\ .` 中间有空格、`{1.3}` 不是合法量词。正确简化形是 **`(\d{1,3}\.){3}\d{1,3}`**。 + +### 更严的教学版 IPv4(每位 0–255) + +**一段(0–255):** `2[0-4]\d|25[0-5]|[01]?\d\d?` + +| 分枝 | 覆盖 | +| --- | --- | +| `2[0-4]\d` | 200–249 | +| `25[0-5]` | 250–255 | +| `[01]?\d\d?` | 0–199(允许前导 0,如 `01`、`001`) | + +**整串校验模式:** `^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$` + +- **✓ 匹配:** `192.168.0.1`、`255.255.255.255`、`01.02.03.04`(前导 0 在 IPv4 文本里可以合法出现) +- **✗ 不匹配:** `256.1.1.1`、`1.1.1.999`、`1.1.1` +- **引擎:** 通用(PCRE)。这是**教学用** IPv4,不是生产级校验(不含 IPv6)。 + +查找(不加 `^$`)时,`256.1.1.1` 里仍会找到子串 `56.1.1.1`,`1.1.1.999` 里会找到 `1.1.1.99`。所以「每位不超过 255」只有在**锚定整串**(或前后都不是数字/点)时才站得住。原文给的不带锚点的「正确 IP」写法,只适合当结构演示。 + +--- + +## 反义 + +「除了这些以外」用大写的反义元字符,或字符类里的 `^`。 + +| 模式 | 含义 | +| --- | --- | +| `\W` | 不是 `\w` 的字符 | +| `\S` | 不是空白 | +| `\D` | 不是数字 | +| `\B` | 不是单词边界的位置 | +| `[^x]` | 不是 `x` 的一个字符 | +| `[^aeiou]` | 不是这些元音的一个字符 | + +`[^…]` 里的 `^` 只有紧跟在开方括号 `[` 后面时才表示取反;写在别的位置就是普通字符。 + +### `\S+` + +- **含义:** 连续的非空白 +- **✓ 匹配:** `hello`、`a_b-1` +- **✗ 不匹配:** 只有空格 / Tab 的片段(作为这一段 `\S+` 本身) +- **引擎:** 通用(PCRE) + +### `]+>` + +- **含义:** 以 `a` 开头、用尖括号包起来的标签粗模(`` 的字符,直到 `>`) +- **✓ 匹配:** `
`、`` +- **✗ 不匹配:** ``(`+` 要求 `a` 和 `>` 之间至少还有一个字符;若也要匹配 `` 请改成 `]*>`)、`` +- **引擎:** 通用(PCRE)。这是教学例子,不是完整 HTML 解析。 + +--- + +## 反向引用 + +括号默认会**捕获**。从左到右,第一个捕获组是 `\1`,第二个是 `\2`。反向引用表示「这里必须再出现**当初捕获到的那串文本**」,不是再匹配一次同样的模式。 + +| 分类 | 模式 | 含义 | +| --- | --- | --- | +| 捕获 | `(exp)` | 匹配并捕获,自动编号 | +| 命名捕获 | `(?exp)` | 捕获到名字 `name`;PCRE / .NET 也可写成 `(?'name'exp)` `(部分引擎写法不同)` | +| 非捕获 | `(?:exp)` | 只分组,不占组号 | +| 命名反向引用 | `\k` | 引用名为 `name` 的捕获 `(JS / PCRE / .NET;Python 常用 `(?P=name)`)` | + +旧笔记把命名捕获写成 `(?(name)exp)`——那是**条件分组**,不是命名捕获。条件写法见 [进阶索引](05-advanced.md#进阶索引)。 + +`.NET` 给未命名组和命名组分配组号的规则更绕(会扫两遍)。PCRE 主路径请优先用 `\1`、`\2` 或 `\k`,不要依赖「命名组一定排在未命名组后面」。 + +### `\b(\w+)\b\s+\1\b` + +- **含义:** 同一个单词连续出现两次(中间有空白),如笔误重复 +- **✓ 匹配:** `go go`、`kitty kitty`(多个空格也可以,因为中间是 `\s+`) +- **✗ 不匹配:** `go to`、`go Go`(默认区分大小写) +- **引擎:** 通用(PCRE) + +### `\b(?\w+)\b\s+\k\b` + +- **含义:** 同上,组名叫 `Word` +- **✓ 匹配:** `go go` +- **✗ 不匹配:** `go to` +- **引擎:** PCRE / .NET / 现代 JavaScript。Python 请写成 `\b(?P\w+)\b\s+(?P=Word)\b`。`(?'Word'…)` 是 PCRE 与 .NET 都认识的另一种命名组写法。 + +--- + +## 环视(零宽断言) + +环视只检查「这个位置的前面/后面像不像」,**不把检查到的字符吃进匹配结果**。所以叫零宽。 + +| 模式 | 含义 | +| --- | --- | +| `(?=exp)` | 后面能匹配 `exp`(正先行) | +| `(?!exp)` | 后面不能匹配 `exp`(负先行) | +| `(?<=exp)` | 前面能匹配 `exp`(正后行) | +| `(?).*?(?=)` + +- **含义:** 简单、无属性的成对标签中间的内容(不含标签本身) +- **✓ 匹配:** `bold` 中的 `bold` +- **✗ 不匹配:** `bold`(开头结尾标签名不同)、带属性的 `
…
`(这个简化模式要求 `<` 后立刻是标签名再 `>`) +- **引擎:** **PCRE2 / .NET**(后行里有变长的 `\w+`)。Python `re` 通常要求后行定长;Perl 也不接受无上界的 `\w+` 后行。regex101 请选 PCRE2。 + +更通用的写法,不依赖后行,内容在第 2 组: + +`<(\w+)>(.*?)` + +--- + +## 贪婪与懒惰 + +能使整个表达式成功的前提下: + +- **贪婪**(默认):尽量多吃 +- **懒惰**(量词后面加 `?`):尽量少吃 + +还有一条更优先的规则:**更早开始的匹配赢**(The match that begins earliest wins)。所以懒惰不是「从最短的子串里随便挑一段」,而是「从左往右,在当前位置用最少的重复把整句配上」。 + +| 模式 | 含义 | +| --- | --- | +| `*?` | 0 次或更多,尽量少 | +| `+?` | 1 次或更多,尽量少 | +| `??` | 0 次或 1 次,尽量少 | +| `{n,m}?` | n 到 m 次,尽量少 | +| `{n,}?` | 至少 n 次,尽量少 | + +下面两组对照**共用同一句测试字符串**。差别只来自量词贪不贪婪,方便在 regex101 里改一个 `?` 就看出结果。 + +### 对照一:`aabab` + +**测试字符串(两行模式都用它):** `aabab` + +| | 贪婪 | 懒惰 | +| --- | --- | --- | +| **模式** | `a.*b` | `a.*?b` | +| **含义** | `a` 与 `b` 之间尽量长 | `a` 与 `b` 之间尽量短 | +| **✓ 第一次匹配** | 整串 `aabab` | 左边起最短成功:`aab`(第 1–3 个字符) | +| **✗ 这一次不会是** | 只吃到 `aab` 或中间的 `ab`(还能更长时贪婪不收手) | 整串 `aabab`(还能更短时懒惰不撑满) | +| **勾上 Global 再找** | 已经吃完整串,没有第二次 | 还会再找到 `ab`(第 4–5 个字符) | +| **引擎** | 通用(PCRE) | 通用(PCRE) | + +懒惰的第一次是 `aab`,**不是**更短的第 2–3 个字符 `ab`:因为匹配从更左边的 `a` 开始。regex101 请勾 Global 才能看到第二次;这就是 [标志与选项对照表](#标志与选项对照表) 里的 `g`——「找出全部」,不是写进模式正文的 PCRE 修饰符。 + +### 对照二:成对标签(还是同一串) + +**测试字符串(两行模式都用它):** `onetwo` + +| | 贪婪 | 懒惰 | +| --- | --- | --- | +| **模式** | `.*` | `.*?` | +| **含义** | 从第一个 `` 撑到**最后一个** `` | 从第一个 `` 撑到**最近一个** `` | +| **✓ 第一次匹配** | 整段 `onetwo` | 第一对 `one` | +| **✗ 这一次不会是** | 只拿到第一对(默认还能更长) | 一次吞掉两对 | +| **勾上 Global 再找** | 没有剩余 | 再找到 `two` | +| **引擎** | 通用(PCRE)。`.` 默认不匹配换行 | 同上 | + +这只是为了把贪婪/懒惰看清楚。真要解析 HTML,请看 [什么时候不该用正则](04-caveats.md#什么时候不该用正则)。 + +--- + +## 注释 + +`(?#comment)` 是内嵌注释,不参与匹配。 + +**模式:** `2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)` +**含义:** 与 [分组](#分组) 里 IPv4「一段」相同,只是加了人读的注释 +**引擎:** 通用(PCRE)。JavaScript **没有** `(?#…)`。 + +需要多行把表达式拆开写时,开 **扩展 / 忽略空白** 模式(PCRE 修饰符 `x`,.NET 的 `IgnorePatternWhitespace`,详见 [标志与选项对照表](#标志与选项对照表))。此时未转义的空白被忽略,`#` 可以当到行尾的注释: + +```regex +(?<= # 前缀:简单标签 + <(\w+)> +) +.*? # 标签里的内容(懒惰) +(?= # 后缀:对应的闭合标签 + +) +``` + +(同样依赖变长后行,见 [环视](#环视零宽断言) 的引擎说明。) + +--- + +## 标志与选项对照表 + +原文把这一节叫「处理选项」,对应 .NET 的 `RegexOptions`。主路径改记 **PCRE / regex101 左侧那一排字母**。下面只列**各语言都常碰到**的几个;没写进表里的(例如 PCRE 的 `A` 锚定、`U` 反转贪婪、Java 的 `UNIX_LINES`)请去该引擎手册查,本文不编造。 + +regex101 验证时:Flavor 选 PCRE2,需要哪个就勾哪个。`m` 和 `s` 可以同时开——一个改 `^$`,一个改 `.`,名字像反义词,实际互不影响。 + +| 常见叫法 | PCRE / regex101 | 做什么 | JS / Python / Java 别当成同一个旋钮 | +| --- | --- | --- | --- | +| `i` | `i` / `(?i)` | 忽略大小写 | 三家都有:JS `i`,Python `re.I`,Java `CASE_INSENSITIVE` / `(?i)` | +| `m` | `m` / `(?m)` | `^` / `$` 变成**行**首行尾,不只整串首尾 | 三家都有。**它不让 `.` 匹配换行** | +| `s` / `dotall` | `s` / `(?s)` | `.` 也匹配换行 | JS 功能名常叫 `dotAll`,字母仍是 `s`(较新的环境才有);Python `re.S` / `re.DOTALL`;Java `DOTALL` / `(?s)` | +| `x` / `extended` | `x` / `(?x)` | 忽略模式里未转义的空白,`#` 当到行尾的注释 | Python `re.X` / `re.VERBOSE`;Java `COMMENTS` / `(?x)`;**JavaScript 没有 `x`**,也不能用 `(?#…)` | +| `u` / `unicode` | PHP/PCRE 的 `u`:按 UTF-8 解释模式和主语。`\w` 会不会匹配汉字还要看是否启用 UCP,不要默认画等号 | 让引擎按 Unicode 文本来读,不是「字符串里有中文就自动打开」 | **不要把各语言的 `u` 画等号。** JS 的 `u` 是 Unicode 模式(代理对、部分转义更严;`\w` 仍多是 ASCII);Python 3 **默认就是 Unicode**,要 ASCII 语义的 `\w` 反而加 `re.A`;Java 的 `(?u)` 是 `UNICODE_CASE`(配合 `i` 做 Unicode 大小写折叠),Unicode 字符类是 **`(?U)`** | +| `g`(找出全部) | regex101 的 **Global**:列出所有匹配。**不是**写进 PCRE 模式正文的标准修饰符 | 「调用引擎时要不要继续往后找」 | JS 有标志 `g`;Python 用 `findall` / `finditer`,**没有** `g`;Java 用 `Matcher.find()` 循环;PHP 是 `preg_match_all`,**不能**在修饰符串里写 `g` | + +`.NET` 名称对照(方便读原文):`i` → IgnoreCase,`m` → Multiline,`s` → Singleline,`x` → IgnorePatternWhitespace。另外还有 `n`(ExplicitCapture):只捕获命名组,普通 `(…)` 变成非捕获。PCRE2 也有类似的「不要自动捕获」选项,日常教学少用。 + +内联写法(PCRE 通用):`(?i)`、`(?m)`、`(?s)`、`(?x)`,或局部 `(?i:exp)`。Java 内联字母和上表不完全同一套,尤其 `u` / `U`。 + +**模式:** `(?i)windows\d+` +**含义:** 忽略大小写的 `Windows` + 数字 +**✓ 匹配:** `Windows11`、`windows11`、`WINDOWS11` +**✗ 不匹配:** `window11`(少了 `s`) +**引擎:** 通用(PCRE) + +--- + +上一章:[基础语法](01-basics.md) · [目录](../README.md) · 下一章:[常用实战模式](03-cookbook.md) diff --git a/docs/03-cookbook.md b/docs/03-cookbook.md new file mode 100644 index 0000000..70ac6e7 --- /dev/null +++ b/docs/03-cookbook.md @@ -0,0 +1,80 @@ +# 常用实战模式 + +上一章:[分组与环视](02-groups-and-lookaround.md) · [目录](../README.md) · 下一章:[什么时候不该用正则](04-caveats.md) + +本章目录: + +- [邮箱(教学简化,不是完整 RFC)](#邮箱教学简化不是完整-rfc) +- [中国大陆手机号(基本)](#中国大陆手机号基本) +- [日期 `YYYY-MM-DD`(基本)](#日期-yyyy-mm-dd基本) +- [URL 味道 / `http(s)` 前缀(基本)](#url-味道--https-前缀基本) +- [整数 / 小数](#整数--小数) + +--- + +前面都在拆零件。下面几条是**教学用**的整串校验(带 `^$`),方便抄去 regex101 对拍。它们**不是**国家标准,也**不是**完整 RFC——能挡住明显乱码,挡不住所有边角。生产环境请再看 [什么时候不该用正则](04-caveats.md#什么时候不该用正则)。 + +座机、IPv4 已经在 [分枝条件](01-basics.md#分枝条件)、[分组](02-groups-and-lookaround.md#分组) 里,这里不重复。 + +### 邮箱(教学简化,不是完整 RFC) + +**模式:** `^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$` + +- **含义:** 本地部分(字母数字和 `._%+-`)+ `@` + 域名标签 + `.` + 至少两位字母的「后缀」 +- **✓ 匹配:** `user@example.com`、`a.b-c@mail.co` +- **✗ 不匹配:** `user@`、`@example.com`、`user@.com`、`user@example`(没有点后缀) +- **引擎:** 通用(PCRE)。**不是完整 RFC 5322**:带引号的本地部分、注释、IP 当域名、中文域名都不覆盖。真正要收邮件,发一封确认信通常比把正则写成百科全书有用。 + +### 中国大陆手机号(基本) + +**模式:** `^1[3-9]\d{9}$` + +- **含义:** `1` + 第二位 `3–9` + 再 9 位数字,一共 11 位 +- **✓ 匹配:** `13812345678`、`19900001111` +- **✗ 不匹配:** `12812345678`(第二位是 `2`)、`1381234567`(10 位)、`138123456789`(12 位)、`138-1234-5678`(有分隔符) +- **引擎:** 通用(PCRE)。号段会变,物联网 / 虚拟号更乱。这是课堂用的「长得像 11 位手机号」,不是运营商数据库。 + +### 日期 `YYYY-MM-DD`(基本) + +**模式:** `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$` + +- **含义:** 四位年 + `-` + 01–12 月 + `-` + 01–31 日 +- **✓ 匹配:** `2026-09-15`、`2026-01-01`、`2026-12-31` +- **✗ 不匹配:** `2026-13-01`、`2026-09-32`、`26-09-15`、`2026/09/15` +- **引擎:** 通用(PCRE)。**不管闰年,也不管大月小月**:`2026-02-31` 也会过。要真日历请用语言自带的日期库。 + +### URL 味道 / `http(s)` 前缀(基本) + +**模式:** `^https?://[A-Za-z0-9.-]+(?::\d{1,5})?(?:/[^\s]*)?$` + +- **含义:** `http://` 或 `https://`,后面一段 ASCII 主机名(可含 `.` `-`),可选端口,可选后面的路径(路径里不出现空白) +- **✓ 匹配:** `https://example.com`、`http://example.com/path`、`https://example.com:8080/a?x=1` +- **✗ 不匹配:** `ftp://example.com`、`example.com`(没有协议)、`https://`(没有主机)、`https://example.com/has space` +- **引擎:** 通用(PCRE)。不处理用户名、IPv6 括号、中文域名、校验端口必须 ≤ 65535。只想判断「是不是 http(s) 开头」时,`^https?://` 这一小段往往就够了。 + +### 整数 / 小数 + +**整数模式:** `^-?\d+$` + +- **含义:** 可选负号,后面全是数字(允许前导 0,如 `007`) +- **✓ 匹配:** `0`、`-42`、`2026`、`007` +- **✗ 不匹配:** `3.14`、`01a`、`+3`(这个简化式不含正号)、空字符串 +- **引擎:** 通用(PCRE)。不要前导 0 时可用 `^-?(?:0|[1-9]\d*)$`。 + +**必须带小数点:** `^-?\d+\.\d+$` + +- **含义:** 整数部分至少一位,点,小数部分至少一位 +- **✓ 匹配:** `3.14`、`-0.5`、`0.0` +- **✗ 不匹配:** `.5`(点前没有数字)、`3.`(点后没有数字)、`42`(没有点)、`3.14.15` +- **引擎:** 通用(PCRE) + +**整数或小数(点可有可无):** `^-?\d+(?:\.\d+)?$` + +- **含义:** 在「必须带小数点」上,小数段改成可选 +- **✓ 匹配:** `42`、`3.14`、`-0.5` +- **✗ 不匹配:** `.5`、`3.` +- **引擎:** 通用(PCRE)。科学计数法、千分位逗号都不覆盖。 + +--- + +上一章:[分组与环视](02-groups-and-lookaround.md) · [目录](../README.md) · 下一章:[什么时候不该用正则](04-caveats.md) diff --git a/docs/04-caveats.md b/docs/04-caveats.md new file mode 100644 index 0000000..6826662 --- /dev/null +++ b/docs/04-caveats.md @@ -0,0 +1,29 @@ +# 什么时候不该用正则 + +上一章:[常用实战模式](03-cookbook.md) · [目录](../README.md) · 下一章:[进阶索引](05-advanced.md) + +--- + +正则很会「在一段文本里找规则」,很不会「理解结构」。能用固定子串或一次 `split` 解决的,就别上正则——模式会过期,后来读的人也更累。 + +- **不要用正则解析 HTML / XML。** 标签会嵌套、会有注释、属性里会出现 `>`、还会有 CDATA。[贪婪与懒惰](02-groups-and-lookaround.md#贪婪与懒惰) 里的 `onetwo` 只为了讲量词。生产环境请用 HTML / XML 解析器。 +- **带嵌套、转义、引号规则的格式,优先用现成解析器**:JSON、字段里带逗号的 CSV、编程语言源码、要拆 host / query / fragment 的完整 URL。 +- **校验「长得像」可以正则;校验「绝对对」往往不够。** 教学用的邮箱、日期、手机号能挡明显乱码,挡不住 RFC 边角、2 月 31 日、已经停用的号段。要准,用专用库,或再走一步服务端确认。 + +### 灾难性回溯(ReDoS)——知道即可,不必吓自己 + +有些模式在**匹配失败**时,会把「量词怎么切分」的组合穷举一遍。输入稍长,就会慢得像死机。这叫灾难性回溯;若有人故意喂长串,概念上就是 [ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)(OWASP 的说明页,了解即可)。 + +教学演示(**不要**拿去当校验规则,也不要拿去压测别人的服务): + +**模式:** `^(a+)+$` +**还算快:** `aaaaaaa`(能匹配,切法虽然多,很快就能成功) +**会明显变慢:** `aaaaaaaaaaaaaaaaaaaaX`(末尾的 `X` 让整句失败,引擎回头试每一种把 `a` 分给内层/外层 `+` 的方法) + +直觉:同一段字符被**两层都能重复的量词**套住(`(a+)+`、`(.*a)+` 这类),又允许失败后再试,就危险。教学上写成 `^a+$` 就没有这层嵌套。生产上:不要把不可信输入直接丢进自己拼的复杂模式;需要稳可以看原子组 `(?>…)`、超时,或干脆不用正则。 + +PCRE 一类引擎往往还有回溯上限,但「写成更朴素的模式」仍然是更好的习惯。 + +--- + +上一章:[常用实战模式](03-cookbook.md) · [目录](../README.md) · 下一章:[进阶索引](05-advanced.md) diff --git a/docs/05-advanced.md b/docs/05-advanced.md new file mode 100644 index 0000000..e19f6fd --- /dev/null +++ b/docs/05-advanced.md @@ -0,0 +1,52 @@ +# 进阶索引与引擎差异 + +上一章:[什么时候不该用正则](04-caveats.md) · [目录](../README.md) · 下一章:[.NET 附录](99-appendix-dotnet.md) + +本章目录: + +- [进阶索引](#进阶索引) +- [附录 B 引擎差异速查](#附录-b-引擎差异速查) + +--- + +## 进阶索引 + +主路径用不到时,不必先记熟。需要再查手册。下列按 PCRE 默认理解;.NET 专有已标出。 + +| 模式 | 含义 | 引擎 | +| --- | --- | --- | +| `\t` `\n` `\r` `\f` `\v` | Tab / 换行 / 回车 / 换页 / 垂直 Tab | 通用(PCRE) | +| `\a` | BEL(响铃) | 通用(PCRE) | +| `\e` | Escape | 通用(PCRE);JS 字符串里含义不同 | +| `\b` | 在字符类 **外面**是单词边界;写在 `[]` **里面**是退格 | 通用(PCRE) | +| `\xnn` | 十六进制字节 | 通用(PCRE) | +| `\unnnn` | Unicode 码位(四位十六进制) | **.NET / JS**;PCRE 更常用 `\x{…}` | +| `\cN` | 控制字符,如 `\cC` 表示 Ctrl+C | 通用(PCRE) | +| `\A` | 整串开头,不受 `m` 影响 | 通用(PCRE);JS 无 `\A` | +| `\Z` | 整串结尾或最后那个换行前 | 通用(PCRE) | +| `\z` | 真正的整串结尾 | 通用(PCRE) | +| `\G` | 上一次匹配结束的位置 | PCRE / .NET / Java;JS 无 | +| `\p{L}` `\p{N}` `\p{Han}` | Unicode 属性 | PCRE 开 Unicode 后常用。`.NET` 示例里的 `\p{IsGreek}` 是 **.NET 命名**;PCRE 写 `\p{Greek}` | +| `(?>exp)` | 原子组:这一段匹配后不回溯 | PCRE / .NET / Java;JS 无此语法。教学见 [什么时候不该用正则](04-caveats.md#什么时候不该用正则) | +| `(?imnsx:exp)` / `(?imnsx)` | 局部或之后改变修饰符 | 通用(PCRE),字母集合因引擎略有出入 | +| `(?(cond)yes\|no)` | 条件:成立走 `yes`,否则 `no` | PCRE / .NET;`cond` 可以是组号、组名或断言 | +| `(?(name)yes)` | 同上,失败分支为空 | 通用(PCRE) 条件语法;**拿它当「堆栈空了没」检查是 .NET 平衡组用法** | +| `(?R)` / `(?1)` | 递归整式或某个捕获组 | **PCRE** 嵌套括号常用这个,而不是平衡组 | +| `(?-exp)` | 平衡组 | `(.NET 专有 / 非 PCRE 默认)` 见 [附录 A](99-appendix-dotnet.md) | + +--- + +## 附录 B 引擎差异速查 + +| 话题 | PCRE / PCRE2(本文默认) | 其他 | +| --- | --- | --- | +| `\w` `\b` | 默认 ASCII | .NET、Python 3 更偏 Unicode;JS 默认 ASCII | +| 汉字 | 不要默认 `\w` 能匹配汉字 | .NET 常常可以 | +| 命名组 | `(?…)` / `(?'n'…)`,`\k` | Python:`(?P…)` / `(?P=n)` | +| 后行断言 | PCRE2 允许变长 | Python `re` 多要求定长;旧 JS 没有 | +| 嵌套配对 | 递归 `(?R)` | .NET:平衡组 | +| 试模式 | regex101 选 PCRE2;修饰符见 [标志与选项对照表](02-groups-and-lookaround.md#标志与选项对照表) | 最终仍要以你代码里的引擎为准 | + +--- + +上一章:[什么时候不该用正则](04-caveats.md) · [目录](../README.md) · 下一章:[.NET 附录](99-appendix-dotnet.md) diff --git a/docs/99-appendix-dotnet.md b/docs/99-appendix-dotnet.md new file mode 100644 index 0000000..717571c --- /dev/null +++ b/docs/99-appendix-dotnet.md @@ -0,0 +1,45 @@ +# 附录 A NET 专有平衡组与递归匹配 + +上一章:[进阶索引](05-advanced.md) · [目录](../README.md) + +--- + +`(.NET 专有 / 非 PCRE 默认)` + +嵌套括号、嵌套标签这类「层层配对」,不能靠贪婪的 `\(.+\)` 保证左右数量相等。`.NET` 提供**命名捕获堆栈**(平衡组)来计数: + +| 模式 | 含义 | +| --- | --- | +| `(?'group'…)` / `(?…)` | 捕获并压栈 | +| `(?'-group'…)` / `(?<-group>…)` | 弹出名为 `group` 的最后一次捕获;栈空则失败 | +| `(?(group)yes\|no)` | 栈上还有 `group` 则走 `yes`,否则走 `no` | +| `(?!)` | 永远失败的负先行(用来在「栈还没空」时让整次匹配失败) | + +用尖括号代替圆括号,避免和分组括号缠在一起。教学结构(匹配最长的配对 `<…>`)大致是: + +```regex +< # 最外层左括号 +[^<>]* +( + ( + (?'Open'<) # 左:压入 Open + [^<>]* + )+ + ( + (?'-Open'>) # 右:弹出 Open + [^<>]* + )+ +)* +(?(Open)(?!)) # 还有没配对的 Open 就失败 +> +``` + +- **✓ 匹配(.NET):** `xx aa> yy` 里最长的那对尖括号及其中内容 +- **✗ 不匹配:** 左右数量对不上、且引擎无法通过回溯缩成配对结构时 +- **引擎:** `(.NET 专有 / 非 PCRE 默认)`。在 regex101 请改 Flavor 为 **.NET** 再试。PCRE 请用递归,例如匹配一层圆括号:`\((?:[^()]|(?R))*\)`。 + +把 `(?'name'exp)` 只当作「另一种命名组写法」时,PCRE 也认识;**把同一套语法当成堆栈计数**,才是 .NET 平衡组。 + +--- + +上一章:[进阶索引](05-advanced.md) · [目录](../README.md) diff --git a/examples.yml b/examples.yml index e69faec..ece3ee0 100644 --- a/examples.yml +++ b/examples.yml @@ -1,4 +1,4 @@ -# 教学用例:从 README 正文的 ✓ / ✗ 抽出来,供 scripts/check_examples.py 自动核对。 +# 教学用例:从 docs/ 正文的 ✓ / ✗ 抽出来,供 scripts/check_examples.py 自动核对。 # # CI 引擎:Python 包 pcre2(真实 PCRE2)+ 默认 ASCII,使 \w/\d/\b 接近文中 # 「PCRE 默认不含汉字」。不是 regex101 的完整 UI 复刻。