Статья Обработка наборов данных в csv-файлах с помощью Python. Часть 1

Когда мы сталкиваемся с задачей, которая предполагает обработку большого количества «.csv» файлов которые имеют различную структуру столбцов, их название, а также содержимое, которое требует очистки, желательно сделать небольшой скрипт, который поможет обработке.

logo.png


Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий.

Итак, требуемая задача: «Обработать несколько «.csv» файлов, нормализовать содержимое столбцов таким образом, чтобы данные были одного формата. Например, номер телефона: 8 (908) 800 80 80 должен стать: 79088008080».

Для этого мы будем использовать скрипт на python, а также вспомогательные инструменты, так как без предварительной обработки файлов нам не обойтись, в связи с разнообразием заголовков столбцов, а то и вовсе их отсутствием. Хотел бы сказать сразу, что данный алгоритм я выработал для себя и просто хочу поделится им с вами. Возможно, кому-то и будет полезен созданный далее инструмент.


Подготовка «.csv» файлов

Перед тем, как скормить файл скрипту я выполняю его подготовку. Так как файлы могут быть очень большого размера, при этом имея миллионы строк, простым блокнотом здесь не обойтись. Для того, чтобы открывать, да еще и обрабатывать большие файлы я пользуюсь замечательным инструментом EmEditor версии 23.0.5. Впрочем, версия здесь не особо важна. Данный редактор позволяет открывать и обрабатывать большие файлы в несколько потоков, что значительно ускоряет данный процесс. Плюс к тому, у него есть рекордер для записи действий, чтобы на их основе создать скрипт для повторяющихся операций. Есть встроенный конвертер CSV. И еще множество плюсов, перечислять которые не имеет особого смысла. Но, есть некоторые ограничения, которые вам предстоит преодолеть, если вы решите им пользоваться, это: 1) Редактор платный; 2) Работает под Windows.

Для примера, вот как выглядит необработанный и только что открытый файл в данном редакторе:

screenshot1.png

В этом файле более 665 тысяч строк. У него есть следующие заголовки: _id,birthdate,companies,counters.comments,counters.hits,counters.likes,country,created,education,email,emailIsActive,expert,facebook.id,facebook.imgUrl,facebook.nickName…

Они приведены не полностью просто для того, чтобы вы имели представление с чем имеете дело. Далеко не все столбцы содержат нужную информацию. Поэтому, для себя я определил, какие столбцы будет содержать обработанный файл. Например, нам необходимо получить Ф.И.О. Ищем поля: firstName, lastName, secondName, ну или похожие на них. Располагаем в том порядке, каком необходимо и выполняем операцию по слиянию данный столбцов. У EmEditor есть хорошая функция «Объединение столбцов». Выделяете нужные вам столбцы, указываете, что необходимо сделать (доступны два варианта: Сцепить и Использовать первое непустое значение), указываете строку для вставки. В данном случае нам нужен пробел.

screenshot2.png

Выполняем операцию получая нужное нам значение Ф.И.О. После чего переименовываем столбец в «fio». Таким же образом можно обработать другие, необходимые значения. Приведу пример, какие именно столбцы я забираю из необработанного файла. Описание будет приведено полностью, так как это будет необходимо в дальнейшем, когда мы будем писать скрипт по обработке файла.

Как вы уже поняли, Ф.И.О. – данный столбец переименовывается в «fio». Смотрим, есть ли в данных дата рождения. Вне зависимости от названия переименовываем в «bdate». Также, если есть в файле адрес электронной почты, данный столбец переименовывается в «email». Если есть столбец с никнеймом, его переименовываем в «uname». Номер телефона: «phone». СНИЛС: «snils». ИНН: «inn».

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

Адрес
. Если есть столбец с полным адресом, то переименовываем его в «address». Если же нет, смотрим, из чего его можно составить. Это может быть почтовый код: «zip», страна: «country», регион: «region», муниципальное образование: «m_obr», область: «area», район: «rayon», город: «city», улица: «street», дом: «house», строение: «build», корпус: «corp», офис: «office», квартира: «kv».

Документ. Для примера, может быть паспорт. Если паспорт расположен в одном столбце, переименовываем его в «passport». Если же нет, смотрим составляющие, серия и номер: «pass_sn» (данные для этого столбца иногда необходимо объединять из двух столбцов), дата выдачи: «pass_dout», кем выдан: «pass_iss», код подразделения: «pass_kp».

