用HTML链接替换文本中的URL

58

这里有一个设计思路:例如,当我在文本区域中放置链接:

http://example.com

如何让PHP检测到它是一个http://链接并将其打印为

print "<a href='http://www.example.com'>http://www.example.com</a>";

我记得以前做过类似的事情,但不是万无一失的,对于复杂的链接经常会出问题。

另一个好主意是,如果你有这样的链接

http://example.com/test.php?val1=bla&val2blablabla%20bla%20bla.bl

修复它使其正常运作。

print "<a href='http://example.com/test.php?val1=bla&val2=bla%20bla%20bla.bla'>";
print "http://example.com/test.php";
print "</a>";

这只是一个想法... stackoverflow 也可能会用得上它 :D

有什么想法吗?


哦,我看到 Stack Overflow 已经做了第一部分了... 发布代码吧,你知道你想这么做的 :D - Angel.King.47
17个回答

123
让我们看一下要求。您有一些用户提供的纯文本,希望显示为超链接URL。
  1. “http://”协议前缀应该是可选的。
  2. 接受域名和IP地址。
  3. 接受任何有效的顶级域名,例如.aero和.xn--jxalpdlp。
  4. 应允许端口号。
  5. URL必须在正常的句子上下文中使用。例如,在“访问stackoverflow.com。”中,最后一个句点不是URL的一部分。
  6. 您可能还想允许“https://”URL,以及其他一些URL。
  7. 像往常一样,在HTML中显示用户提供的文本时,您需要防止跨站脚本(XSS)。此外,您希望URL中的&符号得到正确转义
  8. 您可能不需要支持IPv6地址。
  9. 编辑:如评论中所述,支持电子邮件地址绝对是一个加分项。
  10. 编辑:仅支持纯文本输入-不应该尊重输入中的HTML标记。(Bitbucket版本支持HTML输入。)

编辑:请查看GitHub获取最新版本,支持电子邮件地址,经过身份验证的URL,带引号和括号的URL,HTML输入以及更新的TLD列表。

这是我的看法:

<?php
$text = <<<EOD
Here are some URLs:
stackoverflow.com/questions/1188129/pregreplace-to-detect-html-php
Here's the answer: http://www.google.com/search?rls=en&q=42&ie=utf-8&oe=utf-8&hl=en. What was the question?
A quick look at http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax is helpful.
There is no place like 127.0.0.1! Except maybe http://news.bbc.co.uk/1/hi/england/surrey/8168892.stm?
Ports: 192.168.0.1:8080, https://example.net:1234/.
Beware of Greeks bringing internationalized top-level domains: xn--hxajbheg2az3al.xn--jxalpdlp.
And remember.Nobody is perfect.

<script>alert('Remember kids: Say no to XSS-attacks! Always HTML escape untrusted input!');</script>
EOD;

$rexProtocol = '(https?://)?';
$rexDomain   = '((?:[-a-zA-Z0-9]{1,63}\.)+[-a-zA-Z0-9]{2,63}|(?:[0-9]{1,3}\.){3}[0-9]{1,3})';
$rexPort     = '(:[0-9]{1,5})?';
$rexPath     = '(/[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]*?)?';
$rexQuery    = '(\?[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?';
$rexFragment = '(#[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?';

// Solution 1:

function callback($match)
{
    // Prepend http:// if no protocol specified
    $completeUrl = $match[1] ? $match[0] : "http://{$match[0]}";

    return '<a href="' . $completeUrl . '">'
        . $match[2] . $match[3] . $match[4] . '</a>';
}

print "<pre>";
print preg_replace_callback("&\\b$rexProtocol$rexDomain$rexPort$rexPath$rexQuery$rexFragment(?=[?.!,;:\"]?(\s|$))&",
    'callback', htmlspecialchars($text));
print "</pre>";
  • 为了正确转义<和&字符,我在处理之前将整个文本通过htmlspecialchars进行转义。这不是理想的,因为html转义可能会导致URL边界的误检测。
  • 正如“记住。没有人是完美的。”一行所示(其中remember.Nobody被视为URL,因为缺少空格),可能需要进一步检查有效的顶级域。

编辑:下面的代码修复了上述两个问题,但相当冗长,因为我更多地是重新实现preg_replace_callback使用preg_match。

// Solution 2:

