Как заменить текстовые URL и исключить URL в тегах HTML? - PullRequest
13 голосов
/ 23 октября 2010

Мне нужна ваша помощь здесь.

Я хочу включить это:

sometext sometext http://www.somedomain.com/index.html sometext sometext

в

sometext sometext <a href="http://somedoamai.com/index.html">www.somedomain.com/index.html</a> sometext sometext

Мне удалось это с помощью этого регулярного выражения:

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

Проблема также в том, что он заменяет URL img, например:

sometext sometext <img src="http//domain.com/image.jpg"> sometext sometext

превращается в:

sometext sometext <img src="<a href="http//domain.com/image.jpg">domain.com/image.jpg</a>"> sometext sometext

Пожалуйста, помогите.

Ответы [ 7 ]

7 голосов
/ 28 октября 2010

Модернизированная версия Gumbo выше:

$html = <<< HTML
<html>
<body>
<p>
    This is a text with a <a href="http://example.com/1">link</a>
    and another <a href="http://example.com/2">http://example.com/2</a>
    and also another http://example.com with the latter being the
    only one that should be replaced. There is also images in this
    text, like <img src="http://example.com/foo"/> but these should
    not be replaced either. In fact, only URLs in text that is no
    a descendant of an anchor element should be converted to a link.
</p>
</body>
</html>
HTML;

Давайте используем XPath, который выбирает только те элементы, которые на самом деле являются текстовыми узлами, содержащими http: // или https: // или ftp: // и которые сами не являются текстовыми узлами якорных элементов.

$dom = new DOMDocument;
$dom->loadHTML($html);
$xPath = new DOMXPath($dom);
$texts = $xPath->query(
    '/html/body//text()[
        not(ancestor::a) and (
        contains(.,"http://") or
        contains(.,"https://") or
        contains(.,"ftp://") )]'
);

XPath выше даст нам TextNode со следующими данными:

 and also another http://example.com with the latter being the
    only one that should be replaced. There is also images in this
    text, like 

Начиная с PHP5.3, мы также могли бы использовать PHP внутри XPath , чтобы использовать шаблон Regex для выбора наших узлов вместо трех обращений к контейнеру.

Вместо того, чтобы разделять текстовые узлы в соответствии со стандартами, мы будем использовать фрагмент документа и просто заменим весь текстовый узел на фрагмент. Нестандартный в данном случае означает только то, что метод, который мы будем использовать для этого , не является частью W3C спецификации DOM API .

foreach ($texts as $text) {
    $fragment = $dom->createDocumentFragment();
    $fragment->appendXML(
        preg_replace(
            "~((?:http|https|ftp)://(?:\S*?\.\S*?))(?=\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)~i",
            '<a href="$1">$1</a>',
            $text->data
        )
    );
    $text->parentNode->replaceChild($fragment, $text);
}
echo $dom->saveXML($dom->documentElement);

и тогда будет выведено:

<html><body>
<p>
    This is a text with a <a href="http://example.com/1">link</a>
    and another <a href="http://example.com/2">http://example.com/2</a>
    and also another <a href="http://example.com">http://example.com</a> with the latter being the
    only one that should be replaced. There is also images in this
    text, like <img src="http://example.com/foo"/> but these should
    not be replaced either. In fact, only URLs in text that is no
    a descendant of an anchor element should be converted to a link.
</p>
</body></html>
4 голосов
/ 23 октября 2010

Вы не должны делать это с регулярными выражениями - по крайней мере, не только с регулярными выражениями.Вместо этого используйте правильный HTML-анализатор DOM, такой как PHP DOM-библиотека .Затем вы можете перебирать узлы, проверять, является ли это текстовым узлом, выполнять поиск по регулярному выражению и заменять текстовый узел соответствующим образом.

Что-то вроде этого должно сделать это:

$pattern = "~((?:http|https|ftp)://(?:\S*?\.\S*?))(?=\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)~i";
$doc = new DOMDocument();
$doc->loadHTML($str);
// for every element in the document
foreach ($doc->getElementsByTagName('*') as $elem) {
    // for every child node in each element
    foreach ($elem->childNodes as $node) {
        if ($node->nodeType === XML_TEXT_NODE) {
            // split the text content to get an array of 1+2*n elements for n URLs in it
            $parts = preg_split($pattern, $node->nodeValue, -1, PREG_SPLIT_DELIM_CAPTURE);
            $n = count($parts);
            if ($n > 1) {
                $parentNode = $node->parentNode;
                // insert for each pair of non-URL/URL parts one DOMText and DOMElement node before the original DOMText node
                for ($i=1; $i<$n; $i+=2) {
                    $a = $doc->createElement('a');
                    $a->setAttribute('href', $parts[$i]);
                    $a->setAttribute('target', '_blank');
                    $a->appendChild($doc->createTextNode($parts[$i]));
                    $parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
                    $parentNode->insertBefore($a, $node);
                }
                // insert the last part before the original DOMText node
                $parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
                // remove the original DOMText node
                $node->parentNode->removeChild($node);
            }
        }
    }
}

Ok,поскольку DOMNodeList *s из getElementsByTagName и childNodes являются live , каждое изменение в DOMотражается в этом списке, и поэтому вы не можете использовать foreach, который также будет перебирать вновь добавленные узлы.Вместо этого вам нужно вместо этого использовать циклы for и отслеживать добавленные элементы для увеличения указателей индекса и, в лучшем случае, предварительно рассчитанных границ массива соответствующим образом.

Но так как это довольно сложно в такой сложнойалгоритм (вам понадобится один указатель индекса и граница массива для каждого из трех циклов for), использование рекурсивного алгоритма более удобно:

function mapOntoTextNodes(DOMNode $node, $callback) {
    if ($node->nodeType === XML_TEXT_NODE) {
        return $callback($node);
    }
    for ($i=0, $n=count($node->childNodes); $i<$n; ++$i) {
        $nodesChanged = 0;
        switch ($node->childNodes->item($i)->nodeType) {
            case XML_ELEMENT_NODE:
                $nodesChanged = mapOntoTextNodes($node->childNodes->item($i), $callback);
                break;
            case XML_TEXT_NODE:
                $nodesChanged = $callback($node->childNodes->item($i));
                break;
        }
        if ($nodesChanged !== 0) {
            $n += $nodesChanged;
            $i += $nodesChanged;
        }
    }
}
function foo(DOMText $node) {
    $pattern = "~((?:http|https|ftp)://(?:\S*?\.\S*?))(?=\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)~i";
    $parts = preg_split($pattern, $node->nodeValue, -1, PREG_SPLIT_DELIM_CAPTURE);
    $n = count($parts);
    if ($n > 1) {
        $parentNode = $node->parentNode;
        $doc = $node->ownerDocument;
        for ($i=1; $i<$n; $i+=2) {
            $a = $doc->createElement('a');
            $a->setAttribute('href', $parts[$i]);
            $a->setAttribute('target', '_blank');
            $a->appendChild($doc->createTextNode($parts[$i]));
            $parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
            $parentNode->insertBefore($a, $node);
        }
        $parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
        $parentNode->removeChild($node);
    }
    return $n-1;
}

$str = '<div>sometext http://www.somedomain.com/index.html sometext <img src="http//domain.com/image.jpg"> sometext sometext</div>';
$doc = new DOMDocument();
$doc->loadHTML($str);
$elems = $doc->getElementsByTagName('body');
mapOntoTextNodes($elems->item(0), 'foo');

Здесь mapOntoTextNodes используется для отображения данного обратного вызоваФункция на каждом DOMText узле в документе DOM.Вы можете передать весь узел DOMDocument или только конкретный DOMNode (в данном случае только узел BODY).

Функция foo затем используется для поиска и замены простых URL-адресов в содержимом узла DOMText путем разбиения строки содержимого на non-URL ‍ / ‍ URL частей с использованием preg_split при захвате использованного разделителя, в результате чего получается массив из 1 + 2 · n элементов.Затем части не-URL заменяются новыми узлами DOMText , а части URL заменяются новыми элементами A, которые затем вставляются перед источником DOMText узел, который затем удаляется в конце.Поскольку этот mapOntoTextNodes идет рекурсивно, достаточно просто вызвать эту функцию на определенном DOMNode .

1 голос
/ 24 октября 2010

спасибо за ответ, но он все еще работает. я исправил с помощью этой функции:

function livelinked ($text){
        preg_match_all("#((http|https|ftp)://(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)|^(jpg)#ie", $text, $ccs);
        foreach ($ccs[3] as $cc) {
           if (strpos($cc,"jpg")==false  && strpos($cc,"gif")==false && strpos($cc,"png")==false ) {
              $old[] = "http://".$cc;
              $new[] = '<a href="http://'.$cc.'" target="_blank">'.$cc.'</a>';
           }
        }
        return str_replace($old,$new,$text);
}
0 голосов
/ 03 февраля 2011

соответствует пробелу (\ s) в начале и конце строки URL, это гарантирует, что

"http://url.com" 

не соответствует

http://url.com 

соответствует;

0 голосов
/ 21 ноября 2010

Вы можете попробовать мой код из этого вопроса :

echo preg_replace('/<a href="([^"]*)([^<\/]*)<\/a>/i', "$1", 'sometext sometext <img src="http//domain.com/image.jpg"> sometext sometext');

Если вы хотите включить другие теги - это достаточно просто:

echo preg_replace('/<img src="([^"]*)([^\/><]*)>/i', "$1", 'sometext sometext <img src="http//domain.com/image.jpg"> sometext sometext');
0 голосов
/ 16 ноября 2010

DomDocument более зрелый и работает намного быстрее, так что это просто альтернатива, если кто-то хочет использовать PHP Simple HTML DOM Parser :

<?php
require_once('simple_html_dom.php');

$html = str_get_html('sometext sometext http://www.somedomain.com/index.html sometext sometext
<a href="http://www.somedomain.com/index.html">http://www.somedomain.com/index.html</a>
sometext sometext <img src="http//domain.com/image.jpg"> sometext sometext');

foreach ($html->find('text') as $element)
{
    // you can add any tag into the array to exclude from replace
    if (!in_array($element->parent()->tag, array('a')))
        $element->innertext = preg_replace("#((http|https|ftp)://(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)#ie", "'<a href=\"$1\" target=\"_blank\">$1</a>$4'", $element->innertext);
}

echo $html;
0 голосов
/ 28 октября 2010

Если вы хотите продолжать использовать регулярное выражение (и в этом случае регулярное выражение вполне уместно), вы можете сделать так, чтобы регулярное выражение совпадало только с URL-адресами, которые «автономны».Используя escape-последовательность границы слова (\b), вы можете иметь совпадение с регулярным выражением, где http непосредственно предшествует пробел или начало текста:

preg_replace("#\b((http|https|ftp)://(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)#ie", "'<a href=\"$1\" target=\"_blank\">$1</a>$4'", $text);
            // ^^ thar she blows

Таким образом, "http://..." не будет соответствовать, но http:// как его собственное слово будет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...