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

Урок 8. Использование ссылок назад

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

Понятие ссылки назад

Лучше всего объяснить необходимость в ссылках назад на примере. В HTML-документах заголовочные теги (от <H1> до <H6> вместе с соответствующими закрывающими тегами) позволяют определить и отформатировать тексты заголовков на Web-страницах. Предположим, что нужно определить местонахождение всех текстов заголовков, независимо от уровня заголовка. Вот пример:

Текст

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
</BODY>

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

<[hH]1>.*</[hH]1>

Результат

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
</BODY>

Шаблон <[hH]1>.*</[hH]1> соответствует первому заголовку (от <H1> до </H1>) и соответствует также <h1> (HTML-теги не зависят от регистра). Но каким должен быть шаблон, чтобы он соответствовал любому заголовку (который может относиться к любому из шести правильных (допустимых) уровней заголовков)?

Можно, конечно, было бы использовать простой диапазон вместо 1. Тогда получилось бы вот что:

Текст

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
</BODY>

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

<[hH][1-6]>.*?</[hH][1-6]>

Результат

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
</BODY>

Это, кажется, сработало; <[hH][1-6]> соответствует любому тегу, открывающему заголовок (<H1> и <H2> в этом примере), a </[hH][1-6]> соответствует любому тегу, закрывающему заголовок (</H1> и </H2>).

Замечание

Обратите внимание, что здесь использовался ленивый квантор .*?, а не жадный .*. Как объяснялось в уроке 5, "Повторение совпадений", кванторы типа * являются жадными, и потому шаблон <[hH][1-6]>.*</[hH][1-6]> может соответствовать всему тексту от открывающего тега <H1> во второй строке до закрывающего тега </H2> в шестой строке. Использование ленивого квантора .*? вместо жадного решает эту проблему.
Обратите внимание: шаблон может соответствовать, а не будет соответствовать, потому что в этом конкретном примере шаблон, вероятно, работал бы даже с жадным квантором. Метасимвол . обычно не соответствует концам строк, а в данном примере каждый заголовок находится в своей собственной строке. Однако никаких неудобств от использования здесь ленивого квантора нет, поэтому лучше перестраховаться, чем потом жалеть.

Успех? Не совсем. Рассмотрим следующий пример (используя тот же шаблон):

Текст

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>

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

<[hH][1-6]>.*?</[hH][1-6]>

Результат

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>

Тег заголовка, начинающийся с <H2> и заканчивающийся </H3>, недопустим, и все же шаблон, используемый здесь, нашел его.

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

Соответствие со ссылками назад

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

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

Посмотрим, как это работает. Вот некоторый текст, содержащий три пары повторно напечатанных слов, все эти пары нужно найти:

Текст

This is a block of of text, several words here 
are are repeated, and and they should not be.

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

[ ]+(\w+)[ ]+\1

Результат

This is a block of of text, several words here 
are are repeated, and and they should not be.

Шаблон, очевидно, работал, но как он работал? Выражение [ ]+ соответствует одному или нескольким пробелам, \w+ соответствует одному или нескольким алфавитно-цифровым символам, а [ ]+ соответствует любым пробелам в конце. Но заметьте, что шаблон \w+ заключен в круглые скобки, потому он является подвыражением. Это подвыражение используется не для того, чтобы найти повторные совпадения; никаких символов повторения здесь нет. Подвыражение используется для того, чтобы сгруппировать символы в одно выражение, пометить его и идентифицировать для использования в дальнейшем. Заключительная часть этого шаблона — \1; это и есть ссылка назад, на подвыражение, и всякий раз, когда (\w+) соответствует слову, тому же слову соответствует и \1. Например, когда (\w+) соответствует слову of, \1 также соответствует of, а когда (\w+) соответствует слову and, \1 также соответствует слову and.

Замечание

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

Что же в точности означает \1? Это выражение соответствует первому подвыражению, используемому в шаблоне. Выражение \2 соответствует второму подвыражению, \3 — третьему, и т.д. Таким образом, [ ]+(\w+)[ ]+\1 соответствует любому слову, после которого следует то же самое слово. Именно поэтому в предыдущем примере использовался этот шаблон.

Замечание

Ссылки назад подобны переменным.