$validTlds = array_fill_keys(explode(" ", ".aero .asia .biz .cat .com .coop .edu .gov .info .int .jobs .mil .mobi .museum .name .net .org .pro .tel .travel .ac .ad .ae .af .ag .ai .al .am .an .ao .aq .ar .as .at .au .aw .ax .az .ba .bb .bd .be .bf .bg .bh .bi .bj .bm .bn .bo .br .bs .bt .bv .bw .by .bz .ca .cc .cd .cf .cg .ch .ci .ck .cl .cm .cn .co .cr .cu .cv .cx .cy .cz .de .dj .dk .dm .do .dz .ec .ee .eg .er .es .et .eu .fi .fj .fk .fm .fo .fr .ga .gb .gd .ge .gf .gg .gh .gi .gl .gm .gn .gp .gq .gr .gs .gt .gu .gw .gy .hk .hm .hn .hr .ht .hu .id .ie .il .im .in .io .iq .ir .is .it .je .jm .jo .jp .ke .kg .kh .ki .km .kn .kp .kr .kw .ky .kz .la .lb .lc .li .lk .lr .ls .lt .lu .lv .ly .ma .mc .md .me .mg .mh .mk .ml .mm .mn .mo .mp .mq .mr .ms .mt .mu .mv .mw .mx .my .mz .na .nc .ne .nf .ng .ni .nl .no .np .nr .nu .nz .om .pa .pe .pf .pg .ph .pk .pl .pm .pn .pr .ps .pt .pw .py .qa .re .ro .rs .ru .rw .sa .sb .sc .sd .se .sg .sh .si .sj .sk .sl .sm .sn .so .sr .st .su .sv .sy .sz .tc .td .tf .tg .th .tj .tk .tl .tm .tn .to .tp .tr .tt .tv .tw .tz .ua .ug .uk .us .uy .uz .va .vc .ve .vg .vi .vn .vu .wf .ws .ye .yt .yu .za .zm .zw .xn--0zwm56d .xn--11b5bs3a9aj6g .xn--80akhbyknj4f .xn--9t4b11yi5a .xn--deba0ad .xn--g6w251d .xn--hgbk6aj7f53bba .xn--hlcj6aya9esc7a .xn--jxalpdlp .xn--kgbechtv .xn--zckzah .arpa"), true);

$position = 0;
while (preg_match("{\\b$rexProtocol$rexDomain$rexPort$rexPath$rexQuery$rexFragment(?=[?.!,;:\"]?(\s|$))}", $text, &$match, PREG_OFFSET_CAPTURE, $position))
{
    list($url, $urlPosition) = $match[0];

    // Print the text leading up to the URL.
    print(htmlspecialchars(substr($text, $position, $urlPosition - $position)));

    $domain = $match[2][0];
    $port   = $match[3][0];
    $path   = $match[4][0];

    // Check if the TLD is valid - or that $domain is an IP address.
    $tld = strtolower(strrchr($domain, '.'));
    if (preg_match('{\.[0-9]{1,3}}', $tld) || isset($validTlds[$tld]))
    {
        // Prepend http:// if no protocol specified
        $completeUrl = $match[1][0] ? $url : "http://$url";

        // Print the hyperlink.
        printf('<a href="%s">%s</a>', htmlspecialchars($completeUrl), htmlspecialchars("$domain$port$path"));
    }
    else
    {
        // Not a valid URL.
        print(htmlspecialchars($url));
    }

    // Continue text parsing from after the URL.
    $position = $urlPosition + strlen($url);
}

// Print the remainder of the text.
print(htmlspecialchars(substr($text, $position)));

2
@Rahul:只需将正则表达式设置为不区分大小写:在调用preg_match时,在正则表达式的最后一个}之后添加一个i - Søren Løvborg
3
我建议进行一次检测,判断URL是否被<a href=''></a>包围。如果是,则不做任何处理。 - bart
1
@Guy:那不是一个URL :) 实际上,它是一个IRI(国际化资源标识符)。但是请随意在Bitbucket上创建一个增强请求,并且我可以考虑是否支持它是可行的。 - Søren Løvborg
3
@Sajad:在最后一个“编辑”上面列出了两个问题,最重要的是 htmlspecialchars 可以将有效的 URL 转换为无效的 URL。而且您不应该使用这里显示的任何一个版本;请使用 Bitbucket 上最新的版本。这里的代码仅演示了一般思路,而 Bitbucket 版本包含了大量的错误修复。 - Søren Løvborg
显示剩余21条评论

17

你们在讨论过于高级和复杂的东西,这对某些情况很好,但大多数时候我们需要一个简单无忧的解决方案。这个怎么样?

preg_replace('/(http[s]{0,1}\:\/\/\S{4,})\s{0,}/ims', '<a href="$1" target="_blank">$1</a> ', $text_msg);

