create_function()

’tag’

Применение preg_replace и preg_replace_callback в программировании на PHP

Июнь 2, 2012

Решил вчера написать плагин для своего блога на WordPress. Он заключается в замене содержимого, которое в коде выделено тегом <var>.

Как только дело доходит до использования функции preg_replace() я испытываю неопытность, несмотря на большой опыт программирования. Речь конечно идет о регулярных выражениях.

Сначала я решил, что функция должна вернуть строку, в которой будут заменены все символы на звездочки "*". Рассмотрим следующий код:

preg_replace('/<var>(.*?)<\/var>/', '*', $content);

Содержимое переменной $content:

Строка для замены с секретным кодом <var>836-72</var> и секретной строкой <var>"секрет 2"</var>.

После обработки содержимого переменной $content все найденные совпадения будут заменены на один символ "*", а мне нужно, чтобы звезд было столько же, сколько символов между тегами <var></var>:

Строка для замены с секретным кодом * и секретной строкой *.

Я сразу догадался, что нужно воспользоваться функцией str_repeat() совместно с strlen() с использованием 2-го вхождения $1. Код получился следующий:

preg_replace('/<var>(.*?)<\/var>/', str_repeat("*", strlen("$1")), $content);

Естественно ничего не стало работать и тогда я стал искать решение в Интернете. Приведу одну из страниц, которая мне помогла:

Теперь я понял, что 2-й аргумент функции preg_replace() необходимо брать в кавычки:

preg_replace('/<var>(.*?)<\/var>/e', 'str_repeat("*", strlen("$1"))', $content);

Данный код работает. Но если 1-й момент с кавычками понятен, то почему используется в маске символ "e" после косой черты – нет.

Оказывается добавление данного символа позволяет исполнить код PHP, который содержится в строке. Это та же функция eval().

В процессе изучения вопроса я нашел в официальной документации PHP дополнительную информацию по регулярным  выражениям:

Отлично, символы прекрасно заменяются:

Строка для замены с секретным кодом ****** и секретной строкой ****************.

Но мне этого стало мало. Я захотел, чтобы теперь не все символы заменялись, а только цифры и буквы. Я понял, что нужно использовать функцию preg_replace() в preg_replace().

Сначала у меня ничего не работало (видимо путался в двойных и одинарных кавычках) и тогда я наткнулся на функцию preg_replace_callback(). Данная функция вторым аргументом может принять create_function(). Таким образом, мне просто было легче прописать еще один вызов preg_replace().

В итоге, код стал следующий:

preg_replace_callback('/<var>(.*?)<\/var>/', create_function('$matches', '$matches[1] = preg_replace("/[\w]/", "*", $matches[1]); return "<span class=\"stars\">" . $matches[1] . "</span>";'), $content);

2-й выглядит довольно просто:

preg_replace("/[\w*?]/", "*", $matches[1]);

Несмотря на видимую правильность кода, он не работает с русскими символами. Простая задача превратилась в большую для меня сложность. Текст стал следующим:

Строка для замены с секретным кодом ***-** и секретной строкой "секрет *".

Русские буквы попросту не заменились.

Стал искать ответ в Интернете. Очень помог данный ресурс:

Особенно помогли комментарии под записью. Оказалось, что можно вместо "\w", который в UTF-8 не распознает русские символы воспользоваться "\p{L}".

Но и с "\p{L}" проблема – на локальном компьютере у меня все заработало, в на хостинге нет. Вместо русских символов в некоторых местах появились знаки вопросов и количество звезд увеличилось.

Пошел далее искать в Интернете. По очереди быстро просмотрел 2 следующие страницы и на последней нашел решение:

На всякий случай копирую в запись код, который привел автор статьи:

