Regex 的魔法:编程中的强大工具

原文地址:Regex, the good bits

作者:Vincent Ge

开发者可以分为两类:那些害怕正则表达式因为他们不理解它们的,而另一些则会过度使用正则表达式来展示自己的技能。

正则表达式是编程中的强大工具,它可以帮助我们在不危及安全的前提下,找到那些非常有用且不会引起问题的功能。

等等,正则表达式会很危险吗?

正则表达式可以做一些了不起的事情。你可以用正则表达式编写整个程序。单仅仅因为你可以,并不意味着你应该这样做。一个巨大的正则表达式模式使用了正则表达式的所有强大功能,比如递归模式、条件模式、前瞻和后瞻,以及通过替换引入副作用。

我的意思是看看这个:

scss 复制代码
(function (a, b) {
  if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) 
    || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) 
    window.location = b
})(navigator.userAgent || navigator.vendor || window.opera, 'http://detectmobilebrowser.com/mobile');

这是一种在某些时候检测移动浏览器的常用模式。

我对正则表达式的问题是:

  • 正则表达式语法极其简洁,这意味着读写需要极大的脑力。
  • 正则表达式有很多例外。它的语法和规则充其量是不一致的。
  • 很难分成多行,而且最终会得到一个巨大的字符串。
  • 更新正则表达式以接受新行为,重用部分逻辑非常困难。
  • 当它变得足够大时,每个人都不敢碰它。

如果对正则表达式的滥用,最终会导致无法阅读和维护代码。

谈谈优点

从本质上讲,正则表达式是一种基于规则搜索和匹配文本并将信息提取到变量中的强大方法。它可以用于操作字符串,但我将避免这样做。大多数人并不认为正则表达式会有副作用。

例如提取具有特定类名的 html 标签、格式化电话号码和日志解析等都是使用正则表达式的好例子。

基础模式

以在 JS 中使用正则表达式为例。

vbscript 复制代码
const words = [
    "Hello world",
    "This is a short! message that says \"Hello world\"",
    "I love regular expressions"
];

// this is the pattern
const re = new RegExp("ello")

// the pattern can be used to "test" for matches
console.log(words.filter(value = re.test(value)))

re.test()中使用的正则表达式模式ello将匹配任何包含该模式作为子字符串的字符串。这是最简单的模型类型。

它将匹配以下行:

css 复制代码
[ 'Hello world', 'This is a short! message that says "Hello world"']

这些模式默认区分大小写。您可以使用选项new RegExo("ello", "i");定义不区分大小写。

文本的开始和结束

正则表达式具有"元字符",可定义模式匹配的逻辑规则。

字符^表示文本的开沟,$表示字符串的结尾。

例如:

javascript 复制代码
// This will match "Hello, world!" but not "Message: Hello, world!".
const re = new RegExp("^Hello");

// This will match "Hello, world" but not "Hello, world!"
const re = new RegExp("world$");

匹配其中一个变体

有时,您想要匹配类似模式的变体。在最基本的情况下,单词的变体,如finepineline。在这些情况下,您可以像这样在括号中定义一组选项:[]

javascript 复制代码
const words = [
    "fine",
    "pine",
    "line!"
];

// This will match all the words above
const re = new RegExp("[fpl]ine")

console.log(words.filter(value => re.test(value)))

您可以在这些任意组中使用一定范围的 ASCII 码,例如[a-zA-Z0-9]

ini 复制代码
const words = [
    "1ine",
    "Pine",
    "zine!"
];
// still match all the words
const re = new RegExp("[a-zA-Z0-9]ine");

console.log(words.filter(value => re.test(value)))

另一种方法是使用|来表示逻辑或匹配替代方案。

ini 复制代码
const words = [
    "color",
    "colour",
];
const re = new RegExp("color|colour");

console.log(words.filter(value => re.test(value)))

通配符

有时您不想指定选项,而是想匹配所有可以想象到的变体。我们可以使用.字符来指定通配符。

ini 复制代码
const words = [
    "%ine",
    "}ine",
    "`ine!"
];
const re = new RegExp(".ine");

const.log(words.filter(value => re.test(value)))

重复模式

有时我们想重复匹配一个字符。例如,匹配yeet的每个变体,如yeeeeeeeeeetyeeeeeeeeeeeeeeeeeeeeeeeet

我们可以使用+**匹配前一个元素零次或多次,+匹配前一个元素一次或多次。

ini 复制代码
const words = [
    "yeet",
    "yeeet",
    "yeeeeeeeeet"
];

// matches [ 'yeeet', 'yeeeeeeeeet' ]
const re1 = new RegExp("yeee+t");