Теперь, когда вы знаете, как используются ссылки назад, давайте повторно рассмотрим пример с заголовками HTML-документа. Используя ссылки назад, можно создать шаблон, который найдет любой открывающий тег заголовка и соответствующий закрывающий тег (и игнорирует любой непарный тег). Вот пример:

Текст

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>

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

<[hH][1-6]>.*?</[hH]\1>

Результат

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>

Замечание

К сожалению, синтаксис ссылок назад очень зависит от реализации регулярных выражений. В JavaScript для обозначения ссылки назад служит символ \, а в операциях замены используется $), те же самые обозначения применяются в Macromedia ColdFusion и vi. В Perl используется $ (так что вместо \1 используется $1). В .NET реализация регулярных вы¬ражений возвращает объект, обладающий свойством по имени Groups {Группы), которое содержит пары, так что match.Groups[1] относится к первому совпадению в С#, a match.Groups(1) относится к тому же самому совпадению в Visual Basic .NET. В языке PHP эта информация возвращается в массиве, названном $matches, так что $matches[1] относится к первому соответствию (хотя это поведение может быть изменено с помощью флажков). В языках Java и Python возвращается объект с соответствиями, который содержит массив group.
Особенности реализации обсуждаются в приложении А, "Регулярные выражения в популярных приложениях и языках".

Снова были найдены три совпадения: одна пара <H1> и две пары <H2>. Как и ранее, шаблон <[hH][1-6]> соответствует любому открывающему тегу заголовка. Но в отличие от прежнего шаблона, диапазон [1-6] заключен в скобки ( и ), поэтому он рассматривается как подвыражение. Следовательно, шаблон для закрывающего тега заголовка может именовать это подвыражение как \1 в </[hH]\1>. Подвыражение ([1-6]) соответствует цифрам от 1 до 6, и поэтому \1 соответствует только та же самая цифра, которую нашло это подвыражение. По этой причине <H2>This is not valid HTML</H3> не соответствует шаблону.

Замечание

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

Замечание

Отсчет соответствий обычно начинается с 1. Во многих реализациях совпадение с 0 может использоваться для ссылки на все выражение.

Замечание

На подвыражения ссылаются по их относительным позициям: \1 служит для ссылки на первое подвыражение, \5 — для ссылки на пятое подвыражение и т.д. Хотя обычно этот синтаксис поддерживается, он на самом деле имеет один серьезный недостаток: перемещение или редактирование подвыражений (и таким образом изменение порядка подвыражений) может нарушить шаблон, а добавление или удаление подвыражений может быть даже еще более проблематичным.
Чтобы избавиться от этого недостатка, в некоторых более новых реализациях регулярных выражений поддерживается захват по именам (named capture) — возможность, благодаря которой каждому подвыражению можно дать уникальное название (имя); впоследствии имя может использоваться для того, чтобы обратиться к подвыражению (по этому имени, а не по его относительной позиции). Захват по именам не обсуждается в этой книге, потому что он все еще широко не поддерживается, а его синтаксис значительно зависит от реализации, которая действительно поддерживает захват по именам. Однако если ваша реализация (.NET, например) поддерживает захват по именам, его функциональные возможности нужно использовать обязательно.

Выполнение операций замены

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

Для простых замен текста регулярные выражения не нужны. Например, для замены всех вхождений СА на California, a MI — на Michigan регулярные выражения определенно не нужны. Хотя такие операции с регулярными выражениями вполне законны, у них нет никаких преимуществ, и на самом деле было бы проще использовать любые доступные обычные функции для манипуляций со строками.

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

Текст

Hello, ben@forta.com is my email address.

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

\w+[\w\.]*@[\w\.]+\.\w+

Результат

Hello, ben@forta.com is my email address.

Этот шаблон идентифицирует адреса электронной почты в тексте (см. урок 5, "Повторение совпадений").

Но что, если нужно сделать так, чтобы по любому адресу электронной почты, встречающемуся в тексте, можно было отправить письмо? В HTML-документе вы бы использовали тег <A HREF="mailto:user@address.com">user@address.com</A>, чтобы создать адрес электронной почты, активизирующийся по щелчку на нем. Может ли регулярное выражение конвертировать (автоматически преобразовать) адрес в приведенный выше формат адреса, который активизируется по щелчку на нем? Конечно, да, и притом очень легко, если использовать ссылки назад:

Текст

