Статьи

Радость регулярных выражений [4]

Обретя еще больше радости, пора прервать ваш вечерний просмотр в пятницу , подбирая сагу с того места, где мы остановились в прошлый раз.

содержание

Это свидание?

У вас уже был свой первый опыт использования подшаблонов , где они использовались для захвата слова и переноса его в тег HTML span через preg_replace_callback () . Пришло время изучить дополнительные шаблоны немного дальше …

У вас есть строка, содержащая отметку даты / времени, например «20061028134534» — это год (4 цифры), месяц (2 цифры), день месяца (2 цифры), час (2 цифры, 24-часовой формат), минуты и секунды (обе 2 цифры). Вам нужно разбить его на составные части, чтобы вы могли использовать их для расчетов.

Теперь вы можете использовать несколько вызовов substr (), но альтернативным решением является, например, регулярное выражение;

<php $date = '20061028134534'; # The input date string preg_match( '/^(d{4})(d{2})(d{2})(d{2})(d{2})(d{2})$/', $date, $matches ); print_r($matches); 

Глядя на шаблон в деталях;

 / ^ (Д {4}) (г {2}) (г {2}) (г {2}) (г {2}) (г {2}) $ /

В начале и конце шаблона находятся утверждения ^ и $ вы уже видели , так что никаких проблем нет.

D метасимвол

d — это еще один метасимвол-класс, который соответствует «любой десятичной цифре». Это похоже на метасимвол w вы видели здесь , но для чисел вместо символов слова. На самом деле это сокращение для написания вашего собственного класса символов, например [0-9] (который вы видели ранее здесь )

К каждому вхождению d относится квантификатор длины, такой как d{4} означающий ровно четыре цифры (для соответствия году — 2006) или d{2}ровно две цифры.

Более Sub Patterns

Пока все хорошо, но какова роль всех подшаблонов здесь? Они сообщают механизму PCRE, что я хочу перехватывать каждое совпадение, которое PHP затем сделает доступным через третий аргумент preg_match() — в этом примере переменная $matches match — массив, заполненный ссылкой — подробнее об этом в момент.

Приведенный выше код выводит следующее (содержимое переменной $matches );

 массив
 (
     [0] => 20061028134534
     [1] => 2006
     [2] => 10
     [3] => 28
     [4] => 13
     [5] => 45
     [6] => 34
 )

Первый ( [0] ) элемент массива — это полное совпадение, которое в этом случае совпадает с полной входной строкой. Между тем элементы, индексированные с [1] по [6] являются компонентами даты от года до секунд — они были захвачены подшаблонами.

Вы уже видели, что preg_match () возвращает количество найденных совпадений (которое будет равно либо 0, либо 1), но его третий аргумент — это массив, который действует как средство для возврата значений, которые фактически были сопоставлены. Вместо обычного способа вы получаете результат от функции вроде;

$result = myfunc();
$result = myfunc(); 

… Вы даете preg_replace() имя переменной в качестве аргумента функции, и она заполняет его значениями для вас — что-то вроде;

$result = somefunc($more_results_get_put_here_by_reference);
$result = somefunc($more_results_get_put_here_by_reference); 

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

Итак, теперь мы превратили отметку даты / времени в полезный массив, с которым можно выполнять вычисления, а также проверять его формат (но не фактические значения, конечно — 31 февраля 2006 года пройдет!).

Существует другой и (возможно) более элегантный подход к обработке дат в этом формате, который вы увидите позже при рассмотрении preg_split() .

Дружественные пользователю даты

Приведенная выше отметка времени удобна для файлов журналов и тому подобного, но ее не так просто прочитать. Мы склонны предлагать конечным пользователям даты в формате, например, 28th Oct 2006 Так как насчет регулярного выражения для проверки формата (но не значений!) И извлечения интересных частей?

<?php $date = '28th Oct 2006'; preg_match( '/^ (d{1,2}) # Match the day of the month (?:st|nd|rd|th) # Match English ordinal suffix x20 # Match space character # Match the month.... (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) x20 # Match another space character (d{4}) # Match the year $/x', $date, $matches ); print_r($matches);
<?php $date = '28th Oct 2006'; preg_match( '/^ (d{1,2}) # Match the day of the month (?:st|nd|rd|th) # Match English ordinal suffix x20 # Match space character # Match the month.... (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) x20 # Match another space character (d{4}) # Match the year $/x', $date, $matches ); print_r($matches); 

