从面试到正则

前言

二面的时候正好考了个正则,对于正则我之前学习的时候是基本上原理+实践都会的。但是后来项目开发的时候很少写,而且一般不用的技术你往往会忘掉一些细节。所以在面试的时候让我写邮箱匹配的时候,我只能写出一个大概的套路。不过这也正好给我提了个醒。需要将正则再次熟悉达到给一个匹配能够随手写的程度。

再来谈谈基础

其实正则还是蛮简单的,因为规则就那么多,你记得住你就会,记不住只能查询QAQ,以下是我以前笔记的内容,网上正则基础一抓一大把。相关书籍也不少,这里就不仔细介绍了。

RegExp类型

var exp = /pattern/flags

flags

  • g:全局模式,应用于所有字符串,不会匹配到第一个就停,一直匹配到结束
  • i:不区分大小写模式
  • m:多行模式,到达一行文本末尾还需继续查找下一行

元字符: ( [ { \ ^ $ | ) ? * + . ] }

都需要转义匹配

特殊字符

\

  • 非特殊字符之前的反斜杠表示下一个字符是特殊
  • 模式 /a/ 代表会匹配 0 个或者多个 a。相反,模式 /a*/ 将 ‘’ 的特殊性移除,从而可以匹配像 “a*” 这样的字符串

^

  • 匹配输入的开始
  • /^A/ 并不会匹配 “an A” 中的 ‘A’,但是会匹配 “An E” 中的 ‘A’

$

  • 匹配输入的结束
  • /t$/ 并不会匹配 “eater” 中的 ‘t’,但是会匹配 “eat” 中的 ‘t’

*

  • 匹配前一个表达式0次或多次。等价于 {0,}。

+

  • 匹配前面一个表达式1次或者多次。等价于 {1,}。
  • 例如,/a+/匹配了在 “candy” 中的 ‘a’,和在 “caaaaaaandy” 中所有的 ‘a’。

