X

曜彤.手记

随记,关于互联网技术、产品与创业

吉 ICP 备10004938-2号

《正则指引(第二版)》读书笔记

旧书清理系列。正好赶上新出不久的第二版,年前暂时不读“板砖”了,找点薄的速读一下。

(仅记录 PCRE 相关的正则用法,Posix 等其他用法略。不重要的章节略)

第 1 章 - 字符组

  1. Page 2 字符组
  • 表示“在同一个位置可能出现的各种字符”(只能匹配一个字符)。
// 只要出现字符组中的任意字符即匹配成功;
/[12345]/.test('F1');  // true.
/[1-5]/.test('F1');  // 范围表示法;
/[0-9a-zA-Z]/.test('Hello');
/[\x00-\x7f]/.test('Hello');  // 匹配全 ASCII 码表;
  • “范围表示法”的字符范围是根据 “-” 符号两端字符的 ASCII 码值来确定的(小-大);
  • 可以使用 “\x” 的方式来匹配不可打印或不好打印字符(如“非本地文字”);
  • 转义“元字符”:(当正则是以“字符串”形式给出时,“\” 需要用 “\” 代替。否则会被当做非法的转义字符)。
/[0\-9]/.test('0-9');  // true.
  • 完全匹配:整个字符串需要完全与正则匹配。
// 正则只能匹配一个字符,而字符串多于一个字符;
/^[12\]345]$/.test('2345');  // false.
// 整个字符串的完全匹配;
/^\[2345]$/.test('[2345]');  // true.
  • 排除型”字符组:(必须匹配一个字符)。
/^[^0-9][0-9]$/.test('A2');  // true.
  • 字符组简记法(ASCII 匹配规则):
    • \d:等价于 *[0-9]*(数字);
    • \w:等价于 *[0-9a-zA-Z_]*(单词字符);
    • \s:等价于 *[ \t\r\n\v\f]*(空白字符);
    • \D:与 \d 互补;
    • \W:与 \w 互补;
    • \S:与 \s 互补。
// 匹配十六进制字符;
/^[\da-zA-Z]$/.test('F');  // true.
/^[^\d]$/.test('_');  // true.
/^\d$/.test('8');  // true.
// 匹配所有字符;
/^[\s\S]*$/.test('Hello, world!');  // true.
  • 转义:对于一般形式的字符组 [a-z]转义时仅需要给第一个字符添加反斜线即可,即 “\[a-z]”。

第 2 章 - 量词

  1. Page 17 量词
  • 可用于指定“匹配的次数”;
  • 通常,可匹配次数的上限为 65536 次(实现相关,一般认为不存在上限)。
/^[\d]{6,8}$/.test('123456');  // true.
/^[\d]{4,}$/.test('123456');  // true.
/^[\d]{4}$/.test('123456');  // false.
  • 简记量词
    • +:等价于 *{0,}*;
    • *:等价于 *{1,}*;
    • ?:等价于 *{0,1}*。
/^<[^>]+>$/.test('<div>');  // true.
/^<[^/][^>]*[^/]>$/.test('<div>');  // true.
/^<\/[^>]*>$/.test('</div>');  // true.
/^<[^>\/]*\/>$/.test('<div/>');  // true.
  • 任意字符匹配:“*”;
    • 无法匹配换行符(\n);
  • 上述量词默认均为“匹配优先量词(贪婪匹配)”,即:会匹配符合要求的最长子串。在匹配时会先贪婪地匹配所有符合要求的字符,直到最后再通过“回溯”满足正则其余部分的要求。
/\".*\"/.exec(`"quoted string" and another"`); 
// "quoted string" and another".
/\"[^\"]*\"/.exec(`"quoted string" and another"`);  // "quoted string".
  • 忽略优先量词(懒惰量词,效率相对较低):“*?”,选择能保证后序正则优先匹配的当前最小匹配。在匹配时会先选择“不匹配”的状态,再尝试表达式中之后的元素,如果尝试失败,再回溯,选择之前保存的“匹配”状态。
