PHP验证/正则表达式用于URL

135

我一直在寻找一个简单的URL正则表达式,有没有人手头上有一个很好用的?我在Zend框架验证类中没有找到,但看到过几个不同的实现。


4
这是一个相当不错的资源。提供了许多不同模式和测试的列表:https://mathiasbynens.be/demo/url-regex - omar j
21个回答

214

使用filter_var()函数验证字符串是否为URL:

var_dump(filter_var('example.com', FILTER_VALIDATE_URL));

如果不必要,使用正则表达式是一种不好的实践。

编辑: 注意,此解决方案不安全且不防止跨站脚本攻击。如果需要进行复杂的验证,可能最好去其他地方寻找。


29
5.2.13版本(我认为5.3.2版本也是)存在一个问题,使用这种方法无法验证带有破折号的网址。请修复此问题。 - vamin
4
允许像 'http://www' 这样的 URL。当 URL 像 'http://localhost' 时也是可以的。 - Stanislav
12
这种方法的另一个问题是它不支持Unicode。 - Benji XVI
3
FILTER_VALIDATE_URL存在许多需要修复的问题。此外,描述标志的文档未反映实际源代码,其中对某些标志的引用已完全删除。更多信息请参见:http://news.php.net/php.internals/99018 - S. Imp
显示剩余12条评论

86

我在一些项目中使用了它,我不认为遇到过问题,但我肯定它不是穷尽的:

$text = preg_replace(
  '#((https?|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i',
  "'<a href=\"$1\" target=\"_blank\">$3</a>$4'",
  $text
);

大部分随机的字符是为了处理句子中的情况,比如http://domain.example.(避免匹配句尾的句号)。虽然我知道它可以被简化,但因为这种方式效果不错,所以我只是将其从一个项目复制到了另一个项目。


7
我注意到一些问题:在需要字符类时使用了替代(每个替代只匹配一个字符);而且替换部分不应该需要外面的双引号(它们只是因为正则表达式上无意义的 /e 修改符才需要)。 - Alan Moore
1
@John Scipione:google.com 是一个有效的相对 URL 路径,但不是有效的绝对 URL。我认为这就是他正在寻找的。 - Gumbo
这种情况下不起作用 - 它包括尾随的“:3 cantari noi in albumul <a href="http://audio.resursecrestine.ro/cantece/index-autori/andrei-rosu/diverse">Diverse</a>”。 - Softy
1
@Softy,类似 http://example.com/somedir/... 的 URL 是一个完全合法的 URL,请求名为 ... 的文件 - 这是一个合法的文件名。 - Stephen P
我正在使用Zend\Validator\Regex来使用您的模式验证URL,但它仍然将http://www.example检测为有效。 - Joko Wandiro

31

根据PHP手册,不应该使用parse_url来验证URL。

不幸的是,似乎filter_var('example.com', FILTER_VALIDATE_URL)的表现也不太好。

无论是parse_url()还是filter_var()都会通过格式不正确的URL,例如http://...

因此,在这种情况下,正则表达式更好的方法。


11
这个论点是站不住脚的。如果FILTER_VALIDATE_URL比您想要的宽松一些,可以添加一些额外的检查来处理这些边缘情况。自己尝试使用正则表达式重写URL检查只会让您离完整检查更远。 - Kzqai
2
请查看此页面上所有的拒绝正则表达式,以了解为什么不要编写自己的正则表达式的示例。 - Kzqai
3
Tchalvak,你提出了一个很好的观点。在处理URL等内容时,正则表达式(根据其他回答)可能非常难以正确实现。 正则表达式并不总是万能的解决方案。相反,正则表达式也并不总是错误的答案。 重要的是为工作选择正确的工具(无论是正则表达式还是其他工具),而不是特别地“反对”或“支持”正则表达式。事后看来,你的答案是更好的选择,即使用filter_var与其边缘情况的限制相结合(特别是当正则表达式变得超过100个字符时,维护该正则表达式会成为一场噩梦)。 - catchdave
  1. “filter_var()” 似乎不允许“格式错误的URL,例如http://...”。(好吧,在2008年可能会允许…)在我的当前测试中,它的表现比建议的正则表达式更好。
  2. 因为这个答案没有包含实际的正则表达式,所以它没有用。