Аккаунты в социальных сетях и прочих мессенджерах. Приведу данные столбцы общей строкой: "vk_id", "ok_id", "fb_id", "lj_id", "tg_id", "insta_id", "mailru_id", "yandex_id", "google_id", "x_id", "skype".

Если вам необходимы какие-либо другие столбцы, их также можно оставить, переименовав названия для большего понимания. Данные из дополнительных столбцов я решил добавлять в итоговом файле в разное, то есть: «other».

Вот как выглядит файл, который я показывал на скриншоте для примера, после обработки. А именно его структура полей:
bdate,country,email,fb_id,fb_nickName,fio,uname,skype,x_id,x_nickName,vk_id,vk_nickName.

Да, здесь не все столбцы из описанных, но в этом и задача, брать только те столбцы, данные которых нам необходимы, не добавляя лишних столбцов, чтобы выстроить итоговую структуру. За нас это сделает скрипт.

Далее, я преобразую разделители файла «.csv» из тех, что в оригинале, в «|». Так удобнее удалять еще при обработке редактором лишние кавычки, например.

Вот так выглядит в редакторе обработанный файл:

screenshot3.png

И еще один, небольшой, но важный шажок. Не забываем сохранять файл в кодировке «utf-8» без сигнатуры. Так будет намного удобнее, чем менять кодировку для каждого конкретного файла. Ведь, по сути, любой файл, который мы будем обрабатывать в дальнейшем, так или иначе пройдет через редактор.

С предварительной обработкой файла можно закончить. Пора приступать к написанию кода для обработки данных в столбцах и формированию итогового «.csv» с нужной нам структурой.


Обработка текста Ф.И.О.

Данные, встречающиеся в столбцах с Ф.И.О. в основном, имеют нормализованную структуру. Однако, это далеко не правило. Довольно часто в таких данных можно наблюдать довольно большое количество мусора, плюс к тому, написание Ф.И.О. как кириллицей, так и латинскими буквами. Для примера, нормальный текст с Ф.И.О. будет выглядеть так: Иванов-Сидорович Иван Иванович. Если добавить мусора, то все это превращается в: ---Иванов-Сидоро5656dfdfвич --Иван -Иванdfs564ович, для примера. Также нередко встречается не особо приятная штука, когда буквы кириллицы заменяются на близкие (похожие) по написанию буквы латинского алфавита. Например: ---Иваnов-Сидоро5656dfdfвич --Ивaн -Иванdfs564оvич. И все это необходимо очистить. Конечно, стопроцентной гарантии очистки дать невозможно, однако, большую часть текста можно все же нормализовать.

Создадим файл data_normalize.py. В него мы будем помещать все создаваемые функции по обработке текста.

Импортируем нужные библиотеки.

Python:
import re
import string
from datetime import datetime

Начнем с функции для замены латинских букв на русские. Создадим функцию replacer(txt: str) -> str, на вход которой будем подавать букву латинского алфавита для замены, а на выходе получать эту же букву, но кириллицей.

Python:
def replacer(txt: str) -> str:
    """Замена латинских букв на русские, если такие встретятся в ФИО."""
    symbols = ("ankbtmxcepwvANKBTMXCEPWV",
               "анквтмхсерввАНКВТМХСЕРВВ")
    tr = {ord(a): ord(b) for a, b in zip(*symbols)}
    return txt.translate(tr)

С помощью ord получим символы Unicode для каждого из символов схемы, определенной в symbols. С помощью метода translate вернем копию строки, в которой каждый символ был сопоставлен и преобразован согласно карте перевода symbols.

Создадим функцию is_latin(fio: str) -> bool, которая на входе получает строку и возвращает истину или ложь, в зависимости от того, являются ли все элементы полученного кортежа латинскими буквами. Это необходимо для того, чтобы понять, является ли написание Ф.И.О. латинскими буквами. Для этого будем использовать all, который возвращает True, если все элементы последовательности истинны и False, если это не так.

Python:
def is_latin(fio: str) -> bool:
    """Проверка, является ли ФИО написанным латинскими буквами."""
    return all('a' <= char <= 'z' or 'A' <= char <= 'Z' or char in ['-', ' '] for char in fio)