[...`<div>A</div><div>B</div>`.matchAll(/<div>[\S\s]*?<\/div>/g)]  // (2) [Array(1), Array(1)].
/\/\/.*/.exec(`// comment here.`);  // 匹配“单行注释”;
/\*[\S\s]*?\*\//.exec(`/* comment here */`);  // 匹配“多行注释”(不能嵌套,所以优先选择第一个);
.exec()
[...`<script>let x = 1;</script>`.matchAll(/<script[\s>][\S\s]*?<\/script>/g)];  // 匹配特定 HTML 标签;

  • 转义:对于一般形式的量词 {m,n},其含有的特殊字符不止一个,但转义时仅需要给第一个字符添加反斜线即可。其他形式则需要转义每个字符。

第 3 章 - 括号

  1. Page 33 括号
  • 默认情况下,量词只能应用在“字符(组)”上。使用括号,可以将量词应用在一个整体的表达式规则上。这种功能被称为“分组”;
/(^abc(de)*$)/.test(`abcdedede`);  // true.
  • 多选结构:“(…|…)”。
    • 可以没有最外层的“括号”,但可能由于优先级产生意外的效果(比如 /^ab|cd$//^(ab|cd)$/ 的解析规则并不相同);
    • 没有类似“字符组”的“排除”方式;
    • 分支的排序是有讲究的,大多数实现会选择最左侧的分支
    • 尽量避免多选分支中存在重复匹配(即一段文本可由多个子条件同时匹配,如 /([0-9]|\w)/),否则解析效率可能会受到影响;
    • 整个多选结构被视为单个元素,只要其中某个子表达式能匹配,整个多选结构的匹配就能成功
    • 使用该方式匹配在某个范围内的数值字符串(分段思想):

/([0-9]|[0-9]{2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/.test(`125`);  // true.
  • 引用分组:提取匹配的分组内容。
    • 分组的标号是根据 “(” 出现的顺序来计数的
// (4) ["11-22-33", "11", "22", "33", index: 0, input: "11-22-33", groups: undefined]
/(\d{2})-(\d{2})-(\d{2})/g.exec("11-22-33");
// (5) ["11-33-44", "11-33", "11", "33", "44", index: 0, input: "11-33-44", groups: undefined]
/((\d{2})-(\d{2}))-(\d{2})/g.exec("11-33-44");
  • 反向引用:“\num”。
    • 允许在正则表达式内引用之前捕获分组匹配的文本(非表达式);
    • 在正则替换时分组引用会产生“二义性”:$10,是理解为引用第 10 个分组的匹配内容,还是引用第 1 个分组的匹配内容,并跟随一个 “0”?JavaScript 中会在 10 号分组可引用时选择引用其内容,否则引用第 1 个分组并紧随一个 “0”(造成一定限制);
    • 命名分组:JavaScript 中的“分组” (?<name>...) 与“引用” \k<name>(… 为正则)。
// (2) ["11-11-11", "11", index: 0, input: "11-11-11", groups: undefined]
/(\d{2})-\1-\1/g.exec("11-11-11");
// 命名分组(赋予分组名称);
/(?<seq>\d{2})-\k<seq>-\k<seq>/g.exec("11-11-11");
  • 非捕获分组:“(?:…)”(… 为正则)。
    • 仅限定量词的作用范围,不捕获任何文本。某些情况下可用于提升性能;
    • 此类型分组在被引用时会被略过(即无法引用)。
// (3) ["11-22-33", "22", "33", index: 0, input: "11-22-33", groups: undefined]
/(?:\d{2})-(\d{2})-(\d{2})/g.exec("11-22-33");
  • 转义:括号表达式中的元字符 “(”、“)” 以及 “|” 每一个都需要转义。
  • 动态提取不定长输入的特定匹配
const reg = /(?<id>\d{1})/g;  // 必须是全局匹配;
const str = '1-2-3-4-5';
const arr= [];
while (true) {
  const result = reg.exec(str);
  if (result) {
    arr.push(result.groups.id);
  } else {
    break;
  }
}
console.log(arr);

第 4 章 - 断言

  1. Page 60 断言:正则表达式中的某些结构并不真正匹配文本,而只负责判断在某个位置左/右侧的文本是否符合要求。
  • 常见断言种类:单词边界行起始/结束位置环视
  1. Page 60 断言 - “单词边界”匹配:“\b”。
  • 边界处要求一边是单词字符(\w),另一边不是单词字符(也可以没有任何字符);
  • 反向引用只会引用匹配到的文本,而不会保留之前的断言规则
  • 对应的也有“非单词边界”匹配 — “\B”(对应 “\b” 的补集)。

[...`a sentence contains a lot of words`.matchAll(/\b\w+\b/g)];  // 提取所有单词;
  1. Page 62 断言 - “行起始/结束位置”匹配:
  • ^”:将正则匹配“定位”在字符串的开始位置(行起始位置);
  • $”:将正则匹配“定位”在字符串的结束位置(行结束位置,任何终止符之前。JavaScript 中匹配时会包含终止符);

  • (?m)”:设定匹配模式“多行模式”(大多数编程语言通过其他方式支持)。

[...`a\nsentence\ncontains\na\nlot\nof\nwords`.matchAll(/^\w+/gm)];  // 提取所有单词;
  • \A”:(忽视多行)将正则匹配“定位”在整个字符串的开始位置
  • \Z”:(忽视多行)等价于默认非多行模式下的 “$”;
  • \z”:(忽视多行)不管终止符,只匹配整个字符串的结束位置。

// JavaScript 中多行正则替换的几种方式;
'p1\np2'.replaceAll(/(\w+$)/mg, '<p>$1</p>');
'p1\np2'.replaceAll(/(?<w>\w+$)/mg, '<p>$<w></p>');
  • *一个特殊 case
/**
 * “\s+$” 可以匹配前面的 “\t\n”:以后面的 “\n” 作为行分割符;
 * “^\s+” 可以匹配后面的 “\n”:以前一个 “\n” 作为行分隔符。
 */
'  begin\n between\t\n\nend  '.replaceAll(/(^\s+|\s+$)/mg, '');  // !不正确的结果!
  1. Page 69 环视
  • 在它旁边的文本需要满足某种条件,但本身不匹配任何字符;
  • “环视”匹配的不是字符,而是位置。
/<(?!\/)/.test('<div>');  // 匹配的左尖括号右侧不能有正斜线;
`12345678`.replaceAll(/(?<=\d)(?=(\d{2})+(?!\d))/g, '|');  // 二位均等分割;
/(?![aeiou])[a-z]/.test('b');  // 匹配辅音字母;
  • 四种“环视”类型
    • 肯定环视要判断成功,字符串中必须有字符由环视结构中的表达式匹配;而否定环视要判断成功,却有两种情况:(1) 字符串中出现了字符,但不能由环视结构中的表达式匹配;(2) 字符串中不再有任何字符。

  • 环视的“嵌套”:
// 限制匹配字符串的固定长度;
/^(?=\d{0,3}(?!\d))/.test('111123');
  • 环视的“并列”:
// 对同一个位置附加多个条件;
/^(?=\d+)(?!999)/.test('111123');
/^(?=\d+(?<!999)$)/.test('111999');  // 字符串不能以 “999” 结尾;
  • 多选结构”中的环视:
/((?!\d)|(?=\d\D))/.test('ab');  // 找到一个位置,其后面要么不是数字,要么是一个数字和非数字字符;

第 5 章 - 匹配模式

  1. Page 83 匹配模式
  • 两种指定方式:
    • 模式修饰符形式 “(?modifier)” 嵌在正则开头(仅 JavaScript 不支持);
    • 预定义常量作为特殊参数传入正则函数(通常为先生成 Pattern 对象,然后再调用对应匹配方法)。
  • 不区分大小写模式(“i”):此模式下匹配时不区分字符大小写;
  • 单行模式(“s”):此模式下 “.” 可以匹配包括换行符在内的任何字符;
  • 多行模式(“m”):此模式下 “^” 与 “$” 可以匹配字符串内部某一行文本的起始位置和结束位置;
  • 注释模式(“x”):为正则添加注释 “(?#comment)”。
  • 模式可以通过括号来限定其作用域;
  • 结束模式的生效范围:“(?-modifier)
/(?i)(foo|zee)(?-i)BAR/