Особенности рекурсивных шаблонов

membrilius

Новичок
Доброго времени суток!

У меня есть простой парсер BB кодов, работает с использованием рекурсивных шаблонов.

Урезанная версия для примера:
PHP:
class bb
{
	public $excluded_tags = array(); // запрещённые теги

	//-------------------------------------------------------------------------

	public function parse($text)
	{		
		$text = trim($text);
		
		$text = $this->parse_tag_b_find($text);
		
		$text = str_replace("\r" , "", $text);
		$text = str_replace("\n", "<br>", $text);
		
		return $text;
	}
	
	//-------------------------------------------------------------------------
	
	private function parse_tag_b_find($text)
	{
		if( ! in_array("b", $this->excluded_tags)) {
			$result = preg_replace_callback("#(?:\[(b)\])(?P<text>(?:(?:(?!\[/?\\1\]).)*|(?R))*)(?:\[/\\1\])#is", array($this, 'parse_tag_b_build'), $text);
		}
		else {
			$result = null;
		}
		
		return (is_string($result)) ? $result : $text;
	}
	
	//-------------------------------------------------------------------------
	
	private function parse_tag_b_build($match)
	{
		return "<b>".$this->parse_tag_b_find($match['text'])."</b>";
	}
}
Работа:
PHP:
$bb = new bb();

$text = "[b]«Князь Андрей встал и [b]подошел к [b]окну, чтобы[/b] отворить его.[/b] Как только он открыл ставни, лунный свет, как будто он настороже у окна давно ждал этого, ворвался в комнату.[/b]";

echo $bb->parse($text);
Как видите, рекурсивный шаблон помогает обрабатывать вложенные теги. (Куча вложенные "<B>" для примера. Корректная обработка вложенности нужна для более сложный тегов)

Вся беда в том, что если в теги заключать большой кусок текста, то скрипт завершается крахом.

В Windows версии PHP я могу захватить примерно 250 символов с большой вложенностью.
В Linux версии около 10 000 символов.

Далее скрипт умирает. Как я понимаю дело в ограниченном числе рекурсий. Но причем тут число символов?

Почему 50 вложенных тегов b в маленьком тексте это нормально. А просто текст оформленных одним тегом но состоящий из 10 000 символов приводит к краху?
 

fixxxer

К.О.
Партнер клуба
потому что pcre написан жопой, с рекурсиями на пустом месте, там где достаточно стейт машины
 

membrilius

Новичок
В php.ini есть параметр

pcre.recursion_limit=1000000

По всей видимости он отвечает за максимальное число рекурсий... увеличение ничего не дало.

Если отказываться от этого способа, кроме конечного автомата, может кто-нибудь что-нибудь посоветовать?
 

ksnk

прохожий
"метод рекурсивного спуска" выглядит более "человекопонятным" и более компактен, по сравнению с конечным автоматом со стеком. Хотя, особенной принципиальной разницы между ними нет.
 

membrilius

Новичок
Кстати может кому-нибудь пригодится. Если текст который вы обрабатываете в UTF8, и включить модификатор u, то можно в несколько раз увеличить кол-во символов которые PCRE обработает без краха.
 

serglt

Анус, ой, Ахтунг
А смысл вообще в такой конструкции?
PHP:
echo preg_replace ('~\[(/?)([a-z]+)\]~', '<\\1\\2>', "hello [b]w[/b]world");
 

membrilius

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

Вообще идеальная регулярка для моей цели:

PHP:
"#(?:\[(b)\])(?P<text>(?:(?:(?!\[/?\\1\]).)|(?R))*)(?:\[/\\1\])#is"
PCRE будет сверять 100% посимвольно, а не как хочет. И я найду 100% правильные открывающие и закрывающие теги. Все не закрытые теги или ещё хуже закрытые, но без открывающего тега я смогу проигнорировать или удалить.

Вся беда в том, что если в ОС маленький Stacksize, всё закончится храхом PHP.

Вообще, как я уже написал тег "B" для примера, никто не будет делать таких 10 вложенных тегов. Но если теги более сложные, то необходимо находить их правильно, в независимости от их вложенности друг в друга.
 

fixxxer

