算法与数据结构 / regex · Unicode 字符模型与一个自研引擎 待审核 36 页

regex · Unicode 字符模型与一个自研引擎

一个 😀 在 JS 里 .length === 2,一面 🇨🇳 是两个区域码拼的,一个 👨‍👩‍👧 由五个 code point 用 ZWJ 粘成——emoji 正则难,是因为「一个用户眼里的字符」常常是一串 code point。本系列从 code point ⇄ UTF-16 / UTF-8 两套存储层讲起,认识 \p{…} 属性类、emoji 序列家族、grapheme cluster(字素簇)与归一化 / 大小写 / Han 统一 / 排序等通用 Unicode 概念,最后用本仓库 @vega/parsing/regex 自研引擎拆开 AST 与字节码。本系列从零讲起,不需要前置知识;「语法速览」页会把正则编译成 NFA/DFA 状态机,这部分的背景概念(Thompson 构造 / 子集构造)在 automata 系列有完整讲解。

编码 · 码点怎么存与怎么写

先把逻辑层的 code point两套存储层(UTF-16 / UTF-8)分开,回看 Unicode 之前的中文字节编码(GB / Big5),再看同一个码位在源码里怎么写出来(HTML / CSS / JS 三套转义)。

码点的档案 · \p{…} 属性类

每个 code point 在 UCD 里有一份档案,\p{…} 就是按档案匹配。先认 General_Category / Script / 二元属性三条线,再把 \p{P} 标点、\p{S} 符号两大类展开到子类,顺带看「不是用来写字」的特殊码点。

基础 · 码点的档案 \p{…}

\p{…} 属性类:用码点的 Unicode 档案来匹配

每个码点在 UCD 里有一份档案,\p{…} 就是按档案匹配。三条线是 General_Category、Script(含 sc 与 scx 之辨)与二元属性;Block 那一栏 JS 不支持。

基础 · \p{Math} 全量速查

\p{Math} 全量速查:Unicode 眼里的「数学用」码点

用原生正则逐码点实测,枚举 Unicode 17.0 全部 2322 个 Math=Yes 码点,按 block 分组可搜索。看清「数学符号」这概念如何横跨 Sm、字母、标点几个 gc。

基础 · \p{P} 标点七子类

\p{P} 标点七子类:脚注符号到底归哪一类

标点切成 Pc Pd Ps Pe Pi Pf Po 七个子类,每个码点恰好属一个。脚注符号全落在 catch-all 的 Po;开闭括号与前后引号数量为什么配不齐。

基础 · \p{S} 符号四子类

\p{S} 符号四子类:竖线是符号、脱字符不是重音、emoji 归哪一类

符号切成 Sm Sc Sk So 四子类,每个码点恰好属一个。反直觉的归类集中在此:算符是 Sm、脱字符是 Sk、emoji 挤在 catch-all 的 So,而百分号根本不是符号。

So · 不是字的图形

不是字的图形:用形状符号在文本里画图

画框的 Box Drawing、填色的 Block Elements、方圆三角的 Geometric Shapes 都是 So——没有读音、不属任何 Script,但等宽下各占一格,于是能拼出进度条与 TUI。

码空间 · 特殊用意的码点

不是用来写字的码点:边界、保留、私用与回退记号

码空间里有的码点是边界、有的永久保留给机制(代理区)、有的标准永不定义(私用区与非字符)、有的是控制或回退记号。一个探针把它们逐一点出来。

vo · Vertical_Orientation · UAX #50

竖排朝向:同一个字,横排立着、竖排可能要躺倒

每个码点在 UCD 里记了一栏 Vertical_Orientation,由 UAX #50 定义,横排时毫无作用、一进竖排就逐字决定朝向。四个取值里 Tu / Tr 把最终样子交给了字体。

Emoji · 一个字符是一串码点

emoji 难,是因为「一个用户眼里的字符」常常是一串 code point。从单码点的 Basic_Emoji 起,到一个码点身上的六个 emoji 属性,再到 RGI 序列家族(国旗 / Tag / 肤色)。

