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

Отрывок из книги Дейтел Пол, Дейтел Харви. Python: Искусственный интеллект, большие данные и облачные вычисления.

Строка с регулярным выражением описывает шаблон для поиска совпадений в других строках.

На веб-сайтах:

имеются репозитории готовых регулярных выражений.

см. официальный документ Regular Expression HOWTO

# импортируем модуль для работы с регулярными выражениями: https://docs.python.org/3/library/re.html
import re

Одна из простейших функций регулярных выражений fullmatch проверяет, совпадает ли шаблон, заданный первым аргументом, со всей строкой, заданной вторым аргументом.

Начнем с проверки совпадений для литеральных символов, то есть символов, которые совпадают сами с собой:

pattern = '02215'
# тернарный if
'Match' if re.fullmatch(pattern, '02215') else 'No match'
'Match'
'Match' if re.fullmatch(pattern, '51220') else 'No match'
'No match'

Первым аргументом функции является регулярное выражение — шаблон, для которого проверяется совпадение в строке. Любая строка может быть регулярным выражением. Значение переменной pattern '02215' состоит из цифровых литералов, которые совпадают только сами с собой в заданном порядке. Во втором аргументе передается строка, с которой должен полностью совпасть шаблон.

Если шаблон из первого аргумента совпадает со строкой из второго аргумента, fullmatch возвращает объект с текстом совпадения, который интерпретируется как True.

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

Регулярные выражения обычно содержат различные специальные символы, которые называются метасимволами:

[] {} () \ * + ^ $ ? . |

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

Проверим, что почтовый код состоит из пяти цифр:

'Valid' if re.fullmatch(r'\d{5}', '02215') else 'Invalid'
'Valid'
'Valid' if re.fullmatch(r'\d{5}', '9876') else 'Invalid'
'Invalid'

В регулярном выражении \d{5} \d является символьным классом, представляющим цифру (0–9).

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

Квантификатор {5} повторяет \d пять раз, как если бы мы использовали запись \d\d\d\d\d для совпадения с пятью последовательными цифрами.

Во фрагменте fullmatch возвращает None, потому что '9876' совпадает только с четырьмя последовательными цифровыми символами.

Ниже перечислены некоторые предопределенные символьные классы и группы символов, с которыми они совпадают.

Чтобы любой метасимвол совпадал со своим литеральным значением, поставьте перед ним символ \ (обратный слеш). Например, \\ совпадает с обратным слешем ( \ ), а \$ совпадает со знаком $.

Квадратные скобки [] определяют пользовательский символьный класс, совпадающий с одним символом. Так, [aeiou] совпадает с гласной буквой нижнего регистра, [A-Z] — с буквой верхнего регистра, [a-z] — с буквой нижнего регистра и [a-zA-Z] — с любой буквой нижнего (верхнего) регистра.

Выполним простую проверку имени — последовательности букв без пробелов или знаков препинания. Проверим, что последовательность начинается с буквы верхнего регистра ( A–Z ), а за ней следует произвольное количество букв нижнего регистра ( a–z ):

'Valid' if re.fullmatch('[A-Z][a-z]*', 'Wfhg') else 'Invalid'
'Valid'
'Valid' if re.fullmatch('[A-Z][a-z]*', 'eva') else 'Invalid'
'Invalid'

Имя может содержать неизвестное заранее количество букв.

Квантификатор * совпадает с нулем и более вхождениями подвыражения, находящегося слева (в данном случае [a-z]). Таким образом, [A-Z][a-z]* совпадает с буквой верхнего регистра, за которой следует нуль и более букв нижнего регистра (например, 'Amanda' , 'Bo' и даже 'E').

Если пользовательский символьный класс начинается с символа ^ (крышка), то класс совпадает с любым символом, который не подходит под определение из класса. Таким образом, [^a-z] совпадает с любым символом, который не является буквой нижнего регистра:

'Match' if re.fullmatch('[^a-z]', 'A') else 'No match'
'Match'
'Match' if re.fullmatch('[^a-z]', 'a') else 'No match'
'No match'

Метасимволы в пользовательском символьном классе интерпретируются как литеральные символы, то есть как сами символы, не имеющие специального смысла.