试试吧,然后告诉我哪个疯狂的网址它无法满足。


是的...但是...为什么不添加代码使其可剪切/粘贴?!?!$text_msg= preg_replace('/(http[s]{0,1}://\S{4,})\s{0,}/ims', '<a href="$1" target="_blank">$1</a> ', $text_msg); - pperrin
3
好的方案,但如果字符串中有HTML代码,那么你可能需要用[^<]替换\S - user5147563
[s] 太啰嗦了。 {0,1} 太啰嗦了。 \: 太啰嗦了。 {0,} 太啰嗦了。 ms 毫无意义。我不支持这个答案。 - mickmackusa

15

我找到了一些经过验证的东西

function make_links_blank($text)
{
  return  preg_replace(
     array(
       '/(?(?=<a[^>]*>.+<\/a>)
             (?:<a[^>]*>.+<\/a>)
             |
             ([^="\']?)((?:https?|ftp|bf2|):\/\/[^<> \n\r]+)
         )/iex',
       '/<a([^>]*)target="?[^"\']+"?/i',
       '/<a([^>]+)>/i',
       '/(^|\s)(www.[^<> \n\r]+)/iex',
       '/(([_A-Za-z0-9-]+)(\\.[_A-Za-z0-9-]+)*@([A-Za-z0-9-]+)
       (\\.[A-Za-z0-9-]+)*)/iex'
       ),
     array(
       "stripslashes((strlen('\\2')>0?'\\1<a href=\"\\2\">\\2</a>\\3':'\\0'))",
       '<a\\1',
       '<a\\1 target="_blank">',
       "stripslashes((strlen('\\2')>0?'\\1<a href=\"http://\\2\">\\2</a>\\3':'\\0'))",
       "stripslashes((strlen('\\2')>0?'<a href=\"mailto:\\0\">\\0</a>':'\\0'))"
       ),
       $text
   );
}

对我来说它有效。它适用于电子邮件和URL。很抱歉要回答自己的问题。 :(

但只有这个是有效的。

这是我找到它的链接:http://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/Q_21878567.html

提前道歉,因为它是一个专家交流网站。


我只想提醒一下,这个解决方案不符合我建议的大部分要求,即1、2、3、5和7。但如果这满足您的需求,那很好。只是不要在不可信的输入上使用它,因为它没有执行任何HTML转义。 :-) - Søren Løvborg
你谈论这个转义..如果你能解释一下这个转义是什么,可能会让我和其他人更好地理解你的回答:D - Angel.King.47
3
为了防止跨站脚本攻击,不应该让访客向页面添加任意HTML代码。一个简单的例子是表单处理程序,只需执行print($_POST["text"]);即可。最简单(也是最安全的)防止此类攻击的方法是通过使用htmlspecialchars()函数来处理所有用户提供的文本,它会转义HTML标记和实体,从而将它们有效地转换为纯文本。对于这个问题,您需要允许某些HTML在输出中显示(即链接标记),这使得事情变得更加复杂,因为我们不能再简单地使用htmlspecialchars()函数了。 - Søren Løvborg
2
就像stackoverflow一样,您可以在用户链接中添加rel="nofollow" - Benjamin Crouzier
如果你要转换的字符串来自用户输入,比如存储在数据库中,你可以在保存之前进行转义,以防止 XSS 攻击。这样,当你使用该函数时,就可以检索到已经转义的文本。 - Cedric Ipkiss
显示剩余2条评论

4

我一直使用这个函数,它对我有用

function AutoLinkUrls($str,$popup = FALSE){
    if (preg_match_all("#(^|\s|\()((http(s?)://)|(www\.))(\w+[^\s\)\<]+)#i", $str, $matches)){
        $pop = ($popup == TRUE) ? " target=\"_blank\" " : "";
        for ($i = 0; $i < count($matches['0']); $i++){
            $period = '';
            if (preg_match("|\.$|", $matches['6'][$i])){
                $period = '.';
                $matches['6'][$i] = substr($matches['6'][$i], 0, -1);
            }
            $str = str_replace($matches['0'][$i],
                    $matches['1'][$i].'<a href="http'.
                    $matches['4'][$i].'://'.
                    $matches['5'][$i].
                    $matches['6'][$i].'"'.$pop.'>http'.
                    $matches['4'][$i].'://'.
                    $matches['5'][$i].
                    $matches['6'][$i].'</a>'.
                    $period, $str);
        }//end for
    }//end if
    return $str;
}//end AutoLinkUrls

所有的荣誉归功于 - http://snipplr.com/view/68586/ 享受吧!