Следующей функцией которая нам будет необходима, это функция dash_clear(fio: str) -> str. На вход она принимает строковые данные, а также возвращает их уже в обработанном виде. Это необходимо для того, чтобы исключить из Ф.И.О. ненужные знаки тире. А вот те, что нужно, оставить. Для примера, знак тире в данной строке нужен, так как является разделителем фамилии: Иванов-Сидорович. А вот в следующем примере, знаки тире явно лишние: ---Иванов-Сидоро5656dfdfвич.

Для начала удаляем все тире справа и слева от объекта, предварительно разделив строку по пробелу. Если есть тире отдельное от слова, оно учитываться не будет, равно как и пустые объекты, которые могут присутствовать при разделении Ф.И.О. На следующем этапе проверим каждое из слов, на содержание в них тире. Кто знает, возможно их там более одного, что не является нормой. Поэтому, подсчитываем количество тире в строке после очистки. Если оно больше единицы, делим строку по пробелам, итерируемся по получившемуся списку. Проверяем количество тире в каждом из элементов списка. И если их больше одного, делим элемент по тире, удаляем лишние элементы и снова соединяем с помощью "-".join. Если же в элементе списка нет тире, добавляем его в результирующий список таким, как есть, без изменений. Объединяем список с помощью join и возвращаем из функции.

Python:
def dash_clear(fio: str) -> str:
    """Удаление "-" из ФИО"""
    fio = " ".join([x.strip().lstrip("-").rstrip("-").strip() for x in fio.encode().decode().split()
                    if x.strip() and x.strip() != "-"])

    if fio.count("-") > 1:
        fio_norm = []
        for ch in fio.split():
            if ch.count("-") > 1:
                fio_norm.append("-".join([x.strip().lstrip("-").rstrip("-") for x in ch.split("-")
                                          if x.strip() and x.strip() != "-"]))
            else:
                fio_norm.append(ch)
        fio = " ".join(fio_norm)
    return fio

И вот, наконец мы подошли к функции, которая выполняет логику очистки, объединяя все те функции, которые были созданы нами ранее. Создадим функцию fio_normalize(fio: str) -> str, которая принимает строку с Ф.И.О., а также возвращает строку, очищенную или пустую.

Делаем все буквы Ф.И.О. в нижнем регистре. Удаляем из полученной строки лишние символы, исключая латинские буквы, буквы кириллицы, тире и пробел. Удаляем лишние тире из строки. Проверяем, состоит ли Ф.И.О. из латинских букв. Если да, делаем все буквы каждого слова заглавными и возвращаем строку. Если же строка состоит не только из латинских букв, для начала заменяем те латинские буквы, которые могут заменять похожие буквы кириллицы. Затем выполняем окончательную очистку и удаляем оставшиеся латинские буквы, если они есть, и возвращаем очищенную строку или пустую, если после очистки возвращать просто нечего.

Python:
def fio_normalize(fio: str) -> str:
    """Нормализация ФИО. Проверка, является или ФИО написанным латиницей. Удаление цифр, тире."""
    fio = fio.strip().lower()
    fio = re.sub(r'[^a-zа-я-ё ]', "", fio).strip()
    fio = dash_clear(fio)
    if is_latin(fio):
        fio = fio.title()
    else:
        fio = "".join([replacer(x) if x in string.ascii_letters else x for x in fio])
        fio = re.sub(r'[^а-я-ё ]', '', fio).title()
    return fio if fio.strip() else ""

Python:
# fio normalize
# -------------------------------------------------------------
def replacer(txt: str) -> str:
    """Замена латинских букв на русские, если такие встретятся в ФИО."""
    symbols = ("ankbtmxcepwvANKBTMXCEPWV",
               "анквтмхсерввАНКВТМХСЕРВВ")
    tr = {ord(a): ord(b) for a, b in zip(*symbols)}
    return txt.translate(tr)


def is_latin(fio: str) -> bool:
    """Проверка, является ли ФИО написанным латинскими буквами."""
    return all('a' <= char <= 'z' or 'A' <= char <= 'Z' or char in ['-', ' '] for char in fio)


def dash_clear(fio: str) -> str:
    """Удаление "-" из ФИО"""
    fio = " ".join([x.strip().lstrip("-").rstrip("-").strip() for x in fio.encode().decode().split()
                    if x.strip() and x.strip() != "-"])

    if fio.count("-") > 1:
        fio_norm = []
        for ch in fio.split():
            if ch.count("-") > 1:
                fio_norm.append("-".join([x.strip().lstrip("-").rstrip("-") for x in ch.split("-")
                                          if x.strip() and x.strip() != "-"]))
            else:
                fio_norm.append(ch)
        fio = " ".join(fio_norm)
    return fio


