正则表达式是编程语言中非常重要的一部分, 虽然至今都未被正式引入到C++中,╮(╯▽╰)╭。 因为绝大多数编程语言都内置了字符串类型,编程实践中对字符串的匹配和操作也非常频繁。 而正则表达式在多数情况下都会更加高效,语法也更为简洁。 本文借分析URL的场景,详述JavaScript中正则表达式的基本语法和常用函数。

正则表达式有非常多不同的实现, JavaScript的正则表达式基本符合最初贝尔实验室的规则, 同时从Perl语言引入了一些有用的扩展。 正则表达式最难以让人接受的一点在于太难阅读和调试,不允许空白字符和注释。 当然这也是它迷人的地方,正因如此而非常简洁和高效。 正如Vim一般,你需要学习很多东西才能上手使用,但这些努力绝对值得。

代码

在Web开发中正则表达式一点都不陌生,在表单验证时一定会用到。 甚至在AngularJS中,ng-pattern使用正则表达式增强了表单控件。 下面看一个正则表达式分析URL的例子:

var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
var url = "https://harttle.land:80/tags.html?simple=true#HTML",
    result = parse_url.exec(url);
    blanks = '       ';
    fields = ['url', 'scheme', 'slash', 'host', 'port', 'path', 'query', 'hash'];
fields.forEach(function(field, i){
    console.log(field + ':' + blanks.substr(field.length) + result[i]);
});

上述代码的输出:

url:    https://harttle.land:80/tags.html?simple=true#HTML
scheme: http
slash:  //
host:   harttle.land
port:   80
path:   tags.html
query:  single=true
hash:   HTML

基本语法

下文中所有...均为省略号,并不是正则表达式语法的一部分。

  1. JavaScript中用一对/定义正则表达式,var parse_url = /.../。 所以JavaScript正则表达式特殊的一点在于内容中的/需要转义为\/

  2. (...)用来创建一个捕获组(capturing group), 所有捕获组匹配的文本都会加入到结果数组中(regex.exec的结果,下标从1开始)。 所以匹配()需要转义:\(\)

  3. (?:...)用来创建一个非捕获组(non-capturing group), 仅仅是为了操作方便,匹配的文本不加入到结果数组中。

  4. [...]表示某个取值范围内的单个字符,比如[acd]匹配字母acd。 括号中的-是特殊字符,例如[a-e]相当于[abcde],当然要匹配特殊字符-需要转义: \-

  5. x{3,8}表示x出现3到8次,闭区间。

下面几节便详细解释URL各部分的正则表达式。

Scheme(协议)

^(?:([A-Za-z]+):)?匹配URL的协议。^表示行首,这意味着ahttp将不会被匹配。 接着是一个非捕获组,该组后面的问号表示该组可以出现1次或0次。

非捕获组的内容是一个捕获组加一个冒号:([A-Za-z]+):,它匹配的结果是http:。 捕获组的内容是[A-Za-z]+,后面的+表示至少出现一次,它匹配的结果是http, 这是第一个捕获组,该结果被存到fields[1]中。

fields[0]中存放的是整个正则表达式匹配到的结果字符串,即整个URL。

Slash(斜线)

(\/{0,3})匹配URL中协议后面的0到3个/,因为/是JavaScript正则表达式的定界符, 所以需要转义。匹配结果是//,它也是一个捕获组,//被存放到fields[2]中。

Host(主机名)

([0-9.\-A-Za-z]+)匹配主机名,它可以是数字,字母,点或横线。 匹配结果是harttle.land,被存放到fields[3]中。

Port(端口)

(::(\d+))?匹配端口,结果是:80。因为端口在URL中是可选的, 所以加?表示可以出现0或1次。我们需要捕获的是不包括冒号的端口数字, 所以:(\d+)被一个非捕获组括起来,\d+是被捕获的(\d表示单个数字)。 匹配结果是80,被存放到fields[4]中。

Path(路径)

(?:\/([^?#]*))?匹配路径,其结果是/tags.html。 外面还是一个可选的非捕获组,其内容为\/([^?#]*)/被转义为\/,后面的内容[^?#]*将被捕获,tags.html被存入fields[5]中。 [^...]表示反相匹配,即不是?#的任何其他字符, 因为?表示URL参数部分的起始,#表示页面锚点id的起始。

URL是有限字符集的,这样写是为了最大限度地容错。

Query(参数)

(?:\?([^#]*))?匹配GET方法的参数,结果是?simple=true。 还是一个可选的非获取组,其内容为\?([^#]*)?是正则表达式中的特殊字符,所以需要转义。 后面匹配的结果simple=true被存入fields[6]中。 [^#]表示#之外的任何字符。

Hash(锚点)

(?:#(.*))?$匹配锚点,结果为#HTML$表示行尾,即当前行所有内容都必须被前面的表达式所匹配。

前面是一个可选的非获取匹配,内容为#(.*).表示任何单个字符,即#开头的任何长度的字符串。 被获取的内容HTML存入了fields[7]中。

最佳实践

上文中的URL正则表达式略显复杂,在真实的实践中通常会切分为一系列的正则表达式单元。 为了实现正则表达式的拼接,我们需要定义一系列的字符串来初始化正则表达式。 这意味着需要做一些转义工作,费时费力事小,容易出错事大。例如上述Query参数:

var queryRegex1 = /(?:\?([^#]*))?/;
// 斜线都需要转义
var queryRegex2 = new RegExp("(?:\\?([^#]*))?");

好在JavaScript的RegExp提供了source属性,可以帮我们转义正则表达式:

var queryRegex = /(?:\?([^#]*))?/;
queryRegex.source === "(?:\\?([^#]*))?";
var urlRegex = new RegExp(`...${queryRegex.source}...`);

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/02/23/javascript-regular-expressions.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。