breakinferno / breakinferno.github.io

0 stars 0 forks source link

正则表达式学习 #12

Open breakinferno opened 6 years ago

breakinferno commented 6 years ago

1.Why

作为一个前端菜鸟,当初学习javascript的时候遇见正则表达式我是怀着这个玩意儿上网搜搜基本就可以满足我的需求了,所以只是简单的了解了其中的概念,并没有认真深入的对正则表达式进行研究。有疑问的时候也只是保持这个疑问,没有抽时间去解决这些疑问。但是在实习的过程中不论是导师要求这方面的知识,还是实际实习项目中遇见字符串匹配等问题都让我深感痛苦,果然不是不报,只是时候未到。所以我在嗑磕碰碰的过程中重新梳理了一下这方面的知识,当做是自己正儿八经需要学习的知识点。废话说这么多,下面我就我学习的经历和经验做一个总结。

2.What

说起这个东西,罕有人不知道这个玩意儿是什么。但是更多的人也只是对其一知半解(包括当前的我)。现在我就当自己是一个小白(虽然现在也只是个大白)来一步一步的学习。

先来看看MDN上关于正则的描述:

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、replace、search 和 split 方法

其实正则就是记录或者描述字符串的规则的代码,规定了符合该正则的字符串中字符组合的模式。在javascript中正则表达式是一个内置对象,这个对象叫做RegExp。跟其他对象一样(Array),我们可以有两种方式构造正则表达式:

1.对象字面量

/*
   /pattern/flags 
*/
const regex = /ab+c/;

const regex = /^[a-zA-Z]+[0-9]*\W?_$/gi;

2. 构造函数

/* 
    new RegExp(pattern [, flags])
*/

let regex = new RegExp("ab+c");

let regex = new RegExp(/^[a-zA-Z]+[0-9]*\W?_$/, "gi");

3.入门级

我们先构建一个字符串: Fat cat beat the cat with hat and eat fat bat at the heat mat。

  1. 最基本的匹配:直接使用对应的字符串即可,这也是我们在很多编辑器里查找字符常用的方式。比如我想匹配cat,则正则为/cat/。
  2. 元字符匹配:最基本的匹配不用多说,但是如果我想匹配eat这个单词呢?使用/eat/这正则表达式?不这样会将beat、heat中的eat字符串也匹配出来。我们此时要借助元字符\b了。那什么是元字符?元字符就是具有特殊意义的用于代表一类或者某些特殊含义的字符,是构造各种匹配复杂文本的正则表达式的基本字符,故名为元字符。正则之所以具有处理能力,正是元字符的作用

  下面是常见的元字符:^ $ . \b \s \w \d \W \S \D [] {} * + ?。

  全部的元字符看这里:你要找的所有元字符.

  讲道理,就我看来,只要你能够灵活的使用元字符,那么你就能够使用正则表达式处理大多数相关问题了。当然如果你想将正则使用的更加高效,能够处理更复杂的逻辑或者说实现一些比较cool的功能,那还是远远不够的。废话不多说,我们来看看这些元字符吧。

  元字符在我看来主要分为如下几类:

  1. 一类字符或者某个范围的字符

  比如\w 表示匹配字母、数字、下划线。等价于'[A-Za-z0-9_],这没什么好说的。但是要注意的是不匹配中文,如果你想匹配中文,请使用 [\u4e00-\u9fa5]。类似于这种表示一类字符的元字符常见的有如下几个:

代码 说明
\w 字符,匹配字母、数字、下划线。等价于'[A-Za-z0-9_]
\d 数字
[] 字符组中取一个[abc]表示a或b或c,而[ab|bc]表示abc|中一个,而非ab或bc。其中不在首位的-符号表示一个范围,比如[0-5]表示0-5之间的所有数字,而在首位表示 - 字符(当然可以转义)
\W 非字符
\D 非数字
. 匹配非换行符\n的所有字符(单个),但是凡事都有例外,如果我们指定其模式为单行匹配模式或者点号匹配模式,此时可以匹配换行符
a
f
可以被/(?s)a.f/匹配。(?s)指定(s指single line)模式为单行匹配模式

  *注意:[]中的特殊字符有五个:[]-\^,所以匹配这五个时需要转义,其他的都是普通字符,包括.?**

  2. 位置

    我们要匹配某个位置是必须用到这些元字符。常见的有下面几个:

代码 说明
^ 开始
& 结束
\b 单词边界,实际宽度不存在,所以算是零宽断言
\s 空格或者tab或者回车换行,即[\r\n\t\f\v ]
\t tab
\r 回车
\n 换行

    比如我们想要匹配构造的字符串中eat单词,而非beat或者heat的eat字符串,我们可以使用/\beat\b/来进行匹配

  3. 数目(限定符)

  如果我们要匹配100个a我们不可能使用/aaa...aa/(总共100个a)来匹配该字符串吧,于是表示重复或者说是数目的元字符就出来了。常见的限定符:

代码 说明
0或1个(非贪婪模式后面讲)
+ 一个或多个
* 零次或多次
{n} n次
{n,} 大于等于n次
{n,m} n次到m次

  4. 分支条件

    这个就比较简单了,分支条件使用| 将两个不同的规则分隔开,只要满足其中一个就当成匹配。比如/a|b/匹配a或者b.

  5. 反义

    上面的\S \W \D 就是对 \s \w \d的反义,其中值得注意的是[]中^表示反义,即[^abc]表示匹配非abc中的任何一个字符。这里也要注意^符号必须在最前面才是元字符,表示不能匹配的意思。否则就是一个普通字符

  6. 转义

    我们有那么多的特殊字符,比如? + . 等,如果我想仅仅只是匹配这些字符怎么办。此时必须对字符进行转义,说明此处匹配的是一个普通字符,而非元字符。比如使用/*/匹配字符。当然如果想匹配/本身则需要使用/\/来进行匹配。

  7. 分组

    我们已经提到了怎么重复单个字符(直接在字符后面加上限定符就行了);但如果想要重复多个字符又该怎么办?你可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了,你也可以对子表达式进行其它一些操作(后面会有介绍)。比如a(bc)d中bc就是一个分组,而且分组可以嵌套,比如a(b(cd)e),共有两个分组bcde和cd。这里我们使用实例来解释这个概念:

    const str = 'Fat cat beat the cat with hat and eat fat bat at the heat mat。';
    // 这个结果和直接使用/cat/g效果一样都是直接匹配cat字符串
    const reg1 = /(cat)/g;
    // 但是,但是分组最重要的作用就是能够在后续进行访问该分组,也就是说改分组如果匹配了就像是被捕获了,我们可以直接对其访问,比如我想匹配两个cat之间的内容:
    const reg2 = /(cat)(.*)\1/g  // 匹配cat beat the cat
    // 我们做的最多的还是使用量词来匹配多个相同的组
    const reg3 = /(cat)*/ // 匹配多个连续的cat,比如catcatcat
    // 就如第二个实例所示,如果没有做任何修改,每个组都有一个编号,默认是从0开始,但是编号0的分组代表字符串本身。所以自己写的分组第一个编号为1,我们可以使用\1来访问该组.我们可以使用(?<name>exp)的形式来重新命名该编号,使用\k<name>来访问。
    const reg4 = /(?<WTF>cat)(.*)\k<WTF>/g
    // 最后有捕获,就有不捕获,不捕获分组当然是为了提高效率了。上面说到()作为子模式可以得到它里面的数据,但是,有些时候,()只是作为数据分界功能,并不需要取出来,这时候就要用到非捕获组的概念了。比如:(http|ftp|svn)://([^/]+)只想得到域名,也就是[2],那么(http|ftp|svn)就只是数据分界的功能,这里不需要捕获,因此使用非捕获组功能:(?:exp)
    const reg5 = /(?:cat).*?(at).*?\1/g // 由于cat并没有被捕获,所以第一个分组就是at.匹配结果为:cat beat the cat

  8. 模式修饰符

    这个不用多说,我们上面在正则后面加的g就属于模式修饰符的一种。这里将模式修饰符列一下:

i:字符大小写不敏感 ab 和 aB Ab AB是一样的
m: 多行匹配 意味着^ $匹配每行开头和结尾
s: 单行匹配(?s)
g: 全局匹配,意味着在第一次匹配成功后不会立即结束,而是从当前位置开始匹配其后的字符串,知道所有字符都匹配完全