Таким образом, символьный класс [*+$] совпадает с одним из символов * , + или $:

'Match' if re.fullmatch('[*+$]', '*') else 'No match'
'Match'
'Match' if re.fullmatch('[*+$]', '!') else 'No match'
'No match'

Для того чтобы имя содержало хотя бы одну букву нижнего регистра, квантификатор * во фрагменте можно заменить знаком +, который совпадает по крайней мере с одним вхождением подвыражения:

'Valid' if re.fullmatch('[A-Z][a-z]+', 'Wally') else 'Invalid'
'Valid'
'Valid' if re.fullmatch('[A-Z][a-c]+', 'Wf') else 'Invalid'
'Invalid'
'Valid' if re.fullmatch('[A-Z][a-z]+', 'E') else 'Invalid'
'Invalid'

Квантификаторы * и + являются максимальными (“жадными”) — они совпадают с максимально возможным количеством символов.

Таким образом, регулярные выражения [A-Z][a-z]+ совпадают с именами 'Al' , 'Eva' , 'Samantha' , 'Benjamin' и любыми другими словами, начинающимися с буквы верхнего регистра, за которой следует хотя бы одна буква нижнего регистра.

Квантификатор ? совпадает с нулем или одним вхождением подвыражения:

'Match' if re.fullmatch('labell?ed', 'labelled') else 'No match'
'Match'
'Match' if re.fullmatch('labell?ed', 'labeled') else 'No match'
'Match'
'Match' if re.fullmatch('labell?ed', 'labellled') else 'No match'
'No match'

Регулярное выражение labell?ed совпадает со словами labelled и labeled , но не с ошибочно написанным словом labellled. В каждом из приведенных выше фрагментов первые пять литеральных символов регулярного выражения ( label ) совпадают с первыми пятью символами второго аргумента. Часть l? означает, что оставшимся литеральным символам ed может предшествовать нуль или один символ l .

Квантификатор {n,} совпадает не менее чем с n вхождениями подвыражения. Следующее регулярное выражение совпадает со строками, содержащими не менее трех цифр:

'Match' if re.fullmatch(r'\d{3,}', '123') else 'No match'
'Match'
'Match' if re.fullmatch(r'\d{3,}', '1234567890') else 'No match'
'Match'
'Match' if re.fullmatch(r'\d{3,}', '12') else 'No match'
'No match'

Чтобы совпадение включало от n до m (включительно) вхождений, используйте квантификатор {n,m}. Следующее регулярное выражение совпадает со строками, содержащими от 3 до 6 цифр:

'Match' if re.fullmatch(r'\d{3,6}', '123') else 'No match'
'Match'
'Match' if re.fullmatch(r'\d{3,6}', '123456') else 'No match'
'Match'
'Match' if re.fullmatch(r'\d{3,6}', '1234567') else 'No match'
'No match'
'Match' if re.fullmatch(r'\d{3,6}', '12') else 'No match'
'No match'

Модуль re предоставляет функцию sub для замены совпадений шаблона в строке, а также функцию split для разбиения строки на фрагменты на основании шаблонов.

По умолчанию функция sub модуля re заменяет все вхождения шаблона заданным текстом.

Преобразуем строку, разделенную табуляциями, в формат с разделением запятыми:

import re
re.sub(r'\t', ', ', '1\t2\t3\t4')
'1, 2, 3, 4'

Функция sub получает три обязательных аргумента:

и возвращает новую строку.

Ключевой аргумент count может использоваться для определения максимального количества замен:

re.sub(r'\t', ', ', '1\t2\t3\t4', count=2)
'1, 2, 3\t4'

Функция split разбивает строку на лексемы, используя регулярное выражение для определения ограничителя, и возвращает список строк.

Разобьем строку по запятым, за которыми следует 0 или более пропусков — для обозначения пропусков используется символьный класс \s , а * обозначает 0 и более вхождений предшествующего подвыражения:

'1, 2, 3,4,        5,6,7,8'.split(",")
['1', ' 2', ' 3', '4', '        5', '6', '7', '8']
re.split(r',\s*', '1, 2, 3,4,        5,6,7,8')
['1', '2', '3', '4', '5', '6', '7', '8']

