Генерация логина и пароля студенту (на Python 3)

Логин для студента и пароль к нему: техническое задание

Одно из современных требований к вузам при реализации образовательных программ — предоставление доступа обучающимся к электронной информационно-образовательной среде. Для этого каждый студент должен получить свои логин (по возможности уникальный) и пароль. Разработка программы для генерации студенческих логинов и паролей — прекрасный пример технического задания начинающим программистам. В данной заметке будет показан весь процесс разработки такой программы на языке Python 3-й версии.

Итак, попробуем вкратце сформулировать техническое задание:

  1. Логин должен быть уникальным (ни один другой студент, даже полный тёзка, не должен иметь такой же логин).
  2. Логин, кроме фамилии и инициалов пользователя, должен отражать другую информацию о пользователе — как минимум образовательную программу, на которой он обучается. Расширение требований к представленной в логине информации (год поступления в вуз, номер группы, форма обучения и т.д.) позволит не только быстрее и точнее идентифицировать пользователя по его логину (другими словами, обеспечить информативность логина), но и повысить уникальность логина, т.е. выполнить требование 1.
  3. Следует избрать простой и однозначный принцип транслитерации имени пользователя (как и другой информации на русском языке, закладываемой в логин).
  4. Пароль должен состоять из некоторого минимального количества знаков, включать как латинские буквы (в нижнем и верхнем регистре), так и цифры. Требования к паролю сильно зависят от информационной системы, поэтому для нашего примера условимся, что длина пароля составит ровно 8 символов, причём первый символ может быть только прописной или строчной буквой; остальные 7 символов могут быть прописными или строчными буквами и цифрами, при этом наличие букв разного регистра и цифр в пароле обязательно.

Основные этапы генерации логина и пароля

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

  1. Получить информацию о пользователе, необходимую для формирования уникального логина (в соответствии с техническим заданием).
  2. Транслитерировать фамилию и инициалы студента, а также другую русскоязычную информацию, т.е. передать эту информацию символами латинского (точнее, английского) алфавита.
  3. При необходимости перекодировать другую информацию для унификации и удобства отражения её в логине: например, год набора из 4 цифр (2021) может быть представлен двумя последними цифрами (21), а номер учебной группы из одной цифры (3) — двумя цифрами (03).
  4. Сформировать из транслитерированных и перекодированных данных уникальный логин.
  5. Сгенерировать случайным образом пароль студента.
  6. Проверить пароль на соответствие техническому заданию, в случае несоответствия — повторить операцию.
  7. Вывести сгенерированные логин и пароль студента.
  8. Записать в файл Ф.И.О. студента и другие идентификационные данные, сформированный логин и сгенерированный пароль.

Очевидно, что мы можем упростить данный перечень задач, объединив некоторые задачи, которые будут решаться взаимосвязано (для них, возможно, даже потребуется одна функция). Такими задачами являются, во-первых, задачи формирования логина (2, 3 и 4); во-вторых, генерация и проверка пароля (5 и 6). Окончательный перечень выглядит следующим образом:

  1. Получить информацию о пользователе, необходимую для формирования уникального логина.
  2. Сформировать из полученных данных логин и пароль в соответствии с техническим заданием и принципом уникальности.
  3. Сгенерировать пароль, проверить его на соответствие техническому заданию.
  4. Вывести сгенерированные логин и пароль студента.
  5. Записать в файл информацию о студенте, сгенерированные логин и пароль.

Есть ещё ряд проблем технического характера (например, обеспечение работоспособности программы в случае неверного ввода пользователем данных), но они будут уже решаться в рамках определённых выше пяти этапов.

Главная функция программы

Имея представление об этапах выполнения программы, давайте сформируем главную функцию main и сразу определим в ней вызываемые функции, отвечающие за те или иные задачи:

        def main():
            """Функция генерирует логин и пароль и сохраняет их в файле."""
            # Получить информацию о студенте.
            information = get_info()

            # На основе полученных данных сгенерировать логин студента.
            login = get_login(information)
            print('Сгенерированные данные:')
            print(f'логин - {login}')

            # Сгенерировать корректный пароль студента.
            password = get_password(*parameters)
            print(f'пароль - {password}')

            # Записать информацию в файл.
            save_info(information, login, password)
            print('Информация записана в файл.')
        

При необходимости мы уточним аргументы, которые будем передавать тем или иным вызываемым функциям (как, например, функции get_password), но общая структура программы нам уже вполне ясна. Теперь приступим к написанию этих отдельных функций.

Формирование уникального логина

Прежде, чем писать функцию get_login, давайте всё-таки окончательно определимся со следующими принципиальными вопросами:

Начнём с последнего пункта, так как здесь ответ очевиден. Есть общепринятый принцип транслитерации русскоязычных имён и названий, который используется, в частности, при выдаче гражданам России заграничных паспортов (он отражён в Приложении 2 к приказу МВД России от 27.11.2017 № 889 "Об утверждении Административного регламента Министерства внутренних дел Российской Федерации по предоставлению государственной услуги по оформлению и выдаче паспортов гражданина Российской Федерации, удостоверяющих личность гражданина Российской Федерации за пределами территории Российской Федерации, содержащих электронный носитель информации"). Этот принцип мы и возьмём за основу, что позволит нам сформировать следующий словарь, используемый при перекодировании русскоязычных данных в латиницу:

        # Для транслитерирования используется принцип соотнесения латинских и русских
        # букв, принятый при выдаче заграничных паспортов гражданам России.
        TRANSLIT = {'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e',
                    'ё': 'e', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'i', 'к': 'k',
                    'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r',
                    'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts',
                    'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': 'ie', 'ы': 'y', 'ь': '',
                    'э': 'e', 'ю': 'iu', 'я': 'ia', '_': '_'}
        

Мы только добавили мягкий знак (он в транслитерированных именах и названиях никак не отражен, поэтому на его месте в словаре пустое значение; фамилия Коваль будет записана как Koval) и знак подчёркивания (для технических целей, он будет использоваться в качестве разделительного символа).

Вернёмся к вопросу о формировании логина. Для уникальности выберем такой вариант: помимо фамилии и инициалов, в логине будут отражены направление подготовки(специальность), год поступления студента в вуз, номер учебной группы (цифрами). На тот случай, если у нас и в одной учебной группе появятся два человека, чьи данные при формировании логина полностью совпадут — например, близнецы Александрова Александра Александровна и Александрова Алеся Александровна, поступившие на очную форму обучения на направление подготовки Эб (это буквенный код направления подготовки "Экономика") в 2020 году и зачисленные в группу 5, обе могут получить идентичные логины aleksandrova_aa_eb2005, — мы добавим номер студента в группе, полученный им при формировании группы (потом состав группы может меняться и номера в списке изменятся, но уникальность логина уже будет достигнута). Теперь Александра получит логин aleksandrova_aa_eb2005_01, а её сестра Алесяaleksandrova_aa_eb2005_02.

Да, кстати, чтобы отличить очное обучение от заочного, достаточно промаркировать в логине одно из них: мы будем отражать только заочную форму обучения, по остальным логинам будет понятно, что по умолчанию они принадлежат студентам-очникам. (При вводе данных у нас очной форме обучения будет соответствовать цифра 1, а заочной — 2.)

Итак, будем считать, что у нас есть данные о пользователе (этап получения данных о студенте мы пока пропустили, потому что разбирались с тем, какие данные нам нужны, но ниже к нему вернёмся). Поскольку данных этих достаточно много, не будем загромождать нашу функцию многочисленными параметрами: давайте передавать в неё данные в виде словаря и затем их распаковывать.

Последнее замечание: все буквы в логине у нас будут строчными, а разделительным знаком станет символ подчеркивания (_). Вот теперь можно представить код функции get_login:

        def get_login(information):
            """Функция генерирует логин на основании данных о студенте."""
            # Распаковать данные из словаря.
            fullname = information['fullname']
            code = information['code']
            year = information['year']
            group = information['group']
            number = information['number']
            form = information['form']

            # Преобразовать в строчные буквы и транслитерировать имя и
            # инициалы студента c использованием глобального словаря.
            name = fullname.split()
            if len(name) == 2:
                name, firstname = name
                shortname = f'{name}_{firstname[0]}'
            else:
                name, firstname, patronymic = name
                shortname = f'{name}_{firstname[0]}{patronymic[0]}'
            transl_name = ''
            for ch in shortname.lower():
                transl_name += TRANSLIT[ch]

            # Преобразовать в строчные буквы и транслитерировать код направления
            # подготовки (специальности) c использованием глобального словаря.
            transl_code = ''
            for ch in code.lower():
                transl_code += TRANSLIT[ch]

            # При необходимости добавить к номеру группы и номеру студента начальный 0.
            # Также преобразовать сведения о форме обучения
            # (в логине студента будет отражена только заочная форма обучения).
            w_group = group if int(group) > 10 else '0' + group
            w_num = number if int(number) > 10 else '0' + number
            w_form = '' if form == '1' else 'z'

            # Сформировать логин студента.
            login = f'{transl_name}_{transl_code}{year[-2:]}{w_group}{w_form}_{w_num}'

            # Вернуть сформированный логин.
            return login
        

Генерация пароля и проверка его соответствия