4.提高

  1. 几个概念

    1. 正则里的数据匹配都是单个字符匹配的。我们以为/^cat/表示匹配以cat开头的字符串,但是实际意思是匹配以c开头的后面紧跟at的字符串,虽然结果相同,但是这个理解是不一样的(我们不一样)。     2. 贪婪模式和非贪婪模式。从名字就可以知道,贪婪模式下,正则能匹配多少就匹配多少,而贪婪模式下,能匹配多少就匹配多少(666)。区分贪婪模式和非贪婪模式在于量词后面是否有?字符,默认是贪婪模式,所以没有。举个栗子:


const str = 10001
// 这是贪婪模式,结果为1000和1
const tl = /10*/g
// 这是非贪婪模式,结果为1和1
const ftl = /10*?/g
// 来解释一下下面这个匹配的过程 两个都是匹配10001
// 首先匹配1,然后尽可能给你的匹配多个0,直到发现1不是0结束0的匹配,然后看1是否是1,是则匹配成功,否则匹配失败
const tl1 = /10*1/g
// 非贪婪模式下,同样的首先匹配1,然后就不想匹配了,看后面是否是1,是1结束匹配,不是1则尝试匹配1个0,然后再看后面是否是1,如此往复。
const ftl1 = /10*?1/g

    3.正则匹配的过程

  2. 零宽断言(环视)

    就是先从全局环顾一遍正则,(然后断定结果,)再做进一步匹配处理。环视的作用相当于对其所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。其实我们已经接触过了,^ $ \b都是零宽断言,他们并没有任何实际内容,但是也确定某种限制条件(断言)。

    const str = 'cat mat hat.';
    // 这个匹配情况是这样的:|cat| |mat| |hat|.(|表示匹配的位置)
    const reg = /\b/g
    // 下面给出几个用法
    (?<=exp) 匹配前面是exp的数据 
    (?<!exp) 匹配前面不是exp的数据 
    (?=exp) 匹配后面是exp的数据 
    (?!exp) 匹配后面不是exp的数据
    // 栗子
    (?<=B)AAA 匹配前面是B的数据,即BAAA匹配,而CAAA不匹配 
    (?<!B)AAA 匹配前面不是B的数据,即CAAA匹配,而BAAA不匹配 
    AAA(?=B) 匹配后面是B的数据,即AAAB匹配,而AAAC不匹配 
    AAA(?!B) 匹配后面不是B的数据,即AAAC能匹配,而AAAB不能匹配
    // 总结:环视表达式中,<表示前置,=和!表示是否是该字符
    // 给点实际的栗子
    // 我想匹配前面不是3,4,5的数字
    const rt = /(?![34])[0-9]/g     // 这表示从0-9中排除3,4
    // 可以看出环视用来做排除处理十分实用
    // 举个栗子,下面的正则表示匹配不全是数字或不全是字符的6-16个字符数字的组合
    const reg = (?!^[a-z]+$)(?!^[0-9]+$)^[a-z0-9]{6,16}$

    环视不占宽度,所以又叫零宽断言,这意味着:1. 其匹配结果不会加入数据结果 2. 环视匹配过的地方,下次还可以继续使用其来进行匹配,这一点看下面这个图。

  3. 优先级

    有这么多元字符,所以我们必须知道优先级才能够正确的理解正则表达式,下面优先级从高到低,从左到右运算。当然我们也可以使用()来提高优先级:

代码 说明
\ 转义符
(),(?:),(?=),[] 圆括号和方括号
*,+,?,{n},{n,},{n,m} 量词
^,$, 任何字符
|

  4. js中的方法

    给定义的时候就提了js中相关的方法。这里就简单的说一下吧。


// 1.reg.test(str).大名鼎鼎的test,常和if连用,来判断字符串是否符合该正则
var str = 'abc';
var reg = /\w+/;
console.log(reg.test(str));  //true
// 2.reg.exec(str) 用来捕获符合规则的字符串。匹配成功返回一个带有index和input属性的数组,失败null
// 值得注意的是如果正则模式修饰符是g,则可以多次调用exec来匹配所有的可匹配字符串
var myRe = /ab*/g;
var str = 'abbcdefabh';
var myArray;
while ((myArray = myRe.exec(str)) !== null) {
  var msg = 'Found ' + myArray[0] + '. ';
  msg += 'Next match starts at ' + myRe.lastIndex;
  console.log(msg);
}
// 结果
// Found abb. Next match starts at 3
// Found ab. Next match starts at 9
// 3.str.match(reg) 全局的时候如果匹配成功,就返回匹配成功的数组,如果匹配不成功,就返回null。值得注意的是如果模式不是全局的,返回和exec()相同的结果,返回一个带有index和input属性的数组。

