Добро пожаловать!
Здесь вы можете найти ответ на интересующий вас вопрос в отрасли сайтостроения, познакомится ближе с web технологиями и web стандартами.

Урок 9. Просмотр вперед и назад

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

Что такое поиск контекста

Мы снова начнем с примера. Нужно найти заголовок Web-страницы; заголовки HTML-страницы помещаются между тегами <TITLE> и <TITLE> в разделе <HEAD> HTML-кода. Вот пример:

Текст

<HEAD>
<TITLE>Ben Forta's Homepage</TITLE>
</HEAD>

Регулярное выражение

<[tT][iI][tT][lL][eE]>.*</[tT][iI][tT][lL][eE]>

Результат

<HEAD>
<TITLE>Ben Forta's Homepage</TITLE>
</HEAD>

Регулярное выражение <[tT][iI][tT][lL][eE]>.*</[tT][iI][tT][lL][eE]> соответствует открывающему тегу <TITLE> (на верхнем, нижнем или на обоих регистрах), закрывающему тегу <TITLE> и любому тексту между ними. Кажется, этот шаблон работал правильно.

Однако так ли это на самом деле? Ведь нужен был текст заголовка, а найденный нами текст содержит также открывающий и закрывающий теги заголовка. Можно ли возвратить только текст заголовка?

Одно из решений может использовать подвыражения (см. урок 7, "Использование подвыражений"). С помощью подвыражений можно разбить найденный текст на три части: открывающий тег, текст и закрывающий тег. Разбив текст на части, можно извлечь только нужную часть.

Но едва ли стоит искать то, что фактически не нужно, только для того, чтобы потом вручную удалить найденную часть текста. На самом деле нужно найти способ создавать шаблон так, чтобы он содержал совпадения, которые не возвращаются, иными словами, совпадения, которые используются только для того, чтобы правильно найти местоположение основного совпадения, но сами вспомогательные совпадения не рассматриваются как части найденного совпадения. Другими словами, нужно просто посмотреть вокруг искомого текста.

Замечание

В этом уроке обсуждается просмотр вперед и назад. Просмотр вперед поддерживается во всех основных реализациях регулярных выражений, а просмотр назад поддерживается далеко не во всех реализациях.

Замечание

В языках Java, .NET, PHP и Perl поддерживается просмотр назад (в некоторых реализациях с ограничениями), а в JavaScript и ColdFusion — не поддерживается.

Просмотр вперед

При просмотре вперед определяется шаблон, который будет найден, но он не возвращается. Просмотр вперед фактически определяет подвыражение, так что часть, определяемая при просмотре вперед, форматируется как подвыражение. Синтаксически шаблон для просмотра вперед выглядит как подвыражение, которому предшествует ?=, а искомый текст следует после знака =.

Замечание

Иногда в документации по регулярным выражениям используется термин потреблять (consume) для обозначения того, что будет найдено и возвращено; о том же, что было найдено в результате просмотра вперед, говорят, что оно не потребляется, не используется (not consume).

Вот пример. Следующий текст содержит список URL, и в каждом URL нужно извлечь часть, соответствующую протоколу (возможно, чтобы знать, как обработать эти URL).

Текст

http://www.forta.com/ 
https://mail.forta.com/ 
ftp://ftp.forta.com/

Регулярное выражение

.+(?=:)

Результат

http://www.forta.com/ 
https://mail.forta.com/ 
ftp://ftp.forta.com/

В перечисленных URL протокол отделен от имени хоста двоеточием :. Шаблон .+ соответствует любому тексту (http в первом соответствии), а подвыражение (?=:) соответствует :. Но заметьте, что это двоеточие : не было найдено; ?= указывает, что механизм регулярных выражений должен установить соответствие с двоеточием : и в то же время продолжать просмотр вперед (и потому не потреблять (не использовать) двоеточия).

Чтобы лучше понять, что делает ?=, рассмотрим тот же самый пример, на сей раз без метасимволов просмотра вперед:

Текст

http://www.forta.com/ 
https://mail.forta.com/ 
ftp://ftp.forta.com/

Регулярное выражение

.+(:)

Результат

http://www.forta.com/ 
https://mail.forta.com/ 
ftp://ftp.forta.com/

Подвыражение (:) правильно устанавливает соответствие с двоеточием :, но найденный текст используется и возвращается как часть найденного совпадения.

