# импортируем модуль для работы с регулярными выражениями: https://docs.python.org/3/library/re.html
import reРегулярные выражения в Python и pandas
Отрывок из книги Дейтел Пол, Дейтел Харви. Python: Искусственный интеллект, большие данные и облачные вычисления.
Строка с регулярным выражением описывает шаблон для поиска совпадений в других строках.
На веб-сайтах:
- https://regex101.com
- http://www.regexlib.com
- https://www.regular-expressions.info
имеются репозитории готовых регулярных выражений.
см. официальный документ Regular Expression HOWTO
Одна из простейших функций регулярных выражений 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' совпадает только с четырьмя последовательными цифровыми символами.
Ниже перечислены некоторые предопределенные символьные классы и группы символов, с которыми они совпадают.
\dЛюбая цифра(0–9)\DЛюбой символ, кроме цифр\sЛюбой символ-пропуск (пробелы, табуляции, новые строки)\SЛюбой символ, кроме пропусков\wЛюбой символ слова (также называемый алфавитно-цифровым символом) — то есть любая буква верхнего или нижнего регистра, любая цифра или символ подчеркивания\WЛюбой символ, кроме символов слов
Чтобы любой метасимвол совпадал со своим литеральным значением, поставьте перед ним символ \ (обратный слеш). Например, \\ совпадает с обратным слешем ( \ ), а \$ совпадает со знаком $.
Квадратные скобки [] определяют пользовательский символьный класс, совпадающий с одним символом. Так, [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 rere.sub(r'\t', ', ', '1\t2\t3\t4')'1, 2, 3, 4'
Функция sub получает три обязательных аргумента:
- шаблон для поиска (символ табуляции
'\t'); - текст замены (
', '); - строка, в которой ведется поиск (
'1\t2\t3\t4'),
и возвращает новую строку.
Ключевой аргумент 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 reresult = 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 возвращает объект совпадения только в том случае, если совпадение всего шаблона будет найдено в тексте строки.
Рассмотрим регулярное выражение по частям:
'([A-Z][a-z]+ [A-Z][a-z]+)'совпадает с двумя словами, разделенными пробелом. Каждое слово должно начинаться с буквы верхнего регистра.', e-mail: 'содержит литеральные символы, которые совпадают сами с собой.(\w+@\w+\.\w{3})совпадает с простым адресом электронной почты, состоящим из одного или нескольких алфавитно-цифровых символов (\w+), символа@, одного или нескольких алфавитно-цифровых символов (\w+), точки (\.) и трех алфавитно-цифровых символов (\w{3}). Перед точкой ставится символ\, потому что точка (.) в регулярных выражениях является метасимволом, совпадающим с одним символом.
Метод 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 pdzips = pd.Series({'Boston': '02215',
'Miami': '3310'})zipsBoston 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'])cities0 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 | Phone | ||
|---|---|---|---|
| 0 | Mike Green | demo1@deitel.com | 5555555555 |
| 1 | Sue Brown | demo2@deitel.com | 5555551234 |
Теперь произведем первичную обработку данных с применением программирования в функциональном стиле.
Телефонные номера можно перевести в правильный формат вызовом метода map коллекции Series для столбца 'Phone' коллекции DataFrame .
Аргументом метода map является функция, которая получает значение и возвращает отображенное (преобразованное) значение. Функция get_formatted_phone отображает десять последовательных цифр в формат ###-###-#### :
import redef 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_phone0 555-555-5555
1 555-555-1234
Name: Phone, dtype: object
Убедившись в том, что данные имеют правильный формат, можно обновить их в исходной коллекции DataFrame , присвоив новую коллекцию Series столбцу 'Phone' :
contactsdf['Phone'] = formatted_phonecontactsdf| Name | Phone | ||
|---|---|---|---|
| 0 | Mike Green | demo1@deitel.com | 555-555-5555 |
| 1 | Sue Brown | demo2@deitel.com | 555-555-1234 |