var str = 'For more information, see Chapter 3.4.5.1';
var re = /see (chapter \d+(\.\d)*)/i;
var found = str.match(re);

console.log(found);

// logs [ 'see Chapter 3.4.5.1',
//        'Chapter 3.4.5.1',
//        '.1',
//        index: 22,
//        input: 'For more information, see Chapter 3.4.5.1' ]

// 'see Chapter 3.4.5.1' 是整个匹配。
// 'Chapter 3.4.5.1' 被'(chapter \d+(\.\d)*)'捕获。
// '.1' 是被'(\.\d)'捕获的最后一个值。
// 'index' 属性(22) 是整个匹配从零开始的索引。
// 'input' 属性是被解析的原始字符串。

// 4.str.replace(reg,replacement) 使用replacement替换正则,其中replacement可以使字符串,特殊变量,函数
// 栗子:交换单词
var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
// Smith, John
console.log(newstr);
// 栗子: 函数
function styleHyphenFormat(propertyName) {
  function upperToHyphenLower(match) {
    return '-' + match.toLowerCase();
  }
  return propertyName.replace(/[A-Z]/g, upperToHyphenLower);
}

更多的查看MDN吧

// 5.str.split(reg) 按指定的正则拆分字符串
var myString = "Hello 1 word. Sentence number 2.";
var splits = myString.split(/\d/);
console.log(splits); // ["Hello ", " word. Sentence number ", "."]
// 注意如果有捕获括号,则结果也在返回数组中
var myString = "Hello 1 word. Sentence number 2.";
var splits = myString.split(/(\d)/);

console.log(splits); //[ "Hello ", "1", " word. Sentence number ", "2", "." ]
// 6.str.search(reg) 返回正则开始的位置,否则-1,跟indexOf差不多,一般来说实现的目的和includes一样都是判断是否存在

5.工具/参考

  一个极好的测试网站   一个类似的测试网站   有图的网站   有图的网站2   来做几道题吧   再来闯闯关吧

6.6666的正则


1. 校验密码强度
^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$

2.提取url中属性
function getParamName(attr) { 
    let match = RegExp(`[?&]${attr}=([^&]*)`) //分组运算符是为了把结果存到exec函数返回的结果里
    .exec(window.location.search) //["?name=jawil", "jawil", index: 0, input:
    "?name=jawil&age=23"] return match && decodeURIComponent(match[1].replace(/\+/g, ' ')) // url中+号表示空格,要替换掉 } 
    console.log(getParamName('name')) 

3.数字格式化,1234567890 转为 1,234,567,890
let test1 = '1234567890' 
let format = test1.replace(/\B(?=(\d{3})+(?!\d))/g, ',')

4.判断一个数是否是质数
function isPrime(num) { 
    return !/^1?$|^(11+?)\1+$/.test(Array(num+1).join('1'))
} 
console.log(isPrime(19)) // true

5.字符串去重
var str_arr = ["a", "b", "c", "a", "b", "c"]
function unique(arr) {
    return arr.sort().join(",,").                       //a,,a,,b,,b,,c,,c(两个,,是为了首尾各一个,)
    replace(/(,|^)([^,]+)(,,\2)+(,|$)/g, "$1$2$4").     //a,,b,,c(最重要的是(,,\2)+将所有重复的都匹配了)
    replace(/,,+/g, ",").
    replace(/,$/, "").
    split(",")
}
console.log(unique(str_arr)) // ["a","b","c"]

6.常见的校验E-mail
[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?

7.校验身份证
15位: ^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$
18位: ^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$

8.校验日期
^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$

9.校验手机号
^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$

10.文件路径和扩展名,以txt为例
^([a-zA-Z]\\:|\\\\)\\\\([^\\\\]+\\\\)*[^\\/:*?"<>|]+\\.txt(l)?$

11.提取html中超链接
(<a\\s*(?!.*\\brel=)[^>]*)(href="https?:\\/\\/)((?!(?:(?:www\\.)?'.implode('|(?:www\\.)?', $follow_list).'))[^"]+)"((?!.*\\brel=)[^>]*)(?:[^>]*)>

12.提取网页图片
\\< *[img][^\\\\>]*[src] *= *[\\"\\']{0,1}([^\\"\\'\\ >]*)