Поскольку мы объединили в один этап две задачи (генерацию пароля и его валидацию), то сейчас мы можем написать две функции для решения этих задач: функцию генерации пароля get_password, которая будет вызывать для проверки соответствия вновь сгенерированного пароля установленным требованиям другую функцию — check_password. (Задачи у нас хотя и взаимосвязанные, но разные; поэтому объединять их в одной функции мы не станем.) Если сгенерированный случайным образом с использованием стандартной библиотеки random пароль не подойдёт, то процедура повторится снова. Таким образом, нам целесообразно использовать цикл while.

Метод randint из библиотеки random будет использовать при случайном выборе две последовательности: для первого символа — только латинские буквы в обеих регистрах, которые мы определим в глобальной константе A_B_C; для остальных 7 символов — буквы с добавлением цифр (глобальная константа SYMBOLS).

Вот завершённый код функций:

        import random

        # Определить глобальные константы.
        A_B_C = 'AaBbCcDdEeFfGgHhIiJiKkLlMmNnJjPpQqRrSsTtUuVvWwXxYyZz'
        NUMBERS = '0123456789'
        SYMBOLS = A_B_C + NUMBERS

        def get_password(A_B_C, SYMBOLS):
            """Функция генерирует пароль из 8 символов."""
            # Сгенерировать пароль в цикле с использованием глобальных констант
            # (пока не будет удовлетворены все условия к формированию пароля).
            repeat = True
            password = ''
            while repeat:
                password = ''
                password += A_B_C[random.randint(0, 51)]
                for _ in range(7):
                    password += SYMBOLS[random.randint(0, 61)]

                repeat = check_password(password)

            # Вернуть сгенерированный пароль.
            return password


        def check_password(password):
            """Функция проверяет требования к паролю."""
            # Определить переменные для проверки наличия в пароле цифр,
            # строчных и прописных букв.
            is_digit = False
            is_lower = False
            is_upper = False

            # Проверить наличие в пароле требуемых символов.
            for symbol in password:
                if symbol.isdigit():
                    is_digit = True
                elif symbol.islower():
                    is_lower = True
                elif symbol.isupper():
                    is_upper = True

            # False в случае, если пароль удовлетворяет требованиям
            # (чтобы завершить цикл генерации пароля).
            is_not_valid = True
            if is_digit and is_lower and is_upper:
                is_not_valid = False

            # Вернуть информацию о необходимости заново генерировать пароль.
            return is_not_valid
        

Получение данных о студенте от пользователя

Теперь вернемся к первой задаче: получению данных о студенте. Мы уже понимаем, какие данные нам нужны:

Знаем мы также, что функция get_info должна вернуть эти данные в виде словаря information, содержащего пары ключ-значение: например, {'fullname': 'Иванов Иван Иванович', 'year': '2020', ...}.

Самое сложное в этой задаче — обеспечить бессбойное получение данных, т.е. ошибка пользователя (нажатие Enter без ввода данных или ввод неверных данных, таких, как букв вместо цифр) не должна привести к аварийному завершению программы. Для этого мы будем использовать инструкции assert для проверки вводимых данных и перехватывать исключения. И мы перепишем начало функции main — теперь при возврате из функции get_info сигнала об ошибке (строкового литерала 'Error') будет циклически производиться повторный ввод данных:

            # Получить информацию о студенте.
            information = 'Error'
            while information == 'Error':
                information = get_info()
        

Отлично, но прежде чем привести готовый код функции get_info, поясним, что программа примет за валидные данные фамилию и имя студента даже без отчества (такие варианты тоже возможны в реальной жизни). Вот что получается:

        def get_info():
            """Функция получает от пользователя корректную информацию о студенте."""
            # Получить данные о студенте и проверить их на корректность ввода.
            try:
                fullname = input('Введите Ф.И.О. студента (при наличии): ').strip()
                assert type(fullname) == str and (2 <= len(fullname.split()) <= 3)

                code = input('Введите буквенный код направления подготовки (специальности): ').strip()
                assert len(code) > 0

                year = input('Введите год поступления студента (4 цифры): ').strip()
                assert len(year) == 4
                check_year = int(year)
                assert 1950 < check_year < 2025

                group = input('Введите цифрой номер учебной группы: ').strip()
                assert len(group) > 0
                check_group = int(group)
                group = str(check_group)

                number = input('Введите исходный номер студента в учебной группе: ').strip()
                assert len(group) > 0
                check_number = int(number)
                number = str(check_number)

                form = input('Введите форму обучения (1=очное, 2=заочное): ').strip()
                assert len(code) > 0 and form in '12'

                # Преобразовать данные в словарь.
                information = {'fullname': fullname,
                               'code': code,
                               'year': year,
                               'group': group,
                               'number': number,
                               'form': form}
            except (AssertionError, ValueError):
                # В случае ошибки повторить ввод данных.
                print('Данные введены неверно! Необходимо ввести их заново.')
                return 'Error'
            else:
                # Вернуть словарь с данными.
                return information
        