一个字符到底多长 · grapheme 与切分

把 emoji 的现象收束成通用概念:用户眼里的「一个字符」= grapheme cluster(字素簇);切分不止字素簇一种粒度(词 / 句 / 断行);叠在字母上的组合记号 \p{M} 同样是「多码点 = 一个字符」。

同义写法与同码点多形 · 拍平与展开

同一个字常有多种写法,同一个码点也可能不止一个含义。归一化 / 大小写把「同义写法」拍平;Han 统一 / 拼音则相反——同一码点的字形 / 读音不写在码点里;排序规则同样在码点之外。

实战 · 把字符模型合进工具

把前面拆过的字符模型合进真实工具:一个检视器逐字符拆码点(读),一个按名反查工具按 Unicode 名找码点(查),一个受控输入框按字形限长、注入控制字符(写)。

安全 · 看不见的攻击面

同一段文本,「逐字节」和「屏幕渲染」可以完全不同——bidi 重排同形字是两类看不见的攻击面(Trojan Source / homograph attack),都建立在前面的字符模型(bidi class / Script)之上。

正则语法本身

回到正则语法本身:本仓库 @vega/parsing/regex 基本都覆盖。先过一遍各种写法,再把几个最易踩的点(.\s / 有状态的 g / 命名捕获 vs 反向引用 / 量词下的分组)单独掰开。

syntax · 各种语法一处试

正则语法速览:一处试通各种写法

字面量、字符类、量词、分组、锚点、反向引用、断言与标志位,本仓库的自研引擎基本都支持。同一条 pattern 还能现场编译成 NFA、DFA 与最小化 DFA。

. 与 \s · 看不见的字符

空白与换行:点号与 \s 各管什么

. 默认不匹配四个行终止符,\s 匹配的远不止一个空格——连 BOM 都算,零宽空格反而不算。这类看不见的字符是一大类难定位 bug 的源头。

位置 · 零宽匹配 · 锚点与 lookaround

零宽匹配:正则站在字符之间

"good".match(/o*/g) 凭空多出三个空串。因为正则匹配的不是字符而是位置,长度 n 的串有 n+1 个位置,匹配到的内容允许是零长度的。

g · lastIndex · 有状态匹配

有状态的全局匹配:g 与 lastIndex

g 让一个 regex 对象背上可变游标 lastIndex,这是一连串经典 bug 的源头,也同时解释了空匹配为何不死循环、matchAll 为何强制要 g

capture · backref · 两种「共用」

命名捕获与反向引用:两种共用

命名捕获共用的是 pattern 里那个组的名字,不改变匹配能力;反向引用共用的是那个组实际吃到的文本,因此超出正则语言、要回溯。

分组 + 量词 · 捕获槽只剩一个

量词下的分组:只剩最后一次

(\d)+ 匹配 "123"$1 却只有 "3"。一个捕获组只有一个槽,量词每重复一次就覆盖上一次,别的语言对这一点各有取舍。

自研引擎 · @vega/parsing/regex

最后拆本仓库 @vega/parsing/regex 的实现:pattern → ASTPike VM回溯两条引擎;对比 combinator 与递归下降两种解析写法;以及回溯引擎的 ReDoS 风险与线性引擎为何贴地。

和别的系列串起来看:@vega/parsing/regex 的 Pike VM 正是 有限自动机里 Thompson 构造(regex → NFA)的工程落地;它的线性匹配与 字符串匹配一脉相承。emoji 部分则回答了那些系列里没细讲的「一个字符到底是什么」。