如果您的字符串包含逗号分隔的URL(例如“https://www.google.com,http://www.google.com”),则此代码存在问题。在此示例中,第一个URL将以href =“https://www.google.com,”结尾,包括逗号。以逗号结尾的URL是有效的,因此我想这取决于用例,如果您认为字符串意图使用逗号作为标点符号还是URL的一部分更有可能。 - dan-iel

4

这里是在函数中使用正则表达式的代码

<?php
//Function definations
function MakeUrls($str)
{
$find=array('`((?:https?|ftp)://\S+[[:alnum:]]/?)`si','`((?<!//)(www\.\S+[[:alnum:]]/?))`si');

$replace=array('<a href="$1" target="_blank">$1</a>', '<a href="http://$1" target="_blank">$1</a>');

return preg_replace($find,$replace,$str);
}
//Function testing
$str="www.cloudlibz.com";
$str=MakeUrls($str);
echo $str;
?>

这个程序是否支持字符串中的多个URL? - Amien
很好,它可以处理字符串中的多个URL,你只是在$replace=array('a href缺少"<"。 - Amien

1

我知道这个答案已经被接受,而且这个问题非常古老,但它对于其他寻找其他实现的人可能也有用。

这是一个修改过的代码版本,由 Angel.King.47 在 2009 年 7 月 27 日发布:

$text = preg_replace(
 array(
   '/(^|\s|>)(www.[^<> \n\r]+)/iex',
   '/(^|\s|>)([_A-Za-z0-9-]+(\\.[A-Za-z]{2,3})?\\.[A-Za-z]{2,4}\\/[^<> \n\r]+)/iex',
   '/(?(?=<a[^>]*>.+<\/a>)(?:<a[^>]*>.+<\/a>)|([^="\']?)((?:https?):\/\/([^<> \n\r]+)))/iex'
 ),  
 array(
   "stripslashes((strlen('\\2')>0?'\\1<a href=\"http://\\2\" target=\"_blank\">\\2</a>&nbsp;\\3':'\\0'))",
   "stripslashes((strlen('\\2')>0?'\\1<a href=\"http://\\2\" target=\"_blank\">\\2</a>&nbsp;\\4':'\\0'))",
   "stripslashes((strlen('\\2')>0?'\\1<a href=\"\\2\" target=\"_blank\">\\3</a>&nbsp;':'\\0'))",
 ),  
 $text
);

更改:

  • 我删除了规则#2和#3(我不确定在哪些情况下有用)。
  • 删除了电子邮件解析,因为我真的不需要它。
  • 我添加了一条规则,允许识别形式为:[domain]/*(没有www)的URL。例如:“example.com/faq/”(多个tld:domain.{2-3}.{2-4}/)
  • 解析以“http://”开头的字符串时,将其从链接标签中删除。
  • 将“target ='_blank'”添加到所有链接中。
  • 可以在任何标记之后指定网址。例如:<b>www.example.com</b>

正如“Søren Løvborg”所述,此函数不会转义URL。我尝试过他/她的类,但它并没有像我预期的那样工作(如果您不信任您的用户,请先尝试他/她的代码)。


1

如我在上面的评论中提到过,我的VPS正在运行php 7,开始发出警告Warning: preg_replace(): The /e modifier is no longer supported, use preg_replace_callback instead。 替换之后的缓冲区为空/假。

我已重写代码并进行了一些改进。 如果您认为您应该在作者部分,请随意编辑上面函数make_links_blank名称的注释。 我有意不使用关闭的php ?>以避免在输出中插入空格。

<?php

class App_Updater_String_Util {
    public static function get_default_link_attribs( $regex_matches = [] ) {
        $t = ' target="_blank" ';
        return $t;
    }

    /**
     * App_Updater_String_Util::set_protocol();
     * @param string $link
     * @return string
     */
    public static function set_protocol( $link ) {
        if ( ! preg_match( '#^https?#si', $link ) ) {
            $link = 'http://' . $link;
        }
        return $link;
    }

/**
     * Goes through text and makes whatever text that look like a link an html link
     * which opens in a new tab/window (by adding target attribute).
     * 
     * Usage: App_Updater_String_Util::make_links_blank( $text );
     * 
     * @param str $text
     * @return str
     * @see https://dev59.com/PHM_5IYBdhLWcg3w3nbz
     * @author Angel.King.47 | http://dashee.co.uk
     * @author Svetoslav Marinov (Slavi) | http://orbisius.com
     */
    public static function make_links_blank( $text ) {
        $patterns = [
            '#(?(?=<a[^>]*>.+?<\/a>)
                 (?:<a[^>]*>.+<\/a>)
                 |
                 ([^="\']?)((?:https?|ftp):\/\/[^<> \n\r]+)
             )#six' => function ( $matches ) {
                $r1 = empty( $matches[1] ) ? '' : $matches[1];
                $r2 = empty( $matches[2] ) ? '' : $matches[2];
                $r3 = empty( $matches[3] ) ? '' : $matches[3];

                $r2 = empty( $r2 ) ? '' : App_Updater_String_Util::set_protocol( $r2 );
                $res = ! empty( $r2 ) ? "$r1<a href=\"$r2\">$r2</a>$r3" : $matches[0];
                $res = stripslashes( $res );

                return $res;
             },

            '#(^|\s)((?:https?://|www\.|https?://www\.)[^<>\ \n\r]+)#six' => function ( $matches ) {
                $r1 = empty( $matches[1] ) ? '' : $matches[1];
                $r2 = empty( $matches[2] ) ? '' : $matches[2];
                $r3 = empty( $matches[3] ) ? '' : $matches[3];

                $r2 = ! empty( $r2 ) ? App_Updater_String_Util::set_protocol( $r2 ) : '';
                $res = ! empty( $r2 ) ? "$r1<a href=\"$r2\">$r2</a>$r3" : $matches[0];
                $res = stripslashes( $res );

                return $res;
            },

            // Remove any target attribs (if any)
            '#<a([^>]*)target="?[^"\']+"?#si' => '<a\\1',

            // Put the target attrib
            '#<a([^>]+)>#si' => '<a\\1 target="_blank">',

            // Make emails clickable Mailto links
            '/(([\w\-]+)(\\.[\w\-]+)*@([\w\-]+)
                (\\.[\w\-]+)*)/six' => function ( $matches ) {

                $r = $matches[0];
                $res = ! empty( $r ) ? "<a href=\"mailto:$r\">$r</a>" : $r;
                $res = stripslashes( $res );

                return $res;
            },
        ];

        foreach ( $patterns as $regex => $callback_or_replace ) {
            if ( is_callable( $callback_or_replace ) ) {
                $text = preg_replace_callback( $regex, $callback_or_replace, $text );
            } else {
                $text = preg_replace( $regex, $callback_or_replace, $text );
            }
        }

        return $text;
    }
}