// matches [ 'yeet', 'yeeet', 'yeeeeeeeeet' ]
const re2 = new RegExp("yeee*t");

console.log(words.filter(value => re1.test(value)))
console.log(words.filter(value => re2.test(value)))

如果您希望至少匹配一次该字符,请使用+; 如果可能,请使用*表示该字符是可选的,但尽量匹配多次。

这样做的一个有趣的副作用是他们可以与通配符.结合使用。在您的模式中尝试.*.+,但要小心,.*会匹配任何内容,这很容易出错。

另一个有用的语法是{},它指定字符或模式的一部分重复的次数。例如:

ini 复制代码
const words = [
    "100",
    "1011",
    "222222"
];
// matches [ '1011' ]
const re = new RegExp("^[0-9]{4}$");

console.log(words.filter(value => re.test(value)))

有用的宏

有些元字符的行为有点像宏。这些元字符是构建正则表达式模式以匹配字符串中的特定文本模式的基础。

Metacharacter Description Example Match
\d Digit (0-9) 4, 9, 0
\D Non-digit a, Z, %
\w Word character a, A, 1, _
\W Non-word character !, @, #
\s Whitespace , \t, \n
\S Non-whitespace a, 1, %
\b Word boundary \bword\b, \b123\b
\B Non-word boundary \Bword\B, \B123\B

\d\w\s的含义非常明确。我想重点介绍\b\B元字符 。它们在解析诗歌时非常有用,因为它们尊重自然的单词边界。例如,在"hello,boss"中,hello 是一个独立的单词,但它后面跟着一个,,这意味着如果您天真地匹配模式\shello\s,则会错过该单词。同样,天正的匹配hello也会匹配像phelloplastic这样的单词。

ini 复制代码
const words = [
    "hello, boss",
    "galvanized square steel.",
    "dave has saved up a looooonnnggg time for his new prison-esque house."
];

//matches words ["hello, boss"]
const re1 = new RegExp("\\bhello\\b");

// matches in words ["dave has saved up a looooonnnggg t..."]
const re2 = new RegExp("\\Bsq\\B")

console.log(words.filter(value => re1.test(value)))
console.log(words.filter(value => re2.test(value)))

提取值

正则表达式中的捕获组允许您使用模式提取值。您使用(<subpattrn>)定义捕获组,并返回()中模式匹配所有内容。

例如,解析电子邮件:

ini 复制代码
let email = "example.user123@example.com";
let r = /^([\w\.-]+)@([\w\.-]+)\.([a-zA-Z]{2,6})$/
let match = r.exec(email);

if (!match)
    throw Error("invalid email")
    
// the capture group is returned as an array of matches
let username = match[1] ?? '';
let domain = match[2] ?? '';
let tld = match[3] ?? '';

console.log("Username:", username);
console.log("Domain:", domain);
console.log("Top-Level Domain (TLD):", tld);

更酷的是,捕获组可以命名为更易读的模式,并且可以多次使用他们进行匹配。例如,在这里我们可以提取多个电子邮件:

ini 复制代码
let email = "example.user123@example.com example.user123@example.com example.user123@example.com";
let pattern = /(?<username>[\w\.-]+)@(?<domain>[\w\.-]+)\.(?<tld>[a-zA-Z]{2,6})/;
let match = pattern.exec(email);

if (!match)
    throw Error("invalid email")
    
// loop over all matches
for (group in match.groups) {
    let username = match.groups.username;
    let domain = match.groups.domain;
    let tld = match.groups.tld;
    
    console.log("Username:", username);
    console.log("Domain:", domain);
    console.log("Top-Level Domain (TLD):", tld);
}

等等,你错过了这个很酷的东西!

如果你已经知道很多正则表达式,那太好了!我知道每个人都有自己最喜欢的正则表达式小技巧。这篇文章的目的是消除许多开发人员在代码中看到正则表达式的恐惧。本篇博客中介绍的正则表达式子集足以让你在震泽表达式中变得强大和精通,还不足以让你滥用。

最后再补充一张图片:

相关推荐
程序猿小D1 分钟前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa
john_hjy19 分钟前
【无标题】
javascript
奔跑吧邓邓子33 分钟前
npm包管理深度探索:从基础到进阶全面教程!
前端·npm·node.js
软件开发技术深度爱好者41 分钟前
用HTML5+CSS+JavaScript庆祝国庆
javascript·css·html5
前端李易安1 小时前
ajax的原理,使用场景以及如何实现
前端·ajax·okhttp
汪子熙1 小时前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ2 小时前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.5 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。7 小时前
案例-表白墙简单实现
前端·javascript·css
数云界7 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端