用正则表达式分析 URL
正则表达式是编程语言中非常重要的一部分, 虽然至今都未被正式引入到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
基本语法
下文中所有
...均为省略号,并不是正则表达式语法的一部分。
-
JavaScript中用一对
/定义正则表达式,var parse_url = /.../。 所以JavaScript正则表达式特殊的一点在于内容中的/需要转义为\/。 -
(...)用来创建一个捕获组(capturing group), 所有捕获组匹配的文本都会加入到结果数组中(regex.exec的结果,下标从1开始)。 所以匹配()需要转义:\(,\)。 -
(?:...)用来创建一个非捕获组(non-capturing group), 仅仅是为了操作方便,匹配的文本不加入到结果数组中。 -
[...]表示某个取值范围内的单个字符,比如[acd]匹配字母a或c或d。 括号中的-是特殊字符,例如[a-e]相当于[abcde],当然要匹配特殊字符-需要转义:\-。 -
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。如有疏漏、谬误、侵权请通过评论或 邮件 指出。