阐述 JavaScript RegExp (正则表达式) 引擎的贪婪 (Greedy) 与非贪婪 (Non-Greedy) 匹配,以及 lookahead 和 lookbehind 断言的高级用法。

嘿,大家好!今天咱们来聊聊 JavaScript 正则表达式的那些个小妖精,特别是“贪婪”、“非贪婪”这俩兄弟,还有那些神出鬼没的“lookahead”和“lookbehind”断言。保证让你听完之后,感觉自己好像突然开了天眼,以后写正则再也不头疼了。

第一章:正则引擎的“贪吃蛇”—— 贪婪与非贪婪匹配

想象一下,你手头有一堆糖果,然后来了一条贪吃蛇,它会怎么吃?当然是能吃多少吃多少!这就是正则引擎的“贪婪”模式。

1.1 什么是贪婪匹配?

贪婪匹配,顾名思义,就是正则引擎在匹配的时候,尽可能多地匹配符合模式的字符。 它会尽力扩展匹配范围,直到无法再匹配为止。

举个例子:

const str = "aaaaaa";
const greedyRegex = /a+/; // + 表示匹配一个或多个 'a'
const match = str.match(greedyRegex);

console.log(match); // 输出: [ 'aaaaaa', index: 0, input: 'aaaaaa', groups: undefined ]

在这个例子里, a+ 尽可能地匹配了所有的 a,一次性吞掉了整个字符串。

1.2 贪婪模式的特点

  • 尽最大努力匹配: 正则引擎会尝试匹配尽可能多的字符。
  • 回溯 (Backtracking): 如果贪婪匹配导致整个模式匹配失败,引擎会回溯,尝试减少匹配的字符数量,直到找到一个成功的匹配,或者完全失败。

1.3 什么是懒惰(非贪婪)匹配?

现在,假设来了另一条蛇,它比较害羞,吃东西细嚼慢咽,能少吃就少吃,这就是“非贪婪”模式,也叫“懒惰”模式。

1.4 非贪婪模式的特点

  • 尽可能少地匹配: 正则引擎会尝试匹配尽可能少的字符,只要能满足模式即可。
  • 避免回溯: 由于其最小化匹配的特性,非贪婪模式通常可以减少回溯的次数,从而提高效率。

1.5 如何开启非贪婪模式?

在量词后面加上 ? 就可以将贪婪模式切换为非贪婪模式。 常用的量词包括 *, +, ?, {n}, {n,}, {n,m}.

const str = "aaaaaa";
const lazyRegex = /a+?/; // 注意这里的 ?
const match = str.match(lazyRegex);

console.log(match); // 输出: [ 'a', index: 0, input: 'aaaaaa', groups: undefined ]

看,这次 a+? 只匹配了一个 a,因为它满足了“至少一个 a”的要求,然后就立刻停止了。

1.6 贪婪与非贪婪模式的对比

特性 贪婪匹配 (Greedy) 非贪婪匹配 (Lazy / Non-Greedy)
匹配策略 尽可能多地匹配 尽可能少地匹配
量词 *, +, ?, {n}, {n,}, {n,m} *?, +?, ??, {n}?, {n,}?, {n,m}?
回溯 可能发生 较少发生

1.7 实际应用举例

假设我们想从 HTML 字符串中提取所有的 <a> 标签:

const html = "<a href='link1'>Link 1</a><p>Some text</p><a href='link2'>Link 2</a>";

// 贪婪模式
const greedyRegex = /<a.*>(.*)</a>/;
const greedyMatch = html.match(greedyRegex);
console.log("贪婪匹配:", greedyMatch);
// 输出:
// 贪婪匹配: [
//   '<a href='link1'>Link 1</a><p>Some text</p><a href='link2'>Link 2</a>',
//   'Link 2',
//   index: 0,
//   input: '<a href='link1'>Link 1</a><p>Some text</p><a href='link2'>Link 2</a>',
//   groups: undefined
// ]

// 非贪婪模式
const lazyRegex = /<a.*?>(.*?)</a>/g;
const lazyMatches = Array.from(html.matchAll(lazyRegex));
console.log("非贪婪匹配:", lazyMatches);

// 输出:
// 非贪婪匹配: [
//   [
//     '<a href='link1'>Link 1</a>',
//     'Link 1',
//     index: 0,
//     input: '<a href='link1'>Link 1</a><p>Some text</p><a href='link2'>Link 2</a>',
//     groups: undefined
//   ],
//   [
//     '<a href='link2'>Link 2</a>',
//     'Link 2',
//     index: 41,
//     input: '<a href='link1'>Link 1</a><p>Some text</p><a href='link2'>Link 2</a>',
//     groups: undefined
//   ]
// ]

