Нормализация и валидация номеров телефонов средствами Python и regex
Регулярные выражения для работы с номерами телефонов
При обработке текстов часто возникает необходимость находить, проверять или преобразовывать телефонные номера. Python с модулем re предоставляет гибкие возможности для решения таких задач. Ниже рассмотрено несколько подходов с примерами кода и разбором типичных трудностей.
Основное решение: универсальный шаблон для российских номеров
Наиболее эффективный способ – написать регулярное выражение, которое охватывает распространенные форматы: с кодом страны +7 или 8, с круглыми скобками для кода города, пробелами или дефисами между группами цифр. Пример:
import re
pattern = r'(\+7|8)?[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})'
text = 'Контакты: +7 (495) 123-45-67, 8-912-345-67-89, 84951234567'
matches = re.findall(pattern, text)
normalized = [''.join(m) for m in matches] # объединяем группы
print(normalized)
Python определить язык строки (определение языка строки в python)
['+74951234567', '89123456789', '84951234567']
номера телефонов python (работа с номерами телефонов в python)
Шаблон содержит группы захвата для каждой части номера. При использовании findall возвращается список кортежей. Дополнительная нормализация (например, замена 8 на +7) выполняется отдельно.
Типичные ошибки:
- Неэкранированные скобки – в шаблоне нужно писать \( и \).
- Избыточные квантификаторы – например, [\s\-]? делает разделители необязательными, но не допускает их повторения. Для нескольких пробелов лучше [\s\-]*.
- Слишком широкий шаблон может захватывать последовательности цифр, не являющиеся номерами (например, даты). Для надёжности стоит дополнять шаблон границами слова \b.
Как извлечь все номера из текста с разными разделителями?
Текст может содержать номера в форматах: 123-45-67, (123) 456 78 90, +7 123 456 78 90 и т.д. Используется re.findall с шаблоном, допускающим вариативность разделителей.
text = 'Позвоните по номеру 8(495)1234567 или +7 912 345-67-89'
pattern = r'(\+?7|8)?[\s\.\-]?\(?(\d{3})\)?[\s\.\-]?(\d{3,4})[\s\.\-]?(\d{2,4})[\s\.\-]?(\d{2})?'
matches = re.findall(pattern, text)
for m in matches:
print(''.join(m))
84951234567 +79123456789
Обратите внимание на необязательную последнюю группу – она позволяет захватывать номера из 7 или 10 цифр.
Как проверить, является ли строка корректным номером?
Для полной проверки одной строки используется re.fullmatch. Шаблон должен описывать всю строку от начала до конца.
def is_valid_phone(phone: str) -> bool:
pattern = r'^(\+7|8)?[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})$'
return bool(re.fullmatch(pattern, phone))
print(is_valid_phone('+7 (495) 123-45-67')) # True
print(is_valid_phone('+7 495 123 45 6')) # False (не хватает цифр)
True False
Возможная проблема: проверка допускает лишние пробелы в начале/конце. Решается использование strip() перед проверкой или добавление \s* внутрь шаблона.
Как заменить все номера на единый формат (например, +7XXXXXXXXXX)?
С помощью re.sub и группировки можно перевести разные написания в канонический вид.
text = 'Моб.: 8-912-345-67-89, дом.: +7(495)1234567'
pattern = r'(\+7|8)[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})'
replacement = r'+7(\2)\3-\4-\5'
result = re.sub(pattern, replacement, text)
print(result)
Моб.: +7(912)345-67-89, дом.: +7(495)123-45-67
Обратите внимание: в примере номер 8 заменён на +7, а группы переставлены согласно формату.
Проблема: если в исходном номере нет кода города (7 цифр), шаблон его не захватит. Нужно предусмотреть альтернативу через |.
Как обработать номера с добавочным номером (добавочный код после # или доб.)?
Добавочная часть часто отделяется пробелом и словом "доб." или знаком #. Шаблон расширяется необязательной группой.
text = 'Секретарь: +7 495 123 45 67 доб. 123'
pattern = r'(\+7|8)?[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})(?:[,\s]+(?:доб\.|#)\s*(\d+))?'
match = re.search(pattern, text)
if match:
main = match.group(1) + match.group(2) + match.group(3) + match.group(4) + match.group(5)
ext = match.group(6)
print(f'Основной номер: {main}, добавочный: {ext}')
Основной номер: +74951234567, добавочный: 123
Необязательная группа (?:...)? позволяет захватывать добавочный код, если он есть.
Как работать с международными номерами (код страны от 1 до 3 цифр)?
Для номеров вида +1 800 555-1234 или +44 20 1234 5678 нужен шаблон, учитывающий переменную длину кода страны и города.
pattern_int = r'\+(\d{1,3})[\s\-]?\(?(\d{1,4})\)?[\s\-]?(\d{1,4})[\s\-]?(\d{1,4})[\s\-]?(\d{1,4})?'
text = 'US: +1 800 555-1234, UK: +44 20 1234 5678'
matches = re.findall(pattern_int, text)
for m in matches:
print('+'.join(m))
+18005551234 +442012345678
Такой шаблон менее строгий, поэтому после извлечения стоит дополнительно проверять длину каждой группы (например, код страны – от 1 до 3 цифр, код города – 2-4 цифры).
Типичная ошибка: номер может содержать знак "+" не в начале. Рекомендуется искать "+" с последующими цифрами и разделителями.
Расширенные примеры обработки телефонных номеров
1. Извлечение номеров из текста с последующей нормализацией
Часто требуется не только найти номера, но и привести их к единому цифровому формату без лишних символов. Используется комбинация findall и re.sub с удалением всего, кроме цифр и знака "+".
import re
text = '''
Связаться можно по телефону: +7 (495) 123-45-67
Дополнительный: 8-800-555-35-35
Старый номер: 84951234567
'''
# Шаблон захватывает любую последовательность, похожую на номер
pattern = r'(\+?7|8)?[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})'
raw_matches = re.findall(pattern, text)
# Нормализация: объединяем группы, заменяем 8 на +7
normalized = []
for m in raw_matches:
num = ''.join(m)
if num.startswith('8'):
num = '+7' + num[1:]
normalized.append(num)
print(normalized)
['+74951234567', '+78005553535', '+74951234567']
Пояснение: после findall получаем кортежи. Их объединение даёт строку, которую затем можно привести к международному формату заменой первой цифры. Этот подход удобен, когда исходный текст содержит разные варианты записи.
2. Валидация номера с учётом кода страны и длины (на примере России и Украины)
Регулярное выражение может проверять не только формат, но и соответствие кодов конкретным странам.
import re
def validate_phone(phone: str) -> str:
"""Возвращает 'RU', 'UA' или 'UNKNOWN'"""
ru_pattern = r'^(\+7|8)?[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})$'
ua_pattern = r'^(\+380|0)[\s\-]?\(?(\d{2})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})$'
if re.fullmatch(ru_pattern, phone.replace(' ', '')):
return 'RU'
elif re.fullmatch(ua_pattern, phone.replace(' ', '')):
return 'UA'
else:
return 'UNKNOWN'
print(validate_phone('+7 495 123 45 67')) # RU
print(validate_phone('+380 63 123 45 67')) # UA
print(validate_phone('+1 800 555 1234')) # UNKNOWN
RU UA UNKNOWN
Пояснение: функция сначала удаляет все пробелы (чтобы унифицировать формат), затем применяет полное совпадение с одним из шаблонов. Можно легко расширить для других стран.
3. Использование именованных групп для повышения читаемости
Именованные группы позволяют обращаться к частям номера по имени, а не по индексу.
pattern = r'''(?P\+?7|8)?
[\s\-]?
\(?(?P\d{3})\)?
[\s\-]?
(?P\d{3})
[\s\-]?
(?P\d{2})
[\s\-]?
(?P\d{2})'''
text = '8(495)123-45-67'
match = re.fullmatch(pattern, text, re.VERBOSE)
if match:
print(f"Префикс: {match.group('prefix')}")
print(f"Код города: {match.group('city')}")
Префикс: 8 Код города: 495
Пояснение: флаг re.VERBOSE позволяет разбить шаблон на строки и добавить комментарии. Именованные группы делают код более самодокументированным.
4. Обработка нескольких номеров в одной строке с заменой формата
В тексте могут быть перечислены номера через запятую или точку с запятой. Функция re.sub может заменить каждый найденный номер, сохраняя остальной текст.
text = 'Тел.: +7 (495) 123-45-67, 8-800-555-35-35; факс: 8(495)1234567'
pattern = r'(\+7|8)[\s\-]?\(?(\d{3})\)?[\s\-]?(\d{3})[\s\-]?(\d{2})[\s\-]?(\d{2})'
replacement = r'+7(\2)\3-\4-\5'
result = re.sub(pattern, replacement, text)
print(result)
Тел.: +7(495)123-45-67, +7(800)555-35-35; факс: +7(495)123-45-67
Пояснение: шаблон находит все фрагменты, соответствующие номеру, и заменяет их на единый формат. Обратите внимание, что последний номер в исходном тексте был записан слитно, но шаблон всё равно его захватил.
5. Извлечение номеров с нестандартными разделителями (точки, нижнее подчёркивание)
Иногда номера пишут с точками или другим разделителем. Шаблон легко адаптируется добавлением символов в класс [\s.\-_].
text = 'Тел: +7.495.123.45.67 или 8_912_345_67_89'
pattern = r'(\+7|8)?[\s.\-_]?\(?(\d{3})\)?[\s.\-_]?(\d{3})[\s.\-_]?(\d{2})[\s.\-_]?(\d{2})'
matches = re.findall(pattern, text)
normalized = [''.join(m) for m in matches]
print(normalized)
['+74951234567', '89123456789']
Пояснение: класс [\s.\-_] включает точку и нижнее подчёркивание. Если встречается другой разделитель, его также можно добавить (например, звёздочку).
6. Применение lookahead и lookbehind для исключения ложных срабатываний
Чтобы номер не оказался частью другого слова (например, "1234567890" внутри "ABC1234567890DEF"), используют границы слова или проверки окружения.
text = 'ID: 84951234567, но 123456 не номер'
pattern = r'(?
['84951234567']
Пояснение: (? гарантирует, что перед номером нет цифры (чтобы не захватить, например, "284951234567"), а (?![\d]) – что после него нет цифры. Это повышает точность извлечения.