之前折腾了一个短网址程序,过程挺顺利的,唯一就是在验证网址这一步卡了壳,花费了整个过程的一大半时间,最终经过一番搜索、折腾和测试,才算找到一个完美的解决方案。
在短网址程序中,验证网址无疑是很重要的。且不说各种安全问题,就是一些「浑水摸鱼」的网址占据大量的短网址都非常让人头疼。
提到验证网址,我相信大多数人第一时间反应出来的都是正则表达式,的确,这很科学,但何奈自己正则太渣,平时想要匹配一段复杂的 HTML 都要反复调试半天,更何况我连网址的结构都不能完全梳理清楚,所以还是绕过它,咱能不用就别自己找不自在。
esc_url_raw()
想了想,平时的开发中,验证网址都是直接调的 WordPress 核心给的函数,所以考虑直接把 WordPress 的 esc_url_raw() 函数搬过来,省时又省力。
事实证明我实在是太天真了。复制的过程中我发现它牵连了太多其它的模块,一个模块勾搭一点,想要完全搬过来实在是太过庞大,到最后我都分不清楚哪是哪了,你到底跟几个模块有关系啊?照这样搞下去,还不如直接基于 WordPress 算了。
再想想,其实这个函数的本意是「过滤」网址,尽可能的把不合法的网站变成合法的:没有协议?咱给它添上协议;不合法字符?直接干掉它,别脏了眼;浏览器或服务器根本没法解析?我管你那个?
它只是在把网址变得不出现根本错误就没事,别让你这一个小小的网址把我整个程序都给搞乱了就行,虽然在 WordPress 的环境中,大多数情况这样就够了,但在我们的短网址程序中,这显然不够严格。
filter_var()
自己的想法还没能运行一次,就已经胎死腹中了,没办法,只好去求助万能的搜索引擎大人了。找了半天,看着 Stack Overflow 上的各路大神的一个个长篇大论(其实我现在也好像在长篇大论……),最终决定选择大多数人推荐的 filter_var() 函数试试,其实我以前也听说过这个东西,不过一直没什么机会实际使用下,也可以趁现在好好研究研究。
filter_var() 是 PHP 自带的一个函数,顾名思义,用来验证(过滤)变量,印象中应该不止可以验证 URL,什么邮箱之类的也都玩得转。
如果要用它验证网址,只需要这样:
if ( filter_var( $url, FILTER_VALIDATE_URL ) !== false )
echo '可以,这很网址。'; // 验证成功
else
echo '这是什么鬼?'; // 验证失败,返回 FALSE。
非常的简单,不过要注意,失败时返回 FALSE 并不意味着你可以直接用 if ( !filter_var( ... ) ) 这样的形式来 草率的判定网址不合法,因为它成功时会返回过滤的内容,而不是 TRUE,如果内容是 '0' 那你怎么办?虽然我们验证网址不可能出现这种情况,但这种习惯还是不要养成的为好,我年轻的时候就曾自作聪明的修改例子代码,在 strpos() 函数上栽跟头喽(其实我现在还经常自作聪明,并且为其付出代价,然后继续自作聪明)。
OK,经过一番测试,感觉上是没什么大问题了,所以把代码交了出去。过一会,别人小小测试一下,发现类似百度和谷歌的搜索结果的网址无法验证通过,又是一波紧急研究。
结论是 filter_var() 会认定所有带有中文的网址不合法,虽然可以通过转义中文部分的方法强行通过验证,但我之前已经为了避免重复所以取消了所有 URL 的转义啊,这想想感觉会弄得好混乱。而且过会又发现,即使是在没有中文的情况下,它对锚点(就是网址中「#」后边的那一堆)链接的支持还有些迷之问题,所以果断抛弃之。
Happy End
好,重头戏来了。
无奈,只好继续去搜索解法,偶然间看到了一个网址,进去之后感觉进入了天堂一样,相见恨晚,里边简直有我现在所急需的一切。
如何正确的使用正则表达式验证网址链接
这个页面的作者搜集了很多不同的正则表达式,并用它们对一系列网址进行测试,有的网址需要验证通过,也有恰恰相反的。
最终,只有一位叫 Diego Perini 的选手通过了比赛,完美的达到了所有要求,他的正则表达式足足有 502 个字符,打眼一瞧非常的吓人,反正我是完全看不懂……
为了方便使用,我把它的正则表达式封装成了一个 PHP 函数,想要使用的话,直接调用就行了:
/**
* 检测网址是否合法
*
* @link https://www.bgbk.org/regex-url/
* @link https://gist.github.com/dperini/729294
*
* @param string $url 需要检测的网址。
* @return bool 是否为一个合法的网址。
*/
function is_url( $url ) {
if ( !trim( $url ) )
return false;
if ( strlen( $url ) < 10 )
return false;
$pattern = '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS';
$result = preg_match( $pattern, $url );
$result = (bool) $result;
return $result;
}
就像这样:
if ( is_url( $url ) )
echo '可以,这很网址。'; // 验证成功
else
echo '这是什么鬼?'; // 验证失败
虽然我对正则表达式理解不多,但它看长度就显然是巨耗费性能的,所以我先对网址进行了一些简单的预判断,希望可以减少服务器的压力。
另外,如果你想了解它的原理,或者想在 JavaScript 中使用这个正则表达式的话,可以参考作者在 GitHub 上的一个页面,里边有原版的 JavaScript 版本,并且附有大量注释,方便你学习和使用!
除此之外,你可以点击下载一份所有测试的 URLs 列表,可以亲自尝试各种正则表达式,或者验证你的方法,如果找到了更好的办法,一定要在评论中分享给大家哦!
附赠资料
最后附赠一个也是偶然发现的页面,里边用非常清晰的方式详细的介绍了网址的构成规则,感兴趣的同学可以点击此处去参观参观。
如何正确的使用正则表达式验证网址链接
在 javaScript 中,用 window.location. 加上演示模型里提到的属性名,就可以获取到当前页面网址的那一部分啦,比如 window.location.protocol,一般就会返回「http:」或「https:」。
当然,上边的网址只是展示了网址基本的构成元素,如果你想了解的更多,比如网址每个部分不能出现的字符啊、需要转义的文本啊,以及各种约定俗称的规则和注意事项,都可以在这里找到,非常完整哦!
URI 是 Web上可用的每种资源 - HTML文档、图像、视频片段、程序等 - 由一个通用资源标志符(Uniform Resource Identifier, 简称"URI")进行定位。 对象分组:
^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
12 3 4
测试代码如下:
<?php
$search = '~^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?~i';
$url = 'http://www.jb51.net/pub/ietf/uri/#Gonn';
$url = trim($url);
preg_match_all($search, $url ,$rr);
printf("<p>输出URL数据为:</p><pre>%s</pre>\n",var_export( $rr ,TRUE));
/*
各分组如下
$1 = http:
$2 = http
$3 = //www.111cn.net
$4 = www.111cn.net
$5 = /pub/ietf/uri/
$6 = <undefined>
$7 = <undefined>
$8 = #Gonn
$9 = Gonn
*/
?>
上面的正则表达式可以获取URL中的任何一部分,下面的代码则简单一些:
<?php
// 从 URL 中取得主机名
preg_match("/^(http:\/\/)?([^\/]+)/i", "http://www.jb51.net/index.html", $matches);
$host = $matches[2];
// 从主机名中取得后面两段
preg_match("/[^\.\/]+\.[^\.\/]+$/", $host, $matches);
echo "domain name is: {$matches[0]}\n";
?>