Воистину, нет пределов глубине и ширине простого, на первый взгляд, проекта «Ответов» (ныне почивших). Начавшись на уровне игры, с размещения обычного html-файла, сделанного на коленке вручную, — к базе данных и, затем, к полностью автоматизированной системе импорта через RSS. Самодельный Гугль, типа.
Нет сомнений (мы даже не пытались искать в Сети аналоги), что всё давно кем-то сделано и где-тоработает хорошо. Речь не об этом, а о том, говоря отвлечённо, что любая самостоятельная разработка, как кажется, «ненужного» или сверхпростого ведёт к развитию умений и навыков, делает человека человеком. Именно поэтому мы всегда рекомендуем не гнаться за славой или быстрыми деньгами, а доводить начатое до совершенства; в мелочах, подчас, закопаны сокровища, недостижимые для тех, кто «жить торопится и чувствовать спешит». И не только в области программирования.
Дальнейшее обсуждение предназначено для людей, знакомых хотя бы в общих чертах с регулярными выражениями и программированием, с проблемами, стоящими перед веб-разработчиками.
На этот раз пришлось решать простую с виду задачу автоматического превращения интернет-адресов в ссылки, т. е., адресов вида http://bichkov.com, которые обычно люди копируют из адресной строки и просто «кидают» в текст сообщения, — в полноценную ссылку http://bichkov.com, по которой удобно кликать другим пользователям, читающим текст.
Увы! Если вы думаете, что всё так просто и задача давно решена, попробую переубедить вас, приведя всего два примера. Один из них — из книги «PHP 5» уважаемых авторов М. Кузнецова, И. Симдянова и С. Голышева (С.-П., 2006). Вот что мы читаем в главе 5, посвящённой регулярным выражениям («Автоподсветка URL», листинг 5.7):
<?php
$text = "… на ресурсе, посвящённом PHP-программированию, http://www.example.com вы можете найти…";
$pattern = "/(http:\/\/)(\S+)/i";
$replacement = '<a href="http://\\2">\\2</a>';
echo preg_replace ( $pattern, $replacement, $text );
?>
Нетрудно убедиться, что код работает (мы, правда, исправили одну несомненную опечатку в листинге, приведённом выше) лишь до тех пор, пока после адреса идёт пробел. Адреса, после которых идут знаки препинания, получают эти знаки в конце как часть ссылки, например: http://bichkov.com, Это не всегда хорошо. Скорее, всегда нехорошо для достижения желаемой страницы.
Нет, скажет скептически настроенный читатель, — пример нехорош! Во-первых, он старый, 2006 года,во-вторых, приведён в учебном пособии, для «начинающих». ОК, вот вам другой пример, посвежее. Американский ресурс, имеющий миллионы пользователей, делает так:
† Читайте http://ru.wikipedia.org/wiki/Будители. Или посмотрите историю нацдвижений на территории б. СССР – кто стоял у истоков, что говорил и что делал. [оригинал полностью здесь. — Ю. Б.]
‡ Вас на Гугле не забанили? Ну наберите "Латынина русофобия" и почитайте. Чёрт возьми, это даже в википедии (источнике более чем либеральном) заметили! http://ru.wikipedia.org/wiki/Джаханнам,_или_До_встречи_в_Аду [оригинал здесь. — Ю. Б.]
Нравится!? Трудно даже сказать, какое регулярное выражение используется для получения такого замечательного эффекта. Разумеется, попытка пройти по приведённым ссылкам обречена на неудачу. Придётся ручками-ручками править адрес в адресной строке браузера, чтобы попасть туда, куда предполагал автор цитат.
Кажется, я убедил читателей в том, что очевидные, простые, полезные и само-собой разумеющиеся вещи иногда не работают. Не работают даже там, где, казалось бы, должны и даже обязаны работать.
Перейдём теперь к изложению нашего решения данной проблемы. Оно просто, как всё гениальное :) Надеемся, кому-то будет полезно если не пройти вместе с нами все ступеньки к решению, то хотя бы воспользоваться готовым результатом.
Итак, собираем текст, который будет содержать в себе все вышеприведённые трудности. Единственные ограничения: строка не должна иметь возвратов каретки \n или \r\n, а <br> или <br /> вместо них (потребуется очень незначительное усложнение — сделайте самостоятельно), знаков дюйма «"» и апострофов «'» (это спецсимволы, которые влияют на безопасность веб-страниц и должны быть преобразованы в html-сущности для хранения в базе данных или перед выводом на страницу) 1.
$text = "Пример ссылки, после которой идёт знак препинания: http://ru.wikipedia.org/wiki/Будители. А эта ссылка http://ru.wikipedia.org/wiki/Будители находится в середине предложения — отделена от текста пробелами с обеих сторон. Здесь мы имеем ссылку, после которой идёт тег: http://bichkov.com/wordpress<br /> — может ведь такое случиться, правда? И такое тоже не редкость: http://www.google.com/search?q=%D1%87%D1%82%D0%BE-%D0%BD%D0%B8%D0%B1%D1%83%D0%B4%D1%8C&sourceid=ie7&rls=com.microsoft:en-gb:IE-SearchBox&ie=&oe=. А вот адрес, после которого идёт знак препинания, затем тег: http://bichkov.com;<br /> пробел (любой пробельный символ) всё же должен служить «финальным» ограничителем. Ещё одна сложная ссылка — внутри запятая — размещается в самом конце строки; после неё нет ничего — ни точки, ни запятой, ни пробела: http://ru.wikipedia.org/wiki/Джаханнам,_или_До_встречи_в_Аду";
Попробуем теперь составить регулярное выражение, справляющееся с подобными трудностями. Но вначале перечислим эти трудности — напишем формальную постановку задачи.
Наше выражение должно корректно работать для страниц и интернет-адресов, содержащих символы Юникода (UTF-8).
После интернет-адреса может (без пробела!) следовать любой знак препинания или кавычка; либо даже знак препинания, после которого сразу идёт тег. Причём, последнему символу адреса разрешается быть знаком равенства «=».2
Интернет-адреса вольны находиться в любом месте текста: в начале строки, в конце, быть обрамлёнными тегами.
Всего-то! Начнём, пожалуй.
В PHP для работы с регулярными выражениями используется, в том числе, функцияpreg_replace($pattern, $replacement, $text) — именно она нам и нужна для решения задачи. Начнём с первого аргумента — собственно, самого́ выражения.
1. По п. 1 условия задачи, выражение должно работать с символами Юникода. Модификатор u служит этой цели (хотя, на страницах с отличными от UTF-8 кодировками, оно, наоборот, работать перестанет — тогда этот модификатор лучше будет убрать); i тоже не помешает, чтобы сделать выражение регистронезависимым. (Примечание: при использовании модификатора x (PCRE_EXTENDED) следует пробелы предварить обратным слешем «\»).
Вот так: /выражение/iu, где «выражение» — нерождённое ещё регулярное выражение, заготовка.
2. В начале адреса идёт http://, поэтому пишем: http:\/\/ с «экранированными» для PHP символами. Можно, конечно, усложнить по вкусу: добавить s? для защищённого протокола https:// и ещё наворотить возможность использования ftp:// и проч. Но это всего лишь усложнение, не меняющее сути.
Далее может идти любая последовательность букв, цифр и символов кроме пробела или начала тега «<», обеспечиваемая с помощью выражения в квадратных скобках [^<\s]+ — знак плюс означает, что повторений таких символов должно быть больше нуля. Итого получаем, суммируя: http:\/\/[^<\s]+
На этом мысль многих авторов останавливается. Действительно, приведённое выражение вполне «рабочее». Однако, мы даже не покончили с п. 2. Чтобы было можно безбоязненно поставить любой знак препинания в конце адреса, выражение следует чуть-чуть дописать (наше ноу-хау): [^<.,:;?!”»’“+\-\)] 3 — последнему символу адреса разрешается быть любым, но не знаком препинания (символ «^» в начале означает отрицание)! Обратите внимание, что отсутствует и знак равенства — см. условие задачи в п. 2 и пример адресного запроса к Гуглю в переменной $text.
Конечно, это не всё. Если ограничиться полученным — http:\/\/[^<\s]+[^<.,:;?!”»’“+\-\)] — выражение в первых квадратных скобках захватит и знак препинания в конце, а во вторых скобках — пробел. Надо нам это? Нет, не для этого деды воевали.
Значит, нужен ограничитель. Этим ограничителем будет [.,:;?!”»’“+\-\)]?(?:<br ?\/?>)*\s Сложно? Разберём на части. В квадратных скобках — набор знаков препинания (его можно расширять по вкусу), на одном из которых и должен споткнуться бег поисковой машины из предыдущего абзаца. (?:<br ?\/?>)* показывает, что может несколько раз встретиться <br> 4 или <br />, а может и не встретиться. Кроме того, содержимое круглых скобок не следует помещать в «карман» — для этого использована конструкция (?:подвыражение) — об этом чуть ниже. Важно: после встреченного (или не встреченного — обратите внимание на знак вопроса после закрывающей квадратной скобки; кстати, вместо «?» можно поставить звёздочку «*», если предполагается несколько знаков подряд — вроде «!!!», — в текстах такое встречается достаточно часто) знака препинания и возможных тегов переноса строки идёт обязательный пробел.
Уф! Соберём всё вместе: http:\/\/[^<\s]+[^<.,:;?!”»’“+\-\)][.,:;?!”»’“+\-\)]?(?:<br ?\/?>)*\s Итак, п. 2 победили.
3. Это просто. Может случиться, что интернет-адрес окажется в самом конце строки и наше выражение не сработает — ведь в конце его обязателен знак пробела-табуляции \s. Что-ж, добавим символ конца строки «$» в качестве альтернативы \s. Окончательно: http:\/\/[^<\s]+[^<.,:;?!”»’“+\-\)][.,:;?!”»’“+\-\)]?(?:<br ?\/?>)*\s|$
Наше выражение почти готово к использованию. Самое время вспомнить, что не поиск адресов нужен, а их преобразование в ссылки!
На помощь приходят «карманы», в которые можно помещать содержимое круглых скобок. Главное — их правильно расставить. В первый карман поместим сам адрес как таковой, во второй — всё, что идёт после: знаки препинания, пробел, знак конца строки и проч. Совсем окончательно регулярное выражение, pattern, будет таковым:
$pattern = "/(http:\/\/[^<\s]+[^<.,:;?!”»’“+\-\)])([.,:;?!”»’“+\-\)]?(?:<br ?\/?>)*\s|$)/iu";
Осталось понять, на что меняем шило. Мыло выглядит так:
$replacement = '<a href="$1">$1</a>$2';
Думаю, нет смысла останавливаться на этом — всё очевидно: ссылка из первого кармана и после закрывающего тега </a> знак пунктуации и/или тег переноса строки («брейк») со всем прочим из кармана второго. Заметим лишь, что в старом PHP, до версии 4.0.4, вместо ссылок (backreference) вида $n следует использовать \\n. Обращаем внимание и на использование апострофов в обрамлении выражения.
Окончательно, поместим всё хозяйство в функцию PHP, работающую с регулярными выражениями:
preg_replace ( $pattern, $replacement, $text );
Ну, или, что то же самое, но выглядит более громоздко:
preg_replace ( "/(http:\/\/[^<\s]+[^<.,:;?!”»’“+\-\)])([.,:;?!”»’“+\-\)]?(?:<br ?\/?>)*\s|$)/iu", '<a href="$1">$1</a>$2', $text );
Задача решена — можете проверить самостоятельно! Точки и другие знаки препинания аккуратно отделены от адресов, все остальные условия также выполнены — полюбуйтесь на результат работы:
Пример ссылки, после которой идёт знак препинания: http://ru.wikipedia.org/wiki/Будители. А эта ссылкаhttp://ru.wikipedia.org/wiki/Будители находится в середине предложения — отделена от текста пробелами с обеих сторон. Здесь мы имеем ссылку, после которой идёт тег: http://bichkov.com/wordpress
— может ведь такое случиться, правда? И такое тоже не редкость: http://www.google.com/search?q=%D1%87%D1%82%D0%BE-%D0%BD%D0%B8%D0%B1%D1%83%D0%B4%D1%8C&sourceid=ie7&rls=com.microsoft:en-gb:IE-SearchBox&ie=&oe=. А вот адрес, после которого идёт знак препинания, затем тег: http://bichkov.com;
пробел (любой пробельный символ) всё же должен служить «финальным» ограничителем. Ещё одна сложная ссылка — внутри запятая — размещается в самом конце строки; после неё нет ничего — ни точки, ни запятой, ни пробела: http://ru.wikipedia.org/wiki/Джаханнам,_или_До_встречи_в_Аду
Попытайтесь опровергнуть: найти такой интернет-адрес, который разобьёт наши построения и на котором не будет работать данное регулярное выражение.5 Или, наоборот, написать что-нибудь более простое.6 С признательностью примем любые замечания и улучшения!
Желаем успехов в проектировании сайтов!
Вот это ещё лучше:
$text = "Текст с ссылкой";
$text = preg_replace("/(^|[\n ])([\w]*?)((ht|f)tp(s)?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is", "$1$2<a href=\"$3\" >$3</a>", $text);
$text = preg_replace("/(^|[\n ])([\w]*?)((www|ftp)\.[^ \,\"\t\n\r<]*)/is", "$1$2<a href=\"http://$3\" >$3</a>", $text);
$text = preg_replace("/(^|[\n ])([a-z0-9&\-_\.]+?)@([\w\-]+\.([\w\-\.]+)+)/i", "$1<a href=\"mailto:$2@$3\">$2@$3</a>", $text);