可以看到,贪婪模式一口气匹配到了最后一个 </a>,而我们只想提取每个单独的 <a> 标签。 非贪婪模式则正确地提取了两个 <a> 标签。注意,这里使用了 matchAll 来获取所有匹配项,并使用了全局标志 g

第二章:未卜先知的“预言家”—— Lookahead 和 Lookbehind 断言

想象一下,你是一个预言家,可以提前看到未来会发生什么,或者回顾过去发生了什么,但你并不能改变过去和未来,你只是看到了而已。这就是 Lookahead 和 Lookbehind 断言的作用。

2.1 什么是断言?

断言 (Assertion) 是一种零宽度的匹配。 这意味着断言会匹配一个位置,而不是字符。 断言不会消耗任何输入字符,它们只是用来检查某个位置的前面或后面是否符合某种条件。

2.2 Lookahead 断言

Lookahead 断言用于检查当前位置的后面是否符合某个模式。

  • Positive Lookahead ((?=pattern)): 要求当前位置的后面必须匹配 pattern
  • Negative Lookahead ((?!pattern)): 要求当前位置的后面不能匹配 pattern

举个例子:

const str = "hello world";

// Positive Lookahead: 匹配后面跟着 " world" 的 "hello"
const positiveLookaheadRegex = /hello(?= world)/;
const positiveMatch = str.match(positiveLookaheadRegex);
console.log("Positive Lookahead:", positiveMatch); // 输出: Positive Lookahead: [ 'hello', index: 0, input: 'hello world', groups: undefined ]

// Negative Lookahead: 匹配后面不跟着 " world" 的 "hello"
const negativeLookaheadRegex = /hello(?! world)/;
const negativeMatch = str.match(negativeLookaheadRegex);
console.log("Negative Lookahead:", negativeMatch); // 输出: Negative Lookahead: null

在第一个例子中,hello(?= world) 成功匹配了 "hello",因为它的后面确实跟着 " world"。 在第二个例子中,hello(?! world) 匹配失败,因为 "hello" 的后面跟着的是 " world",而不是其他内容。

2.3 Lookbehind 断言

Lookbehind 断言用于检查当前位置的前面是否符合某个模式。

  • Positive Lookbehind ((?<=pattern)): 要求当前位置的前面必须匹配 pattern
  • Negative Lookbehind ((?<!pattern)): 要求当前位置的前面不能匹配 pattern

注意: 并非所有的 JavaScript 引擎都支持 Lookbehind 断言。 较新的 ES2018 及以后的版本通常支持。 如果你的环境不支持,可以考虑使用 polyfill 或者其他替代方案。

举个例子:

const str = "world hello";

// Positive Lookbehind: 匹配前面是 "world " 的 "hello"
const positiveLookbehindRegex = /(?<=world )hello/;
const positiveMatch = str.match(positiveLookbehindRegex);
console.log("Positive Lookbehind:", positiveMatch); // 输出: Positive Lookbehind: [ 'hello', index: 6, input: 'world hello', groups: undefined ]

// Negative Lookbehind: 匹配前面不是 "world " 的 "hello"
const negativeLookbehindRegex = /(?<!world )hello/;
const negativeMatch = str.match(negativeLookbehindRegex);
console.log("Negative Lookbehind:", negativeMatch); // 输出: Negative Lookbehind: null

在第一个例子中,(?<=world )hello 成功匹配了 "hello",因为它的前面确实是 "world "。 在第二个例子中,(?<!world )hello 匹配失败,因为 "hello" 的前面是 "world ",而不是其他内容。

2.4 Lookahead 和 Lookbehind 断言的对比

断言类型 描述 语法
Positive Lookahead 要求当前位置的后面必须匹配某个模式 (?=pattern)
Negative Lookahead 要求当前位置的后面不能匹配某个模式 (?!pattern)
Positive Lookbehind 要求当前位置的前面必须匹配某个模式 (?<=pattern)
Negative Lookbehind 要求当前位置的前面不能匹配某个模式 (?<!pattern)

2.5 实际应用举例

2.5.1 提取价格(只提取美元价格)

