Нормализация и валидация номеров телефонов средствами 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]) – что после него нет цифры. Это повышает точность извлечения.

Работа с номерами телефонов в Python - comments

En
номера телефонов python (python)