def fio_normalize(fio: str) -> str:
    """Нормализация ФИО. Проверка, является или ФИО написанным латиницей. Удаление цифр, тире."""
    fio = fio.strip().lower()
    fio = re.sub(r'[^a-zа-я-ё ]', "", fio).strip()
    fio = dash_clear(fio)
    if is_latin(fio):
        fio = fio.title()
    else:
        fio = "".join([replacer(x) if x in string.ascii_letters else x for x in fio])
        fio = re.sub(r'[^а-я-ё ]', '', fio).title()
    return fio if fio.strip() else ""

Вот пример обработки Ф.И.О., которая была приведена в начале.

screenshot4.png


Нормализация номера телефона

Следующие данные, которые очень часто требуют нормализации и приведения к общему виду, это номера телефонов. В данном конкретном случае учитываются, преимущественно, номера сотовых телефонов, либо номера телефонов не менее 6 символов. И то, эти номера уже достаточно трудно поддаются идентификации в дальнейшем, так как являются, по большей части номерами стационарных телефонов, которые без кода города не представляют какой-либо особой ценности.

Создадим функцию phone_normalize(phone: str) -> str, которая на входе получает строку с номером, а возвращает нормализованный номер, либо пустую строку, в зависимости от результатов обработки.

Для начала удаляем все лишние символы из строки, исключая цифры. Проверяем, является ли полученная строка цифрами, если да, определяем количество символов в строке. В сотовом номере телефона таких цифр как правило 11. Если их больше, то обрабатывать такую строку не имеет особого смысла. Поэтому, возвращаем пустое значение. Если количество символов в строке находится в диапазоне от 6 до 10, возвращаем такой номер без обработки, будем считать, что это номер стационарного телефона. Если количество символов равно 10, Проверяем, является ли первая цифра 9-кой. Если да, добавляем 7 и возвращаем из функции, если нет, возвращаем значение в неизменном виде. И если количество символов равно 11, проверяем первые цифры строки. Если это 8, а вторая цифра 9, заменяем 8 на 7 и возвращаем из функции. Если первая цифра 7, возвращаем без изменений.

Python:
# phone normalize
# -------------------------------------------------------------
def phone_normalize(phone: str) -> str:
    """Нормализация номера телефона.
    Приведение номера к единому виду с семеркой в начале, если это сотовый телефон."""
    phone = re.sub(r'[^0-9]', '', phone.encode().decode())
    if phone.strip().isdigit():
        if len(phone.strip()) > 11:
            return ""
        elif 6 <= len(phone.strip()) < 10:
            return phone.strip()
        elif len(phone.strip()) == 10:
            if phone.strip().startswith("9"):
                return f"7{phone.strip()}"
            else:
                return phone.strip()
        elif len(phone.strip()) == 11:
            if phone.strip().startswith("8") and phone.strip()[1] == "9":
                return f"7{phone.strip()[1:]}"
            elif phone.strip().startswith("7"):
                return phone.strip()
            else:
                return ""
        return ""
    return ""

Небольшая демонстрация работы функции:

screenshot5.png


Нормализация адреса электронной почты

Электронная почта также требует нормализации, так как иногда при простой ошибке ввода данных и отсутствии валидации в электронной почте присутствует бардак. Например: @Primer или primer@primer\ru.

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

Создадим функцию validate_email(mail: str) -> bool. На вход она получает адрес, а возвращает истину или ложь, в зависимости от результата. Проверяем, есть ли @ в строке. Если да, проверяем, не пустое ли первое значение после разделения строки по @. Если нет, проверяем, есть ли «.» во втором значении. Затем проверяем, количество значение после разделения второго значения по точке. Если оно больше 1, возвращаем True. Во всех остальных случаях возвращаем False.

Python:
def validate_email(mail: str) -> bool:
    """
    Простая валидация e-mail на наличие левой части до @, на наличие в правой части точки, а также
    на количество букв в домене верхнего уровня.
    """
    if "@" in mail:
        if mail.strip().split("@")[0].strip():
            if "." in mail.strip().split("@")[1]:
                if len(mail.strip().split("@")[1].strip().split(".")[1]) > 1:
                    return True
                return False
            return False
        return False
    return False