К.О.
Партнер клуба
можно попробовать по частям - вложенности сваливать в callback, то есть перенести рекурсию в php
тогда регулярки хоть и несколько раз будут вызываться но будут попроще
 

ksnk

прохожий
Насчёт рекурсивного спуска...
Вот такое сочинилось, вдруг понравится ;)
PHP:
/**
 *   рекурсивный спуск для bb кодов
 */
class bb
{
    /** string @var - при возврате из parse_tag - имя закрывающего тега */
    private $closedTag;
    /** string @var - при возврате из parse_tag - параметры закрывающего тега */
    private $closedTagArgs;

//-------------------------------------------------------------------------
    function error($msg, $start = 0)
    {
        echo $msg;
    }

    function unslash($text)
    {
        return strtr($text, array(
            "\r\n" => "\n",
            '\[' => '[', '[[' => '[', '\]' => ']', ']]' => ']'
        ));
    }

    /**
     * разбор параметров в стиле
     * =XXX yyy=zzz параметры разделены пробелами или взяты в разнообразные кавычки
     * @param $par
     * @return string
     */
    function param($par)
    {

    }

    /**
     * основная функция парсинга - поиск тега и его анализ посредством рекурсивного спуска
     * @param $text
     * @param $start
     * @return string
     */
    private function parse_tags(&$text, &$start)
    {
        $parsed = '';
        while (true) {
            //  find next tag
            if (preg_match('#((?:\\\\\[|\[\[|.)*?)\[(\/)?(\w+)(.*?)\]#u'
                , $text, $m, 0, $start)
            ) {
                // is it closed?
                $parsed .= $this->unslash($m[1]);
                $start += strlen($m[0]);
                $method = 'parse_tag_' . strtolower($m[3]);
                if (method_exists($this, $method)) {
                    if (!empty($m[2])) {
                        // is it closed?
                        $this->closedTag = $m[3];
                        $this->closedTagArgs = $m[4];
                        return $parsed;
                    } else {
                        // it's opened
                        $parsed .= $this->$method($text, $start, $m[4]);
                    }

                } else {
                    $parsed .= '[' . $m[2] . $m[3] . $m[4] . ']';
                }
            } else {
                $parsed .= $this->unslash(substr($text, $start));
                $start += strlen($text) - $start;

                break;
            }
        }
        return $parsed;
    }

    /**
     * внешняя  функция. - Парсинг текста с вв кодами.
     * @param $text
     * @return mixed|string
     */
    public function parse($text)
    {
        $text = trim($text);
        $start = 0;
        $parsed = $this->parse_tags($text, $start);
        if ($start != strlen($text)) $this->error('wtf?', $start);
        return $parsed;
    }

    /**
     * Парсинг тега b
     * @param $text
     * @param $start
     * @param $par
     * @return mixed|null
     */
    private function parse_tag_b(&$text, &$start, $par)
    {
        $parsed = '<b>' . $this->parse_tags($text, $start) . '</b>';
        if ($this->closedTag != 'b') $this->error('tag B not closed', $start);
        return $parsed;
    }

    /**
     * Парсинг тега hr
     * @param $text
     * @param $start
     * @param $par
     * @return mixed|null
     */
    private function parse_tag_hr(&$text, &$start, $par)
    {
        return '<hr>';
    }


}

// so test it
header('Content-Type:text/html; charset=UTF-8');
$bb = new bb();

$text = "[b]«Князь Андрей [[b]встал и [b]подошел к [b]окну, чтобы[/b] отворить его.[/b] Как только он открыл ставни, лунный свет, [url=google.com]как будто[/url] он настороже у окна давно ждал этого, ворвался в комнату.[/b] комнату.";

echo $bb->parse($text);
Изначальный текст и данные в utf-8. Для примера привёл парсинг одиночного тега - hr. Обработка параметров тега не входила в задачу демонстрации метода ;)

Реасширение парсера для других тегов достаточно очевидно, imho.

Некие странности в регулярке позволяют вставлять в текст квадратные скобки с помощью удвоения или обслешивания
 

membrilius

Новичок
Интересная реализация. Я думал разбить текст посимвольно в массив и с ним работать. В принципе тоже самое, только без preg_match.
 
Сверху