Различие между этими двумя примерами состоит в том, что для того чтобы установить соответствие с двоеточием :, в предыдущем примере используется шаблон (?=:), а в последнем примере используется шаблон (?=:). Эти оба шаблона соответствуют тому же самому тексту; они оба устанавливают соответствие с двоеточием : после протокола. Различие состоит в том, включается ли найденное двоеточие : в найденный (возвращаемый) текст. При использовании просмотра вперед синтаксический анализатор регулярных выражений смотрит вперед, чтобы обработать соответствие с двоеточием :, но не обрабатывает его как искомую часть. Выражение .+(:) находит текст вместе с двоеточием :. Выражение .+(?=:) находит текст до двоеточия :, но само двоеточие : не включает в найденный (возвращаемый) текст.

Замечание

Операция просмотра вперед (а также и просмотра назад) фактически возвращает результат, но эти результаты всегда имеют длину 0 символов. Поэтому иногда операции поиска контекста называются операциями нулевой длины или ширины (zero-width).

Замечание

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

Просмотр назад

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

Замечание

Как отличать операторы ?= и ?<= друг от друга? Вот способ запомнить, что означают эти операторы: тот, который содержит стрелку, указывающую назад (символ <), является оператором просмотра назад.

Оператор ?<= используется так же, как и ?=; этот оператор используется в подвыражении и после него следует текст, с которым устанавливается соответствие.

Рассмотрим пример. Пусть в результате поиска в базе данных получен список товаров, а извлечь нужно только цены.

Текст

АВС01: $23.45 
HGG42: $5.31 
CFMX1: $899.00 
ХТС99: $69.96 
Total items found: 4

Регулярное выражение

\$[0-9.]+

Результат

АВС01: $23.45 
HGG42: $5.31 
CFMX1: $899.00 
ХТС99: $69.96 
Total items found: 4

Метасимвол \$ соответствует $, а выражение [0-9.]+ соответствует цене.

Этот шаблон отработал правильно. Но что делать, если символы $ в найденном тексте не нужны? Можно ли просто опустить \$ в шаблоне?

Текст

АВС01: $23.45 
HGG42: $5.31 
CFMX1: $899.00 
ХТС99: $69.96 
Total items found: 4

Регулярное выражение

[0-9.]+

Результат

АВС01: $23.45 
HGG42: $5.31 
CFMX1: $899.00 
ХТС99: $69.96 
Total items found: 4

Это, очевидно, не сработало. Действительно, \$ необходим для того, чтобы указать, с каким текстом нужно установить соответствие, хотя возвращать $ не требуется.

Решение состоит в том, чтобы установить соответствие при просмотре назад следующим образом:

Текст

АВС01: $23.45 
HGG42: $5.31 
CFMX1: $899.00 
ХТС99: $69.96 
Total items found: 4

Регулярное выражение

(?<=\$)[0-9.]+

Результат

АВС01: $23.45 
HGG42: $5.31 
CFMX1: $899.00 
ХТС99: $69.96 
Total items found: 4

Этим мы добились цели. Шаблон (?<=\$) соответствует \$, но не потребляет его, и именно поэтому шаблон возвращает только цены (без ведущих знаков \$).

Сравните первое и последнее выражения, используемые в этом примере. Выражение \$[0-9]+ находит $, за которым следует цена в долларах. Выражение (?<=\$)[0-9.]+ также соответствует $, за которым следует цена в долларах, Различие между этими двумя выражениями состоит не в том, что они нашли при выполнении поиска; а в том, что они включили в результаты. Предыдущее выражение нашло $ и включило его в результат. Последнее выражение определило местонахождение $ только для того, чтобы найти цены, перед которыми стоял найденный знак $, но сам знак $ в результаты не включило.

Замечание

Шаблоны, используемые при просмотре вперед, могут иметь переменную длину; они могут содержать, например, . и +. Вследствие этого им могут соответствовать самые различные тексты.
Несколько иная ситуация при просмотре назад. Обычно требуется, чтобы шаблоны, используемые при просмотре назад, имели фиксированную длину. Это ограничение налагается почти во всех реализациях регулярных выражений.

Совместное использование просмотра вперед и просмотра назад

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

Текст

<HEAD>
<TITLE>Ben Forta's Homepage</TITLE>
</HEAD>

Регулярное выражение

(?<=\<[tT][iI][tT][lL][eE]>).*(?=</[tT][iI][tT][lL][eE]>)