Запись информации в СSV-файл

Сгенерированную информацию следует сохранить, привязав к полученным данным о студенте. Для записи можно использовать файлы различного формата: от простых текстовых файлов до баз данных. В данном случае мы избрали формат СSV (Comma-Separated Values — значения, разделённые запятыми), т.е. текстовый формат, предназначенный для представления табличных данных. Именно в формате таблицы удобнее всего визуализовать полученную информацию, хотя файлы .csv открываются в любом текстовом редакторе. Поэтому для работы функции save_info мы импортируем стандартную библиотеку csv.

Назвать сохраняемый файл можно по-разному; здесь мы для удобства ориентирования в его название добавим дату создания, для чего задействуем возможности модуля datetime из одноимённой стандартной библиотеки.

Вот наша функция save_info:

        import csv
        from datetime import datetime

        def save_info(information, login, password):
            """Функция сохраняет всю информацию о студенте в датированном CSV-файле."""
            # Распаковать словарь с информацией о студенте.
            fullname = information['fullname']
            code = information['code']
            year = information['year']
            group = information['group']
            number = information['number']

            # Скорректировать для записи в файл информацию о форме обучения
            # (в удобном для чтения виде).
            form = information['form']
            fullform = 'ОФО' if form == '1' else 'ЗФО'

            # Сформировать строку для записи в CSV-файл.
            w_info = [fullname, code, year, group, number, fullform, login, password]

            # Сформировать дату для имени файла.
            cur_datetime = datetime.now()
            w_datetime = f'{cur_datetime.year}-{cur_datetime.month}-{cur_datetime.day}'

            # Открыть файл для записи (дозаписи) и записать в него строку.
            with open(f'logins_{w_datetime}.csv', 'a') as file:
                writer = csv.writer(file)
                writer.writerow(w_info)
        

Рефакторинг основной функции для повторного выполнения

Поскольку мы с самого начала спроектировали нашу программу, существенного её рефакторинга не требуется; в главную функцию main мы уже внесли некоторые изменения на этапе написания функции get_info.

В принципе, программа готова. Но теперь возникает вопрос: а что если мы хотим за один сеанс работы сгенерировать логины и пароли нескольким студентам и записать их в один и тот же файл (благо, мы уже предусмотрели в функции save_info возможность дозаписать данные)? Давайте видоизменим нашу основную функцию main с учётом этого пожелания, которого первоначально не было в техническом задании, а заодно внесём в код маленькие, но приятные улучшения — в частности, явно обозначим для пользователя момент запуска программы и завершения ею работы. Для обеспечения цикличности программы задействуем всё тот же цикл while и переменную-флаг:

        def main():
            """Функция генерирует логин и пароль и сохраняет их в CSV-файле."""
            # Инициализировать флаг, который будет повторять выполнение программы.
            another = 'д'

            # Сообщить о начале работы программы.
            print('Данная программа генерирует пароли и логины обучающимся.')
            print('Пароли и логины сохраняются в CSV-файле.')

            # Запустить цикл выполнения программы.
            while another == 'д'.lower():
                # Получить информацию о студенте.
                information = 'Error'
                while information == 'Error':
                    information = get_info()

                # На основе полученных данных сгенерировать логин студента.
                login = get_login(information)
                print('Сгенерированные данные:')
                print(f'логин - {login}')

                # Сгенерировать корректный пароль студента.
                password = get_password(A_B_C, SYMBOLS)
                print(f'пароль - {password}')

                # Записать информацию в CVS-файл.
                save_info(information, login, password)
                print('Информация записана в файл.')

                # Запросить у пользователя необходимость повтора программы
                # для генерации пароля и логина ещё одному студенту.
                print('Повторить операции для ещё одного студента?')
                another = input('д=да, всё остальное=нет: ')
                if another.lower() == 'да':
                    another = another[0]

            # Сообщить о завершении программы.
            print('Работа программы завершена.')
        

Пример выполнения программы и чтение полученных данных

Можно добавить в код программы и другие украшения (такие, как отступы между сообщениями программы), а также развернуть документацию в функциях. Итоговый код представлен в файле loginpassword.py и на GitHub.

Вот пример запуска программы в IDLE:

Запуск программы в Python 3 IDLE

Полученный файл, как уже говорилось, можно открыть в текстовом редакторе:

Файл программы в текстовом редакторе

Кстати, файлы .csv отлично смотрятся в программах, работающих с электронными таблицами — таких, как Excel от Microsoft или Numbers от Apple (на скриншоте), а также их аналогах:

Файл программы в Numbers

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

Автор благодарен всем терпеливым читателям, которые смогли дойти до конца этой достаточно объёмной заметки.