26、正则表达式高级技巧与应用

正则表达式高级技巧与应用

1. 懒惰匹配

在正则表达式中,重复元字符(如 * + ? {n} {n,} {n,m} )默认是贪婪匹配,即尽可能多地匹配字符。但我们可以通过在重复元字符后面加上问号 ? 来使其变为懒惰匹配,也就是尽可能少地匹配字符。

例如,我们想要将 <i> 标签替换为 <strong> 标签:

const input = "这是 <i>斜体</i> 文本";
const result = input.replace(/<i>(.*?)<\/i>/ig, '<strong>$1</strong>');
console.log(result); 

在这个正则表达式 /<i>(.*?)<\/i>/ig 中, (.*?) 采用了懒惰匹配,只要遇到 </i> 就会停止匹配。

所有重复元字符都可以通过跟问号变为懒惰匹配,但实际中常用的是 * +

2. 反向引用

分组功能引出了反向引用技术。在正则表达式中,每个分组(包括子分组)从左到右依次被分配一个编号,从 1 开始。我们可以在正则表达式中使用反斜杠加编号来引用这些分组。

例如,我们要匹配符合 XYYX 模式的乐队名称:

const promo = "Opening for XAAX is the dynamic GOOG!  At the box office now!";
const bands = promo.match(/(?:[A-Z])(?:[A-Z])\2\1/g);
console.log(bands); 

在这个例子中,如果第一个分组匹配到 X,第二个分组匹配到 A,那么 \2 就会匹配 A, \1 就会匹配 X。

反向引用在匹配引号时也很有用。在 HTML 中,属性值可以使用单引号或双引号:

const html = `<img alt='A "simple" example.'>` +
         `<img alt="Don't abuse it!">`;