1
这个正则表达式应该匹配任何链接,但排除那些新的三个以上字符的顶级域名...
{
  \\b
  # 匹配前导部分(proto://hostname,或仅主机名)
  (
    # http://,或 https:// 前导部分
    (https?)://[-\\w]+(\\.\\w[-\\w]*)+
  |
    # 或者,尝试找到更具体的子表达式的主机名
    (?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \\. )+ # 子域名
    # 现在以 .com、等结尾。对于这些,要求小写
    (?-i: com\\b
        | edu\\b
        | biz\\b
        | gov\\b
        | in(?:t|fo)\\b # .int 或 .info
        | mil\\b
        | net\\b
        | org\\b
        | [a-z][a-z]\\.[a-z][a-z]\\b # 两个字母的国家代码
    )
  )
# 允许可选的端口号 ( : \\d+ )?
# URL 的其余部分是可选的,并以 / 开始 ( / # 其余部分是启发式算法,看起来很有效 [^.!,?;"\\'()\[\]\{\}\s\x7F-\\xFF]* ( [.!,?]+ [^.!,?;"\\'()\\[\\]\{\\}\s\\x7F-\\xFF]+ )* )? }ix

这不是我写的,我不太确定它来自哪里,很抱歉我无法给出任何来源...


我知道上面是模式,但是我很迷茫。抱歉。 - Angel.King.47

1

这可以让你获取电子邮件地址:

$string = "bah bah steve@gmail.com foo";
$match = preg_match('/[^\x00-\x20()<>@,;:\\".[\]\x7f-\xff]+(?:\.[^\x00-\x20()<>@,;:\\".[\]\x7f-\xff]+)*\@[^\x00-\x20()<>@,;:\\".[\]\x7f-\xff]+(?:\.[^\x00-\x20()<>@,;:\\".[\]\x7f-\xff]+)+/', $string, $array);
print_r($array);

// outputs:
Array
(
    [0] => steve@gmail.com
)

0

大致意思是:

<?php
if(preg_match('@^http://(.*)\s|$@g', $textarea_url, $matches)) {
    echo '<a href=http://", $matches[1], '">', $matches[1], '</a>';
}
?>

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