Ключевой аргумент maxsplit задает максимальное количество разбиений:

re.split(r',\s*', '1,   2, 3,4,            5,6,7,8', maxsplit=3)
['1', '2', '3', '4,            5,6,7,8']

В данном случае после трех разбиений четвертая строка содержит остаток исходной строки.

Ранее мы использовали функцию fullmatch для определения того, совпала ли вся строка с регулярным выражением. Но существует и ряд других функций поиска совпадений.

Функция search ищет в строке первое вхождение подстроки, совпадающей с регулярным выражением, и возвращает объект совпадения (типа SRE_Match), содержащий подстроку с совпадением.

Метод group объекта совпадения возвращает эту подстроку:

import re
result = re.search('Python', 'Python is fun')
result.group() if result else 'not found'
'Python'

Функция match ищет совпадение только от начала строки.

Метасимвол ^ в начале регулярного выражения (и не в квадратных скобках) — якорь, указывающий, что выражение совпадает только от начала строки:

result = re.search('^Python', 'Python is fun')
result.group() if result else 'not found'
'Python'
result = re.search('^fun', 'Python is fun')
result.group() if result else 'not found'
'not found'

Аналогичным образом символ $ в конце регулярного выражения является якорем, указывающим, что выражение совпадает только в конце строки:

result = re.search('Python$', 'Python is fun')
result.group() if result else 'not found'
'not found'
result = re.search('fun$', 'Python is fun')
result.group() if result else 'not found'
'fun'

Функция findall находит все совпадающие подстроки и возвращает список совпадений.

Для примера извлечем все телефонные номера в строке, полагая, что телефонные номера записываются в форме ###-###-#### :

contact = 'Wally White, Home: 555-555-1234, Work: 555-555-4321'
re.findall(r'\d{3}-\d{3}-\d{4}', contact)
['555-555-1234', '555-555-4321']

Функция finditer работает аналогично findall , но возвращает итерируемый объект, содержащий объекты совпадений, с отложенным вычислением.

При большом количестве совпадений использование finditer позволит сэкономить память, потому что она возвращает по одному совпадению, тогда как findall возвращает все совпадения сразу:

for phone in re.finditer(r'\d{3}-\d{3}-\d{4}', contact):
    print(phone.group())
555-555-1234
555-555-4321

Метасимволы ( и ) (круглые скобки) используются для сохранения подстрок в совпадениях.

Для примера сохраним отдельно имя и адрес электронной почты в тексте строки:

text = 'Charlie Cyan, e-mail: demo1@deitel.com'
pattern = r'([A-Z][a-z]+ [A-Z][a-z]+), e-mail: (\w+@\w+\.\w{3})'
result = re.search(pattern, text)

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

Рассмотрим регулярное выражение по частям:

Метод groups объекта совпадения возвращает кортеж совпавших подстрок:

result.groups()
('Charlie Cyan', 'demo1@deitel.com')

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

Нумерация сохраненных подстрок начинается с 1 (в отличие от индексов списков, которые начинаются с 0):

result.group(1)
'Charlie Cyan'
result.group(2)
'demo1@deitel.com'

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

Начнем с создания коллекции Series почтовых кодов, состоящих из пяти цифр, на базе словаря пар “название-города/почтовый-код-из-5-цифр”. Мы намеренно указали ошибочный индекс для Майами:

# импортируем pandas
import pandas as pd
zips = pd.Series({'Boston': '02215',
                  'Miami': '3310'})
zips
Boston    02215
Miami      3310
dtype: object

Для проверки данных можно воспользоваться регулярными выражениями с pandas.

Атрибут str коллекции Series предоставляет средства обработки строк и различные методы регулярных выражений. Чтобы проверить правильность каждого отдельного почтового кода, воспользуемся методом match атрибута str :

zips.str?
Type:        StringMethods

String form: <pandas.core.strings.accessor.StringMethods object at 0x0000017551C97980>

File:        c:\users\dfedorov\appdata\local\anaconda3\lib\site-packages\pandas\core\strings\accessor.py