🔗 相关链接

  • The Absolute Minimum Every Developer Must Know About Unicode · tonsky.me 通用 Unicode 必读:code point、UTF-8/16、字素簇、归一化一篇讲透。本系列的 UTF-8 / 字素簇 / 归一化几页正是把它的核心做成了可上手的 demo。
  • UTS #51 · Unicode Emoji(权威规范) · unicode.org emoji 半边的源头标准:Emoji/Emoji_Presentation 等属性、ZWJ 序列、肤色 modifier、国旗与 Tag、RGI 子集全部定义于此。本系列 Basic_Emoji / 六属性 / RGI 几页讲的规则,出处就是这份报告。
  • UAX #29 · Unicode Text Segmentation(权威规范) · unicode.org 字素簇 / 词 / 句切分的源头标准:Grapheme_Cluster_Break 属性值与边界规则表(GB1~GB999)全部定义于此。本系列字素簇一页的「手写规则引擎」就是照这份报告实现 Intl.Segmenter 的 grapheme 切分。
  • UAX #14 · Unicode Line Breaking Algorithm(权威规范) · unicode.org 「哪里允许换行」的源头标准:每个码点的 Line_Break 属性值与折行规则全部定义于此。本系列分词与断行一页里浏览器实测出的折点,背后跑的就是这套规则。
  • UTS #10 · Unicode Collation Algorithm(权威规范) · unicode.org 排序的源头标准:多级权重(primary/secondary/tertiary)与 DUCET 默认权重表全部定义于此。本系列排序一页用的 Intl.Collator 正是它的工程实现,locale 差异则来自 CLDR 的 tailoring。
  • UAX #50 · Unicode Vertical Text Layout(权威规范) · unicode.org 竖排朝向的源头标准:Vertical_Orientation(vo)属性的四个取值 U/R/Tu/Tr、未分配码点的默认区间、与字体竖排替代字形的关系全部定义于此。本系列竖排朝向一页每格的 vo 值就照它的 VerticalOrientation.txt 实现。
  • UTS #39 · Unicode Security Mechanisms(权威规范) · unicode.org 同形混淆与脚本混用的源头标准:confusables 表、skeleton 算法、mixed-script 检测全部定义于此。本系列同形字一页的 skeleton 与混合脚本判定,出处就是这份报告。
  • Codepoints · 逐码点数据库 · codepoints.net 查任意 code point 的属性大全:名称、General_Category、所属 block/script、各编码字节、组合/大小写映射、相关字符。验证本系列各页拆出来的码点、属性时一键可查。
  • Unicode Code Charts · 官方字符表 · unicode.org 最权威的源头:按 Block 一份份 PDF,逐个 code point 列出官方字形与名称——「特殊用意的码点」那页的 Block / 边界 / 私用区,在这里能找到每段的官方定义图表。
  • Full Emoji List · emoji 全表 · unicode.org 官方 emoji 总表:逐个列出 code point、CLDR 名称,并排展示各厂商(Apple/Google/Microsoft…)的实际渲染。对照本系列 Basic_Emoji / RGI / ZWJ 序列拆出的码点最直观——一眼看清同一序列在不同平台长什么样。
  • Emojipedia · emoji 百科 · emojipedia.org 逐个 emoji 的词条:code point、shortcode、各平台 / 各 Unicode 版本的历代渲染、ZWJ 组成与含义流变。比 Full Emoji List 更适合「查某一个 emoji 的来龙去脉」。
  • RegexLearn · 分步交互课程 · regexlearn.com 从零开始的闯关式教程,每步即时校验;有中文,适合系统补一遍语法。
  • learn-regex · 图解速成(中文) · github.com 用图把元字符 / 量词 / 分组 / lookaround 讲清楚的速查仓库,这里直达其中文翻译。
  • 正则表达式 30 分钟入门教程 · deerchao.cn 流传多年的中文经典长文,从字符匹配一路讲到分组 / 反向引用 / lookaround,术语对照清楚,适合一口气通读入门。
  • RegexBuddy · 正则构建与调试工具 · regexbuddy.com 桌面端正则 IDE:逐节点解析正则、单步调试匹配过程、跨多种 flavor(PCRE / JS / .NET / Java…)对照语法差异,并生成各语言调用代码。出自 regular-expressions.info 同一作者。
  • The Typing of the Regex · 正则打字游戏 · thetypingoftheregex.com 敌人是一串文本,你得现写正则把它们「打」掉——边玩边练手感与直觉。