Alfr3d

BlogAboutShort

[Regex]手把手教你解析一个URL字符串

avatar
Alfxjx

需求

实现对网页地址链接的匹配; 一些典型的网页链接如下:

如上所示,需要匹配下面的 6 个:

协议

协议头一般有 http、https、ftp 等这里写一下匹配的方式:

let protocol = /(?:ht|f)tp(?:s)?(?=:\/\/)/;
protocol.exec('http://1234');
// ["http", index: 0, input: "http://1234", groups: undefined]

解释:

  • 使用非捕获组/(?:ht|f)/,只对值判断,但是不将结果,也就是(ht|p)看作是一个捕获组;
  • 使用正向零宽断言(?=:\/\/),匹配后面的://,只有有此符号才算匹配。

    正则表达式之捕获组和非捕获组

域名

对于域名,一般来说有两种情况,一个事数字的 ip 域名,一种是字符域名。

IP 地址的长度为 32 位(共有 2^32 个 IP 地址),分为 4 段,每段 8 位 用十进制数字表示,每段数字范围为 0 ~ 255,段与段之间用句点隔开。 https://www.jianshu.com/p/82886d77440c

  1. 对于 ip 域名:

    1. 数字在 0~255 之间,/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/
    2. 总共 4 段: /^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:(?:\.)(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3,3}$/
    3. 解释一下,上面的匹配数字的方法避免了 001 这样的匹配,然后匹配四段后面的{3,3}则是保证只能是 4 段。

      DNS规定,域名中的标号都由英文字母和数字组成,每一个标号不超过 63 个字符,也不区分大小写字母。标号中除连字符(-)外不能使用其他的标点符号。级别最低的域名写在最左边,而级别最高的域名写在最右边。由多个标号组成的完整域名总共不超过 255 个字符。 https://developer.aliyun.com/article/297853

  2. 对于字符的域名:

    1. 按照规则可以写出一个域名: 'test-12.admin.abandon.work'
    2. 匹配它: /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/
  3. 将两中匹配的规则合并起来:

    1. 首先匹配 ip,然后再匹配域名
    2. 将二者合并起来
    3. 参考: Regular expression to match DNS hostname or IP Address?
    4. /^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:(?:\.)(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3,3}$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/
/^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:(?:\.)(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3,3}$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/.exec(
  'www.abandon.work'
);
// ["www.abandon.work", undefined, "abandon.", "abandon", "work", index: 0, input: "www.abandon.work", groups: undefined]

在线演示请看这里 这里还可以对捕获组进行一点优化,聪明的小伙伴你看看是怎么搞的。

端口号

其实最困难的部分已经过去啦,下面对端口号的匹配就很简单了,匹配的是冒号后面的数字。

/(?<=\:)[0-9]+$/.exec('http://www.abandon.wor:8080');
// ["8080", index: 23, input: "http://www.abandon.wor:8080", groups: undefined]
  • 需要注意的是这里使用了一个正向后行断言,用来判断端口号前面的冒号,这样匹配的捕获组里面就没有冒号啦。
  • 当然 不是所有的域名都会把端口号暴露在外面,为了保护自己的底裤,很多的页面是没有端口号显示的,因此在最后拼接正则表达式的时候需要注意这一点。

路由

对于路由,如法炮制,注意路由是由一个/开始的,并且这个斜杠的后面一个字符一定不是/:

/(\/[0-9a-z#.]+)+|(\/)/.exec('abandon.work/');

// ["/", undefined, "/", index: 12, input: "abandon.work/", groups: undefined]

/(\/[0-9a-z#.]+)+|(\/)/.exec('abandon.work/admin/#/articles/id');

// ["/admin/#/articles/id", "/id", index: 12, input: "abandon.work/admin/#/articles/id", groups: undefined]
  • 这里我认为还有优化的空间,可以把每一个都匹配出来才是最方便的。

query 参数

query 参数从?开始,中间是&链接,键值对保持 key=value 形态。 上代码:

/(\?[0-9a-z&=]+)/.exec('abandon.work/api?tab=10&date=10-11');

// ["?tab=10&date=10", "?tab=10&date=10", index: 16, input: "abandon.work/api?tab=10&date=10-11", groups: undefined]

要是可以直接匹配出键值对就好了。或者匹配出 key=value 的形式。

var url = 'name=ooo&age=10';
var reg = /([^&=]+)=?([^&]*)/g;
// 每执行一次就吐出一堆key&value
reg.exec(url)
["name=ooo", "name", "ooo", index: 0, input: "name=ooo&age=10", groups: undefined]
reg.exec(url)
["age=10", "age", "10", index: 9, input: "name=ooo&age=10", groups: undefined]

根据上面的代码可以写出一个很常见的查询参数的方法

const getParams = (url, name) => {
  let reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
  let r = url.match(reg);
  if (r != null) {
    return decodeURIComponent(r[2]);
  }
  return null;
};
let url =
  'https://cn.bing.com/search?q=ip%E5%9C%B0%E5%9D%80+%E8%A7%84%E5%88%99&qs=n&form=QBRE&sp=-1&pq=ip%E5%9C%B0%E5%9D%80+%E8%A7%84%E5%88%99&sc=1-7&sk=&cvid=FC57C982563B4F188629B32CAE541761';
getParams(url, 'pq');
// "ip地址+规则"

另外, 正则表达式获取 url 中的所有参数和值

页面 hash

页面的 hash 匹配的是文章阅读的锚点,注意不要和 hash 路由搞混了。

/#([0-9a-zA-Z\-]+)/.exec('abandon.work/api#introduction');

// ["#introduction", "introduction", index: 16, input: "abandon.work/api#introduction", groups: undefined]

总结

合并上面的正则的时候要注意有时候 url 中没有某些参数也是正确的。

const http = /(?:ht|f)tp(?:s)?(?=:\/\/)/;
const domain = /^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:(?:\.)(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3,3}$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/;
const port = /((?<=\:)[0-9]+)$/;
const route = /(\/[0-9a-z#.]+)+|(\/)/;
const query = /(\?[0-9a-z&=]+)/;
const hash = /#([0-9a-zA-Z\-]+)/;

const url = "https://www.abandon.work:6001/#/blog?startDate=1&endDate=10&promote=false#test";
const regex = /^*$/i; <TODO>
regex.exec(url);
regex.test(url)
regex.exec(url)
[
  "http://www.abandon.work:6001/#/blog?startDate=1&endDate=10&promote=false#test",
  "http://",
  "www.abandon.work",
  ":6001",
  "/#/blog",
  "?startDate=1&endDate=10&promote=false",
  "#test",
  index: 0,
  input: "http://www.abandon.work:6001/blog?startDate=1&endDate=10&promote=false#test",
  groups: undefined
]

大功告成!希望之后你能用正则解决更多的问题。

Ref

正则表达式之捕获组和非捕获组 菜鸟教程 Regex

regexjavascript