- Melebius

15

根据John Gruber(Daring Fireball)的说法:

正则表达式:

(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))

在 preg_match() 中使用:

preg_match("/(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))/", $url)

以下是带有注释的扩展正则表达式模式:

(?xi)
\b
(                       # Capture 1: entire matched URL
  (?:
    https?://               # http or https protocol
    |                       #   or
    www\d{0,3}[.]           # "www.", "www1.", "www2." … "www999."
    |                           #   or
    [a-z0-9.\-]+[.][a-z]{2,4}/  # looks like domain name followed by a slash
  )
  (?:                       # One or more:
    [^\s()<>]+                  # Run of non-space, non-()<>
    |                           #   or
    \(([^\s()<>]+|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
  )+
  (?:                       # End with:
    \(([^\s()<>]+|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
    |                               #   or
    [^\s`!()\[\]{};:'".,<>?«»“”‘’]        # not a space or one of these punct chars
  )
)

欲了解更多详情,请查看: http://daringfireball.net/2010/07/improved_regex_for_matching_urls


1
为了使其正常工作,该模式需要在三个点上使用反斜杠转义正斜杠:preg_match("/(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|(([^\s()<>]+|(([^\s()<>]+)))*))+(?:(([^\s()<>]+|(([^\s()<>]+)))*)|[^\s`!()[]{};:'".,<>?«»“”‘’]))/", $url) - Ben Birney

13

如果您想知道 URL 是否真的存在:

function url_exist($url){//se passar a URL existe
    $c=curl_init();
    curl_setopt($c,CURLOPT_URL,$url);
    curl_setopt($c,CURLOPT_HEADER,1);//get the header
    curl_setopt($c,CURLOPT_NOBODY,1);//and *only* get the header
    curl_setopt($c,CURLOPT_RETURNTRANSFER,1);//get the response as a string from curl_exec(), rather than echoing it
    curl_setopt($c,CURLOPT_FRESH_CONNECT,1);//don't use a cached version of the url
    if(!curl_exec($c)){
        //echo $url.' inexists';
        return false;
    }else{
        //echo $url.' exists';
        return true;
    }
    //$httpcode=curl_getinfo($c,CURLINFO_HTTP_CODE);
    //return ($httpcode<400);
}

1
在实际验证 URL 是否真实存在之前,我仍然会对 $url 进行某种形式的验证,因为上述操作是昂贵的 - 取决于文件大小,可能高达 200 毫秒。在某些情况下,URL 实际上可能没有可用的资源(例如,创建指向尚未上传的图像的 URL)。此外,您没有使用缓存版本,因此不像 file_exists() 那样会缓存文件的状态并几乎立即返回。您提供的解决方案仍然很有用。为什么不直接使用 fopen($url, 'r') - Yzmir Ramirez
谢谢,这正是我在寻找的。然而,我在尝试使用它时犯了一个错误。函数应该是"url_exist"而不是"url_exists",呃;-) - PJ Brunet
9
直接访问用户输入的URL是否存在安全风险? - siliconpi
你想要添加一个检查是否发现了404错误的功能:<code> $httpCode = curl_getinfo( $c, CURLINFO_HTTP_CODE ); //echo $url . ' ' . $httpCode . '<br>'; if( $httpCode == 404 ) { echo $url.' 404'; } </code> - Camaleo
根本不安全...任何输入的URL都会被主动访问。 - dmmd

10

我认为在这种情况下使用正则表达式并不明智。无法匹配所有可能性,即使你能匹配上,还是有可能链接根本不存在。

这里有一种非常简单的方法来测试 URL 是否存在并可读:

if (preg_match("#^https?://.+#", $link) and @fopen($link,"r")) echo "OK";

(如果没有 preg_match,这将同时验证您服务器上的所有文件名)


8

在我看来最好的URL正则表达式:

function valid_URL($url){
    return preg_match('%^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@|\d{1,3}(?:\.\d{1,3}){3}|(?:(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)(?:\.(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)*(?:\.[a-z\x{00a1}-\x{ffff}]{2,6}))(?::\d+)?(?:[^\s]*)?$%iu', $url);
}

示例:

valid_URL('https://twitter.com'); // true
valid_URL('http://twitter.com');  // true
valid_URL('http://twitter.co');   // true
valid_URL('http://t.co');         // true
valid_URL('http://twitter.c');    // false
valid_URL('htt://twitter.com');   // false

valid_URL('http://example.com/?a=1&b=2&c=3'); // true
valid_URL('http://127.0.0.1');    // true
valid_URL('');                    // false
valid_URL(1);                     // false

Source: http://urlregex.com/


8
我曾经使用过这个,效果不错——我不记得它是从哪里得到的。
$pattern = "/\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i";

^(http://|https://)?(([a-z0-9]?([-a-z0-9]*[a-z0-9]+)?){1,63}.)+[a-z]{2,6}(可能有点贪心,还不确定,但在协议和前导www方面更加灵活) - andrewbadera

7
    function validateURL($URL) {
      $pattern_1 = "/^(http|https|ftp):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+.(com|org|net|dk|at|us|tv|info|uk|co.uk|biz|se)$)(:(\d+))?\/?/i";
      $pattern_2 = "/^(www)((\.[A-Z0-9][A-Z0-9_-]*)+.(com|org|net|dk|at|us|tv|info|uk|co.uk|biz|se)$)(:(\d+))?\/?/i";       
      if(preg_match($pattern_1, $URL) || preg_match($pattern_2, $URL)){
        return true;
      } else{
        return false;
      }
    }

不能处理像这样的链接: 'www.w3schools.com/home/3/?a=l' - user3396065

5
这就是你的答案 =) 试着破解它,你做不到!!!
function link_validate_url($text) {
$LINK_DOMAINS = 'aero|arpa|asia|biz|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|mobi|local';
  $LINK_ICHARS_DOMAIN = (string) html_entity_decode(implode("", array( // @TODO completing letters ...
    "&#x00E6;", // æ
    "&#x00C6;", // Æ
    "&#x00C0;", // À
    "&#x00E0;", // à
    "&#x00C1;", // Á
    "&#x00E1;", // á
    "&#x00C2;", // Â
    "&#x00E2;", // â
    "&#x00E5;", // å
    "&#x00C5;", // Å
    "&#x00E4;", // ä
    "&#x00C4;", // Ä
    "&#x00C7;", // Ç
    "&#x00E7;", // ç
    "&#x00D0;", // Ð
    "&#x00F0;", // ð
    "&#x00C8;", // È
    "&#x00E8;", // è
    "&#x00C9;", // É
    "&#x00E9;", // é
    "&#x00CA;", // Ê
    "&#x00EA;", // ê
    "&#x00CB;", // Ë
    "&#x00EB;", // ë
    "&#x00CE;", // Î
    "&#x00EE;", // î
    "&#x00CF;", // Ï
    "&#x00EF;", // ï
    "&#x00F8;", // ø
    "&#x00D8;", // Ø
    "&#x00F6;", // ö
    "&#x00D6;", // Ö
    "&#x00D4;", // Ô
    "&#x00F4;", // ô
    "&#x00D5;", // Õ
    "&#x00F5;", // õ
    "&#x0152;", // Œ
    "&#x0153;", // œ
    "&#x00FC;", // ü
    "&#x00DC;", // Ü
    "&#x00D9;", // Ù
    "&#x00F9;", // ù
    "&#x00DB;", // Û
    "&#x00FB;", // û
    "&#x0178;", // Ÿ
    "&#x00FF;", // ÿ 
    "&#x00D1;", // Ñ
    "&#x00F1;", // ñ
    "&#x00FE;", // þ
    "&#x00DE;", // Þ
    "&#x00FD;", // ý
    "&#x00DD;", // Ý
    "&#x00BF;", // ¿
  )), ENT_QUOTES, 'UTF-8');

  $LINK_ICHARS = $LINK_ICHARS_DOMAIN . (string) html_entity_decode(implode("", array(
    "&#x00DF;", // ß
  )), ENT_QUOTES, 'UTF-8');
  $allowed_protocols = array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal');

  // Starting a parenthesis group with (?: means that it is grouped, but is not captured
  $protocol = '((?:'. implode("|", $allowed_protocols) .'):\/\/)';
  $authentication = "(?:(?:(?:[\w\.\-\+!$&'\(\)*\+,;=" . $LINK_ICHARS . "]|%[0-9a-f]{2})+(?::(?:[\w". $LINK_ICHARS ."\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})*)?)?@)";
  $domain = '(?:(?:[a-z0-9' . $LINK_ICHARS_DOMAIN . ']([a-z0-9'. $LINK_ICHARS_DOMAIN . '\-_\[\]])*)(\.(([a-z0-9' . $LINK_ICHARS_DOMAIN . '\-_\[\]])+\.)*('. $LINK_DOMAINS .'|[a-z]{2}))?)';
  $ipv4 = '(?:[0-9]{1,3}(\.[0-9]{1,3}){3})';
  $ipv6 = '(?:[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
  $port = '(?::([0-9]{1,5}))';

  // Pattern specific to external links.
  $external_pattern = '/^'. $protocol .'?'. $authentication .'?('. $domain .'|'. $ipv4 .'|'. $ipv6 .' |localhost)'. $port .'?';

  // Pattern specific to internal links.
  $internal_pattern = "/^(?:[a-z0-9". $LINK_ICHARS ."_\-+\[\]]+)";
  $internal_pattern_file = "/^(?:[a-z0-9". $LINK_ICHARS ."_\-+\[\]\.]+)$/i";

  $directories = "(?:\/[a-z0-9". $LINK_ICHARS ."_\-\.~+%=&,$'#!():;*@\[\]]*)*";
  // Yes, four backslashes == a single backslash.
  $query = "(?:\/?\?([?a-z0-9". $LINK_ICHARS ."+_|\-\.~\/\\\\%=&,$'():;*@\[\]{} ]*))";
  $anchor = "(?:#[a-z0-9". $LINK_ICHARS ."_\-\.~+%=&,$'():;*@\[\]\/\?]*)";

  // The rest of the path for a standard URL.
  $end = $directories .'?'. $query .'?'. $anchor .'?'.'$/i';

  $message_id = '[^@].*@'. $domain;
  $newsgroup_name = '(?:[0-9a-z+-]*\.)*[0-9a-z+-]*';
  $news_pattern = '/^news:('. $newsgroup_name .'|'. $message_id .')$/i';

  $user = '[a-zA-Z0-9'. $LINK_ICHARS .'_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
  $email_pattern = '/^mailto:'. $user .'@'.'(?:'. $domain .'|'. $ipv4 .'|'. $ipv6 .'|localhost)'. $query .'?$/';

  if (strpos($text, '<front>') === 0) {
    return false;
  }
  if (in_array('mailto', $allowed_protocols) && preg_match($email_pattern, $text)) {
    return false;
  }
  if (in_array('news', $allowed_protocols) && preg_match($news_pattern, $text)) {
    return false;
  }
  if (preg_match($internal_pattern . $end, $text)) {
    return false;
  }
  if (preg_match($external_pattern . $end, $text)) {
    return false;
  }
  if (preg_match($internal_pattern_file, $text)) {
    return false;
  }

  return true;
}

还有很多更多的顶级域名。 - Jeff Puckett
你的字符类中不需要转义 ., ?, +, ^, {, }, =, |, $, backtick 和 [。甚至在你的某个字符类中重复了 +: 不需要转义。 - mickmackusa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接