const matches = html.match(/<img alt=(?:['"]).*?\1/g);
console.log(matches); 

这个正则表达式中,第一个分组匹配单引号或双引号,然后通过 \1 引用这个匹配结果。

3. 分组替换

分组的一个好处是可以进行更复杂的替换操作。例如,我们要从 <a> 标签中提取 href 属性:

let html = '<a class="nope" href="/yep">Yep</a>';
html = html.replace(/<a .*?(href=".*?").*?>/, '<a $1>');
console.log(html); 

在这个正则表达式中, (href=".*?") 是第一个分组,在替换字符串中使用 $1 来引用这个分组。

我们还可以保留 class href 属性,而去除其他属性:

let html = '<a class="yep" href="/yep" id="nope">Yep</a>';
html = html.replace(/<a .*?(class=".*?").*?(href=".*?").*?>/, '<a $2 $1>');
console.log(html); 

除了 $1 $2 等,还有 $``(匹配前的所有内容)、 $& (匹配本身)和 $’ (匹配后的所有内容)。如果要使用字面量美元符号,使用 $$`:

const input = "One two three";
console.log(input.replace(/two/, '($`)')); 
console.log(input.replace(/\w+/g, '($&)')); 
console.log(input.replace(/two/, "($')")); 
console.log(input.replace(/two/, "($$)")); 
4. 函数替换

函数替换是正则表达式中一个强大的功能,它可以将复杂的正则表达式分解为多个简单的正则表达式。

假设我们要将 HTML 中的所有 <a> 标签转换为特定格式,只保留 class id href 属性,去除其他属性。输入的 HTML 可能格式混乱,属性不一定存在,且顺序也不固定:

const html =
   `<a class="foo" href="/foo" id="foo">Foo</a>\n` +
   `<A href='/foo' Class="foo">Foo</a>\n` +
   `<a href="/foo">Foo</a>\n` +
   `<a onclick="javascript:alert('foo!')" href="/foo">Foo</a>`;

function sanitizeATag(aTag) {
   const parts = aTag.match(/<a\s+(.*?)>(.*?)<\/a>/i);
   const attributes = parts[1].split(/\s+/);
   return '<a ' + attributes
      .filter(attr => /^(?:class|id|href)[\s=]/i.test(attr))
      .join(' ')
      + '>'
      + parts[2]
      + '</a>';
}

const result = html.replace(/<a .*?<\/a>/ig, sanitizeATag);
console.log(result); 

在这个例子中, sanitizeATag 函数使用了多个正则表达式来处理 <a> 标签。我们还可以将这个函数直接传递给 String.prototype.replace 方法。

5. 锚定

锚定用于匹配字符串的开头或结尾,或者整个字符串。 ^ 匹配行的开头, $ 匹配行的结尾:

const input = "It was the best of times, it was the worst of times";
const beginning = input.match(/^\w+/g); 
const end = input.match(/\w+$/g); 
const everything = input.match(/^.*$/g); 
const nomatch1 = input.match(/^best/ig);
const nomatch2 = input.match(/worst$/ig);
console.log(beginning); 
console.log(end); 
console.log(everything); 
console.log(nomatch1); 
console.log(nomatch2); 

如果要将字符串按多行处理,需要使用 m (多行)选项:

const input = "One line\nTwo lines\nThree lines\nFour";
const beginnings = input.match(/^\w+/mg); 
const endings = input.match(/\w+$/mg); 
console.log(beginnings); 
console.log(endings); 
6. 单词边界匹配

单词边界匹配是正则表达式中一个容易被忽视的有用特性。单词边界元字符 \b 及其反向 \B 不消耗输入字符。

单词边界定义为 \w 匹配被 \W (非单词字符)或字符串开头、结尾所包围的位置。例如,我们要将英文文本中的电子邮件地址替换为超链接:

const inputs = [
    "john@doe.com",                 
    "john@doe.com is my email",     
    "my email is john@doe.com",     
    "use john@doe.com, my email",   
    "my email:john@doe.com.",       
];
const emailMatcher =
   /\b[a-z][a-z0-9._-]*@[a-z][a-z0-9_-]+\.[a-z]+(?:\.[a-z]+)?\b/ig;
const results = inputs.map(s => s.replace(emailMatcher, '<a href="mailto:$&">$&</a>'));
console.log(results); 

单词边界匹配在搜索特定单词开头、结尾或包含特定单词的文本时也很有用:
| 正则表达式 | 匹配情况 |
| ---- | ---- |
| /\bcount/ | 匹配 count countdown ,不匹配 discount recount accountable |
| /\bcount\B/ | 只匹配 countdown |
| /\Bcount\b/ | 匹配 discount recount |
| /\Bcount\B/ | 只匹配 accountable |

7. 前瞻匹配

前瞻匹配是正则表达式中的高级技巧,它不消耗输入字符,可以匹配任何子表达式而不占用它。前瞻匹配分为正向前瞻 (?=<subexpression>) 和负向前瞻 (?!<subexpression>)

例如,验证密码是否符合要求,密码必须包含至少一个大写字母、一个数字、一个小写字母,且不包含非字母和非数字字符:

function validPassword(p) {
    return /(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?!.*[^a-zA-Z0-9])/.test(p);
}
console.log(validPassword("Abc123")); 
console.log(validPassword("abc")); 

前瞻匹配在处理重叠内容时非常有用,可以简化某些类型的匹配。

8. 动态构建正则表达式

通常我们建议使用正则表达式字面量,但在需要动态构建正则表达式时,就需要使用 RegExp 构造函数。例如,我们要匹配字符串中包含的用户名:

const users = ["mary", "nick", "arthur", "sam", "yvette"];
const text = "User @arthur started the backup and 15:15, " +
   "and @nick and @yvette restored it at 18:35.";
const userRegex = new RegExp(`@(?:${users.join('|')})\\b`, 'g');
const matches = text.match(userRegex);
console.log(matches); 

在这个例子中,我们通过 RegExp 构造函数动态构建了正则表达式。

总之,正则表达式是一个强大的工具,但要熟练掌握它,需要理解其理论知识并进行大量的实践。使用一个强大的正则表达式测试工具(如 Regular Expressions 101)可以帮助我们更好地学习和使用正则表达式。最重要的是要理解正则表达式引擎如何处理输入,这是避免很多困惑的关键。

正则表达式高级技巧与应用(续)

9. 综合案例分析

为了更好地理解上述正则表达式的高级技巧,我们来看一个综合案例。假设我们要处理一段包含 HTML 标签和文本的字符串,需要完成以下几个任务:
- 提取所有 <a> 标签,并将其 href 属性值提取出来。
- 替换所有电子邮件地址为超链接。
- 验证所有密码是否符合要求。

以下是实现这些任务的代码:

// 输入的字符串
const input = `这是一段包含 <a href="https://example.com">链接</a> 的文本,
我的邮箱是 john@doe.com,密码是 Abc123。`;

// 提取 <a> 标签的 href 属性
const hrefRegex = /<a.*?href="(.*?)".*?>/ig;
let hrefMatches;
const hrefs = [];
while ((hrefMatches = hrefRegex.exec(input))!== null) {
    hrefs.push(hrefMatches[1]);
}
console.log("提取的 href 属性值:", hrefs);

// 替换电子邮件地址为超链接
const emailMatcher = /\b[a-z][a-z0-9._-]*@[a-z][a-z0-9_-]+\.[a-z]+(?:\.[a-z]+)?\b/ig;
const replacedInput = input.replace(emailMatcher, '<a href="mailto:$&">$&</a>');
console.log("替换后的文本:", replacedInput);

// 验证密码是否符合要求
function validPassword(p) {
    return /(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?!.*[^a-zA-Z0-9])/.test(p);
}
const passwordRegex = /(?<!\S)[a-zA-Z0-9]{6,}(?!\S)/g;
let passwordMatches;
while ((passwordMatches = passwordRegex.exec(input))!== null) {
    const password = passwordMatches[0];
    console.log(`${password} 是否为有效密码:`, validPassword(password));
}

这个案例展示了如何结合使用正则表达式的不同技巧来处理复杂的文本。

10. 正则表达式的性能优化

在使用正则表达式时,性能也是一个需要考虑的重要因素。以下是一些性能优化的建议:
- 避免使用贪婪匹配 :贪婪匹配会尝试尽可能多地匹配字符,可能会导致回溯,影响性能。尽量使用懒惰匹配。
- 减少分组数量 :分组会增加正则表达式的复杂度,尽量减少不必要的分组。
- 使用预编译的正则表达式 :如果同一个正则表达式需要多次使用,建议预编译它,避免重复编译的开销。

例如,以下是一个预编译正则表达式的示例:

// 预编译正则表达式
const emailRegex = /\b[a-z][a-z0-9._-]*@[a-z][a-z0-9_-]+\.[a-z]+(?:\.[a-z]+)?\b/ig;
const texts = [
    "john@doe.com",
    "my email is john@doe.com",
    "use john@doe.com, my email"
];
texts.forEach(text => {
    const matches = text.match(emailRegex);
    console.log(`文本 "${text}" 中的电子邮件地址:`, matches);
});
11. 正则表达式的常见错误及解决方法

在使用正则表达式时,我们可能会遇到一些常见的错误。以下是一些常见错误及解决方法:
| 错误类型 | 错误描述 | 解决方法 |
| ---- | ---- | ---- |
| 回溯过多 | 贪婪匹配导致回溯过多,性能下降 | 使用懒惰匹配 |
| 分组编号错误 | 在反向引用或分组替换时,分组编号使用错误 | 仔细检查分组的定义和编号 |
| 转义字符问题 | 忘记对特殊字符进行转义,导致正则表达式无法正常工作 | 对特殊字符进行正确的转义 |

12. 正则表达式的工作流程

下面是一个正则表达式处理字符串的简单流程图,使用 mermaid 格式表示:

graph TD;
    A[输入字符串] --> B[应用正则表达式];
    B --> C{是否匹配};
    C -- 是 --> D[处理匹配结果];
    C -- 否 --> E[结束处理];
    D --> F[输出处理结果];

这个流程图展示了正则表达式处理字符串的基本流程:输入字符串,应用正则表达式进行匹配,根据匹配结果进行相应的处理,最后输出处理结果。

13. 总结与展望

正则表达式是一种强大的文本处理工具,通过掌握其高级技巧,如懒惰匹配、反向引用、分组替换、函数替换、锚定、单词边界匹配、前瞻匹配和动态构建等,我们可以处理各种复杂的文本处理任务。

在实际应用中,我们需要根据具体需求选择合适的技巧,并注意性能优化和避免常见错误。同时,不断实践和使用正则表达式测试工具可以帮助我们更好地掌握这门技术。

未来,随着文本处理需求的不断增加,正则表达式可能会在更多领域得到应用,如自然语言处理、数据挖掘等。我们可以期待正则表达式技术的进一步发展和创新。

总之,正则表达式的学习是一个持续的过程,希望本文介绍的内容能帮助你更好地理解和使用正则表达式。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值