Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
<[Pp][^>]*?(?!)> соответствуют открывающие теги параграфов (такие как или ). Соответствие начинается с символов любые атрибуты (используется минимальный квантификатор) и, нако нец, закрывающий символ > при условии, что ему не предшествует сим вол слеша / (выполняется негативная ретроспективная проверка), по скольку наличие этого символа могло бы указывать на закрывающий тег параграфа. Второй вызов функции re.sub() использует это регу лярное выражение для замены каждого открывающего тега параграфа на два символа перевода строки (обычный способ разделения парагра фов в простых текстовых файлах). Третьему регулярному выражению <[^>]*?> соответствуют любые те ги, и оно используется в третьем вызове функции re.sub() для удале ния всех остальных тегов. Сущности HTML – это способ определить символы, не входящие в на бор ASCII, с помощью символов ASCII. Сущности могут записываться в двух формах: &name;, где name – это имя символа, например, сущность © обозначает символ ©, и digits, где digits – это десятичные циф ры, определяющие числовой код символа в кодировке Юникод, напри мер, ¥ обозначает символ Y =. Четвертый вызов функции re.sub() ис пользует регулярное выражение (\d+);, которому соответствуют чи словые формы сущностей, а сами цифры сохраняются в группе 1. Вме сто фактического текста замены мы передаем лямбдафункцию. Когда функция re.sub() получает другую функцию, она будет вызывать ее для каждого найденного совпадения, передавая ей объект совпадения в виде единственного аргумента. Внутри лямбдафункции мы извлека ем цифры (в виде строки), преобразуем их в целое число с помощью встроенной функции int() и затем с помощью встроенной функции Модуль для работы с регулярными выражениями 545 chr() получаем символ Юникода с заданным числовым кодом. Возвра щаемое значение функции (или, в случае использования лямбдавыра жения, результат выражения) используется в качестве текста замены. Пятый вызов функции re.sub() использует регулярное выражение &([A Za z]+); , которое сохраняет именованные сущности. Модуль html.entities из стандартной библиотеки содержит словари сущностей, включая name2codepoint, ключами которого являются имена сущно стей, а значениями – целочисленные коды символов. Функция re.sub() вызывает локальную функцию char_from_entity() всякий раз, когда об наруживает совпадение. Функция char_from_entity() использует метод dict.get() со значением 0xFFFD аргумента, используемым по умолча нию (код символа Юникода, обычно используемого для замены и часто изображаемого как ¥ ? ). Это гарантирует получение кода символа и передачу его функции chr(), которая вернет соответствующий сим вол для замены именованной сущности, – с использованием предопре деленного символа Юникода для замены неопознанной сущности. Регулярное выражение \n(?:[ \xA0\t]+\n)+ в шестом вызове функции re.sub() используется для удаления строк, содержащих только про бельные символы. Используемый здесь символьный класс содержит пробел, неразрывный пробел (представляемый сущностью , ко торая будет замещена предыдущим регулярным выражением) и сим вол табуляции. Регулярному выражению соответствует символ пере вода строки (предшествующей одной или более строкам, состоящим только из пробельных символов) и по меньшей мере одна строка (или более, благодаря максимальному квантификатору), состоящая только из пробельных символов. Поскольку совпадение включает в себя сим вол перевода строки из строки, предшествующей строкам из пробель ных символов, необходимо заменить совпадение одиночным символом перевода строки; в противном случае будут удалены не только про бельные строки, но и символ перевода строки из предшествующей им строки. Результат седьмого и последнего вызова функции re.sub() возвращает ся вызывающей программе. Данное регулярное выражение \n\n+ ис пользуется для замены последовательностей из двух и более символов перевода строки точно двумя символами перевода строки, чтобы га рантировать, что параграфы будут отделяться друг от друга только од ной пустой строкой. В этом примере ни одна из замещающих строк не была взята непосред ственно из совпадения (впрочем, здесь использовались именованные и числовые сущности HTML), но в некоторых ситуациях может воз никнуть необходимость включить в строку замены весь текст совпаде ния или его часть. Например, если представить, что имеется список имен в формате Имя ВтороеИмя1 ... ВтороеИмяN Фамилия, в кото ром может указываться произвольное число вторых имен (или даже ни одного), и нам необходимо создать новый список, каждый элемент 546 Глава 12. Регулярные выражения которого имеет формат: Фамилия Имя ВтороеИмя1 ... ВтороеИмяN, то этого легко можно было бы добиться с помощью регулярного вы ражения: new_names = [] for name in names: name = re.sub(r"(\w+(?:\s+\w+)*)\s+(\w+)", r"\2, \1", name) new_names.append(name) В первой части (\w+(?:\s+\w+)*) регулярного выражения первому под выражению \w+ соответствует имя, а подвыражению (?:\s+\w+)* – ноль или более вторых имен. Подвыражению второго имени соответствуют ноль или более вхождений комбинации из пробельного символа и сле дующего за ним слова. Второй части \s+(\w+) регулярного выражения соответствует пробельный символ, за которым следует имя (и вторые имена) и фамилия. Если такое регулярное выражение кажется вам бессмысленным нагро мождением, можно попробовать использовать именованные сохра няющие группы, чтобы повысить удобочитаемость и простоту сопро вождения: name = re.sub(r"(?P r"\s+(?P r"\g Получить доступ к сохраненному тексту в функциях или в методах sub() и subn() можно с помощью синтаксической конструкции \i или \g<id> , где i – это номер сохраняющей группы, а id – это имя или номер сохраняющей группы, то есть \1 – это то же самое, что и \g<1>, а в дан ном примере это то же самое, что и \g Почему первая часть регулярного выражения не захватывает имя, вторые имена и отчество целиком? В конце концов, в ней используется максимальный квантификатор. В действительности именно это и про исходит, но тогда вторая часть регулярного выражения терпит неуда чу. Часть, соответствующая вторым именам, допускает ноль или более совпадений, а часть, соответствующая фамилии, должна иметь точно одно совпадение, но изза своей жадности выражение, соответствую щее вторым именам, захватило все. Когда обнаруживается неудача, механизм регулярных выражений выполняет шаг назад, освобождая последнее «второе имя» и обеспечивая тем самым совпадение для час ти выражения, которая соответствует фамилии. Несмотря на то, что подвыражения с максимальными квантификато рами стремятся отыскать соответствие максимально возможной дли ны, тем не менее они останавливаются, достигнув позиции, когда дальнейшее увеличение длины совпадения может привести к неудаче. Модуль для работы с регулярными выражениями 547 Например, если представить, что текст содержит имя «James W. Loe wen», регулярное выражение сначала найдет совпадение всего имени с первой частью, то есть . Это удовлетворяет первую часть регулярного выражения, но оставляет ни с чем вторую часть, со ответствующую фамилии, а поскольку совпадение с этой частью явля ется обязательным (она имеет неявный квантификатор {1}), то регуляр ное выражение в целом потерпит неудачу. Так как часть выражения, соответствующая вторым именам, снабжена квантификатором *, для нее может иметься ноль или более совпадений (в текущий момент для нее обнаружено два совпадения: «W.» и «Loewen»), поэтому механизм регулярных выражений заставляет эту часть выражения отдать по следнее совпадение, чтобы все регулярное выражение не потерпело не удачу. Такой шаг назад возвращает последнее совпадение с \s+\w (то есть текст «Loewen»); тогда совпадение приобретает вид , которое удовлетворяет все регулярное выражение целиком, и в двух группах сохраняется корректный текст. При использовании оператора выбора (|) с двумя или более сохраняю щими альтернативами нет никакой возможности определить, какая из альтернатив обнаружила совпадение, поэтому нельзя определить, из какой группы следует извлекать сохраненный текст. Конечно, можно выполнить итерации по всем группам и отыскать непустую, но очень часто в подобной ситуации атрибут lastindex объекта совпадения может содержать номер нужной группы. Чтобы проиллюстрировать это и получить дополнительные практические навыки работы с регу лярными выражениями, рассмотрим последний пример. Предположим, что нам требуется определить, какая кодировка ис пользуется в файлах HTML, XML или Python. Мы могли бы открыть файл в двоичном режиме и прочитать, например, первые 1 000 байтов в объект типа bytes. После этого мы могли бы закрыть файл, опреде лить кодировку по фрагменту в объекте bytes и вновь открыть файл в текстовом режиме с использованием обнаруженной кодировки или кодировки по умолчанию (UTF8). Механизм регулярных выражений ожидает, что регулярные выражения будут поставляться ему в виде строк, а текст, к которому применяется регулярное выражение, может быть представлен объектом типа str, bytes или bytearray, причем, ко гда используются объекты типа bytes или bytearray, все функции и ме тоды вместо строк возвращают результат типа bytes, при этом неявно устанавливается флаг re.ASCII. В файлах HTML кодировка обычно указывается в теге (если он вообще присутствует), например, tent='text/html; charset=ISO88591'/>. В файлах XML по умолчанию подразумевается кодировка UTF8, но имеется возможность явно оп ределить другую кодировку, например, Shift_JIS"?> . В Python 3 для файлов с исходным программным кодом также по умолчанию подразумевается кодировка UTF8, но здесь так James W. Loewen James W. Loewen 548 Глава 12. Регулярные выражения же имеется возможность явно определить другую кодировку, включив в файл такую строку: latin1 или # – * – coding: latin1 – * – сразу вслед за строкой «shebang». Ниже показано, как отыскать кодировку в предположении, что пере менная binary ссылается на объект bytes, содержащий первые 1 000 бай тов из файла HTML, XML или Python: match = re.search(r"""(? (?:(?:en)?coding|charset) #2 (?:=(["'])?([\w]+)(?(1)\1) #3 |:\s*([\w]+))""".encode("utf8"), binary, re.IGNORECASE|re.VERBOSE) encoding = match.group(match.lastindex) if match else b"utf8" Чтобы выполнить поиск по объекту типа bytes, необходимо указать текст регулярного выражения также в виде объекта bytes. В данном случае мы предпочли воспользоваться удобствами, которые дают «сы рые» строки, поэтому использовали в первом аргументе функции re.search() такую строку, попутно преобразовав ее в объект типа bytes. Первая часть самого регулярного выражения является ретроспективной проверкой, которая говорит, что совпа дению не может предшествовать дефис или символ «сло ва». Второй части выражения соответствуют слова «en coding», «coding» и «charset», и ее можно было бы запи сать как (?:encoding|coding|charset). Третья часть выра жения была записана в двух строках, чтобы подчеркнуть, что она состоит из двух вариантов: =(["'])?([ \w]+) (?(1)\1) и :\s*([ \w]+), из которых только один может совпадать с текстом. Первой части соответствует знак равенства, за которым следует один или более символов «слова» или дефисов (которые, возможно, заключены в пару однотипных кавычек; применяется прием опре деления совпадения по условию), а второй части соответ ствует символ двоеточия, за которым следует необяза тельный пробельный символ и один или более символов «слова» или дефисов. (Не забывайте, что внутри сим вольных классов символ дефиса интерпретируется про сто как символ, если он стоит первым; в противном слу чае он обозначает диапазон символов, например, [0 9].) Мы использовали флаг re.IGNORECASE, чтобы избежать необходимости записывать выражение (?:(?:[Ee][Nn])?[Cc][Oo][Dd][Ii][Nn][Gg]|[Cc][Hh] [Aa][Rr][Ss][Ee][Tt]) , и флаг re.VERBOSE, чтобы регулярное выражение можно было оформить более удобочитаемым способом и снабдить его комментариями (в данном случае это всего лишь числа, отмечающие части регулярного выражения, на которые потом делаются ссылки в тексте). Поиск совпадения по условию, стр. 536 В заключение 549 Здесь имеется три сохраняющих группы, причем все они находятся в третьей части: первая группа (["'])? сохраняет необязательную от крывающую кавычку, вторая группа ([ \w]+) сохраняет название ко дировки, следующее за знаком =, и третья группа ([ \w]+) (в следую щей строке) сохраняет название кодировки, следующее за двоеточием. Нас интересует только название кодировки, поэтому нам требуется из влечь только фрагмент, сохраненный второй или третьей группой, причем только из той, которая участвовала в совпадении, так как они являются альтернативными. Атрибут lastindex содержит индекс по следней совпавшей группы (либо 2, либо 3 в этом примере, если совпа дение с регулярным выражением будет найдено), поэтому мы извлека ем содержимое совпавшей группы или используем кодировку по умол чанию, если совпадение не было найдено. К настоящему моменту мы познакомились со всеми наиболее часто ис пользуемыми функциональными возможностями модуля re в дейст вии; тем не менее в заключение этого раздела упомянем еще одну, по следнюю функцию. Функция split() (или метод split() объекта регу лярного выражения) может разбивать строки с применением регуляр ных выражений. На практике часто бывает необходимо разбить текст по пробельным символам, чтобы получить список слов (или, более точ но, список строк, каждая из которых соответствует регулярному вы ражению \S+). Регулярные выражения обладают широчайшими воз можностями и, изучив их, легко заметить, что любые задачи, связан ные с обработкой текста, могут быть решены с привлечением регуляр ных выражений. Но иногда строковые методы дают более высокую производительность и в большей степени соответствуют решаемой за даче. Например, мы легко можем разбить текст по пробельным симво лам, используя метод text.split(), так как метод str.split() по умол чанию (или с первым аргументом None) выполняет разбиение по регу лярному выражению \s+ В заключение Регулярные выражения обладают широкими возможностями поиска в тексте строк, соответствующих определенному шаблону, и замены таких строк другими строками, содержимое которых может созда ваться на основе найденных строк. В этой главе мы узнали, что большинство символов в регулярных вы ражениях соответствуют сами себе и неявно сопровождаются кванти фикатором {1}. Мы также узнали, как определять символьные клас сы – множества символов, как инвертировать такие множества и вклю чать в них диапазоны символов, не указывая каждый символ в от дельности. Мы узнали, как квантифицировать регулярные выражения, чтобы обеспечить поиск определенного числа совпадений или числа совпаде 550 Глава 12. Регулярные выражения ний не менее и не более определенного числа раз. Как использовать максимальные и минимальные квантификаторы. Мы также узнали, как группировать подвыражения, чтобы к ним можно было приме нить квантификатор как к единому целому (и при желании обеспе чить сохранение совпавшего фрагмента). В этой главе также было показано, какое влияние на совпадения ока зывают различные проверки, такие как позитивные и негативные опе режающая и ретроспективная проверки, и различные флаги, которые, например, управляют интерпретацией символа точки и обеспечивают возможность поиска совпадений без учета регистра символов. В заключительном разделе было показано, как использовать регуляр ные выражения в контексте программирования на языке Python. В этом разделе мы узнали, как использовать функции, предоставляемые мо дулем re, и методы объектов скомпилированных регулярных выраже ний и объектов совпадений. Мы также узнали, как замещать найден ные совпадения простыми строками, строками, содержащими обрат ные ссылки, и результатами вызова функций или лямбдавыражений, а также – как сделать регулярные выражения более удобочитаемыми, используя именованные сохраняющие группы и комментарии. Упражнения 1. Во многих случаях (например, при заполнении вебформ), пользо ватели должны вводить номер телефона, причем в некоторых слу чаях допускается ввод только в определенном формате, что достав ляет дополнительные неудобства пользователям. Напишите про грамму, которая будет читать номера телефонов США, где за пер выми тремя цифрами, обозначающими код города, следуют семь цифр местного номера, принимая их как десять цифр, идущих под ряд или разделенных на блоки с помощью пробелов и дефисов, при чем код города может быть заключен в необязательные круглые скобки. Например, все следующие номера считаются допустимы ми: 5555555555, (555) 5555555, (555) 555 5555 и 5555555555. Ка ждый номер телефона должен читаться из sys.stdin и выводиться в формате «(555) 555 5555», а в случае ввода недопустимого номера должно выводиться сообщение об ошибке. Регулярное выражение, которому соответствуют номера телефо нов, занимает примерно восемь строк (при использовании флага re.VERBOSE ) и само по себе достаточно простое. Решение приводится в файле phone.py, размер которого составляет двадцать пять строк. 2. Напишите небольшую программу, читающую файлы XML и HTML, имена которых задаются в командной строке. Программа должна отыскивать теги с атрибутами и выводить имена тегов с их атрибу тами под ними. Например, ниже приводится фрагмент вывода про |