Docstring:  

Vectorized string functions for Series and Index.



NAs stay NA unless handled otherwise by a particular method.

Patterned after Python's string methods, with some inspiration from

R's stringr package.



Examples

--------

>>> s = pd.Series(["A_Str_Series"])

>>> s

0    A_Str_Series

dtype: object



>>> s.str.split("_")

0    [A, Str, Series]

dtype: object



>>> s.str.replace("_", "")

0    AStrSeries

dtype: object
zips.str.match(r'\d{5}')
Boston     True
Miami     False
dtype: bool

Метод match применяет регулярное выражение \d{5} к каждому элементу Series , чтобы убедиться в том, что элемент состоит ровно из пяти цифр.

Явно перебирать все почтовые коды в цикле не нужно — match сделает это за вас. Метод возвращает новую коллекцию Series , содержащую значение True для каждого действительного элемента.

В данном случае почтовый код Майами проверку не прошел, поэтому его элемент равен False .

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

В этом случае следует использовать метод contains вместо match .

Создадим коллекцию Series строк, каждая из которых содержит название города в США, штата и почтовый код, а затем определим, содержит ли каждая строку подстроку, совпадающую с шаблоном ' [A-Z]{2} ' (пробел, за которым следуют две буквы верхнего регистра, и еще один пробел):

cities = pd.Series(['Boston, MA 02215',
                    'Miami, FL 33101'])
cities
0    Boston, MA 02215
1     Miami, FL 33101
dtype: object
cities.str.contains(r' [A-Z]{2} ')
0    True
1    True
dtype: bool
cities.str.match(r' [A-Z]{2} ')
0    False
1    False
dtype: bool

От очистки данных перейдем к первичной обработке данных в другой формат.

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

Создадим коллекцию DataFrame :

contacts = [['Mike Green', 'demo1@deitel.com', '5555555555'],
            ['Sue Brown', 'demo2@deitel.com', '5555551234']]
contacts
[['Mike Green', 'demo1@deitel.com', '5555555555'],
 ['Sue Brown', 'demo2@deitel.com', '5555551234']]
contactsdf = pd.DataFrame(contacts,
                          columns=['Name', 'Email', 'Phone'])
contactsdf
Name Email Phone
0 Mike Green demo1@deitel.com 5555555555
1 Sue Brown demo2@deitel.com 5555551234

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

Телефонные номера можно перевести в правильный формат вызовом метода map коллекции Series для столбца 'Phone' коллекции DataFrame .

Аргументом метода map является функция, которая получает значение и возвращает отображенное (преобразованное) значение. Функция get_formatted_phone отображает десять последовательных цифр в формат ###-###-#### :

import re
def get_formatted_phone(value):
    result = re.fullmatch(r'(\d{3})(\d{3})(\d{4})', value)
    return '-'.join(result.groups()) if result else value

Регулярное выражение в первой команде блока совпадает только с первыми десятью последовательно идущими цифрами. Оно сохраняет подстроки, которые содержат первые три цифры, следующие три цифры и последние четыре цифры. Команда return работает следующим образом: - Если результат равен None , то значение просто возвращается в неизменном виде. - В противном случае вызывается метод result.groups() для получения кортежа, содержащего сохраненные подстроки. Кортеж передается методу join строк для выполнения конкатенации элементов, с разделением элементов символом '-' для формирования преобразованного телефонного номера.

Метод map коллекции Series создает новую коллекцию Series , которая содержит результаты вызова ее функции-аргумента для каждого значения в столбце.

Фрагмент выводит результаты, включающие имя и тип столбца:

formatted_phone = contactsdf['Phone'].map(get_formatted_phone)
formatted_phone
0    555-555-5555
1    555-555-1234
Name: Phone, dtype: object

Убедившись в том, что данные имеют правильный формат, можно обновить их в исходной коллекции DataFrame , присвоив новую коллекцию Series столбцу 'Phone' :

contactsdf['Phone'] = formatted_phone
contactsdf
Name Email Phone
0 Mike Green demo1@deitel.com 555-555-5555
1 Sue Brown demo2@deitel.com 555-555-1234