用正则表达式分析 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。如有疏漏、谬误、侵权请通过评论或 邮件 指出。