Ключевые слова:regex, php, (найти похожие документы)
From: Sergey Kolesnichenko
Newsgroups: http://detail.phpclub.net
Date: Mon, 20 Sep 2004 18:21:07 +0000 (UTC)
Subject: Регулярные выражения в PHP
Оригинал: http://detail.phpclub.net/article/regexp_1http://detail.phpclub.net/article/regexp_2
Регулярные выражения, Часть I
Sergey Kolesnichenko
2004-03-23
Глава 1. Шерлок Холмс спешит на помощь вебпрограммисту или регулярные
выражения на пальцах.
Введение.
Каждый веб-программист сталкивался с задачей, когда в произвольном
тексте нужно найти какие-то данные по какому-то закону, проверить
данные, которые поступили от пользователя, подвергнуть найденные
данные сложной модификации. Можно изобретать велосипед, а можно
использовать средства, которые используют программисты всего мира.
Иной раз кажется, что профи пользуются какими-то инструментами,
приемами, которые доступны только им. Разочарую читателя, что профи
используют те же средства и инструменты, что и вы, только разница
состоит в том, что они ими умеют пользоваться и умеют выбирать, какой
инструмент стоит использовать в конкретном случае.
Данный материал призван помочь программистам решать насущные задачи
при помощи регулярных выражений. Я постараюсь описать самые основы
использования этого инструмента, чтобы вы не смотрели на комбинацию
подобную этой: /^(?:http:\/\/)?[-0-9a-z._]*.\w{2,4}[:0-9]*$/ как баран
на новые ворота.
Общая задача механизма регулярных выражений - находить или не находить
совпадения строки или ее части с шаблоном. Проанализируем первое
предложение этого абзаца на предмет непонятных или пугающих слов:
"Механизм регулярных выражений" и "шаблон" - вот два слова, которые
меня повергли в уныние, когда я понял, что без использования
регулярных выражений мне не обойтись. Имеем какой-то механизм, который
что-то ищет и находит либо ищет и не находит, с этим "что-то" связаны
такие понятия как строка и шаблон. Вот с них и будем разбираться и ими
же и закончим, потому что после того, как мы разберемся, что делает
этот механизм со строками и шаблонами, нам не надо будет лезть в
учебники по математике и искать, что означают слова "регулярные
выражения".
Часть 1.
Где мы видели шаблон? Пойдем к знакомой секретарше и спросим у нее.
Правильно, ответ - шаблоны Microsoft Word! Чем отличается шаблон
"Календарь" от шаблона "Изысканное резюме"? Данными и способом их
подачи. Человек, который хотя бы раз видел и то и другое, без труда
будет отличать календарь от резюме. Так почему тогда регулярные
выражения пугают программиста? Ведь это почти то же самое! О чем
думает человек, который видит календарь и знает что это такое, как он
его узнает? Календарь - это документ, который разделен на блоки,
каждый блок состоит из цифр, которые соответствуют дням месяца.
Каждому месяцу соответствует только один блок, в месяце бывает не
больше 31 дня, в феврале не бывает больше 28 (за исключением
високосного года), дни, которым соответствует день недели воскресенье
или государственный праздник выделены красным, можно продолжить
систематизировать данные дальше, указав месяцы в которых ровно 30 и 31
день. Что мы сделали? Мы создали описание календаря, по-другому, мы
описали данные, найдя которые в произвольном тексте, можно с
определенной уверенностью сказать, что перед нами календарь. Подобное
описание я называю шаблоном в контексте разговора о регулярных
выражениях.
Грядет день, когда программист будет говорить компьютеру, какую он
хочет написать программу, а компьютер будет ее писать, но пока что
программисту приходится трудиться самому. Т.е. если я скажу, что ищу
отрывок текста, который удовлетворяет описанию календаря, то в наше
время компьютер меня не поймет. Садитесь поудобней, ибо если гора не
идет к Магомету, то Магомету идет к горе. Как все программисты к горе
пойдем сидя перед монитором. Наша задача - научиться описывать данные,
которые мы хотим найти, в понятной компьютеру форме. А что вы еще не
умеете? Стыдно! Это тоже умеет каждая секретарша. Надеюсь, что такое
командная строка программисту объяснять не надо. Start=>Run=>cmd.
Наберем в командной строке слово dir.
Вот, что я получил:
C:\Documents and Settings\Administrator>dir
Volume in drive C has no label.
Volume Serial Number is 3CC6-6445
Directory of C:\Documents and Settings\Administrator
13.10.2003 18:03 <DIR> .
13.10.2003 18:03 <DIR> ..
18.07.2003 21:55 <DIR> .java
18.07.2003 21:54 <DIR> .javaws
18.07.2003 21:55 <DIR> .jpi_cache
15.10.2003 16:33 694 .plugin141.trace
05.10.2003 11:40 <DIR> Desktop
16.10.2003 13:08 <DIR> Favorites
08.10.2003 16:42 <DIR> My Documents
18.08.2003 20:51 <DIR> Start Menu
04.07.2003 21:24 <DIR> WINDOWS
1 File(s) 694 bytes
10 Dir(s) 2 162 040 832 bytes free
Знакомо? Естественно! Считайте, что вы уже умеете пользоваться
регулярными выражениями, осталось только совершенствовать свои навыки.
Что вы сделаете, если файлов и директорий много, а вам нужно проверить
только те, которые вас интересуют непосредственно? Вы попробуете
уменьшить количество выводимых данных, указав условие поиска, описав
данные, которые хотите получить. Обратите внимание, что нужно
описывать данные, а значит, вы стоите на пороге создания шаблона.
Допустим, нас интересуют все файлы и директории, которые имеют
название, которое имеет в себе слово java. Уверен, что вы думаете так
же, как и я и получаете вот такой результат:
C:\Documents and Settings\Administrator>dir *java*
Volume in drive C has no label.
Volume Serial Number is 3CC6-6445
Directory of C:\Documents and Settings\Administrator
18.07.2003 21:55 <DIR> .java
18.07.2003 21:54 <DIR> .javaws
0 File(s) 0 bytes
2 Dir(s) 2 161 618 944 bytes free
Попробуем перевести строку dir *java* на русский язык: Найти и
показать все файлы и директории в текущей директории, в названии
которых присутствует слово java.
Вроде правильно, а вот и нет! Понимание регулярных выражений
заключается в первую очередь в правильном описании закона совпадения
(либо несовпадения), а также знания средств, которыми это описание
можно "рассказать" компьютеру. Большинство статей в Сети занимаются
решением второго пункта, и полностью опускают первый, они рассказывают
программисту при помощи каких средств можно "рассказать" компьютеру
свое описание интересующих данных. Почему я сказал, что описание
неправильное? Потому что dir java* отличается одним символом от dir
*java*, но под описание русским языком тоже попадает, ведь слово
"java" в названии присутствует?
Делаем вторую попытку описания строки dir *java*
Найти и показать все файлы и директории в текущей директории (и до
этого момента все идет правильно), название которых начинается на
какой угодно символ, таких символов может быть сколь угодно (в том
числе их может и не быть), но после них обязательно идут подряд
символы "j", "a", "v", "a", после которых может идти какой угодно
символ, этих символов может быть сколько угодно (а может вообще не
быть).
Отличается? Еще как! Продолжаем учиться описывать данные, которые мы
хотим найти. Ищем командой dir, работаем с командной строки. Но
задание усложним. Теперь я дам описание данных, которые хочу найти, а
вы попробуете это сделать, все инструменты вам знакомы.
Найти и показать все файлы и директории в текущей директории, название
которых начинается на какой угодно символ, таких символов может быть
сколько угодно (а может не быть вообще), но после них обязательно идет
символ "j", после которого снова идет любое количество каких угодно
символов, но в конце названия стоят подряд символы "w" и "s".
Если вы читали внимательно, то без труда введете: dir *j*wsи получите
ответ:
C:\Documents and Settings\Administrator>dir *j*ws
Volume in drive C has no label.
Volume Serial Number is 3CC6-6445
Directory of C:\Documents and Settings\Administrator
18.07.2003 21:54 <DIR> .javaws
0 File(s) 0 bytes
1 Dir(s) 2 161 504 256 bytes free
Просто? Еще бы! А теперь представьте, что я хочу найти не сколько
угодно, а определенное количество каких угодно символов. Для этого
используется символ "?" (знак вопроса). Вот и задача:
Найти и показать все файлы и директории в текущей директории, название
которых начинается на какой угодно символ, таких символов может быть
сколько угодно, но после них обязательно идет символ "j", после
которого идет ровно четыре любых символа, а после них стоят подряд
символы "w" и "s". Ее решение: dir *j??ws
C:\Documents and Settings\Administrator>dir *j?ws
Volume in drive C has no label.
Volume Serial Number is 3CC6-6445
Directory of C:\Documents and Settings\Administrator
18.07.2003 21:54 <DIR> .javaws
0 File(s) 0 bytes
1 Dir(s) 2 161 504 256 bytes free
Первая часть закончена. Краткий итог:
* данные для поиска поступают на вход в нашем случае команды dir в
каком-то унифицированном виде, который я назвал описанием данных
или шаблоном;
* механизмом вывода команды dir можно управлять, меняя шаблон;
* команда dir выводит данные, только если они удовлетворяют
заданному шаблону;
* в шаблон входят как обычные буквы, так и специальные символы.
Часть 2.
В шаблон входят как обычные буквы, так и специальные символы, в
прошлой части это была последняя строка, она же первая в этой. Вы сами
не заметили, что мыслите абстрактно, описывая данные, которые вы
хотите найти. Пока вас нужно к этому подталкивать, но скоро вы
научитесь это делать самостоятельно, и я вам уже буду не нужен.
Оказывается, что обычные символы и специальные символы имеют название.
Каждый символ в отдельности называется литералом, каждый специальный
символ называется метасимволом. Оставим в покое литералы, они пока нам
не интересны, а лучше разберемся с метасимволами. Пока мы их знаем
два, но нам и этого хватает.
Посмотрите, что обозначает символ "*" и символ "?". Первый означает
любое количество любых символов, второй означает только один любой
символ. Ну, если с количеством пока все понятно, то вот что означает
"любой символ"? Спрашиваете - отвечаем! "Любой символ" означает в
нашем контексте работы с командой dir из командной строки любой из
литералов. А значит звездочка "*" означает "сколько угодно литералов",
"?" означает один литерал. Понятно? Вроде. Понятно, но не совсем
определени дано не совсем точно. А почему? Потому что когда я описывал
данные для поиска командой dir, я писал примерно вот так: на какой
угодно символ, таких символов может быть сколь угодно (в том числе их
может и не быть)
Звездочка "*" означает сколько угодно литералов, а равно и их
отсутствие! Что это значит? Это значит, что литералов может быть
сколько угодно, но для совпадения при поиске не требуется ни один! И
это нужно понимать и запомнить.
Знак вопроса "?" означает, что литералов может быть только один! Т.е.
на данной позиции может стоять только один литерал, не больше и не
меньше!
Рассмотрим подробнее понятие литерал, немного с другой стороны. Что из
этих двух символов литерал, а что метасимвол? "j" "*"
Элементарно, Ватсон, "j" - литерал, "*" - метасимвол.
Зреет вполне логичный вопрос, а как отличил?
На логичный ответ даем логичный ответ. Мы знаем, что символы "*" и "?"
имеют какое-то магическое действие, по крайней мере команда dir их
воспринимает по-другому, не так как симоволы "j", "a", "v", "a". Вот
мы и разделили все символы на два класса, один - литералы, второй -
метасимволы. Мы точно знаем, что в класс литералов входят все символы
латиницы a, b, c, d и так далее до z, а так же входят цифры 0, 1, 2 и
так до 9. Т.е класс литералов мы разбили еще на подклассы. Вам не
кажется, что мы уже слишком много знаем, чтобы до сих пор возиться с
командной строкой? Пора уже попробовать свои знания.
Кратко итог второй части:
* обычные символы называются "литералами";
* специальные символы называются "метасимволами";
* литералы означают сами себя;
* метасимволы предназначены для описания диапазнона литералов,
каких-то применимых к литералам условий, свойств литералов, их
количества;
* все литералы можно классифицировать, собрав их вместе по какому-то
признаку.
Часть 3.
Ну наконец-то этот автор перестал рассказывать байки про командную
строку, а начал что-то говорить про то, ради чего собственно писалась
статья, использование регулярных выражений в веб-программировании.
Остановитесь и подумайте, все ли понятно в предыдущих двух частях?
Если нет, прочтите их еще раз, проделайте все примеры,
эксперементируйте! Читать часть 3 без понимания предыдущих частей не
советую, потратите время зря.
Из великого и могучего языка PHP нам понадобится только одна функция:
preg_match()
Ее общий формат таков:
preg_match("шаблон_поиска", "строка_в_которой_проводится_поиск",
массив_с_результами_поиска)
Эта функция реализует обращение к механизму обработки регулярных
выражений, поиск совпадения в строке и возврат совпадений в массив.
Так. Стоять! Что значит обращение к механизму, поиск и возврат? Мы
привыкли оперировать немного другими понятиями. Вернемся снова к
команде dir. В ее состав входит какой-то механизм, который читает Ваш
шаблон и ищет по нему файлы и директории в текущей директории, ведь
так? Для простоты назовем этот механизм тоже механизмом обработки
регулярных выражений. Представим себе, что названия файлов и
директорий записаны в строку, шаблон поиска записан в другую строку, а
результаты выводятся не на черный экран, а в массив, и для поиска
информации о файлах и директориях мы используем не команду dir, а
функцию PHP preg_match(). Все стало на свои места. Функция
preg_match() передает шаблон для поиска и строку, в которой будем
искать, механизму поиска совпадений (механизму обработки регулярных
выражений), и обеспечивает вывод результатов поиска в массив. Вылитая
команда dir! Теперь понятно, почему я в начале сказал, что вас не
нужно учить пользоваться регулярными выражениями, вы уже знаете, как
это делается, просто не осознаете сам факт. Ну вот, поздравляю, вы
теперь осознаете и факт. На этом можно было бы и закончить, если бы
возможности поиска в PHP ничем не отличались от возможностей команды
dir. Но это не так, поэтому будем учиться дальше и будем читать Главу
2.
Глава 2. Основы.
Еще одно введение
Регулярные выражения - язык шаблонов (на самом деле это математический
термин, кому интересно, читайте про детерминированные и
недетерминированные конечные автоматы)
Для того, чтобы выполнить какое-то действие, надо указать какое
именно, обычно действие указывается функцией:
В РНР это будет, например, вот так:
* preg_replace - заменить
* preg_match - найти соответствие
* preg_split - разбить
тоже самое с функциями стандарта Posix типа ereg, только они
отличаются тем, что в отличие от Preg у них другой механизм обработки
поиска совпадений.
Обработка шаблона происходит посимвольно, как вы будете искать букву d
в слове stadium? правильно, первое, что приходит на ум, это перебрать
все буквы слова stadium и сравнить их с буквой, которую ищем, итак
получаем простой перебор. Считайте, что вы уже научились использовать
РВ, только осталось выучить как задавать шаблон того, что вы хотите
найти в строке.
Шаблон - это своеобразный указатель, что искать в строке.
Искать можно цифры, буквы, невидимые символы (пробел, таб)
Как вы в слове stadium будете искать букву d либо букву m пока не
произойдет первое совпадение одного из символов? правильно, тоже
перебором, возьмете каждую букву слова и будете сравнивать с тем, что
вам надо найти поочередно с d и c m. Но вам каждый раз теперь придется
сравнивать букву слова (строки) с двумя буквами условия поиска. Таким
образом, вы создали свой первый символьный класс, который на языке
регулярных выражений записывается вот так: [dm] - это означает, что вы
ищете либо d либо m
Что нужно, чтобы указать, что вы ищете в строке то, что может быть
любой буквой алфавита? надо либо перечислить их все [abc....xyz] либо
просто написав интервал [a-z]
Внимание смертельный номер, такое возможно с русскими буквами [а-я] а
так же с цифрами [0-9] но есть еще заглавные [A-Z] т.е. чтобы получить
символьный класс со всеми буквами латинского алфавита надо поставить в
шаблоне [a-zA-Z]
Но вот такой символьный класс описывает только один символ, а у вас в
строке их вон сколько, это решается при помощи квантификаторов.
Квантификаторы
Итак, продолжаем, как я уже говорил, что условие поиска можно описать
при помощи символьных классов.
Один символьный класс может совпасть только с одним символом! - это
надо понимать!
Как в условии задать поиск двух символов?
Приведу простой пример без символьных классов, который уже был
затронут выше:
Ищем последовательность iu в слове stadium. Как вы это сделаете? Ну
естественно перебором:
1. берем первый символ слова и сравниваем его с первым символом
условия 's' неравно 'u'
2. берем второй символ слова и сравниваем его с первым символа
условия 't' неравно 'i'
3. делаем так до тех пор, пока j-й символ в слове не совпадет с
первым символом условия поиска. В этот момент 'i' равно 'i' (мы
находимся в выделенной позиции в слове stadium, и выделенной
позиции условия поиска iu ), как только выполнилось это условие,
надо взять следующий символ из слова и из условия (предварительно
запомнив, где произошло первое совпадение, очень важный шаг!)
4. сравниваем следующий символ слова со вторым символом условия 'u'
равно 'u' Поздравляю, исходная комбинация найдена!
5. Что делать если условие 4 не выполнилось, если вместо слова
stadium нам подсунули слово с ошибкой stadiem?
Вернитесь к шагу 3 и вспомните, какой важный шаг вы сделали. Вы
запомнили, где нашли первое совпадение. Совпадения следующего символа
слова со следующим символом условия не произошло, поэтому надо взять
следующий символ слова, в нашем слове с ошибкой это будет stadiem и
сравнить снова с первым символом условия! и продолжать выполнять пункт
3. до конца слова, понятно, что совпадение найдено не будет.
Искать по двум определенным символам мы уже научились, теперь будем
учиться пользоваться символьными классами.
Допустим, у нас есть строки:
abcd12345efg
fghi56789qwe
Условие: Найти в строках участки, которые состоят из четырех любых
букв латинского алфавита, после которых следуют пять любых цифр.
Выше я рассказывал, как описать символ, который будет совпадать с
любой латинской буквой, напомню, для этого мы используем символьный
класс: [a-z] (пока не обращаем внимания на то, что есть еще заглавные
буквы) символ условия, который совпадет с любой цифрой, описывается
вот таким символьным классом: [0-9]
Вернемся к первоначальному заданию. Нам надо найти строки с четырьмя
буквами вначале, сразу после которых идут пять цифр.
Из того, что мы уже знаем, можно написать:
[a-z][a-z][a-z][a-z][0-9][0-9][0-9][0-9][0-9]
Посмотрите сами, такая форма записи условия поиска будет работать! так
как мы описали каждым символьным классом один символ в условии поиска.
Вам не кажется, что слишком громоздкое условие поиска получается? И
тут на помощь приходят квантификаторы. Вспоминаем английский: quantity
- количество. Т.е. квантификатор - то, что выражает количество
чего-то, в нашем случае количество символов в условии поиска. Упрощаем
условие поиска при помощи квантификаторов: [a-z]{4}[0-9]{5}
Все! кто не догадался, в фигурных скобках написано, сколько символов,
описанных в символьном классе, может идти подряд в строке в которой
ищем совпадение.
Вот и получается, что после пяти любых букв латинского алфавита идет 5
любых цифр. Поиск происходит точно также как я описывал выше, только
символы в условии поиска задаются не явно, а при помощи символьных
классов.
Каждый символьный класс описывает только один символ, количество
схожих символов идущих подряд описывается квантификаторами.
Естественно подобное задание количества символов в условии поиска не
является единственным. Квантификаторы бывают разные! [a-z]{1,3}
означает, что подряд может идти от одного до трех букв латинского
алфавита. [a-z]{2,} означает, что может идти минимум 2 буквы
латинского алфавита подряд.
Но квантификатор в фигурных скобках не является единственным способом
задать количество символов идущих подряд, которые описаны символьным
классом. [a-z]* означает, что подряд может идти сколь угодно букв
латинского алфавита, а может быть, что и ни одной, идентично [a-z]{0,}
. [a-z]+ означает, что обязательно подряд должна идти минимум одна
буква латинского алфавита, но максимальное количество не указано,
идентично [a-z]{1,} [a-z]?
означает, что количество латинских букв не должно превышать 1, буква
также может вообще отсутствовать, идентично [a-z]{0,1}.
Применение квантификаторов к литералам
Вернемся в самое начало статьи, где мы искали перебором букву d в
слове stadium. Условие поиска совпадения, которое описывается одним
неспециальным символом, называется литералом.
Есть строки:
abcdefg
abcddefg
abcdddefg
abcddddefg
Требование: Написать условие поиска совпадения для всех строк.
Решение: abcd{1,4}efg
Только что вы видели, как я применил квантификатор к литералу. К
литералам можно применять любой из вышеприведенных квантификаторов.
Ясно, что применив условие поиска avcd{1,4}efg для строки abcefg,
совпадение найдено не будет, так как квантификатор {1,4}
подразумевает, что после abc перед efg идет миним одна, максимум
четыре буквы d.
Символьные классы "для продвинутых"
Вы уже увидели, как символьные классы облегчают описание условия
совпадения. Разберемся с ними окончательно.
Что может входить в символьный класс? В символьный класс может входить
любой литерал, а так же интервалы литералов. Для описания интервалов
литералов используется символ '-', который ставится между первым
символом интервала и последним. Приведу примеры задания различных
интервалов в одном символьном классе: [1-5] - числа в диапазоне от 1
до 5 [a-f] - буквы латинского алфавита от a до f [a-fq-x] - буквы
латинского алфавита от a до f и от q до x как вы заметили, в последнем
символьном классе я использую два диапазона.
Представим, что вам нужно описать условие, что в определенном месте
строки могут стоять символы: либо a, либо g, либо 7, либо 4. Что
делать? Писать символьный класс: [ag47]
Объяснение простое, в символьном классе можно перечислять допустимые в
условии поиска литералы. Перечисление литералов можно совмещать с
указанием интервалов: [14a-kz] - это означает, что символ в строке
может совпадать с 1, 4, буквами латинского алфавита c a по k, а так же
с буквой z. Естественно литералами могут быть не только буквы и цифры,
а так же знаки препинания, математические знаки, например ','
(запятая), '!' (восклицательный знак), '+' (плюс), '-' (минус). Стоп,
скажете вы, какой минус, если он же используется для описания
интервалов. Правильно, если вы поставите минус между a и z, то это
будет интервал, но если вы поставите минус сразу же после открытой
квадратной скобки, то это будет минус! И вот пример: [-,a-z] -
означает, что в символьный класс входят минус, запятая, а так же буквы
латинского алфавита от a до z.
А как написать символьный класс, в который входят все символы кроме
заданных? Например, все кроме a, b, c? Для этого существует
специальный спецсимвол отрицания: ^ (крышка) Пишем: [^abc] - все
символы (не буквы, а именно символы) кроме букв латинского алфавита a,
b, c.
Запомнить все!
Наверное, у многих возникает вопрос, что если надо проверить всю
строку на соответствие условию, а вернуть при помощи функций языка
только часть строки?
Как при помощи функций языка выполнить операцию не над всей строкой,
которая удовлетворяет условию, а только над ее частью?
Специально для таких целей существуют группирующие и одновременно
сохраняющие круглые скобки.
Пример:
Строка может состоять из 5-ти букв латинского алфавита, после которых
следует знак минуса, после которого следуют четыре цифры от 1 до 8.
Задание:
1. проверить строки на соответствие условию
2. вернуть в программу четыре цифры, что на данном этапе будет
означать только то, что эти четыре цифры надо как-то выделить в
условии поиска
Если мы просто хотим проверить строку на соответствие условию
совпадения, то условие поиска будет вот таким: [a-z]{5}[1-8]{4} но нам
надо запомнить цифры, поэтому добавляем инструкцию запоминания:
[a-z]{5}([1-8]{4})
Как видите, то, что надо запомнить, выделено круглыми скобками. Внутри
функции, которая будет выполнять операцию со строкой при помощи
вышеприведенного условия, совпадение будет запоминаться в специальных
переменных, в PHP к ней можно обращаться через \1 в Perl - $1. В одном
условии поиска может быть несколько инструкций запоминания:
([a-z]{5})([1-8]{4}) - проверит строку на совпадение с условием, в
случае удачного совпадения, запомнит пять букв в \1 ($1), четыре цифры
в \2 ($2). Если обратиться к переменной \0, то окажется, что в ней
хранится вся совпавшая строка, которая была описана условием.
Ближе к реалиям:
Тот, у кого хватило желания дочитать до этого места, уже много
понимает, пытается писать свои собственные условия поиска по строке,
но у него не получается. Скорее всего это происходит из-за того, что
вы не знаете многих мелочей работы с регулярными выражениями. Про
некоторые я расскажу прямо сейчас. Строка имеет начало и конец. Вы
скажете, что это понятно каждому, но скорее всего вам придется немного
изменить свое понятие о строках. Считайте, что вначале каждой строки и
в конце каждой строки стоит невидимый символ. При поиске в реальной
программе это надо учитывать, поставив в начале условия поиска символ
'^' (начало строки), а в конце условия поиска '$' (конец строки).
Поэтому пример с пятью буквами и четырьмя цифрами (как и все остальные
после них вышеприведенные) надо переписать: ^[a-z]{5}[1-8]{4}$ теперь
при помощи этого условия на самом деле можно проверить строки
asbvc1234
sdwtsv1234
на соответствие условию.
Не стоит путать '^' (крышку) в символьном классе и ее же при описании
условия совпадения. Крышка, поставленная вначале условия, поиска
соответствует началу строки, крышка, поставленная после квадратной
скобки открывающей описание символьного класса, будет спецсимволом
отрицания (все, кроме того, что присутствует в символьном классе),
крышка, поставленная на любой другой позиции внутри символьного
класса, кроме первой, будет просто литералом.
Ваше условие поиска может не работать по причине того, что вы не
подозреваете о количестве, а так же виде пробельных символов, которые
находятся в строке. Пробелы описываются либо литералом, просто в
условии поиска ставьте пробел, либо видимой комбинацией символов: \s
Считайте, что \s соответствует литералу 'пробел', либо литералу
'перевод строки', либо литералу 'табулятор'.
В программах для того, чтобы показать,где начинается условие поиска
(регулярное выражение), используют разделители.
Пример:
preg_match("/^[a-z0-9]/", $string,$mathces);
посмотрите что указано в кавычках первого аргумента функции, сначала
идет слеш, потом символ начала строки, потом идет символьный класс,
потом идет снова слеш. Вот именно первый и последний слеш
символизируют, что внутри них заключено регулярное выражение. Это
пришло из Perl, где сразу за разделитемя еще можно указывать
модификаторы, про которые позже. На данном этапе нужно знать, что в
записи условия поиска нужны ограничители (два идентичных символа, не
обязательно слеш, многие используют тильду)
Примеры к главе
Все примеры взяты из вопросов, которые люди задают на различных
форумах. Примеры буду разбирать на том языке, на котором сам пишу:
PHP.
1. Очень часто встречается ситуация, когда надо написать регистрацию
на ресурсе, который вы поддерживаете. В силу каких-либо причин
выдвигаются требования к введенной информации. Например, часто
спрашивают, как проверить, что пользователь ввел в качестве ника
(nickname, login) слово, состоящее только из букв латинского
алфавита и цифр.
Проверку условия приведу сразу внутри кода, чтобы было понятно,
что и куда писать:
<?php
$user = $_POST['username'];
if(!preg_match("/^[a-zA-Z0-9]+$/", $user)) {
echo "Имя пользователя задано в неправильном формате";
} else {
echo "Имя пользователя задано в правильном формате";
}
?>
А теперь разбираем само регулярное выражение.
Так как регистрационное имя пользователя должно состоять из
латинских букв, а также цифр, то надо написать символьный класс,
который будет удовлетворять этому условию: [a-zA-Z0-9] в этот
символьный класс входит три интервала, первый интервал a-z (все
символы от маленькой буквы a до маленькой буквы z), второй
интервал A-Z (аналогично, но с большими буквами), третий интервал
0-9 (цифры от 0 до 9). Мы описали только одну букву, из которой
может состоять регистрационное имя, но таких букв может быть... а
теперь как раз надо ответить на вопрос, сколько таких букв может
быть? Да сколько угодно, скажете вы, а я скажу, что вы неправы.
Регистрационное имя должно состоять минимум из одной буквы! и я
считаю, чти это обязательное условие при прохождении регистрации,
поэтому надо данный факт описать. Вспоминаем про квантификаторы:
[a-zA-Z0-9]+
Плюс '+', как раз тот квантификатор, который говорит, что в
строковой переменной $user должен быть минимум один символ,
который соответствует условию.
А теперь надо сказать регулярному выражению, что условию должна
соответствовать вся строка, от начала до конца, поэтому добавляем
в регулярное выражение символ начала строки '^' вначале
регулярного выражения и символ конца строки '$' в конец:
^[a-zA-Z0-9]+$
Теперь надо объяснить функции preg_match, что строка
^[a-zA-Z0-9]+$ является регулярным выражением, надо поставить
ограничители, я ставлю слеш '/':
preg_match("/^[a-zA-Z0-9]+$/",$user)
2. Есть люди, которые сами себе усложняют жизнь, эти люди называются
программистами и системными администраторами, причем часто они
усложняют жизнь не только себе, а еще и друг другу, а тут еще и
босс приходит и видит, что на сайте компании можно
регистрироваться, а имя пользователя еще как-то хитро проверяется,
в 80% случаев вышеприведенная задача усложняется боссом, надо либо
расширить набор символов, что делается простым добавлением
символов в символьный класс, либо ужесточаются условия. Например,
ставится условие, что имя пользователя может состоять как из букв
латинского алфавита, так и из цифр, но первый символ имени
пользователя должен быть обязательно буквой латинского алфавита!
Дописываем регулярное выражение: ^[a-zA-Z][a-zA-Z0-9]*$
Как уже давно известно, символьный класс описывает только один
символ. Наша задача как раз в том и состоит, чтобы описать один
символ (первый): [a-zA-Z] тут два интервала, описывают первый
символ и дают понять, что никаких цифр в первом символе
проверяемой строки быть не может.
Из размышлений в предыдущем примере явно следовало, что
регистрационное имя пользователя должно быть длинной минимум в
один символ. Мы уже описали первый символ строки, но он может быть
и единственным, после него может быть сколько угодно символов,
либо может их вообще не быть, но регулярное выражение совпадет
только тогда, когда все последующие символы будут удовлетворять
условию, что они являются либо буквами латинского алфавита, либо
цифрами. [a-zA-Z0-9] Первый обязательный символ описан, остальные
символы не обязательны, поэтому меняем квантификатор с '+' на '*',
Ставим символы начала и конца строки: ^[a-zA-Z][a-zA-Z0-9]*$
Глава 3.
После изучения основ работы, стоит перейти к практическому применению
регулярных выражений. Но если посмотреть на большинство регулярных
выражений с теми знаниями, которые у вас есть, получится, что они до
сих пор являются набором значков, правда, некоторые из них уже
узнаваемы. В этой части я буду, базируясь на том, что рассказал в
предыдущей главе, расширять ваш кругозор.
Спецсимволы.
Как показать невидимое? Как указать, например, что в условии поиска
присутствует пробел? Наверное. многие догадываются, что для того,
чтобы показать невидимое, надо сделать его видимым, т.е. ввести
какой-то символ, набор символов, которые будут интерпретироваться, как
невидимые. Вот с этими знаниями мы и пришли к изучению спецсимволов в
регулярных выражениях.
* \s - если вы в условии поиска поставите друг за другом символ
обратного слеша, а после него сразу букву s, то таким образом вы
опишите либо пробел, либо символ табуляции. Конечно в условии
поиска можно поставить пробел так, как вы его обычно ставите на
письме, но запись [a-z\s] будет намного понятнее и читабельней чем
[a-z ], с первого взгляда видно, что в первый символьный класс
входит пробел, а вот со вторым символьным классом надо
присмотреться, а так как регулярные выражения итак представляют
для многих набор значков, то пропустить пробел, поставленный таким
образом будет очень просто. Внимательно используйте этот
спецсимвол, так в дополнении к тому, что он совпадает с пробелом и
табулятором, он совпадет также с символом новой строки.
* \S - скажу просто, что в своем большинстве это видимые символы,
т.е. все, что не совпадает с \s
* \w - спецсимвол, который призван заменить целый символьный класс,
в него входят все символы, которые могут входить в слово, обычно
это [a-zA-Z_], хотя много может зависеть от установленной локали,
поддержки юникода и т.д.
* \W - все что не входит в определение \w. либо [^a-zA-Z_]
* \d - все цифры т.е. уже известный вам символьный класс [0-9]
* \D - все, что не является цифрой
Как видите, часто спецсимволы описывают какие-то частоиспользуемые
множества символов, которые используются программистами каждый день,
но эти множества имеют границы, т.е. либо цифры, либо буквы и
подчеркивание, либо невидимые символы, но как описать все символы?
Просто! Надо поставить точку!
А вы спросите тогда, а что делать, если надо описать именно точку, а
не все символы? Поставьте перед точкой обратный слеш: \.
Но вы не унимаетесь и хотите знать, что делать если вы ищите в тексте
обратный слеш, после которого идет точка. Все просто, как видите,
обратный слеш, входит практически в любой спецсимвол, а так же нужен,
чтобы из спецсимвола сделать литерал, значит этот обратный слеш, сам
по себе является спецсимволом, и для того, чтобы превратить его в
литерал, надо следовать тем же правилам, которые были использованы при
превращении точки из спецсимвола в литерал, а именно перед
спецсимволом, поставить еще один спецсимвол, чтобы больше не
выдумывать никаких специсимволов, решили пользоваться тем же слешем.
Итак чтобы поставить обратный слеш в виде литерала в условии
поиска,надо его удвоить вот так: \\
Аналогично, чтобы поставить два обратных слеша их надо тоже удвоить
вот так: \\\\
Альтернативы
Быть или не быть... Извечная альтернатива! После прочтения этой части,
вы сможете самостоятельно записать гамлетовскую альтернативу при
помощи регулярных выражений. Сначала как всегда разберемся с данными,
которые надо обработать. У Гамлета был выбор между быть (be) и не быть
(not to be). В любом случае он получал на выходе какое-то решение,
которое равнялось true :) Чтобы описать условие выбора при помощи
регулярных выражений надо сначала определиться между чем и чем
выбирать будем. В случае Гамлета мы выбираем между группами литералов,
одна группа состоит из двух литералов идущих друг за другом b и e,
вторая группа состоит из девяти литералов идущих друг за другом: n, o,
t, \s, t, o, \s, b, e
Понятно, что \s представляет собой один пробел. Я, кажется, употребил
новое слово, на которое вы не обратили свое внимание: группа
литералов. Группа литералов - последовательность символов, которые
описаны либо символьными классами, либо собственно литералами. Группу
литералов описывают в круглых скобках, они же сохраняют совпавшую
группу литералов в специальных переменных, о которых я писал в
предыдущей статье. Вот примеры групп литералов:
(be)
(not\sto\sbe)
Теперь вернемся к выбору из двух групп литералов: (be)|(not\sto\sbe)
вот эта палочка | между группами литералов и есть условие выбора,
читается как 'или'. Теперь представьте, что ответ Гамлета обрабатывает
компьютер, которому известно, что может быть только два ответа либо
"быть" либо "не быть". Все остальные ответы просто не принимаются за
ответы как таковые. Пишем регулярное выражение проверки ответов
Гамлета:
preg_match("/^(be)|(not\sto\sbe)$/", $alternate, $answer);
Регулярное выражение совпадет в случае
* если $alternate равно "be"
* если $alternate равно "not to be"
Вначале объяснения я вопрошал, между чем выбирать будем. Думаю, самое
время разобраться, между чем вообще можно выбирать. Выбирать можно
между литералами и между группами литералов. Как уже было сказано,
группы литералов объединяются круглыми скобками, если надо выбрать
между одиночными литералами, то два литерала между, которым стоит
вертикальная черта, надо сгруппировать скобками.
Пример выбора из двух литералов: s(o|u)n совпадет как с son, так и с sun.
Пример выбора из двух групп литералов: (son)|(sun) аналогично совпадет
с son и с sun
В случае с выбором между группами литералов, либо между одиночными
литералами, литералы объединяются при помощи круглых скобок, но в
первой статье в части Запомнить все, я сказал, что все, что
заключается в круглые скобки, запоминается в специальных переменных.
Запоминается, значит использует ресурс "память". Что делать, если нам
не нужно запоминать выделенные группы литералов, на надо их
сгруппировать? Надо заставить скобки не запоминать! Делается это при
помощи последовательности ?: вот так: (?:be)|(?:not\sto\sbe) теперь ни
группа литералов "be" ни группа литералов "not to be" не будут
заноситься в переменные \1, \2 (Perl $1, $2), но зато будут отлично
группироваться.
Понятно, что, если надо поставить литерал "|", то надо перед ним
поставить спецсимвол, который будет обозначать, что в данном случае
вертикальная черта является литералом. Этим спецсимволом служит
обратный слеш: \|
Пример к главе
У администратора есть список пользователей, которые генерирует
программа, либо утилита, генерирует она этот список в формате:
фамилия имя отчество
фамилия и о
фамилия и.о.
Как всегда боссу требуется какая-то статистика, но его абсолютно не
интересуют ни имена ни отчества, ему нужна фамилия и инициалы в виде:
фамилия ио.
Надо что-то делать... я сделал это вот так (цикл перебора по всем
строкам, которые генерирует утилита, напишете сами):
preg_match("/([^\s]+)\s+([^\s.])[^\s.]*(?:\s|\.)([^\s.])[^\s.]*/",$income_str,$out_arr);
print_r($out_arr);
Сначала разберемся с данными, которые поступают на вход, чтобы
регулярное выражение срабатывало в 100% случаев. Данные не надо
проверять на совпадение с условием, так данные хранящиеся в системе
прошли эту проверку при их вводе. И фамилия и имя и отчество могут
состоять из любых символов, так как мы не знаем, какие символы
разрешает система, и какие символы ввел в систему пользователь, но
достоверно известно, что фамилия отделена от имени пробелом, имя и
отчество могут быть полными, могут сотоять из одной буквы-сокращения,
могут быть разделены пробелом, либо после каждой буквы стоит точка.
Если вы научитесь сами сотавлять подобные описания данных, по которым
вам нужно проводить поиск, то большая часть вашей работы сделана,
потому что осталось только записать предыдущее предложение на языке
регулярных выражений:
* [^\s] - любой символ, который не является пробелом! включая символ
новой строки, можно было написать \S без крышки вначале
символьного класса, но это на любителя.
* [^\s]+ - минимум один символ, который не является пробелом, т.е.
фамилию мы уже описали.
* \s+ - минимум один пробел между фамилией и именем
Для обозначения имени исползуется несколько способов:
* полное имя и после него пробел
* первая буква имени и после него пробел
* первая буква имени и после него точка
Что надо искать? Все, после пробела между фамилией и именем и до
следующего пробела, либо точки, причем имя или его сокращение состоит
минимум из одной буквы.
* [^\s] - все что не пробел, первый обязательный символ имени
(сокращения)
* [^\s.] - все что не пробел и не точка, в символьном классе точка -
литерал
* [^\s.]* - в случае полного имени, это будет означать, что надо
найти все, что не точка и не пробел и идет после обязательного
первого символа имени, который мы описали выше
Получается, что имя мы описали вот такой конструкцией: [^\s.][^\s.]*
но первую букву имени (даже если там сокращение) по условию задачи
надо запомнить: ([^\s.])[^\s.]*
Что идет после имени? в любом случае там идет либо пробел, либо точка.
Значит нужно пройти этот участок так, чтобы выполнилось условие выбора
из двух символов: (\s|\.) - выбор между двумя литералами
осуществляется путем группировки их в круглые скобки и написания между
литералами вертикальной черты. Если в строке гарантировано будет
только один пробел, то можно так и оставить, если вы не уверены,
поставьте символ '+' после \s: (\s+|\.)
Вам нужно запоминать что стояло между фамилией и отчеством, пробел или
точка? Мне нет! Поэтому из сохраняющих и группирующих скобок делаем
только группирующие: (?:\s|\.)
А теперь посмотрите на отчество, с точки зрения регулярных выражений,
символы, из которых оно составлено, подчиняется тем же правилам, что и
имя, поэтому описание следующей части регулярного выражения и до конца
смотрите выше.
Регулярные выражения, Часть II
Использование позиционных проверок
Многим материал этой статьи покажется ненужным, так как большинство
задач с использованием регулярных выражений решаются средствами,
которые я описал в предыдущей.
Сначала немного отвлечемся. Как вы опишите что-то неприметное, либо
что вообще трудно описать? Думаю, что надо описать что-то приметное,
либо то, что легко описать, а потом указать, где находится искомое
"что-то неприметное" относительно описанного "приметного".
Пример: Мой директор спрашивает, как найти такой-то банк в Киеве. Я
знаю, что объяснить местоположение банка голландсокму директору,
используя транслитерированные русские названия, невозможно! Но я
программист и объясняю, что нужный банк находится на центральной
площади. В тоже время площадь описываю словами, что с одной стороны
стоит женщина на огромной колонне, а с другой стороны Мак Дональдс.
Даже некиевляне догадались, что я имею в виду Площадь Независимости.
В синтаксисе регулярных выражений есть возможность описать, что стоит
до, а что стоит после того участка строки, который нам нужно найти.
Считайте, что есть возможность, не называя точного адреса, описать
местоположение банка. Если сумеете описать, между чем и чем находится
банк, а также сумеете описать, что это именно банк, а не кафе, то
считайте, что вам не нужно давать точный адрес.
Каждый раз при помощи регулярных выражений происходит поиск
совпадений. Поиск проводится по строке, в строке ищется подстрока.
Теперь научимся задавать условие, что должно совпасть перед подстрокой
и после нее. Условие совпадения, которое идет перед подстрокой,
называют ретроспективной проверкой, условие совпадения, которое идет
после подстроки, называется опережающей проверкой. Можно объяснить
также, что ретроспективная проверка совпадает слева, опережающая -
справа.
Позитивные и негативные проверки
Хорошо, если можно описать, что следует перед искомой подстрокой, а
что после. А что если нужно описать, что НЕ идет перед либо после
искомой подстроки? Для этого опережающую и ретроспективную проверку
делят еще каждую на две:
* Опрежающая позитивная, опережающая негативная проверки.
* Ретроспективная позитивная, ретроспективная негативная проверка.
Опережающая позитивная проверка указывает, что условие описывает, что
должно обязательно присутствовать после искомой подстроки. Опережающая
негативная проверка описывает, что ни в коем случае не должно стоять
после искомой строки. Аналогично с ретроспективной проверкой только
условие ищется перед искомой подстроки.
Ретроспективная проверка
Позитивная ретроспективная проверка либо проверка слева описывается
специальной последовательностью символов: (?<=) скобки группируют
условие, вопросительный знак, показывает, что содержимое скобок надо
рассматривать не как группировку символов, чтобы их запомнить, а
группировку символов для выполнения над выражением внутри скобок
других действий. Вспомним, что просто скобки описывают группу символов
и запоминают в переменные все, что в них находится, а скобки с
вопросом и двоеточием указывают на специальное действие: группировка
без запоминания. Символ(ы) после вопрсительного знака воспринимаются
как указание, какое именно действие надо выполнить. Итого: знак
больше-равно идущий после вопросительного знака, который стоит внутри
круглых скобок сразу после открывающейся скобки показывает, что
последующие символы являются позитивной ретроспективной проверкой,
т.е. условием, что должно обязательно идти перед подстрокой, которую
мы ищем.
Ретроспективная негативная проверка описывается подобной
последовательностью символов: (?<!) Посмотрите, что знак равно
сменился на восклицательный знак и проверка сразу поменяла
"полярность" т.е. из позитивной превратилась в негативную. Логика
простая если вспомнить, что для проверки условия "неравно", отрицания
равно, во многих языках программирования используется
последовательность символов: !=
Опережающая проверка
Если вы поняли описание ретроспективной проверки, то понять суть
опережающей проверки вам будет несложно.
Для описания позитивной опережающей проверки используется
последовательность символов: (?=) все как и раньше, скобки указывают
что символы группируются для какого-то действия, вопросительный знак
указывает на то, что следующим символом будет символ, который
описывает, какое именно действие будет выполнено над группой символов,
либо как будет трактоваться данная группа символов. Символ '=',
который идет после вопросительного знака, указывает, что выражение в
скобках - опережающая проверка, проверка того, что должно совпасть
справа от искомой подстроки.
Для описания негативной опережающей проверки используется
последовательность символов: (?!) тоже самое, только знак равно
заменили отрицанием - восклицательным знаком.
Пример No.1
Часто возникает проблема по парсингу интересующих программиста данных
из HTML, который не всегда хорошего качества, все было бы терпимо,
если бы еще не вставки на javascript'е, вот пример такого текста:
<TD>20.02<BR>05:30
<TD class=l>Товар 1<BR>Товар 2
<TD><B>35</B>
<TD><A href="http://ссылка/" id=sfsd32dfs
onclick="return m(this)">26.92</A><BR><A href="http://ссылка/"
id=r3_3143svsfd onclick="return m(this)">27.05</A>
<TD><B>270.5</B>
</TR>
Те цифры, которые написаны через точку, являются ценами. Задача
состоит в том, чтобы собрать все цены, которые находятся между тегами
<a>... </a>
Видим, что помимо цен между заданными тегами, есть такие, которые идут
сразу после тега <TD>, а также стоят между тегами <B>...</B>. Ясно,
что описать достаточно точно содержимое атрибутов тега <A>
представляется задачей не самой легкой, поэтому надо ее упростить!
Любой тег имеет закрывающий знак '>', наша задача описать, что этот
знак идет перед ценой, но так как перед ценой может стоять тег <B> и
тег <TD>, но эти цены нам не нужны. Каким образом мы узнаем, что цена
стоит между тегами <A>...</A>? По тегу, который идет после цены, если
это не тег </B>, то это будет либо тег </A> либо <BR>, а так же по
тегу перед ценой если этот тег <TD>.
Путем таких размышлений мы пришли к выводу, что должно стоять справа,
а что должно стоять слева искомой строки, которая описывается как
цифры, разделенные точкой: \d*\.\d*
То, что должно совпасть слева, мы описали как символ '>', записываем:
(?<=>) - выглядит немного странно, но совпадение справа записывается
вот так (?<=), а внтури него после ?<= идет символ '>'
То, что должно совпасть спарава описывается (?=) внутри мы пишем </A>.
Теперь опишем, что не должно стоять перед ценой: (?<!<TD>) перед ценой
не должен стоять тег <TD>, это и есть негативная ретроспективная
проверка.
При помощи негативной опережающей проверки опишем, что не должно
стоять справа цены: (?!<\/B>) справа от цены не должен стоять тег
</B>.
Результирующее регулярное выражение, которое описывает все приведенные
условия выглядит вот так:
preg_match_all("/(?<!<TD>)(?<=>)\d*\.\d*(?!<\/B>)(?=<\/A>)/", $string, $matches);
print_r($matches);
После рассмотрения первого примера стоит сделать замечания и пояснения
по поводу использования позиционных проверок.
1. Написанные друг за другом проверки применяются независимо друг от
друга в одной точке, не меняя ее. Естественно, что совпадение
будет найдено, если все проверки совпадут. В нашем примере это
были точки перед и после цены. С точки зрения логики применения
проверок нет никакой разницы, будет ли стоять проверка на тег <TD>
перед проверкой на знак '>'. Правда, с точки зрения оптимизации
первой позиционной проверкой должна идти та, которая имеет
наибольшую вероятность несовпадения.
2. Совпавшие значения ретроспективных проверок не сохраняются. Т.е.
если в нашем примере совпадает опережающая проверка, которая
указывает, что после цены идет тег </A>, то сам тег </A>, который
заключен в конструкцию (?=) не будет запоминаться в специальных
перменных /1,/2 и т.д. Сделано это из-за того, что позиционная
проверка совпадает не со строкой, а с местом в строке (она
описывает место, где произошло совпадение, а не символы, которые
совпали).
3. Нужно указать что PCRE не позволяет делать проверки на совпадение
текста произвольной длинны. То есть нельзя делать, например, такую
проверку: /(?<=\d+)
Механизм поиска совпадения в ретроспективной проверке реализован так,
что при поиске механизму должна подаваться строка фиксированной длины,
для того, чтобы в случае несовпадения, механизм мог вернуться назад на
фиксированое количество символов и продолжить поиск совпадений в
других позиционных проверках. Думаю, что сразу это понять сложно, но
представьте себе как происходит поиск совпадения в части (?)(?<=>)
вышеописанного регулярного выражения. Берется строка, в которой
происходит поиск, отсчитывается от начала столько символов, сколько
символов будет в совпадении позиционной проверки, в нашем варианте это
4: <, T, D, > с этого места происходит "заглядывание назад"
(ретроспективные проверки на английском языке звучит как lookbhind
assertions), т.е. все предыдущие 4 символа проверяются на совпадение
со строкой <TD>, если механизм не нашел совпадения, то ему надо
вернуться на 4 символа назад, выполнить тоже самое с проверкой (?<=>),
т.е. отсчитать один символ, "заглянуть" назад, попробовать найти
проверку предыдущего символа с символом '>'. Представьте себе, что
условие совпадения состоит из строки нефиксированной длинны: (??)
подобная запись должна означать, что перед ценой, не должен стоять тег
<TD> в количестве максимимум один экземпляр (либо вообще не стоять).
Вот и получается, что после того, как механизм отсчитает 4 символа от
начала, он проверит на совпадение с <TD>, но в условии указано, что
тега может и не быть вообще, тогда возникает вопрос, на сколько знаков
верунться назад, чтобы проверить на совпадение другие проверки. На 4
или вообще не возвращаться?
Сразу возникает вопрос, а зачем идти вперед, чтобы потом "заглянуть"
назад? Делается это для того, чтобы в случае совпадения всех проверок
сразу же начать проверку тех символов, которые идут после позиционных
проверок.
Пример No.2
Как-то мне нужно было получить все изображения, которые использовались
на сайте. Что для этого надо сделать? Правильно, надо в браузере
нажать на "Сохранить как", указать куда сохранить страницу. Появится
файл с исходным кодом страницы и папка с изображениями. Но вы никогда
не сохраните в эту папку изображения, которые прописаны в стилях
объектов по крайней мере в эксплорере:
style="background-image:url(/editor/em/Unlink.gif);"
Для проведения вышеописанной операции надо:
1. попросить хозяина хоста использовать контент, размещенный на его
сайте.
2. найти в тексте все строки, подобные приведенной выше, и выделить в
них относительный путь к файлу
3. сформировать файл в котором будут выводиться изображения при
помощи <img src=полный_путь_к_изображению>
Делаем: В переменную $content получаем исходный код страницы. А дальше
используя регулярные выражения ищем относительные пути, которые
прописаны в стилях. Каждый раз, когда я описываю, как я реализовал
пример, я сначала тщательно описываю, что ищем, и тщательно описываю,
в каком контексте происходит поиск. Проанализировав исходный код
страницы стало понятно, что кроме как в описании стилей относительные
пути к изображениям нигде не используются. Слева от относительного
пути идет последовательность символов: url(
Справа от относительного пути стоит закрывающаяся круглая скобка.
Между этими последовательностями символов могут быть буквы латинского
алфавита, цифры и слеши, а также точка перед расширением файла.
Начнем с простого. Символы латинсокого алфавита, цифры, точка и слеш
описываются символьным классом: [a-z.\/] их может быть сколько угодно,
на самом деле больше 3 (имя файла, минимум один символ, точка,
расширение, минимум один символ), но в данном случае, зная контекст,
это некритично, поэтому указываем квантификатор * [a-z.\/]*
Слева должны идти 'url(' и мы это описываем при помощи позитивной
ретроспективной проверки: (?<=url()
Но обратите внимание на то, что скобка в регулярных выражениях
является спецсимволом группировки, поэтому чтобы она стала символом,
надо перед ней поставить другой спецсимвол - слеш. (?<=url\()
Справа от относительного пути должна стоять закрывающаяся круглая
скобка. Это условие описывается при помощи позитивной опережающей
проверки: (?=\))
Как видите, перед одной из скобок стоит слеш, что означает, что она
интепретируется не как спецсимвол, а как литерал.
Ниже приведен полный код на PHP, который выполняет все действия, кроме
вопроса о разрешении использовать контент:
preg_match_all("/(?<=url\()[a-z.\/]*(?=\))/i", $content, $matches);
foreach($matches[0] as $item) {
echo "<img src = http://www.a???s.ru".$item.">";
}
Когда и почему не следует использовать регулярные выражения
Эта заметка написана по мотивам обсуждений на одном из форумов и
призвана насторожить программистов, чтобы они не почувствовали
всемогущество регулярных выражений при работе со строковыми данными. Я
попытаюсь рассказать, в каком случае стоит поискать более приемлемое
решение своей задачи, чем использование регулярных выражений.
Описание проблемы поставленной на форуме было таким: Есть некая
страница, созданная в результате работы шаблонизатора, в ней
присутствуют элементы типа <input type=checkbox>, авторизированный
пользователь работает с этой страницей и отмечает какие-то чекбоксы
<input type=checkbox checked>, после отправки формы на сервер страницы
снова генерируются шаблонизатором и программист делает так, что эта
страница сохраняются в кеше, причем уже с отмеченными чекбоксами.
Задача программиста: Ему надо по результатам отправленной формы
запомнить выделенные чекбоксы и при последующем обращении этого
пользователя к этой странице выводить его выбор с уже отмеченными
элементами.
Что он делает: Вместо того, чтобы взять шаблон, выбрать из базы данные
и на основе их отметить заново чекбоксы, прогнав шаблон через
шаблонизатор, программист берет сохраненную в кеше страницу и парсит
ее при помощи регулярных выражений, в зависимости от данных, которые
он выбрал для данного пользователя из базы, он либо удаляет данные из
страницы либо добавляет новые 'checked' в нужных местах в тексте
страницы.
Его аргументация: Так быстрее работает чем обработка первоначального
шаблона.
Попробую привести свои аргументы против: Смысл кешировать, если потом
данные из кеша надо менять при помощи регулярных выражений? Кеш как
раз и нужен для того, чтобы лишний раз не гонять ресурсоемкие
процессы, такие как например обработка ВСЕГО текста регулярными
выражениями... по нескольку раз.
Представим себе текст, в котором n символов, а также регулярное
выражение, в котором
1. есть три условия выбора part1|part2|part3|
2. каждая из частей parti состоит из символьных классов, а также
заданных последовательностей литералов.
3. максимальная длинна совпадения регулярного выражения равна m
символов.
4. программист использовал функцию preg_replace, мы знаем принцип
работы механизма регулярных выражений, а значит можем повторить
его работу.
Приведу эти шаги одновременно с аргументами против подхода, который
хотел применить программист, которые из них вытекают:
1. берем символ из текста с индексом 1 пытаемся применить регулярное
выражение к тексту, который начинается с этого символа. Перебираем
каждый символ от начального и сравниваем его с шаблоном (подходит
ли).
2. следуя каждой инструкции выбора в регулярном выражении мы можем
совершить максимум три выбора и перебрать m последующих символов
для КАЖДОГО i-го символа текста.
3. несложно посчитать, какое количество символов будет перебрано
впустую, если совпадение не будет найдено.
4. закешированный документ уже является результатом работы
регулярного выражения (многие шаблонизаторы работают на регулярных
выражениях) таким образом просто напросто дополнительно вызывается
механизм регулярных выражений, чтобы отпарсить сырой
документ-полуфабрикат, вместо того, чтобы сразу его сделать
готовым за один проход.
5. если сравнить регулярные выражения, на которых работают
шаблонизаторы и регулярное выражение, которое получится у нашего
программиста, то сразу будет видно, что первые проще! По этой
причине совпадения будут находиться быстрее.
6. Часто работа регулярных выражений в шаблонизаторе будет
происходить с маленькими участками текста (не секрет, что страницы
могут собираються из маленьких кусочков), а не сразу к
сгенерированной странице.
Надеюсь, я смог объяснить, почему регулярные выражения, являсь мощным
средством работы со строками не подходят для решения подобных
глобальных задач, надо научиться ставить себе задачи и уметь решать ее
несколькими способами, тогда у вас появится выбор в сторону лучшего
способа решения. Регулярные выражения - это всего лишь один из многих
способов, но никак не панацея.
В заметке описан случай, когда человек, знал как пользоваться
регулярными выражениями, а также знал, как использовать в своей работе
шаблонизатор, т.е. у него был выбор, он просто не мог остановиться на
одном из вариантов. Есть категория начинающих программистов, которая
увидев, кучу значков, при помощи которых решаются много проблем со
строками, воздвигают себе кумира в виде статуи этому "волшебному
средству" и даже не догадываются, что их задача по замене слова на
другое слово в строке решается простой функцией str_replace(). Не
понимая, как работают регулярные выражения, они приходят на форум и
спрашивают, как заменить abcd на asdf при помощи preg_replace и очень
обижаются, когда дают ссылку на str_replace в мануале на
http://www.php.net/. Это наверное самый распространенный случай
неправильного применения регулярных выражений, когда их можно заменить
при помощи функций работы со строками.
долгое время рв были для меня камнем преткновения, теперь - нет! :) спасибо!! :) это действительно _понятная_ статья, помогающая разложить рв по полочкам, не то что у некоторых авторов с большими гонорарами :)
>Найти и показать все файлы и директории в
>текущей директории, название которых начинается >на какой угодно символ, таких символов может >быть сколько угодно, но после них обязательно >идет символ "j", после которого идет
ровно четыре любых символа,
>а после них стоят подряд >символы "w" и "s".
>Ее решение: dir *j??ws
Хочу также выразить свой респект автору. Действительно просто делает понятным механизм составления регэкспов во что никак не мог проникнуть пытаясь читать официозные талмуды на эту тему.
Облазил пол и-нета чтоб найти что-то стоящее по регексп и нашел вот эту статью - то что нужно, а самое главное что здесь приводятся не стандартные примеры (как например в мануалах)и так сказать алгоритмы мышления (например про описание того что нам нужно выделить), что является самым главным имхо
Вобщем спасибо (сорри за пеззнакоприпинательный текст ;) )
Статья супер! А кто автор? Что-то подписи негде не вижу, хотелось бы почитать и другие его статьи...
Если кому не лень киньте ссылку плиз!
a13ks3y@bk.ru
статья очень стоящая! при перечитывании заметил маленькую опечатку, а именно в этом куске текста
2 глава. основы
<<Ясно, что применив условие поиска avcd{1,4}efg <<для строки abcefg,
<<совпадение найдено не будет, так как <<квантификатор {1,4}
<<подразумевает, что после abc перед efg идет <<миним одна, максимум
<<четыре буквы d.
avcd{1,4}efg => abcd{1,4}efg
На самом деле данный раздел тяжело изучить, когда его излагают в академическом стиле, изощряясь при этом терминологией. Такое нужно действительно на пальцах объяснять как сделано тут. Большой респект автору.
Отличная статья! Я изучаю регулярные выражения для использования в Java и достаточно долго не мог найти подходящую статью. Теперь нашел. Желаю Вам успехов в работе, тока не расслабляйтесь. Еще пишите.
:)
Автору огромное спасибо за статью! Статья в закладках лежит уже с годик и я регулярно :) к ней обращаюсь. Служит своеобразным пособием-справочником по регулярным выражениям. Еще раз спасибо!
Модификаторы:
• i - этот модификатор обозначает что не нужно обращать внимания на регистр литералов.
• e - этот модификатор обозначает что строку php нужно воспринимать как сценарий.
условие поиска при помощи квантификаторов: [a-z]{4}[0-9]{5}
Все! кто не догадался, в фигурных скобках написано, сколько символов,
описанных в символьном классе, может идти подряд в строке в которой
ищем совпадение.
Вот и получается, что после пяти любых букв латинского алфавита идет 5
любых цифр. Поиск происходит точно также как я описывал выше, только
Добрый день у меня такой вопрос
у меня есть с генерированный отчёт в html
мне нужно в этом файле найти определённые строки с информацией как сделать такую чтуку не знаю
помогите пожалуйста дайте код
Огромное спасибо за статью. Все по полочкам разлажилось! Все кто прочтут будут ГУРУ в регулярных выражениях! Легко читать и сразу все понятно. Старался использовать filter_input, а теперь сам любой фильтр накатаю, и RewriteRule проще пойдет! Спасибо Огромное!
Автору огромное человеческое спасибо за такое простое и доступное изложение материала!!! Я абсолютно не знал РВ, но после прочтения многое стало понятно и все постепенно встает на свои места и раскладывается по полочка... [С-О!]
:-)
Снимаю шляпу перед Sergey Kolesnichenko за статью. regex давно применяю в Linux. Помог понять один затык который выплыл при разработке сайта о linux http://info-linux.ru/index Спасибо за подробный ман.