Hello, ben@forta.com is my email address.

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

(\w+[\w\.]*@[\w\.]+\.\w+)

Замена

<A HREF="mailto:$1">$1</A>

Результат

Hello, <A HREF="mailto:ben@forta.com">ben@forta.com</A> is my email address.

В операциях замены используются два регулярных выражения: одно — для того, чтобы определить шаблон поиска, а второе — для того, чтобы указать, чем заменить найденный текст. Ссылки назад могут содержать шаблоны, так что подвыражение, найденное в первом шаблоне, может использоваться во втором шаблоне. (\w+[\w\.]*@[\w\.]+\.\w+) — тот же самый шаблон, который использовался ранее (чтобы определить местонахождение адреса электронной почты), но на сей раз он определен как подвыражение. Благодаря этому найденный текст может использоваться в шаблоне замены. В <A HREF="mailto:$1">$1</A> используется найденное подвыражение дважды: один раз в атрибуте HREF (чтобы определить mailto:) и другой раз в качестве текста, активизируюемого по щелчку. Так ben@forta.com превращается в <A HREF="mailto:ben@forta.com">ben@forta.com</A> — а это именно то, что нужно.

Замечание

Как уже отмечалось, обозначение ссылки назад зависит от используемой реализации. В JavaScript применяется знак $, а не \. В ColdFusion применяется \ и для операций поиска, и для операций замены.

Замечание

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

Рассмотрим еще один пример. Информация о пользователях хранится в базе данных, причем номера телефона хранятся в формате 313-555-1234. Необходимо переформатировать номера телефонов, чтобы представить их в виде (313) 555-1234. Вот пример:

Текст

313-555-1234
248-555-9999
810-555-9000

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

(\d{3})(-)(\d{3})(-)(\d{4})

Замена

($1) $3-$5

Результат

(313) 555-1234
(248) 555-9999
(810) 555-9000

Здесь снова используются два шаблона регулярных выражений. Первый выглядит намного более сложным, чем он является на самом деле, так что давайте разберем его. Выражение (\d{3})(-)(\d{3})(-)(\d{4}) соответствует номеру телефона, но разбивает его на пять подвыражений (чтобы найти его части). Первое подвыражение (\d{3}) соответствует первым трем цифрам, второе подвыражение (-) соответствует - и т.д. Конечный результат состоит в том, что номер телефона разбивается на пять частей (для каждой части есть ее собственное подвыражение): код города, дефис, первые три цифры номера, другой дефис, а затем заключительные четыре цифры. Эти пять частей могут использоваться независимо, причем так, как необходимо, так что ($1) $3-$5 просто переформатирует номер, используя только три подвыражения и игнорируя другие два. В результате 313-555-1234 превращается в (313) 555-1234.

Замечание

При переформатировании текста часто полезно разбить текст на большое количество небольших подвыражений, чтобы облегчить переформатирование.

Замена регистра

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

Таблица 8.1. Метасимволы замены регистра

Метасимвол Описание метасимвола
\E Заканчивает преобразование \L или \U
\l Конвертирует (преобразовывает) следующий символ в символ нижнего регистра
\L Конвертирует (преобразовывает) все символы до \E в символы нижнего регистра
\u Конвертирует (преобразовывает) следующий символ в символ верхнего регистра
\U Конвертирует (преобразовывает) все символы до \E в символы верхнего регистр

Чтобы изменить регистр следующего символа, \l и \u помещаются перед символом (или выражением). Метасимволы \L и \U конвертируют (преобразовывают) регистр всех символов, пока не встретится метасимвол завершения этой операции \E.

Приведем простой пример, в котором текст внутри пары заголовочных тегов уровня <H1> преобразовывается к верхнему регистру:

Текст

<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>

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

(<[hH]1>)(.*?)(</[hH]1>)

Замена

$1\U$2\E$3

Результат

<BODY>
<H1>WELCOME TO MY HOMEPAGE</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>

Шаблон (<[hH]1>)(.*?)(</[hH]1>) разбивает заголовок на три подвыражения: открывающий тег, текст и закрывающий тег. Второй шаблон затем собирает текст снова $1 содержит открывающий тег, \U$2\ конвертирует (преобразовывает) второе подвыражение (текст заголовка) к верхнему регистру, а $3 содержит закрывающий тег.

Резюме

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