Модификатор расширенного шаблона PCRE

Первое, что может поразить вас — регулярное выражение заполнено пробелами и комментариями — это потому, что я использую модификатор шаблона /x , о котором я упоминал ранее. Из руководства

Если этот модификатор установлен, символы данных пробела в шаблоне полностью игнорируются, за исключением случаев, когда они экранированы или находятся внутри класса символов, а символы между неэкранированным символом # вне класса символов и следующим символом новой строки, включительно, также игнорируются . Это эквивалентно модификатору Perl / x и позволяет включать комментарии в сложные шаблоны. Обратите внимание, однако, что это относится только к символам данных. Символы пробела могут никогда не появляться в последовательностях специальных символов в шаблоне, например в последовательности (? (Которая вводит условный подшаблон).

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

 / ^ (D {1,2}) (?: й | й | е | е) x20 (январь | февраль | март | апрель | май | июнь | июль | август | сентябрь | октябрь | ноябрь | декабрь) x20 (d {4}) $ /

Итак, изучая этот шаблон по частям…

В начале у нас есть обычное утверждение «начало предмета» ^ за которым следует подшаблон (d{1,2}) ; это соответствует цифрам для дня месяца — это может быть первая цифра месяца (только одна цифра) или, в случае 28-го, это две цифры, следовательно, квантификатор длины {1,2} .

Не захватывая суб-шаблоны