const text = "The price is $100, and €50. Also, $200 is another price.";
const regex = /(?<=$)d+/g; // 匹配前面是 '$' 符号的数字
const prices = text.match(regex);
console.log(prices); // 输出: [ '100', '200' ]

2.5.2 验证密码强度(必须包含大小写字母和数字)

function isValidPassword(password) {
  const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d).+$/;
  return regex.test(password);
}

console.log(isValidPassword("Password123")); // 输出: true
console.log(isValidPassword("password123")); // 输出: false
console.log(isValidPassword("PASSWORD123")); // 输出: false
console.log(isValidPassword("Password"));    // 输出: false

这个例子使用了多个 Positive Lookahead 断言来确保密码同时包含小写字母、大写字母和数字。

第三章: 贪婪/非贪婪与 Lookaround 断言的综合运用

现在,让我们把这些知识点结合起来,看看它们如何协同工作。

3.1 提取 HTML 标签属性值

假设我们有以下 HTML 片段:

<img src="image.jpg" alt="My Image" width="200" height="100">

我们想要提取 src 属性的值,但是只针对 <img> 标签。

const html = '<img src="image.jpg" alt="My Image" width="200" height="100">';
const regex = /(?<=<img.*?src=")(.*?)(?=")/;
const match = html.match(regex);

console.log(match); // 输出: [ 'image.jpg', index: 10, input: '<img src="image.jpg" alt="My Image" width="200" height="100">', groups: undefined ]

这个正则使用了:

  • *Positive Lookbehind `(?<=<img.?src="):** 确保匹配的是标签的src属性。.*?` 使用非贪婪模式,避免匹配到其他标签。
  • *非贪婪模式 `(.?):** 提取src` 属性的值,尽可能少地匹配,直到遇到下一个引号。
  • Positive Lookahead (?="): 确保匹配到的是引号结束的位置。

3.2 替换字符串中的特定部分

假设我们想要把字符串中所有价格低于 100 的美元价格替换为 "LOW",但只替换美元价格,不替换欧元价格。

const text = "The price is $50, and €120. Also, $200 and $80 are other prices.";
const regex = /(?<=$)(d+)/g;

const replacedText = text.replace(regex, (match) => {
  if (parseInt(match) < 100) {
    return "LOW";
  } else {
    return match;
  }
});

console.log(replacedText); // 输出: The price is LOW, and €120. Also, $200 and LOW are other prices.

这个例子使用了:

  • Positive Lookbehind (?<=$): 确保匹配的是美元价格。
  • (d+): 匹配数字部分,并将其作为 match 传递给替换函数。
  • 替换函数: 根据价格是否低于 100 来决定替换成 "LOW" 还是保持原样。

第四章:实战案例分析

4.1 邮箱验证

function isValidEmail(email) {
  const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
  return regex.test(email);
}

console.log(isValidEmail("[email protected]")); // true
console.log(isValidEmail("invalid-email"));    // false

虽然这个正则很简单,但是可以进行更复杂的优化,例如使用 Lookahead 断言来验证域名后缀是否合法。

4.2 URL 提取

const text = "Visit my website at https://www.example.com or check out http://blog.example.org.";
const regex = /(https?://[^s]+)/g;
const urls = text.match(regex);

console.log(urls); // 输出: [ 'https://www.example.com', 'http://blog.example.org' ]

这个正则提取了字符串中的所有 URL。 [^s]+ 匹配除了空白字符以外的所有字符,直到遇到空白字符为止。

第五章: 总结与建议

  • 理解贪婪与非贪婪模式的区别: 选择合适的模式可以避免不必要的匹配和回溯,提高效率。
  • 灵活运用 Lookahead 和 Lookbehind 断言: 断言可以让你更精确地定位匹配位置,实现更复杂的匹配逻辑。
  • 多练习,多实践: 正则表达式是一门需要不断练习才能掌握的技能。 尝试解决各种实际问题,你会越来越熟练。
  • 使用在线工具进行测试: 有很多在线正则表达式测试工具可以帮助你验证你的正则表达式是否正确。
  • 注意兼容性: 某些高级特性(例如 Lookbehind 断言)可能在某些 JavaScript 引擎中不受支持。

好了,今天的讲座就到这里。希望大家通过今天的学习,对 JavaScript 正则表达式有了更深入的理解。 记住,正则表达式是一把强大的工具,掌握它,你就能轻松解决各种文本处理问题。 以后遇到正则的问题,别害怕,勇敢地去探索吧! 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注