在Twitter上关注我,很高兴接受你对主题或改进的建议/Chris
我把这篇文章写给未来的自己。事实上,我的很多文章都是写给未来的自己的,因为我忘记了如何做某件事情的一切。RegEx,正则表达式是我们工具箱中一个非常强大的工具。可悲的是,我们把它称为黑魔法、魔鬼和其他迷人的东西。它不一定是这样的。RegEx与普通的编程肯定是不同的,但它也是非常非常强大的东西。让我们来学习它是如何工作的,以及如何实际使用它,并将它应用于你所认识的日常问题。
TLDR; 这本书长吗?是的,但它确实经历了RegEx中的主要结构。此外,我还在最后提供了一些很好的配方,如RegEx处理电子邮件、密码、日期格式转换以及如何处理URL。如果你以前从未使用过RegEx,或者你努力想看清所有奇怪的魔法--这是为你准备的。阅读愉快 :)
参考资料
在RegEx方面有一些很好的资源,我经常查阅。花点时间阅读它们。有时他们会解释RegEx是如何被处理的,并能解释为什么会发生这样的魔法。
- 正则表达式信息一个涵盖大量RegEx信息的好网站。
- Mozillas关于RegEx的文档页面很好的网站,有深入的解释和例子。
- JavaScript info我见过的关于RegEx组的一些最好的解释。
- 命名组
- 正则表达式文档尽管这是一个.NET指南,但正则表达式信息是相当普遍和适用的。
如何练习
-
Node.js REPL,如果你安装了Node.js,我建议在终端键入
node。这将启动REPL,这是一个测试模式的好方法。 -
JavaScript REPL,这是一个VS Code扩展,可以评估你输入的内容。你会得到即时的结果反馈
-
浏览器,在浏览器中拉出开发工具并使用控制台也能正常工作。
-
RegEx 101
伟大的沙盒环境。谢谢你的提示 Lukasz :)
正则表达式
正则表达式或RegEx是关于模式匹配的。如果我们仔细想想,我们所做的很多事情其实都是关于模式匹配的。RegEx在匹配模式和从找到的模式中提取值方面非常出色。那么,我们可以解决什么样的问题呢?
- URL,一个URL包含很多有趣的信息,如
hostname,route,port,route parameters和query parameters。我们希望能够提取这些信息,但也要验证其正确性。 - 密码,密码越长越好,这通常是我们想要的。还有其他方面,如复杂性。关于复杂性,我们的意思是我们的密码应该包含例如数字、特殊字符和更多。
- 查找和提取数据,例如,有能力在一个网页上查找数据,使用几个写得很好的正则表达式,可以变得非常容易。实际上,有一大类计算机程序专门用于此,称为屏幕刮擦器。
一个正则表达式是这样创建的。
/pattern/
它以/ 开始和结束。
或者像这样,我们从RegEx 类中创建一个对象。
new RegEx(/pattern/)
方法
有一些不同的方法用于不同类型的使用。学习使用正确的方法是很重要的。
exec(), 在一个字符串中执行搜索匹配。它返回一个信息数组或在不匹配时返回null。test(), 测试字符串中的匹配,用true或false回答。match(), 返回一个包含所有匹配信息的数组,包括捕获组,如果没有找到匹配信息,则返回null。matchAll(), 返回一个包含所有匹配的迭代器,包括捕获组。search(), 测试一个字符串中的匹配。它返回匹配的索引,如果搜索失败,则返回-1。replace(), 在一个字符串中执行一个匹配的搜索,并且用一个替换的子串来替换匹配的子串。split(), 使用正则表达式或固定的字符串将一个字符串分解成一个子串阵列。
让我们来展示一些给定的上述方法的例子。
test(), 测试字符串的真/假
让我们看一个使用test() 的例子。
/\w+/.test('abc123') // true
上面我们正在测试字符串abc123 是否包含所有字母字符\w+ ,我们正在回答这个问题,你是否包含字母字符。
match(), 查找匹配
我们来看看一个例子。
'orders/items'.match(/\w+/) // [ 'orders', groups: undefined, index: 0, input ]
上面的数组响应告诉我们,我们能够用我们的模式\w+ 匹配orders 。我们没有捕获任何组,如groups:undefined 所示,我们的匹配在index:0 。如果我们想匹配字符串中的所有字母,我们就需要使用一个标志g 。g 表示全局匹配,像这样。
'orders/items'.match(/\w+/g) // ['orders', 'items']
组
我们也有组的概念。要开始使用组,我们需要用小括号把我们的模式包起来,像这样。
const matchedGroup = 'orders/114'.match(/(?<order>\d+)/) // [114, 114, groups: { order: 114 }]
使用结构?<order> ,就可以创建一个所谓的命名组。
标志
有不同的标志。让我们列出其中一些。所有的标志都加在正则表达式的末尾。所以一个典型的用法是这样的。
var re = /pattern/flags;
g, 你想说的是你想匹配整个字符串,而不仅仅是第一次出现的字符串。i, 这意味着我们想要一个不区分大小写的匹配。
断言
有不同类型的断言。
- 边界,这是为了匹配一个词的开头和结尾的东西
- 其他断言,这里我们讨论的是向前看、向后看和条件断言。
让我们看一些例子。
/^test/.test('test123') // true
上面我们在测试字符串test123 ,是否以^ 这个词开始test 。
反过来看会是这样的。
/test$/.test('123test')
字符类
字符类是关于不同种类的字符,如字母和数字。让我们列出其中一些。
., 匹配任何单个字符,除了像\n或\r这样的行结束符。\d, 匹配数字,相当于[0-9]\D,这是对匹配数字的否定。因此,任何东西,而不是一个数字。相当于^[0-9]\w, 匹配任何字母字符,包括_。相当于[a-zA-Z0-9_]\W,是对上述内容的否定。匹配一个%,例如\s, 匹配白色空间字符\t, 匹配一个制表符\r, 匹配回车符\n, 匹配换行\,转义字符。它可以用来匹配一个/,像这样\/。也用于赋予字符特殊含义
定量词
量词是关于要匹配的字符的数量。
*, 0到多个字符+, 1到多个字符{n}, 匹配n个字符{n,}, 匹配>=n个字符{n,m}, 匹配 >= n && =< m个字符?, 非贪婪匹配
我们来看看一些例子
/\w*/.test('abc123') // true
/\w*/.test('') // true. * = 0 to many
在下一个例子中,我们使用? 。
/\/products\/?/.test('/products')
/\/products\/?/.test('/products/')
\/?上面我们可以看到,当我们使用这种类型的匹配时,? 的用法使得结尾的/ 可选。
DEMO
好了,这就是很多理论和一些例子的混合。接下来让我们看看一些实际的匹配,即我们在生产中实际使用的匹配。
如果你在后端使用JavaScript,你可能已经在使用Express、Koa或Nest.js等框架。你知道这些框架在路由匹配、参数等方面为你做了什么吗?那么,现在是时候找出答案了。
匹配一个路由
一个简单如/products 的路由,我们该如何匹配它?好吧,我们知道我们的URL应该包含那个部分,所以为它写一个RegEx是很简单的。我们还要考虑到,有些人会输入/products ,有些人则会输入/products/ 。
/\products\/?$/.test('/products')
上述RegEx满足了我们所有的需求,从用\/匹配/ 到用\/? 匹配最后的可选/ 。
提取/匹配路由参数
好的,让我们来看看一个类似的案例。/products/112.途径/products ,最后有一个数字。让我们开始看看传入的路由是否匹配。
/\/products\/\d+$/.test('/products/112') // true
/\/products\/\d+$/.test('/products/') // false
要提取路由参数,我们可以像这样输入。
const [, productId] = '/products/112'.match(/\/products\/(\d+)/)
// productId = 112
匹配/提取几个路由参数
好吧,假设你有一个路由看起来像这样/orders/113/items/55 。这大致是指订单的ID为113 ,订单项目的ID为55 。首先,我们要确保我们传入的URL是匹配的,所以让我们看看RegEx的内容。
/\orders\/\d+\/items\/\d+\/?/.test('/orders/99/items/22') // true
上面的RegEx读作如下,匹配/orders/[1-n digits]/items/[1-n digits][optional /]
现在我们知道我们能够匹配上述路线。接下来让我们抓取这些参数。我们可以使用命名的组来做。
var { groups: { orderId, itemId } } = '/orders/99/items/22'.match(/(?<orderId>\d+)\/items\/(?<itemId>\d+)\/?/)
// orderId = 99
// items = 22
上面的表达式通过创建命名的组orderId 和itemId ,分别用构造(?<orderId>\d+) 和(?<itemId>\d+) ,来引入组。这个模式与使用test() 方法的模式非常相似。
路线分类器
我相信你已经看到一个路由是如何被分成几个部分的,比如protocol,host,route,port 和query parameters 。
这是很容易做到的。让我们假设我们正在看一个看起来像这样的URLhttp://localhost:8000/products?page=1&pageSize=20 。我们想解析这个URL,最好能得到一些好的东西,就像这样。
{
protocol: 'http',
host: 'localhost',
route: '/products?page=1&pageSize=20',
port: 8000
}
我们如何达到这个目的呢?好吧,你正在看的东西遵循一个非常可预测的模式,而RegEx是模式匹配方面的神兵利器。让我们这样做吧 :)
var http = 'http://localhost:8000/products?page=1&pageSize=20'
.match(/(?<protocol>\w+):\/{2}(?<host>\w+):(?<port>\d+)(?<route>.*)/)
// http.groups = { protocol: 'http', host: 'localhost', port: 8000, route: '?page=1&pageSize=20' }
让我们把上面的内容分解一下。
(?<protocol>\w+):, 这匹配了n个以:结尾的字母字符。此外,它被放入命名的组protocol。\/{2}, 这只是说我们有//,通常在http://。(?<host>\w+):匹配 n 个以:结尾的字母字符,所以在这种情况下,它匹配localhost。此外,它还被放入命名组host。(?<port>\d+),这与主机后面的一些数字相匹配,这些数字就是端口。此外,它还被放入命名组port。(?<route>.*)最后,我们有一个路由匹配,它只是匹配任何字符,这将确保我们得到部分?page=1&pageSize=20。此外,它还被放入命名的组route。
为了解析出查询参数,我们只需要一个RegEx和一个对reduce() 的调用,就像这样。
const queryMatches = http.groups.route.match(/(\w+=\w+)/g) // ['page=1', 'pageSize=20']
const queryParams = queryMatches.reduce((acc, curr) => {
const [key, value] = curr.split('=')
arr[...arr, [key]: value ]
}, {}) // { page: 1, pageSize : 20 }
上面我们正在处理来自我们第一个模式匹配http.groups.route 的响应。我们现在正在构建一个模式,它将匹配以下[any alphabetic character]=[any alphabetic character] 。此外,由于我们有一个全局匹配g ,我们得到一个响应数组。这对应于我们所有的查询参数。最后,我们调用reduce() ,将数组变成一个对象。
密码的复杂性
密码复杂性的问题是,它有不同的标准,比如。
- 长度,它应该超过n个字符,也许少于m个字符
- 数字,应该包含一个数字
- 特殊字符,应该包含特殊字符
那么我们安全吗?嗯,更安全,别忘了2FA,在一个应用程序上,而不是你的电话号码。
让我们来看看这个的RegEx。
// checking for at least 1 number
var pwd = /\d+/.test('password1')
// checking for at least 8 characters
var pwdNCharacters = /\w{8,}/.test('password1')
// checking for at least one of &, ?, !, -
var specialCharacters = /&|\?|\!|\-+/.test('password1-')
正如你所看到的,我把每个要求作为自己的模式匹配来构建。你需要把你的密码通过每个匹配来确保它是有效的。
完美的约会
在我目前的工作中,我遇到的同事都认为他们的日期格式是我们其他人应该使用的。目前,这意味着我可怜的大脑必须处理。
// YY/MM/DD , European ISO standard
// DD/MM/YY , British
// MM/DD/YY, American, US
所以你可以想象,每次我收到一封有日期的邮件时,我都需要知道给我发邮件的人的国籍。这是很痛苦的:)。因此,让我们建立一个RegEx,这样我们就可以根据需要轻松地交换这个。
比方说,我们得到一个美国日期,像这样MM/DD/YY 。我们想提取重要的部分并交换日期,这样欧洲人/英国人就能理解了。我们还假设下面的输入是美国的。
var toBritish = '12/22/20'.replace(/(?<month>\d{2})\/(?<day>\d{2})\/(?<year>\d{2})/, '$2/$1/$3')
var toEuropeanISO = '12/22/20'.replace(/(?<month>\d{2})\/(?<day>\d{2})\/(?<year>\d{2})/, '$3/$1/$2')
上面我们就可以做到这一点。在我们给replace() 的第一个参数中,我们给了它我们的RegEx。我们的第二个参数是我们要如何交换它。对于英国日期,我们只需交换月和日,大家就会很高兴。对于欧洲日期,我们需要做得更多一些,因为我们希望它以年、月、日为开头。
电子邮件
好了,对于电子邮件,我们需要考虑几件事
@, 应该在中间的某个地方有一个@字符first name, 人们可以有很长的名字,有或没有破折号/hyphen。这意味着人们可以被称为:per、per-albin,等等。last name, 他们需要一个姓氏,或者电子邮件只是一个姓或一个名。domain,我们需要白名单几个域名,如.com,.gov,.edu
考虑到所有这些,我给你一个所有RegEx之母。
var isEmail = /^(\w+\-?\w+\.)*(\w+){1}@\w+\.(\w+\.)*(edu|gov|com)$/.test('per-albin.hansson@sweden.gov')
让我们把它分解一下,因为它很费字。
^,这意味着它的开头是。(\w+\-?\w+\.)*,这意味着一个词与我们没有-,因为我们有模式-?,并以一个.,所以per.,per-albin.。另外,我们以*结尾,所以这个词的数量为0。(\w+){1},这个意味着正好是一个词,如电子邮件只包括一个姓氏或只包括一个名字。这为1)+2)的组合打开了大门,所以per-albin.hansson或per.hansson,或者2)单独,这将是每一个或hansson。@, 我们需要匹配一个@字符\w+\., 这里我们要匹配一个以.结尾的名字,例如:sweden.(\w+\.)*, 在这里,我们正在为一些子域或没有子域开放,鉴于*,如sthlm.region。等等。(edu|gov|com), 域名,这里我们列出允许的域名是edu,gov或com$,需要以,这意味着我们确保有人不会在域名后面输入一些废话
摘要
你一路走到了这里。在RegEx这个话题上,我们真的涵盖了很多内容。希望你现在能更好地掌握它由哪些部分组成。此外,我希望真实世界的例子让你意识到,你可能根本不需要安装额外的节点模块。希望你通过一点实践,能感觉到RegEx是很有用的,它真的能让你的代码变得更短、更优雅,甚至可读。是的,我说的是可读性。一旦你掌握了事情是如何被评估的,RegEx是相当可读的。你会发现,你在这上面花的时间越多,就越有收获。不要再试图把它放逐到一个恶魔空间,给它一个机会吧 :)