<?
	print "Локаль: " . setLocale(LC_ALL, 0) . "\n";

	/**
	 * Выводит результаты функции preg_match_all
	 * @param string $comment  Комментарий
	 * @param string $pattern  Паттерн для preg_match_all
	 * @param bool   $usePatch Использовать ли патч
	 * @return void
	 */

	function preg_test($comment, $pattern, $usePatch = false) {

		$test = "one два два three";

		print "\n<strong>{$comment}:</strong> <u>{$pattern}</u>\n";

		if ($usePatch) mb_preg_match_all($pattern, $test, $matches, PREG_OFFSET_CAPTURE);
		else preg_match_all($pattern, $test, $matches, PREG_OFFSET_CAPTURE);

		foreach ($matches[0] as $v) print "  Подстрока: «{$v[0]}», смещение: {$v[1]}\n";

	}

	/**
	 * Патч для устранения проблемы с оффсетами, осуществляет только их пересчет
	 */

	function mb_preg_match_all(
		$ps_pattern,
		$ps_subject,
		&$pa_matches,
		$pn_flags = PREG_PATTERN_ORDER,
		$pn_offset = 0,
		$ps_encoding = NULL
	) {

		// WARNING! - All this function does is to correct offsets, nothing else:
		//(code is independent of PREG_PATTER_ORDER / PREG_SET_ORDER)

		if (is_null($ps_encoding)) $ps_encoding = mb_internal_encoding();

		$pn_offset = strlen(mb_substr($ps_subject, 0, $pn_offset, $ps_encoding));
		$ret = preg_match_all($ps_pattern, $ps_subject, $pa_matches, $pn_flags, $pn_offset);

		if ($ret && ($pn_flags & PREG_OFFSET_CAPTURE))
			foreach($pa_matches as &$ha_match)
				foreach($ha_match as &$ha_match)
					$ha_match[1] = mb_strlen(substr($ps_subject, 0, $ha_match[1]), $ps_encoding);

		return $ret;

	}

	preg_test("«В лоб» — не ловятся русские буквы", "/[\w]+/i");
	preg_test("Character range — ловится все, что нужно, но кривые offset'ы", "/[а-яa-z]+/i");
	preg_test("«В лоб» с ключем «/u» — снова не ловятся русские буквы", "/[\w]+/ui");
	preg_test("Character range с ключем «/u» — ловится все, что нужно, но кривые offset'ы", "/[а-яa-z]+/ui");
	preg_test("Модификатор «\pL», можно даже без «/u» — ловится все, что нужно, но кривые offset'ы", "/[\pL]+/i");
	preg_test("Модификатор «\p{Cyrillic}», можно тоже без «/u» — ловится все, что нужно, но кривые offset'ы", "/[\p{Cyrillic}]+/i");
	preg_test("(!) Модификатор «\pL» с патчем — ловится все, правильные offset'ы — ЭТО ПРАВИЛЬНЫЙ СПОСОБ", "/[\pL]+/i", true);

	$source = highlight_file(__FILE__, true);
?>

На хостинге у меня заработал вариант "/[а-яa-z0-9]/ui". Однако, автор пишет, что правильный способ: "/[\pL]+/i", который мне выдал знаки вопросов. И тогда я понял, что решение заключается в модификаторе "u". Смотрим в документацию:

и читаем:

u (PCRE_UTF8) – этот модификатор включает дополнительную функциональность PCRE, которая не совместима с Perl: шаблоны обрабатываются как UTF-8 строки. Модификатор u доступен в PHP 4.1.0 и выше для Unix-платформ, и в PHP 4.2.3 и выше для Windows платформ. Валидность UTF-8 в шаблоне проверяется начиная с PHP 4.3.5.

Именно это и было нужно для моего хостинга. Итоговый и работающий вариант выглядит следующим образом:

preg_replace_callback('/<var>(.*?)<\/var>/', create_function('$matches', '$matches[1] = preg_replace("/[\pL]/ui", "*", $matches[1]); return "<span class=\"stars\">" . $matches[1] . "</span>";'), $content);

На экран выводится следующая строчка:

Строка для замены с секретным кодом ***-** и секретной строкой "****** *".

Спасибо тем, кто до меня копал в этой теме, я знаю, как это сложно и сколько отнимает времени.

Кстати также рабочий вариант прописать полностью русский алфавит в квадратных скобках. Замена произойдет без знаков вопросов, русские символы будут обработаны. Только одна исключение – звезд вновь будет больше, чем должно быть.

Переставил последнюю строчку кода в 1-й preg_replace() и все также заработало:

preg_replace('/<var>(.*?)<\/var>/e', 'preg_replace("/[\pL]/ui", "*", "$1")', $content);

Tags: , , , , , , , , ,
Записано в PHP, Программирование    |    Постоянная ссылка