Теперь создадим функцию email_normalize(mail: str) -> str, которая на входе принимает стоку с адресом, а возвращает либо нормализованный адрес, либо пустую строку. Для начала проверяем, есть ли @ в адресе. Если нет, дальнейшая обработка не имеет смысла. Возвращаем пустую строку. Если @ есть, делим адрес по @. Проверяем, не является ли первый элемент списка пустым значением. Если да, возвращаем пустую строку. Если нет, проверяем, есть ли точка во втором элементе списка. Если да, делим по точке, проверяем количество элементов после разделения. Если их больше или 2, возвращаем обработанный адрес, предварительно его валидировав. Если нет точки, предполагаем, что она может быть заменена неким символом. Пытаемся заменить символы. Затем снова выполняем валидацию. Если все хорошо, возвращаем обработанный адрес. Если нет, возвращаем пустую строку.

Python:
def email_normalize(mail: str) -> str:
    """Нормализация e-mail, с проверками наличия необходимых атрибутов и валидацией."""
    mail = mail.encode().decode()
    if "@" in mail:
        mail = mail.strip().split("@")
        if mail[0].strip():
            if "." in mail[1]:
                ch_mail = mail[1].split(".")
                if len(ch_mail) >= 2:
                    return "@".join(mail) if validate_email("@".join(mail)) else ""
            else:
                two_side = re.sub(r'[,:;!_*+()/#%&-\\]', '.', mail[1])
                return "@".join((mail[0], two_side)) if validate_email("@".join((mail[0], two_side))) else ""
        return ""
    return ""

Python:
# email normalize
# -------------------------------------------------------------
def validate_email(mail: str) -> bool:
    """
    Простая валидация e-mail на наличие левой части до @, на наличие в правой части точки, а также
    на количество букв в домене верхнего уровня.
    """
    if "@" in mail:
        if mail.strip().split("@")[0].strip():
            if "." in mail.strip().split("@")[1]:
                if len(mail.strip().split("@")[1].strip().split(".")[1]) > 1:
                    return True
                return False
            return False
        return False
    return False


def email_normalize(mail: str) -> str:
    """Нормализация e-mail, с проверками наличия необходимых атрибутов и валидацией."""
    mail = mail.encode().decode()
    if "@" in mail:
        mail = mail.strip().split("@")
        if mail[0].strip():
            if "." in mail[1]:
                ch_mail = mail[1].split(".")
                if len(ch_mail) >= 2:
                    return "@".join(mail) if validate_email("@".join(mail)) else ""
            else:
                two_side = re.sub(r'[,:;!_*+()/#%&-\\]', '.', mail[1])
                return "@".join((mail[0], two_side)) if validate_email("@".join((mail[0], two_side))) else ""
        return ""
    return ""

Пример работы функции:

screenshot6.png


Нормализация даты рождения

В данном случае я пока что, не стал усложнять данную функцию. Поэтому, обработка сводится к тому, что распознаются разделители «-» и «/». После чего дата меняется на более привычный нам формат: 01.01.9999. Во всех остальных случаях дата рождения не изменяется. Здесь можно было бы использовать сторонние библиотеки, например: dateutil, arrow, moment, maya, delorean, freezegun. Так я и делал раньше, однако, в какой-то момент было замечено, что использование данных библиотек значительно увеличивает время обработки файла. Поэтому необходимо больше экспериментов с данными библиотеками.

Если дата состоит только из цифр, пытаемся обойтись возможностями стандартной библиотеки datetime. В случаях если есть разделители, о которых я говорил выше, проверяем количество символов в первом элементе, если разделить по разделителю. Если символов 4, то можно судить о том, что это год рождения. И переставлять значения в соответствии с этим. Если же количество последних символов 4, обрабатываем дату с учетом этого.

Возможно, это самый непродуманный модуль. Поэтому, работы с ним, думаю, будут продолжены в случае необходимости. Или будут заменены на одну из сторонних библиотек.

Python:
# birthdate normalize
# -------------------------------------------------------------
def bdate_normalize(bdate: str) -> str:
    """Нормализация даты рождения, если это возможно. Иначе, возвращаем в том виде, как и получили"""
    bdate = bdate.strip().replace(" ", "")
    if bdate:
        if bdate.isdigit():
            bdate = datetime.fromtimestamp(int(bdate)).strftime('%d.%m.%Y')
            return bdate
        if "-" in bdate and bdate.count("-") == 2:
            bdate = bdate.split("-")
            if len(bdate[0]) == 4:
                bdate = f"{bdate[2]}.{bdate[1]}.{bdate[0]}"
                return bdate
            if len(bdate[2]) == 4:
                bdate = f"{bdate[0]}.{bdate[1]}.{bdate[2]}"
                return bdate
        elif "/" in bdate and bdate.count("/") == 2:
            bdate = bdate.split("/")
            if len(bdate[0]) == 4:
                bdate = f"{bdate[2]}.{bdate[1]}.{bdate[0]}"
                return bdate
            if len(bdate[2]) == 4:
                bdate = f"{bdate[0]}.{bdate[1]}.{bdate[2]}"
                return bdate
        else:
            return bdate
    else:
        return ""