?

  • 匹配前面一个表达式0次或者1次。等价于 {0,1}。
  • 例如,/e?le?/ 匹配 “angel” 中的 ‘el’,和 “angle” 中的 ‘le’ 以及”oslo’ 中的’l’。
  • 如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪的(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。
  • 对 “123abc” 应用 /\d+/ 将会返回 “123”,如果使用 /\d+?/,那么就只会匹配到 “1”。

.

  • 小数点 匹配除换行符之外的任何单个字符

(x)

  • 匹配 ‘x’ 并且记住匹配项,就像下面的例子展示的那样。括号被称为 捕获括号
  • 模式 /(foo) (bar) \1 \2/ 中的 ‘(foo)’ 和 ‘(bar)’ 匹配并记住字符串 “foo bar foo bar” 中前两个单词。模式中的 \1 和 \2 匹配字符串的后两个单词。注意 \1、\2、\n 是用在正则表达式的匹配环节。在正则表达式的替换环节,则要使用像 $1、$2、$n 这样的语法,例如,’bar foo’.replace( /(…) (…)/, ‘$2 $1’ )。

{n}

  • n是一个正整数,匹配了前面一个字符刚好发生了n次。
  • 比如,/a{2}/不会匹配“candy”中的’a’,但是会匹配“caandy”中所有的a,以及“caaandy”中的前两个’a’。

{n,m}

  • n 和 m 都是正整数。匹配前面的字符至少n次,最多m次。如果 n 或者 m 的值是0, 这个值被忽略。
  • 例如,/a{1, 3}/ 并不匹配“cndy”中得任意字符,匹配“candy”中得a,匹配“caandy”中得前两个a,也匹配“caaaaaaandy”中得前三个a。注意,当匹配”caaaaaaandy“时,匹配的值是“aaa”,即使原始的字符串中有更多的a。

(?:x)

  • 匹配 ‘x’ 但是不记住匹配项。这种叫作非捕获括号,使得你能够定义为与正则表达式运算符一起使用的子表达式。
    来看示例表达式 /(?:foo){1,2}/。
    如果表达式是 /foo{1,2}/,{1,2}将只对 ‘foo’ 的最后一个字符 ’o‘ 生效。
    如果使用非捕获括号,则{1,2}会匹配整个 ‘foo’ 单词。

x(?=y)

  • 匹配’x’仅仅当’x’后面跟着’y’.这种叫做正向肯定查找。
  • 例如,/Jack(?=Sprat)/会匹配到’Jack’仅仅当它后面跟着’Sprat’。/Jack(?=Sprat Frost)/匹配‘Jack’仅仅当它后面跟着’Sprat’或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。

x(?!y)

  • 匹配’x’仅仅当’x’后面不跟着’y’,这个叫做正向否定查找。
  • /\d+(?!.)/匹配一个数字仅仅当这个数字后面没有跟小数点的时候。正则表达式/\d+(?!.)/.exec(“3.141”)匹配‘141’但是不是‘3.141’

x|y

  • 匹配‘x’或者‘y’。

[xyz]

  • 一个字符集合。匹配方括号的中任意字符,包括转义序列。你可以使用破折号(-)来指定一个字符范围。对于点(.)和星号(*)这样的特殊符号在一个字符集中没有特殊的意义。他们不必进行转义,不过转义也是起作用的
  • [abcd] 和[a-d]是一样的。他们都匹配”brisket”中得‘b’,也都匹配“city”中的‘c’。/[a-z.]+/ 和/[\w.]+/都匹配“test.i.ng”中得所有字符

[^xyz]

  • 一个反向字符集。也就是说, 它匹配任何没有包含在方括号中的字符。你可以使用破折号(-)来指定一个字符范围。任何普通字符在这里都是起作用的
  • [^abc] 和 [^a-c] 是一样的。他们匹配”brisket”中得‘r’,也匹配“chop”中的‘h’。

\b

  • 匹配一个词的边界。一个词的边界就是一个词不被另外一个词跟随的位置或者不是另一个词汇字符前边的位置。注意,一个匹配的词的边界并不包含在匹配的内容中。换句话说,一个匹配的词的边界的内容的长度是0(不要和[\b]混淆了)
  • 例子: /\bm/匹配“moon”中得‘m’; 
/oo\b/并不匹配”moon”中得’oo’,因为’oo’被一个词汇字符’n’紧跟着。 
/oon\b/匹配”moon”中得’oon’,因为’oon’是这个字符串的结束部分。这样他没有被一个词汇字符紧跟着。

[\b]

  • 匹配一个退格(U+0008)。(不要和\b混淆了。)

\B

  • 匹配一个非单词边界。他匹配一个前后字符都是相同类型的位置:都是单词或者都不是单词。一个字符串的开始和结尾都被认为是非单词。
  • 例如,/\B../匹配”noonday”中得’oo’, 而/y\B./匹配”possibly yesterday”中得’ye‘

\d

  • 匹配一个数字。 等价于[0-9]。
  • /\d/或者/[0-9]/匹配”B2 is the suite number.”中的’2’

\D

  • 匹配一个非数字字符。 等价于[^0-9]。
  • /\D/或者/[^0-9]/匹配”B2 is the suite number.”中的’B’

\n

  • 匹配一个换行符 (U+000A)

\r

  • 匹配一个回车符 (U+000D)

\s

  • 匹配一个空白字符,包括空格、制表符、换页符和换行符。

等价于[\f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。
  • /\s\w*/匹配”foo bar.”中的’ bar’

\S

  • 匹配一个非空白字符 

等价于[^\f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
  • /\S\w*/匹配”foo bar.”中的’foo’

\w

  • 匹配一个单字字符(字母、数字或者下划线)等价于[A-Za-z0-9_]
  • 例如,/\w/匹配 “apple,” 中的 ‘a’,”$5.28,”中的 ‘5’ 和 “3D.” 中的 ‘3’。

\W

  • 匹配一个非单字字符

等价于[^A-Za-z0-9_]
  • /\W/或者/[^A-Za-z0-9_]/匹配 “50%.” 中的 ‘%’

方法

exec

  • 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回null)

test

  • 一个在字符串中测试是否匹配的RegExp方法,它返回true或false。

match

  • 一个在字符串中执行查找匹配的String方法,它返回一个数组或者在未匹配到时返回null
  • 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1

replace

  • 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串
  • var re = /(\w+)\s(\w+)/; 
var str = “John Smith”; 
var newstr = str.replace(re, “$2, $1”); 
console.log(newstr);

split

  • 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的String方法

我们来举一箱栗子

正则的学习更多的是通过分析结构然后写匹配。因此我们需要大量的例子来分析学习。当然前提是我们先不看别人的代码,而是先想想这个思路该怎么写。

匹配 {hello Linshui Zhaoying2333}

这里的需求是之前群讨论里有人放出的题,大意是不用split,join等等来写个函数最后将字符串变为 Linshui Zhaoying2333 hello

这里很理所当然的是用正则来实现了。

这里什么都不需要考虑,考虑分割空白字符即可


 var regex = /^(\w+)\s(\w+)\s(\w+)/gi
 
 var s = 'hello Linshui Zhaoying2333'
 
 var news = s.replace(regex,'$2 $3 $1')	
 console.log(news) 
 //Linshui Zhaoying2333 hello 

手机号码匹配

这个是最常见的,但是我觉得我们可以再较真一点。

因为我们需要的是按照规则来匹配,那么我们需要一开始确保规则是正确的。

比如我们先看下手机号码组成:


前3位:网络识别号,如134-139、158、159属中国移动;130-132、156属中国联通。

第4~7位(中四位):地区编码,如0252代表湖南省长沙市。 

第8~11位(后四位):用户号码,从0000至9999,每段1万户

看来我们只需要前三位正确匹配就好了,所以我先去下载了一份国家的手机号码格式

imgn

然后我们找下规律

第一位肯定是1

第二位可以是 3,5,4,8,7

第三位可以是 任意数字

那么只要保证输入手机号符合前面三个的规律加上保证11位就是我们需要的正则规律了。

regex = /^1[3|5|4|8|7]\d{9}/gi

然后我们看看网上的手机正则:


/^1[34578]\d{9}$/

我看可以看到最后多了个$

从上面的基础资料可以知道$匹配输入的结束,不加$的后果是如果你输入十二位(前11位正常),然后你用

if(!(/^1[34578]\d{9}/.test(phone))){console.log('error')}else{console.log('ok')}

它会输出ok

也就是没有匹配位数。因此如果有位数限制一定记得加在最后加$.

匹配中文

对于中文匹配我们先收集网上一些正则例子


匹配中文:[\u4e00-\u9fa5] 

匹配双字节字符(包括汉字在内):[^x00-xff] 

正则匹配中文汉字根据页面编码不同而略有区别:
GBK/GB2312编码:[x80-xff>]+ 或 [xa1-xff]+
UTF-8编码:[x{4e00}-x{9fa5}]+/u

2E80~33FFh:中日韩符号区。收容康熙字典部首、中日韩辅助部首、注音符号、日本假名、韩文音符,中日韩的符号、标点、带圈或带括符文数字、月份,以及日本的假名组合、单位、年号、月份、日期、时间等。
3400~4DFFh:中日韩认同表意文字扩充A区,总计收容6,582个中日韩汉字。
4E00~9FFFh:中日韩认同表意文字区,总计收容20,902个中日韩汉字。

可以看到有两种说法,为了保险起见我们可以适当用比较大的范围来包含[\u4e00-\u9fff]

因此我们可以这么写


  var regex = /([\u4e00-\u9fff])/gi
  var s = 'hello 啊Linshui哈 Zhaoying2333'
  var news = s.match(regex)	
  console.log(news.join('')) // 啊哈

匹配邮箱

当初我理解的邮箱是这样的任意字符串@任意字符串[.com .cn .cn.com]然后写到后面突然发现邮箱后缀其实层出不穷啊…因此我当时理解的规则肯定是不对的。

用户名@主机名(域名)

关于这个错误我没有好的思路,去找了一下Google的错误提示


您的用户名或电子邮件地址中包含多余的空格。
最常见的原因是您尝试使用自己的姓名(而非用户名/电子邮件地址)登录。例如,使用“John Smith”(而非johnsmith332)登录。
包含多个@符号。
包含无效的字符,如括号、冒号、分号等

这样我们可以分开来看

首先@有且只能有一个,用户名必须没有括号,冒号,分号这些奇怪的,当然去除奇怪的字符不符合我们正常的思维逻辑,我们应该是需要白名单,去看了下网易邮箱注册格式: 6~18个字符,可使用字母、数字、下划线,需以字母开头

基本就是这个了。而且手动测试了一下,其实也不允许_作为结尾。而且我们常用的也有数字开头的,因此字母开头这条去掉。

之后要考虑那么多域名不现实,我们需要看看域名的命名规范:


域名由各国文字的特定字符集、英文字母、数字及“ - ” ( 即连字符或减号 ) 任意组合而成,但开头及结尾均不能含有“ - ”。域名中字母不分大小写

所以对域名我们可以得到以下正则:

^[a-z0-9]*[-]?[a-z0-9]*$/gi

然后需要加上.com .cc .me .com.cn这类匹配

因此我们可以改成下面这样

^[a-z0-9]+[-]?[a-z0-9]+[.][a-z]+([.][a-z]+)?$

这里必须好好思考为什么是+而不是*

最后加?是为了匹配只有一个域名后缀。

然后我们可以加上前面的用户名规则

^[a-z0-9]*([_]?[a-z0-9]+)@[a-z0-9]+[-]?[a-z0-9]+[.][a-z]+([.][a-z]+)?$/i

这样我们的邮箱正则就出来了,它可以匹配用户名只有1个比如a@qq.com

我们再来看看网上是怎么写的:


^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$

这份还验证了用户名可以加.的的情况

当然还有Stack Overflow最高票的答案


/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

处理不同细节大家都有不同规则,不过按道理我这份已经够用了~需要再细化自己加新的也方便

写个函数来测试


	var regex = /^[a-z0-9]*([_]?[a-z0-9]+)@[a-z0-9]+[-]?[a-z0-9]+[.][a-z]+([.][a-z]+)?$/i

  var array = ["4799109@qq.com","a@qq.com.cn","asd_@qq.com","ccc@mail."]

  for (var i = array.length - 1; i >= 0; i--) {
  	console.log(array[i] + "  " + (regex.test(array[i]) == true? "验证通过" : "验证失败"))
  }
  
log:
ccc@mail.  验证失败
asd_@qq.com  验证失败
a@qq.com.cn  验证通过
4799109@qq.com  验证通过

匹配URL

这个匹配URL我之前也写过,当时是为了配对&=参数,将URL参数解析成字典

这里直接看代码


function getQueryObject(url) {
    url = url == null ? window.location.href : url;
    var search = url.substring(url.lastIndexOf("?") + 1);
    var obj = {};
    var reg = /([^?&=]+)=([^?&=]*)/g;
    search.replace(reg, function (rs, $1, $2) {
        var name = decodeURIComponent($1);
        var val = decodeURIComponent($2);                
        val = String(val);
        obj[name] = val;
        return rs;
    });
    return obj;
}

结尾

正则表达式复习到这里,其实更多的是你要在写正则之前考虑好你所要匹配的规则,把规则列出来,再拆开一个个匹配,基本都是能写出来的。

Table of Contents