Повторное выражение PHP regex apache

У меня есть регулярное выражение, которое подходит для системы шаблонов, которая, к сожалению, кажется, разбивает apache (она работает в Windows) на некоторые скромно-тривиальные запросы. Я исследовал проблему, и есть несколько предложений по увеличению размера стека и т.д., Ни одна из которых, похоже, не работает, и мне не нравится работать с такими проблемами, увеличивая ограничения в любом случае, поскольку в целом это просто подтолкнуло ошибку к будущему.

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

Идея состоит в том, чтобы поймать самый внутренний блок (в данном случае {block:test}This should be caught first!{/block:test}), который затем будет str_replace из начальных/конечных тегов и повторно запускает все это через регулярное выражение, пока не останется блоков.

Regex:

~(?P<opening>{(?P<inverse>[!])?block:(?P<name>[a-z0-9\s_-]+)})(?P<contents>(?:(?!{/?block:[0-9a-z-_]+}).)*)(?P<closing>{/block:\3})~ism

Пример шаблона:

<div class="f_sponsors s_banners">
    <div class="s_previous">&laquo;</div>
    <div class="s_sponsors">
        <ul>
            {block:sponsors}
            <li>
                <a href="{var:url}" target="_blank">
                    <img src="image/160x126/{var:image}" alt="{var:name}" title="{var:name}" />
                </a>
            {block:test}This should be caught first!{/block:test}
            </li>
            {/block:sponsors}
        </ul>
    </div>
    <div class="s_next">&raquo;</div>
</div>