Обработка полей с дополнительными данными

Как помните, я упоминал в начале, что, если вам требуются дополнительные данные, их можно оставить в наборе, переименовав столбец в удобочитаемое значение. Для примера, так произошло с полями vk_nickName и подобными. Данные поля не подлежат обработке, а добавляются в новый набор данных в том виде, в котором они содержатся в исходном файле в столбец «other» и разделяются «---». Согласен, здесь можно придумать что-то изящнее, но в данном случае это не имеет особого значения. И если мне придется получить каждое из значений в отдельности, я легко смогу это сделать, разделив строку по разделителю «---».

Создадим функцию other_check(heads_list: list, heads: list, row: list) -> str. На вход она принимает список заголовков столбцов, которые у нас будут определены позже и являются неким стандартным набором. Список заголовков, полученных из файла. И текущую строку с данными. Возвращает обработанную и объединенную разделителями строку, либо пустое значение. Итерируемся по полученным заголовкам. Проверяем, есть ли текущее значение в стандартном наборе заголовков и не является ли пустым значение столбца. Если все в порядке, добавляем в список заголовок и его значение. После чего объединяем данные «---» и возвращаем из функции.

Python:
def other_check(heads_list: list, heads: list, row: list) -> str:
    """Получение значений из столбцов, не являющихся основными."""
    other = []
    for item in heads:
        if item not in heads_list and row[heads.index(item)].strip():
            other.append(f"{item}: {row[heads.index(item)].replace("'", "").replace('"', '')}")
    return "---".join(other)


Проверка количества непустых ячеек в строке

Создадим функцию count_val(items: list) -> bool, которая на входе получает список со значениями, а возвращает истину или ложь, в зависимости от полученного результата. Подсчитываем количество непустых значений. И если их больше 1, возвращаем True. Иначе, возвращаем False.

Это необходимо для того, чтобы в наш формируемый файл не попадали строки с одним значением или вовсе пустые строки, так как такие строки не имеют какой-либо ценности или информативности.

Python:
def count_val(items: list) -> bool:
    """Подсчет количества пустых ячеек в строке."""
    return True if sum(1 for x in items if x) > 1 else False

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

Python:
import re
import string
from datetime import datetime


# fio normalize
# -------------------------------------------------------------
def replacer(txt: str) -> str:
    """Замена латинских букв на русские, если такие встретятся в ФИО."""
    symbols = ("ankbtmxcepwvANKBTMXCEPWV",
               "анквтмхсерввАНКВТМХСЕРВВ")
    tr = {ord(a): ord(b) for a, b in zip(*symbols)}
    return txt.translate(tr)


def is_latin(fio: str) -> bool:
    """Проверка, является ли ФИО написанным латинскими буквами."""
    return all('a' <= char <= 'z' or 'A' <= char <= 'Z' or char in ['-', ' '] for char in fio)


def dash_clear(fio: str) -> str:
    """Удаление "-" из ФИО"""
    fio = " ".join([x.strip().lstrip("-").rstrip("-").strip() for x in fio.encode().decode().split()
                    if x.strip() and x.strip() != "-"])

    if fio.count("-") > 1:
        fio_norm = []
        for ch in fio.split():
            if ch.count("-") > 1:
                fio_norm.append("-".join([x.strip().lstrip("-").rstrip("-") for x in ch.split("-")
                                          if x.strip() and x.strip() != "-"]))
            else:
                fio_norm.append(ch)
        fio = " ".join(fio_norm)
    return fio


def fio_normalize(fio: str) -> str:
    """Нормализация ФИО. Проверка, является или ФИО написанным латиницей. Удаление цифр, тире."""
    fio = fio.strip().lower()
    fio = re.sub(r'[^a-zа-я-ё ]', "", fio).strip()
    fio = dash_clear(fio)
    if is_latin(fio):
        fio = fio.title()
    else:
        fio = "".join([replacer(x) if x in string.ascii_letters else x for x in fio])
        fio = re.sub(r'[^а-я-ё ]', '', fio).title()
    return fio if fio.strip() else ""