Результат

<HEAD>
<TITLE>Ben Forta's Homepage</TITLE>
</HEAD>

Выражение отработало правильно. Шаблон (?<=<[tT][iI][tT][lL][eE]>) определяет операцию просмотра назад, которая устанавливает соответствие с открывающим тегом заголовка <TITLE> (но не потребляет его); подобным образом (?=</[tT][iI][tT][lL][eE]>) устанавливает со¬ответствие с закрывающим тегом заголовка </TITLE>. Возвращается только текст заголовка (поскольку это все, что было использовано).

Замечание

Обратите внимание, что в предыдущем примере знак < (первый символ, с которым устанавливается соответствие) защищен для того, чтобы предотвратить двусмысленность. Поэтому написано (?<=\<, а не (?<=<.

Отрицание поиска контекста, или негативный поиск контекста

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

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

Вы, возможно, ожидали, что для обозначения отрицания поиска контекста используется знак ^, но это не так; синтаксис отрицания поиска немного отличается от синтаксиса отрицания набора. Для инвертирования операции поиска контекста используется знак ! (который заменяет знак =). В табл. 9.1 приведен список всех операций поиска контекста.

Таблица 9.1. Операции поиска контекста

Класс Описание
(?=) Положительный просмотр вперед
(?!) Отрицательный (негативный) просмотр вперед
(?<=) Положительный просмотр назад
(?<!) Отрицательный (негативный) просмотр назад

Замечание

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

Чтобы продемонстрировать различие между положи¬тельным и отрицательным просмотрами назад, рассмотрим пример. Следующий блок текста содержит числа — цены и количества фруктов. Сначала мы получим только цены:

Текст

I paid $30 for 100 apples, 50 oranges, and 60 pears. 
I saved $5 on this order.

Регулярное выражение

(?<=\$)\d+

Результат

I paid $30 for 100 apples, 50 oranges, and 60 pears. 
I saved $5 on this order.

Этот пример очень похож на пример, рассмотренный ранее. Шаблон \d+ соответствует числам (одной или нескольким цифрам), а шаблон (?<=\$) организовывает просмотр назад, чтобы установить соответствие со знаком $ (но не потребляет его). (Этот знак защищен и потому задан как \$.) Следовательно, эти два найденных числа представляют собой цены, а не количества фруктов.

Теперь мы сделаем противоположный поиск: определим местонахождение только количеств фруктов, а не цен:

Текст

I paid $30 for 100 apples, 50 oranges, and 60 pears. 
I saved $5 on this order.

Регулярное выражение

\b(?<!\$)\d+\b

Результат

I paid $30 for 100 apples, 50 oranges, and 60 pears. 
I saved $5 on this order.

И снова, \d+ соответствует числам, но на сей раз были найдены только количества фруктов, а не цены. Выражение (?<!\$) организует негативный просмотр назад, в результате которого соответствие будет установлено только тогда, когда числам не предшествует $. Замена знака = в выражении просмотра назад изменяет тип шаблона с положительного на негативный.

Может возникнуть вопрос: почему в этом примере в шаблоне отрицательного просмотра назад указаны границы слова (с помощью \b)? Чтобы понять, почему это необходимо, рассмотрим тот же самый пример без этих границ:

Текст

I paid $30 for 100 apples, 50 oranges, and 60 pears. 
I saved $5 on this order.

Регулярное выражение

(?<!\$)\d+

Результат

I paid $30 for 100 apples, 50 oranges, and 60 pears. 
I saved $5 on this order.

Без границ слова был также найден 0 в $30. Почему? Потому что перед ним не стоит знак $. Чтобы решить эту проблему, мы указали, что границы текста, соответствующего шаблону, находятся на границах слова.

Резюме

Просмотр вперед и назад позволяет более гибко управлять тем, что возвращается в случае установления совпадения. Операции поиска контекста позволяют использовать подвыражения для того, чтобы определить местоположение текста, с которым будет установлено соответствие, но не использовать этот текст (иными словами, найти некоторый текст, но не включать его в тот текст, который будет выдан в качестве результата). Положительный просмотр вперед определяется с помощью (?=), а отрицательный просмотр вперед — с помощью (?!). Некоторые реализации регулярных выражений также поддерживают просмотр назад, определяемый с помощью (?<=), и отрицательный просмотр назад, определяемый с помощью (?<!).