Следующая часть выражения вводит сразу две новые функции: (?:st|nd|rd|th) . На первый взгляд это также может выглядеть как подшаблон, но более пристально: (?: Меняет его значение на «не захват». Вы можете думать о нем как о «пользовательском утверждении», если это помогает. Так же, как ^ и $ утверждения, которые вы видели здесь, и утверждение границы слова b обсуждаемое здесь , оно утверждает условие, которое должно быть выполнено, но не становится захваченным подшаблоном.

Другими словами, подшаблон в форме (?: ) Означает «это нешаблонный подшаблон; все, что соответствует, не должно возвращаться в результатах ».

Итак, почему я не хочу захватить мой (?:st|nd|rd|th) суб-шаблон? Он предназначен для совпадения английского порядкового суффикса с числом, например, 1- го , 2- го , 3- го или 4- го числа . Я решил, что это должно быть включено для допустимого формата даты, но на самом деле меня не интересует само значение, поэтому нет необходимости фиксировать его.

Не требующие захвата субшаблоны также следует учитывать в отношении памяти и накладных расходов на обработку, как Андрей упоминает в этой клинике Regex (pdf, стр. 99/100) — движку PCRE не нужно выделять память для их содержимого. В этом примере влияние будет незначительным, но для более крупных документов / более сложных шаблонов производительность и накладные расходы памяти могут стать критическими.

разветвление

Итак, теперь у нас есть закрытый шаблон не захвата, другим новым поступлением здесь (?:st|nd|rd|th) является символ «вертикальная черта» | , Это «оператор ветвления», позволяющий указать альтернативные шаблоны, которые могут быть сопоставлены.

Это что-то вроде оператора «или» в PHP — оно позволяет вам устанавливать альтернативные условия, одно из которых может быть выполнено. Здесь я утверждаю, что за цифрой дня месяца должна следовать любая из строк «st», «nd», «rd» или «th» — которые охватывают английский порядковый суффикс для любого дня месяц.

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

preg_match('#some [b]bold[/b] text|some [i]italic[/i] text#',$text, $m);
preg_match('#some [b]bold[/b] text|some [i]italic[/i] text#',$text, $m); 

Примечание: я использовал # в качестве разделителя выражений, так как сам шаблон содержит косую черту. Мне также пришлось избегать [ и ] символов, иначе они будут считаться классом символов (см. Здесь ). Это может быть шаблон, связанный с соответствием BBCode . Это может соответствовать либо ;

some [b]bold[/b] text 

или

some [i]italic[/i] text 

… Благодаря оператору ветвления в середине шаблона.

Шестнадцатеричные литералы

Далее в паттерне это: x20 — это символ, представляющий его шестнадцатеричный код . Если вы перейдете к своей таблице ASCII , вы увидите, что символ с шестнадцатеричным кодом 20 — это не что иное, как пробел. А? Зачем использовать шестнадцатеричный код, когда я могу просто использовать настоящий символ? Если вы помните, я использую модификатор шаблона /x , который инструктирует движку регулярных выражений игнорировать пробелы, чтобы мы могли иметь красиво отформатированное регулярное выражение. Но я хочу, чтобы пробелы в «28 октября 2006» были частью шаблона, поэтому мне нужно шестнадцатеричное представление, чтобы сообщить механизму PCRE о символе пробела, которому он должен соответствовать.

Вы увидите больше символов, указанных их шестнадцатеричным кодом в другой раз. Вы также можете использовать трехзначные восьмеричные коды для символов, но обязательно внимательно прочитайте руководство в особых случаях, которые к ним относятся. И остерегайтесь строк в двойных кавычках — PHP также имеет мнение о том, что означает x20

Таким образом, шаблон, наконец, начинает обретать смысл … Эта часть (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) представляет собой еще один захватывающий вложенный шаблон, снова содержащий оператор перехода, позволяющий Мне нужно указать альтернативные трехбуквенные строки для месяца, в то время как конец шаблона содержит четырехзначный год, как вы видели раньше. Итак, наконец, вывод этого скрипта PHP, учитывая, что вход «28 октября 2006» выглядит так;

 массив
 (
     [0] => 28 октября 2006
     [1] => 28
     [2] => октябрь
     [3] => 2006
 )

Опять же, нулевым элементом массива является полное совпадение, в то время как следующие три дают мне день, месяц и год соответственно — теперь мне нужно только поменять местами «окт» с числом, и я могу начать вычисление.

Поддержка нескольких форматов даты

Объединение подшаблонов с ветвями может привести к мощным выражениям. Так как насчет расширения предыдущего примера для принятия другого формата даты, например 2006-10-28 (гггг-мм-дд)?

Соответствие этому формату само по себе потребует выражения вроде;

/^(d{4})-(d{1,2})-(d{1,2})$/ 

… ничего нового там нет.

Но как мы можем объединить это с предыдущей моделью? На самом деле в этом нет ничего сложного — нам просто нужно вложить каждый шаблон в другой суб-шаблон, а затем поместить ветку между ними (обратите внимание на комментарии ниже);

<?php function match_date ($date) { if ( preg_match( '/^ ( # First date format... (d{1,2}) (?:st|nd|rd|th) x20 (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) x20 (d{4}) ) | # branch ( # Second date format... (d{4})-(d{1,2})-(d{1,2}) ) $/x', $date, $matches ) ) { return $matches; } return FALSE; } print_r(match_date('28th Oct 2006')); print_r(match_date('2006-10-28'));
<?php function match_date ($date) { if ( preg_match( '/^ ( # First date format... (d{1,2}) (?:st|nd|rd|th) x20 (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) x20 (d{4}) ) | # branch ( # Second date format... (d{4})-(d{1,2})-(d{1,2}) ) $/x', $date, $matches ) ) { return $matches; } return FALSE; } print_r(match_date('28th Oct 2006')); print_r(match_date('2006-10-28')); 

Все, что на самом деле здесь произошло, — это датировать два шаблона формата даты и встроить их в другой шаблон, который имеет форму /^( )|( )$/ .

Однако есть одна небольшая проблема — как вы помните, первый шаблон, когда ему присваивается дата, подобная «28 октября 2006», возвращает месяц в форме «октябрь», а мой второй шаблон, учитывая «2006-10-28», в качестве входных данных возвращает месяц. как «10». Мне нужно иметь возможность точно определить, какой формат даты соответствует, поэтому я могу при необходимости предпринять правильные шаги для преобразования месяца в целое число.

На самом деле это легко сделать, зная, что индекс preg_match() присваиваемый каждому вложенному шаблону, является фиксированным. Вы можете увидеть это, изучив вывод: print_r(match_date('28th Oct 2006')); производит;

 массив
 (
     [0] => 28 октября 2006
     [1] => 28 октября 2006
     [2] => 28
     [3] => октябрь
     [4] => 2006
 )

Элемент [0] — это полное совпадение по всему шаблону, как обычно. Между тем элемент [1] — это то, что было сопоставлено с первым основным подшаблоном, содержащим первый шаблон формата даты. Элементы [2][4] являются компонентами даты.

Теперь сравните, что вывод, который я получаю при подаче паттерна в альтернативном формате даты print_r(match_date('2006-10-28')); ;

 массив
 (
     [0] => 2006-10-28
     [1] => 
     [2] => 
     [3] => 
     [4] => 
     [5] => 2006-10-28
     [6] => 2006
     [7] => 10
     [8] => 28
 )

Теперь элементы [1][4] , соответствующие первому формату даты, являются просто пустыми значениями. Сопоставления для второго формата даты начинаются с элемента [5] , который соответствует второму главному подшаблону, за которым следуют элементы [6][8] которые снова являются компонентами даты.

Таким образом, изучая положение совпадений в возвращаемом массиве, я могу определить, какой поднабор был сопоставлен, и соответствующим образом обработать результаты. В этом случае я мог бы взять нулевой элемент массива и выполнить поиск в остальной части массива, чтобы найти индекс подшаблона, который соответствует — что-то вроде;

printf( "Matched subpattern %d", (array_search($m[0],array_slice($m,1))/4) )."n";
printf( "Matched subpattern %d", (array_search($m[0],array_slice($m,1))/4) )."n"; 

Я вернусь к этой идее в другой раз, когда мы перейдем к (простому) анализу с помощью PCRE.

Взрыв с узорами

Теперь вы привыкли к preg_match() , пришло время представить другую функцию PCRE — preg_split () . Концептуально он выполняет ту же функцию, что и функция explode (), но вместо простого разделителя строк для разбиения строки вы можете использовать регулярное выражение для соответствия разделителю.

Например, как насчет того, чтобы разбить текст, содержащий теги HTML <br>, на строки? Может случиться так, что в некоторых случаях вы имеете дело с <br>, а в других — <br/> (обратите внимание на косую черту);

<?php $comment = "This is a comment<br>with mixed breaks<br/>in it"; print_r(preg_split('#<br/?>#',$comment));
<?php $comment = "This is a comment<br>with mixed breaks<br/>in it"; print_r(preg_split('#<br/?>#',$comment)); 

Я использовал # в качестве альтернативного шаблона , поскольку сам шаблон содержит косую #<br/?># : #<br/?># .

Тем временем ? квантификатор (который вы видели ранее здесь ) после косой черты в шаблоне означает ноль или единицу, что позволяет мне сопоставить как <br>, так и <br/>. Вывод выглядит так:

 массив
 (
     [0] => Это комментарий
     [1] => со смешанными перерывами
     [2] => в нем
 )

Как насчет применения того же подхода для разделения документа по абзацам, которые он содержит? Если вход выглядит так;

 <Р>
     Абзац первый
 </ Р>

 <Р>
     Абзац второй
 </ Р>

 <Р>
     Абзац третий
 </ Р>

В качестве первой попытки попробуем выполнить следующее (входные данные находятся в переменной $doc );

print_r(preg_split('#</?p>#', $doc));
print_r(preg_split('#</?p>#', $doc)); 

Шаблон очень похож на тот, который используется для тегов <br>, за исключением того, что косая черта сместилась, что позволяет мне сопоставлять теги открытия и закрытия абзаца. Вот вывод;

 массив
 (
     [0] => 
     [1] => 
     Абзац первый

     [2] => 


     [3] => 
     Абзац второй

     [4] => 


     [5] => 
     Абзац третий

     [6] => 
 )

Хммм — там много пустого пространства, поэтому я обновлю шаблон так, чтобы пустое пространство по обе стороны от открывающего или закрывающего тега абзаца становилось частью разделителя разделения;

print_r(preg_split('#s*</?p>s*#', $doc));
print_r(preg_split('#s*</?p>s*#', $doc)); 

Метасимвол белого пространства

Помните * это ноль или более квантификатор, вы уже видели здесь .

Так что же делать? Это еще один символ-класс-мета-символ, который соответствует любому символу пробела, например пробелу или новой строке.

Вот как теперь выглядит результат;

 массив
 (
     [0] => 
     [1] => Абзац первый.
     [2] => 
     [3] => Параграф второй.
     [4] => 
     [5] => Абзац третий.
     [6] => 
 )

Становится лучше, но что там делают пустые элементы массива? Они являются результатом закрывающего тега рядом со следующим открывающим тегом, например </p><p> . Между ними нет ничего, но поскольку они являются двумя отдельными разделителями, preg_match() создает пустое значение для «пустоты» между ними.

Было бы хорошо, если бы мы могли избавиться от них, что мы можем использовать с PREG_SPLIT_NO_EMPTY константного флага PREG_SPLIT_NO_EMPTY , который передается в preg_split() в качестве четвертого аргумента (третий аргумент задает максимальное количество разбиений или кусков, которые мы хотим вернуть, -1 что означает «без ограничений»). Согласно руководству, использование флага PREG_SPLIT_NO_EMPTY означает;

Если этот флаг установлен, preg_split () возвращает только непустые фрагменты.

Таким образом, мой сплиттер становится;

print_r(preg_split('#s*</?p>s*#', $doc, -1, PREG_SPLIT_NO_EMPTY));
print_r(preg_split('#s*</?p>s*#', $doc, -1, PREG_SPLIT_NO_EMPTY)); 

… производя следующий вывод …

 массив
 (
     [0] => Абзац первый.
     [1] => Параграф второй.
     [2] => Абзац третий.
 )

…намного лучше.

Захват разделенных разделителей

Как я упоминал в конце первого примера , существует другой подход к извлечению компонентов из отметки даты / времени, например «20061028134534», который включает (возможно) незаконное присвоение preg_split() , используя преимущество флага PREG_SPLIT_DELIM_CAPTURE . Консультируясь с руководством

PREG_SPLIT_DELIM_CAPTURE: Если этот флаг установлен, выражение в скобках в шаблоне разделителя будет также захвачено и возвращено.

Примечание: не все движки регулярных выражений поддерживают возврат разделителей, как я уже стонал, — общий знаменатель, похоже, корпоративные движки, которые, кажется, недовольны облегчением жизни разработчиков. В любом случае … в Perl, PHP, Ruby, Python и (Mozilla!) Javascript вы должны обнаружить, что поддерживается возврат разделителя регулярных выражений (и фактически в ICU ), что упрощает создание простых токенизаторов.

Итак, вот пример, который делает более или менее то же самое, что и предыдущий экстрактор preg_match() даты / времени на основе preg_match() ;

$date = '20061028134534'; print_r( preg_split( '/^(d{4})|(d{2})/' ,$date, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) );
$date = '20061028134534'; print_r( preg_split( '/^(d{4})|(d{2})/' ,$date, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) ); 

И выход …

 массив
 (
     [0] => 2006
     [1] => 10
     [2] => 28
     [3] => 13
     [4] => 45
     [5] => 34
 )

Он использует компоненты даты в качестве разделителей, чтобы разделить дату, а затем возвращает эти разделители. Поскольку введенная дата (в данном случае) является правильным форматом, я получаю то, что ищу, но стоит отметить, что этот подход не позволяет проверить формат даты — вы можете добавить к этому практически все, и вы получить какой-то результат. Но что меня привлекает, так это возможность обрабатывать метки времени переменной длины, такие как «20061028» и «200610281234567890».

Я оставлю это вам, чтобы выяснить все PREG_SPLIT_DELIM_CAPTURE того, что он делает, попробуйте удалить флаги PREG_SPLIT_DELIM_CAPTURE и PREG_SPLIT_NO_EMPTY и посмотреть, что вы получите. Я вернусь к PREG_SPLIT_DELIM_CAPTURE и PREG_SPLIT_DELIM_CAPTURE / PREG_SPLIT_DELIM_CAPTURE на PREG_SPLIT_DELIM_CAPTURE другой раз.

Заворачивать

Этого более чем достаточно для одного выстрела. Ключевыми моментами в этом раунде были подшаблоны, оператор ветвления и preg_split() . Еще в другой раз (когда бы то ни было).