# phone normalize
# -------------------------------------------------------------
def phone_normalize(phone: str) -> str:
    """Нормализация номера телефона.
    Приведение номера к единому виду с семеркой в начале, если это сотовый телефон."""
    phone = re.sub(r'[^0-9]', '', phone.encode().decode())
    if phone.strip().isdigit():
        if len(phone.strip()) > 11:
            return ""
        elif 6 <= len(phone.strip()) < 10:
            return phone.strip()
        elif len(phone.strip()) == 10:
            if phone.strip().startswith("9"):
                return f"7{phone.strip()}"
            else:
                return phone.strip()
        elif len(phone.strip()) == 11:
            if phone.strip().startswith("8") and phone.strip()[1] == "9":
                return f"7{phone.strip()[1:]}"
            elif phone.strip().startswith("7"):
                return phone.strip()
            else:
                return ""
        return ""
    return ""


# email normalize
# -------------------------------------------------------------
def validate_email(mail: str) -> bool:
    """
    Простая валидация e-mail на наличие левой части до @, на наличие в правой части точки, а также
    на количество букв в домене верхнего уровня.
    """
    if "@" in mail:
        if mail.strip().split("@")[0].strip():
            if "." in mail.strip().split("@")[1]:
                if len(mail.strip().split("@")[1].strip().split(".")[1]) > 1:
                    return True
                return False
            return False
        return False
    return False


def email_normalize(mail: str) -> str:
    """Нормализация e-mail, с проверками наличия необходимых атрибутов и валидацией."""
    mail = mail.encode().decode()
    if "@" in mail:
        mail = mail.strip().split("@")
        if mail[0].strip():
            if "." in mail[1]:
                ch_mail = mail[1].split(".")
                if len(ch_mail) >= 2:
                    return "@".join(mail) if validate_email("@".join(mail)) else ""
            else:
                two_side = re.sub(r'[,:;!_*+()/#%&-\\]', '.', mail[1])
                return "@".join((mail[0], two_side)) if validate_email("@".join((mail[0], two_side))) else ""
        return ""
    return ""


# birthdate normalize
# -------------------------------------------------------------
def bdate_normalize(bdate: str) -> str:
    """Нормализация даты рождения, если это возможно. Иначе, возвращаем в том виде, как и получили"""
    bdate = bdate.strip().replace(" ", "")
    if bdate:
        if bdate.isdigit():
            bdate = datetime.fromtimestamp(int(bdate)).strftime('%d.%m.%Y')
            return bdate
        if "-" in bdate and bdate.count("-") == 2:
            bdate = bdate.split("-")
            if len(bdate[0]) == 4:
                bdate = f"{bdate[2]}.{bdate[1]}.{bdate[0]}"
                return bdate
            if len(bdate[2]) == 4:
                bdate = f"{bdate[0]}.{bdate[1]}.{bdate[2]}"
                return bdate
        elif "/" in bdate and bdate.count("/") == 2:
            bdate = bdate.split("/")
            if len(bdate[0]) == 4:
                bdate = f"{bdate[2]}.{bdate[1]}.{bdate[0]}"
                return bdate
            if len(bdate[2]) == 4:
                bdate = f"{bdate[0]}.{bdate[1]}.{bdate[2]}"
                return bdate
        else:
            return bdate
    else:
        return ""


# other field check/count field check
# -------------------------------------------------------------
def other_check(heads_list: list, heads: list, row: list) -> str:
    """Получение значений из столбцов, не являющихся основными."""
    other = []
    for item in heads:
        if item not in heads_list and row[heads.index(item)].strip():
            other.append(f"{item}: {row[heads.index(item)].replace("'", "").replace('"', '')}")
    return "---".join(other)


def count_val(items: list) -> bool:
    """Подсчет количества пустых ячеек в строке."""
    return True if sum(1 for x in items if x) > 1 else False


А на сегодня все. Все файлы из статьи вы найдете во вложении, вместе с файлом примера.

Спасибо за внимание. Надеюсь, данная информация будет вам полезна
 

Вложения

  • code 1.zip
    2,5 КБ · Просмотры: 242
  • primer.zip
    2,5 МБ · Просмотры: 238
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!