Обретя еще больше радости, пора прервать ваш вечерний просмотр в пятницу , подбирая сагу с того места, где мы остановились в прошлый раз.
содержание
Это свидание?
У вас уже был свой первый опыт использования подшаблонов , где они использовались для захвата слова и переноса его в тег 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()
. Еще в другой раз (когда бы то ни было).