你如何在JavaScript中学习足够多的RegEx以达到危险的程度

86 阅读11分钟

Twitter上关注我,很高兴接受你对主题或改进的建议/Chris

我把这篇文章写给未来的自己。事实上,我的很多文章都是写给未来的自己的,因为我忘记了如何做某件事情的一切。RegEx,正则表达式是我们工具箱中一个非常强大的工具。可悲的是,我们把它称为黑魔法、魔鬼和其他迷人的东西。它不一定是这样的。RegEx与普通的编程肯定是不同的,但它也是非常非常强大的东西。让我们来学习它是如何工作的,以及如何实际使用它,并将它应用于你所认识的日常问题。

TLDR; 这本书长吗?是的,但它确实经历了RegEx中的主要结构。此外,我还在最后提供了一些很好的配方,如RegEx处理电子邮件、密码、日期格式转换以及如何处理URL。如果你以前从未使用过RegEx,或者你努力想看清所有奇怪的魔法--这是为你准备的。阅读愉快 :)

参考资料

在RegEx方面有一些很好的资源,我经常查阅。花点时间阅读它们。有时他们会解释RegEx是如何被处理的,并能解释为什么会发生这样的魔法

如何练习

  • Node.js REPL,如果你安装了Node.js,我建议在终端键入node 。这将启动REPL,这是一个测试模式的好方法。

  • JavaScript REPL,这是一个VS Code扩展,可以评估你输入的内容。你会得到即时的结果反馈

  • 浏览器,在浏览器中拉出开发工具并使用控制台也能正常工作。

  • RegEx 101
    伟大的沙盒环境。谢谢你的提示 Lukasz :)

正则表达式

正则表达式或RegEx是关于模式匹配的。如果我们仔细想想,我们所做的很多事情其实都是关于模式匹配的。RegEx在匹配模式和从找到的模式中提取值方面非常出色。那么,我们可以解决什么样的问题呢?

  • URL,一个URL包含很多有趣的信息,如hostname,route,port,route parametersquery parameters 。我们希望能够提取这些信息,但也要验证其正确性。
  • 密码,密码越长越好,这通常是我们想要的。还有其他方面,如复杂性。关于复杂性,我们的意思是我们的密码应该包含例如数字、特殊字符和更多。
  • 查找和提取数据,例如,有能力在一个网页上查找数据,使用几个写得很好的正则表达式,可以变得非常容易。实际上,有一大类计算机程序专门用于此,称为屏幕刮擦器

一个正则表达式是这样创建的。

/pattern/

它以/ 开始和结束。

或者像这样,我们从RegEx 类中创建一个对象。

new RegEx(/pattern/)

方法

有一些不同的方法用于不同类型的使用。学习使用正确的方法是很重要的。

  • exec(), 在一个字符串中执行搜索匹配。它返回一个信息数组或在不匹配时返回null
  • test(), 测试字符串中的匹配,用truefalse回答。
  • 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 。如果我们想匹配字符串中的所有字母,我们就需要使用一个标志gg 表示全局匹配,像这样。

'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

上面的表达式通过创建命名的组orderIditemId ,分别用构造(?<orderId>\d+)(?<itemId>\d+) ,来引入组。这个模式与使用test() 方法的模式非常相似。

路线分类器

我相信你已经看到一个路由是如何被分成几个部分的,比如protocol,host,route,portquery 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。这意味着人们可以被称为:perper-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')

让我们把它分解一下,因为它很费字。

  1. ^,这意味着它的开头是。
  2. (\w+\-?\w+\.)*,这意味着一个词与我们没有- ,因为我们有模式-? ,并以一个. ,所以per.per-albin. 。另外,我们以* 结尾,所以这个词的数量为0。
  3. (\w+){1},这个意味着正好是一个词,如电子邮件只包括一个姓氏或只包括一个名字。这为1)+2)的组合打开了大门,所以per-albin.hanssonper.hansson ,或者2)单独,这将是每一个或hansson
  4. @, 我们需要匹配一个@ 字符
  5. \w+\., 这里我们要匹配一个以.结尾的名字,例如:sweden.
  6. (\w+\.)*, 在这里,我们正在为一些子域或没有子域开放,鉴于* ,如sthlm.region。等等。
  7. (edu|gov|com), 域名,这里我们列出允许的域名是edu,govcom
  8. $,需要以,这意味着我们确保有人不会在域名后面输入一些废话

摘要

你一路走到了这里。在RegEx这个话题上,我们真的涵盖了很多内容。希望你现在能更好地掌握它由哪些部分组成。此外,我希望真实世界的例子让你意识到,你可能根本不需要安装额外的节点模块。希望你通过一点实践,能感觉到RegEx是很有用的,它真的能让你的代码变得更短、更优雅,甚至可读。是的,我说的是可读性。一旦你掌握了事情是如何被评估的,RegEx是相当可读的。你会发现,你在这上面花的时间越多,就越有收获。不要再试图把它放逐到一个恶魔空间,给它一个机会吧 :)