Это длинный выстрел, я полагаю.: (

Ответ 1

Попробуйте следующее:

'~(?P<opening>\{(?P<inverse>[!])?block:(?P<name>[a-z0-9\s_-]+)\})(?P<contents>[^{]*(?:\{(?!/block:(?P=name)\})[^{]*)*)(?P<closing>\{/block:(?P=name)\})~i'

Или в читаемой форме:

'~(?P<opening>
  \{
  (?P<inverse>[!])?
  block:
  (?P<name>[a-z0-9\s_-]+)
  \}
)
(?P<contents>
  [^{]*(?:\{(?!/block:(?P=name)\})[^{]*)*
)
(?P<closing>
  \{
  /block:(?P=name)
  \}
)~ix'

Самая важная часть в группе (?P<contents>..):

[^{]*(?:\{(?!/block:(?P=name)\})[^{]*)*

Начиная, единственным интересующим нас персонажем является открывающая фигурная скобка, поэтому мы можем разбить любые другие символы с помощью [^{]*. Только после того, как мы увидим {, мы проверим, является ли это началом тега {/block}. Если это не так, мы идем вперед и потребляем его и начинаем сканирование следующего, и повторяем по мере необходимости.

Используя RegexBuddy, я проверил каждое регулярное выражение, поместив курсор в начало тега {block:sponsors} и отлаживая. Затем я удалял конечную скобку с закрывающего тега {/block:sponsors}, чтобы принудительно выполнить неудачное совпадение и отлаживать его снова. Ваше регулярное выражение выполнило 940 шагов для успеха и 2265 шагов для отказа. Шахта сделала 57 шагов, чтобы преуспеть, и 83 шага к неудаче.

На стороне примечания я удалил модификатор s, потому что потому, что я не использую точку (.) и модификатор m, потому что это никогда не было необходимо. Я также использовал названный backreference (?P=name) вместо \3 в соответствии с замечательным предложением @DaveRandom. И я избежал всех фигурных скобок ({ и }), потому что мне легче читать этот путь.


РЕДАКТИРОВАТЬ: Если вы хотите совместить самый внутренний именованный блок, измените среднюю часть регулярного выражения следующим образом:

(?P<contents>
  [^{]*(?:\{(?!/block:(?P=name)\})[^{]*)*
)

... к этому (как предложил @Kobi в своем комментарии):

(?P<contents>
  [^{]*(?:\{(?!/?block:[a-z0-9\s_-]+\})[^{]*)*
)

Первоначально группа (?P<opening>...) захватила бы первый открытый тег, который она увидела, тогда группа (?P<contents>..) будет потреблять что угодно, включая другие теги, - пока они не были закрывающим тегом, чтобы соответствовать найденному группой (?P<opening>...). (Тогда группа (?P<closing>...) будет идти вперед и потреблять это.)

Теперь группа (?P<contents>...) отказывается соответствовать любому тегу, открывая или закрывая (обратите внимание на /? в начале), независимо от имени. Таким образом, регулярное выражение сначала начинает соответствовать тегу {block:sponsors}, но когда он встречает тег {block:test}, он отказывается от этого совпадения и возвращается к поиску открывающего тега. Он начинается снова с тега {block:test}, на этот раз успешно заканчивая совпадение, когда находит тег закрытия {/block:test}.

Звучит неэффективно, описывая это так, но это действительно не так. Трюк, который я описал ранее, перекрывая не-фигурные скобки, заглушает эффект этих ложных запусков. Там, где вы делали негативный взгляд почти на каждую позицию, теперь вы делаете это только тогда, когда вы сталкиваетесь с {. Вы могли бы даже использовать притяжательные квантификаторы, как предлагал @godspeedlee:

(?P<contents>
  [^{]*+(?:\{(?!/?block:[a-z0-9\s_-]+\})[^{]*+)*+
)

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

Ответ 2

Вы можете использовать atomic group: (?>...) или possessive quantifiers: ?+ *+ ++.. для подавления/ограничения обратного отслеживания и ускорения согласования по методу unrolling loop. Мое решение:

\{block:(\w++)\}([^<{]++(?:(?!\{\/?block:\1\b)[<{][^<{]*+)*+)\{/block:\1\}

Я тестировал http://regexr.com?31p03.

соответствует {block:sponsors}...{/block:sponsors}:
\{block:(sponsors)\}([^<{]++(?:(?!\{\/?block:\1\b)[<{][^<{]*+)*+)\{/block:\1\}
http://regexr.com?31rb3

соответствует {block:test}...{/block:test}:
\{block:(test)\}([^<{]++(?:(?!\{\/?block:\1\b)[<{][^<{]*+)*+)\{/block:\1\}
http://regexr.com?31rb6

другое решение:
в исходном коде PCRE вы можете удалить комментарий из config.h:
/* #undef NO_RECURSE */

после копирования текста из config.h:
PCRE использует рекурсивные вызовы функций для обработки обратного отслеживания при сопоставлении. Иногда это может быть проблемой для систем, имеющих стеки ограниченного размера. Определите NO_RECURSE, чтобы получить версию, которая не использует рекурсию в функции match(); вместо этого он создает свой собственный стек паролем, используя pcre_recurse_malloc() для получения памяти из кучи.

или вы можете изменить pcre.backtrack_limit и pcre.recursion_limit из php.ini (http://www.php.net/manual/en/pcre.configuration.php)

Ответ 3

Должно ли решение быть одним регулярным выражением? Более эффективным подходом может быть просто поиск первого вхождения {/block: (который может быть простым строковым поиском или регулярным выражением), а затем поиск назад с этой точки, чтобы найти соответствующий ему соответствующий тег открытия, соответствующим образом заменить диапазон и повторить до тех пор, пока блоков больше нет. Если каждый раз вы просматриваете первый закрывающий тег, начиная с вершины шаблона, то это даст вам самый глубоко вложенный блок.

Алгоритм зеркального отображения будет работать так же хорошо - найдите последний открывающий тег, а затем выполните поиск вперед оттуда для соответствующего закрывающего тега:

<?php

$template = //...

while(true) {
  $last_open_tag = strrpos($template, '{block:');
  $last_inverted_tag = strrpos($template, '{!block:');
  // $block_start is the index of the '{' of the last opening block tag in the
  // template, or false if there are no more block tags left
  $block_start = max($last_open_tag, $last_inverted_tag);
  if($block_start === false) {
    // all done
    break;
  } else {
    // extract the block name (the foo in {block:foo}) - from the character
    // after the next : to the character before the next }, inclusive
    $block_name_start = strpos($template, ':', $block_start) + 1;
    $block_name = substr($template, $block_name_start,
        strcspn($template, '}', $block_name_start));

    // we now have the start tag and the block name, next find the end tag.
    // $block_end is the index of the '{' of the next closing block tag after
    // $block_start.  If this doesn't match the opening tag something is wrong.
    $block_end = strpos($template, '{/block:', $block_start);
    if(strpos($template, $block_name.'}', $block_end + 8) !== $block_end + 8) {
      // non-matching tag
      print("Non-matching tag found\n");
      break;
    } else {
      // now we have found the innermost block
      // - its start tag begins at $block_start
      // - its content begins at
      //   (strpos($template, '}', $block_start) + 1)
      // - its content ends at $block_end
      // - its end tag ends at ($block_end + strlen($block_name) + 9)
      //   [9 being the length of '{/block:' plus '}']
      // - the start tag was inverted iff $block_start === $last_inverted_tag
      $template = // do whatever you need to do to replace the template
    }
  }
}

echo $template;