Цель этого руководства — научить вас создавать собственный генератор наборов данных CLI (интерфейс командной строки) с использованием языка программирования Python.

Эта программа будет генерировать набор данных в виде файлов CSV (текстовые файлы с информацией, разделенной запятыми, сохраненные с расширением «.csv»).

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

Если вам не терпится или вы предпочитаете изучать код самостоятельно, а не по туториалам, вот ссылка на исходники — https://github.com/touhf/Dataset_Generator

Всего около 400 строк кода.

Вы также можете прочитать это руководство на github — https://github.com/touhf/build-your-own-dataset-generator

Создание виртуальной среды

Для этой программы вам нужно будет установить несколько модулей pip, поэтому рекомендуется создать виртуальную среду.

Чтобы создать виртуальную среду, введите следующую команду в своем терминале.

>>> python -m venv /path/to/new/virtual/environment

Активация виртуальной среды.

>>> source /path/to/new/virtual/environment/bin/activate

Итак, когда вы будете устанавливать модули pip для этого проекта, не забудьте сначала активировать созданную виртуальную среду, иначе вы будете устанавливать модули глобально.

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

Если вы хотите узнать больше о виртуальных средах в Python, вот ссылка на официальную документацию — https://docs.python.org/3/library/venv.html

Подготовка

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

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

Создайте скрипт Python «main.py», здесь будет основной код.

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

class Header:
    columns = []  # list of Column objects

    def __init__(self):
        pass

    def add_column(self, col: Column):
        self.columns.append(col)

    def remove_column(self, index: int):
        del self.columns[index]

columns – это список объектов Column, которые содержат имя столбца и тип данных, которые необходимо сгенерировать. Чуть позже мы создадим класс Column.

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

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

class DataRow:
    cells = []  # row of data

    def __init__(self):
        pass

Объект DataRow будет использоваться для создания строки данных, записи ее в файл CSV, затем очистки ячеек и создания следующей строки данных.

Ряды будут выглядеть так:

age, name, city,
20, Sharon Evans, Helsinki,
49, Mary Carey, Buluan,
47, Deanna Guerra, Dhampur,

Это пример данных, сгенерированных программой, первый столбец имеет имя «возраст» и тип «случайное_число», указанный от 0 до 100, второй «имя» имеет тип «случайное_имя», третий — «случайный_город» любой страны.

Теперь давайте создадим класс Column.

class Column:
    def __init__(self, name, col_type):
        self.name = name
        self.col_type = col_type

 def __str__(self):
        return "{0}, ".format(self.name)

Функция, которая будет генерировать данные, будет проверять col_type каждого объекта столбца в заголовке и соответственно генерировать данные.

Теперь мы добавим все типы столбцов для программы.

from enum import Enum
# available column types for generation
class ColumnType(Enum):
    NAME          = 1
    PHONE_NUMBER  = 2
    EMAIL         = 3
    RANDOM_NUMBER = 4
    CITY          = 5
    COUNTRY       = 6
    PASSWORD      = 7

Для некоторых типов столбцов потребуются дополнительные данные для указания параметров, например нижняя и верхняя границы для RANDOM_NUMBER или страна для типа CITY.

После завершения этого урока не стесняйтесь добавлять свои собственные функции в качестве упражнения.

# print all column types
def list_column_types():
    print("Available column types:")
    for col_type in ColumnType:
        print("\t{0}) {1}".format(col_type.value, col_type.name))

Эта функция будет использоваться для отображения всех доступных типов данных для пользователя.

Во время создания данных для двух из этих типов данных требуются дополнительные параметры: для RANDOM_NUMBER требуются нижняя и верхняя границы, а для CITY требуется страна.

Чтобы не добавлять дополнительные операторы if в функцию генератора из-за двух типов, которые требуют дополнительных входных данных, мы добавим два параметра в класс Column для каждого объекта.

class Column:
    def __init__(self, name, col_type):
        
        #...
        
        # borders for RANDOM_NUMBER
        # lo also used for specifying country for CITY type
        self.lo = 0
        self.hi = 0

Добавьте функцию «__str__» в класс DataRow, чтобы добавлять целые строки данных в CSV-файл во время генерации.

class DataRow:

 # ...

    def __str__(self):
        str_row = ""
        
        for cell in self.cells:
            str_row += "{0}, ".format(cell)
        str_row += "\n"
        
        return str_row

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

Создайте файл «generators.py», чтобы основной файл не был слишком большим.

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

Активируйте созданную ранее виртуальную среду, если она еще не активирована, и установите модуль, используя «имена установки pip» в терминале, а затем импортируйте его в файл «generators.py».

import names

Добавить функцию генерации случайного имени.

def get_random_name(arg1, arg2):
    return names.get_full_name()

Чтобы сгенерировать случайное число, мы импортируем «random». Вам не нужно устанавливать его с помощью pip, потому что он является частью стандартной библиотеки Python.

import random

Создать функцию для генерации случайного номера телефона.

def get_random_phone_number(arg1, arg2):
    first = str(random.randint(100, 999))
    second = str(random.randint(1, 888)).zfill(3)

    last = str(random.randint(1, 9998)).zfill(4)
    while last in ["1111", "2222", "3333", "4444", "5555", "6666", "7777", "8888"]:
        last = str(random.randint(1, 9998)).zfill(4)

    return "{}-{}-{}".format(first, second, last)

Функция zfill добавляет нули в начало строки до тех пор, пока ее длина не станет равной параметру, переданному функции zfill.

Для лучшего понимания, вот пример случайных чисел, сгенерированных этой функцией:

723-432-1925
143-003-3234
357-794-5677
712-437-5587
126-061-8502

Для генерации случайного адреса электронной почты нам нужно будет импортировать «строку», она также является частью стандартной библиотеки, поэтому ее не нужно устанавливать.

import string

Создайте функцию для генерации случайного письма.

def get_random_email(arg1, arg2):
    name = "".join(
        random.choice(string.ascii_letters) for i in range(random.randint(6, 12))
    )
    return name + "@gmail.com"

Здесь мы генерируем случайные буквы ASCII, от 6 до 12 букв, чтобы все письма были разного размера.

Функция генерации случайного числа довольно проста и универсальна благодаря нижним и верхним границам.

Вы можете, например, установить границы от 1 до 100 и назвать столбец «возраст» или сделать большие границы для генерирования населения страны или любых других данных, которые вам нужно генерировать.

def get_random_number(low, high):
    return random.randint(low, high)

Для генерации случайного названия города или страны нам понадобится модуль pip «rcoc».

Сначала установите его с помощью pip, используя «pip install rcoc», затем импортируйте в наш скрипт с помощью генераторов.

import rcoc

Добавьте функцию для генерации случайного названия города.

def get_random_city(arg1, arg2):
    res = rcoc.get_random_city_by_country(arg1)
    if res.strip() == "":
        return rcoc.get_random_city()
    else:
        return res

Пользователь может выбрать страну для создания случайного города.

«rcoc.get_random_city_by_country(arg1)» возвращает пустую строку, если страна не найдена, поэтому в этом случае мы генерируем город из любой страны, чтобы ячейки не оставались пустыми.

Затем добавьте функцию для генерации случайной страны.

def get_random_country(arg1, arg2):
    return rcoc.get_random_country()

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

def get_all_countries():
    return rcoc.get_all_countries()

Он возвращает все страны в массиве.

Давайте теперь добавим функцию для генерации случайного пароля.

def get_random_password(arg1, arg2):
    characters = string.ascii_letters + string.digits
    password = "".join(random.choice(characters) for i in range(random.randint(8, 16)))
    return password

Здесь мы генерируем случайные буквы ASCII и случайные цифры, всего от 8 до 16 знаков, чтобы пароли были разного размера.

Теперь, когда мы закончили писать все наши функции генератора, давайте вернемся к «main.py».

Прежде всего, нам нужно импортировать генераторы в основной скрипт.

import generators

Затем создайте массив со всеми функциями генератора.

generator_functions = [
    generators.get_random_name,
    generators.get_random_phone_number,
    generators.get_random_email,
    generators.get_random_number,
    generators.get_random_city,
    generators.get_random_country,
    generators.get_random_password,
]

Когда пользователь будет добавлять новые столбцы и выбирать типы данных, программа выведет список всех доступных типов данных с их номерами (1–7) и попросит пользователя ввести идентификатор желаемого типа данных, затем функция генератора вызовет «generator_functions[ id-1]()” из этого массива.

Поэтому важно, чтобы все эти функции добавлялись в массив именно в таком порядке.

Помните, что индексы в массивах начинаются с 0, поэтому мы используем «id-1» для выбора функции.

Наконец, давайте добавим функцию генератора в класс DataRow.

class DataRow:

  # ...

  def generate(self):
      self.cells.clear()
      for col in Header.columns:
          self.cells.append(
              generator_functions[ColumnType[col.col_type].value - 1](col.lo, col.hi)
          )

Как уже объяснялось, эта функция используется для генерации одной строки данных, записи ее в файл набора данных (используя функцию «__str__» класса DataRow), затем очищает массив, и начинает генерировать следующую строку.

функция show_header

Для удобства пользователей давайте добавим в класс Header функцию для отображения всех определенных пользователем столбцов.

Он покажет все столбцы в виде списка с их идентификатором, типом и именем.

# print all values in columns list
def show_header(self):
    print("+--------+--------------+--------+")
    print("|   id   |     type     |  name  |")
    print("+--------+--------------+--------+")
    for i in range(len(self.columns)):
        # printing with index in list

Во-первых, отобразите красивый заголовок для таблицы.

Мы также хотим отображать дополнительные параметры типов данных RANDOM_NUMBER и CITY, для этого мы добавляем сюда дополнительные операторы if.

Добавьте следующий код внутрь цикла for.

if self.columns[i].col_type == ColumnType.RANDOM_NUMBER.name:
    print(
        "[{0}]\t{1} ({3}-{4})\t{2}".format(
            i,
            self.columns[i].col_type,
            self.columns[i].name,
            self.columns[i].lo,
            self.columns[i].hi,
         )
        )

Это для отображения типа RANDOM_NUMBER.

Теперь добавим код для отображения типа CITY.

elif self.columns[i].col_type == ColumnType.CITY.name:
    print(
        "[{0}]\t{1} ({3})\t{2}".format(
            i,
            self.columns[i].col_type,
            self.columns[i].name,
            self.columns[i].lo,
            )
        )

И, наконец, для всех остальных типов, не имеющих дополнительных параметров.

else:
    print(
        "[{0}]\t{1}\t{2}".format(
            i, self.columns[i].col_type, self.columns[i].name
        )
    )

В конце функции вызовите пустую функцию «print()», чтобы добавить новую строку.

print()

Вот полный код функции show_header:

# print all values in columns list
def show_header(self):
    print("+--------+--------------+--------+")
    print("|   id   |     type     |  name  |")
    print("+--------+--------------+--------+")
    for i in range(len(self.columns)):
        # printing with index in list
        if self.columns[i].col_type == ColumnType.RANDOM_NUMBER.name:
            print(
                "[{0}]\t{1} ({3}-{4})\t{2}".format(
                    i,
                    self.columns[i].col_type,
                    self.columns[i].name,
                    self.columns[i].lo,
                    self.columns[i].hi,
                )
            )
        elif self.columns[i].col_type == ColumnType.CITY.name:
            print(
                "[{0}]\t{1} ({3})\t{2}".format(
                    i,
                    self.columns[i].col_type,
                    self.columns[i].name,
                    self.columns[i].lo,
                )
            )
        else:
            print(
                "[{0}]\t{1}\t{2}".format(
                    i, self.columns[i].col_type, self.columns[i].name
                )
            )
    print()

изменение имени и типа столбцов

Теперь мы добавим функции для изменения имени столбца и типа внутри класса Header.

def change_column_name(self, index: int, new_name: str):
    self.columns[index].name = new_name

Изменить имя довольно просто, теперь давайте создадим функцию для изменения типа.

def change_column_type(self, index: int, new_type: ColumnType):
    # if new column type is CITY
    if new_type == ColumnType.CITY.name:
        print("enter 'countries' to see list of all countries.")
        lo = input("(specify country/leave empty if any)~> ").strip().title()
        while lo.lower() == "countries":
            print(generators.get_all_countries())
            lo = input("(specify country/leave empty if any)~> ")

        # if country not found in list of countries write warning and exit
        if lo.strip() != "" and lo.strip() not in generators.get_all_countries():
            print("Country not found.")
            return

        # if no input then any country
        if lo == "":
            lo = "any"

        self.columns[index].col_type = new_type
        self.columns[index].lo = lo

Эта функция принимает новый тип столбца в качестве параметра. Если новый тип — CITY, мы добавляем возможность пользователю выбирать страну или оставлять поле пустым, если это не имеет значения.

На этом функция не заканчивается, теперь давайте добавим новый оператор if для типа RANDOM_NUMBER.

 # if new column type is RANDOM_NUMBER
 elif new_type == ColumnType.RANDOM_NUMBER.name:
     try:
         lo = int(input("(lowest number)~> "))
         hi = int(input("(highest number)~> "))

         if lo > hi:
             print("Incorrect input. First number must be lower than second.")
         else:
             self.columns[index].col_type = new_type
             self.columns[index].lo = lo
             self.columns[index].hi = hi

     except:
         print("Incorrect number.")

Он просит пользователя указать нижнюю и верхнюю границы.

Теперь функция завершена.

Нам нужен только один объект заголовка для генерации данных, добавьте следующий код в конец скрипта.

header = Header()

Затем добавьте функциональность для очистки заголовка.

def clear_table():
    header.columns.clear()

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

Основной цикл

Создайте основной цикл в конце скрипта.

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

running = True
# introduction, waiting for commands
print("====== Dataset Generator ======\n")
print('Type "help" for information.')
while running == True:
    user_input = input(">>> ")
    # list of available commands
    if user_input.lower().strip() == "help":
  # ...

 # show header
 elif user_input.lower().strip() == "h":
  # ...

 # add new column
 elif user_input.lower().strip() == "n":
  # ...

 # remove column by id
 elif user_input.lower().strip() == "r":
  # ...

 # change column
 elif user_input.lower().strip() == "c":
  # ...

 # generate dataset
 elif user_input.lower().strip() == "g":
  # ...

 # clear header
 elif user_input.lower().strip() == "reset":
  # ...

 # terminating on exit command
 elif user_input.lower().strip() == "exit":
  running = False

 elif user_input.strip() == "":
  pass

 else:
        print("Unknown command. Type 'help' for information.")

После цикла в самом низу скрипта добавьте эту строку.

print("Program complete.")

Теперь давайте заставим команды работать.

help — показать список команд

# list of available commands
if user_input.lower().strip() == "help":
    print(
        "List of commands:\n"
        "\th - show created table\n"
        "\tn - add new column\n"
        "\tr - remove column\n"
        "\tc - change column\n"
        "\tg - generate dataset\n"
        "\treset - clear table template\n"
        "\texit - terminate app\n"
    )

h — показать заголовок

# show header
elif user_input.lower().strip() == "h":
    header.show_header()

n — добавить новый столбец

# add new column
elif user_input.lower().strip() == "n":
    input_name = input("(column name)~> ").strip()

    # column name can't be empty
    if input_name.strip() == "":
        print("Column name can't be empty.")
        continue

Здесь мы добавили проверку, если ввод для имени столбца пуст.

 # specifying type of column
 try:
     list_column_types()
     input_type = input("(column type)(1-7)~> ")

     input_type = ColumnType(int(input_type)).name
 except:
     print("Incorrect data type. Enter type id from list of available options.")
     continue

 created_column = Column(input_name, input_type)

Указание типа столбца.

 # if type is CITY ask user to enter country
 # if country not found generates city from random country
 if input_type == ColumnType.CITY.name:
     print("enter 'countries' to see list of all countries.")
     created_column.lo = (
         input("(specify country/leave empty if any)~> ").strip().title()
     )
     while created_column.lo.lower() == "countries":
         print(generators.get_all_countries())
         created_column.lo = input("(specify country/leave empty if any)~> ")

     # if country not found in list of countries write warning and exit
     if (
         created_column.lo.strip() != ""
         and created_column.lo.strip() not in generators.get_all_countries()
     ):
         print("Country not found.")
         continue

     # if no input then any country
     if created_column.lo == "":
         created_column.lo = "any"

Здесь мы добавили код для типа CITY.

И, наконец, добавим код для типа RANDOM_NUMBER.

 if type is RANDOM_NUMBER ask user to enter borders
     if input_type == ColumnType.RANDOM_NUMBER.name:
         try:
             created_column.lo = int(input("(lowest number)~> "))
             created_column.hi = int(input("(highest number)~> "))

             if created_column.lo > created_column.hi:
                 print("Incorrect input. First number must be lower than second.")
                 continue
         except:
             print("Incorrect number.")
             continue

     header.add_column(created_column)

r — удалить столбец

# remove column
elif user_input.lower().strip() == "r":
    # displaying all columns
    header.show_header()

    # getting id of column to delete
    user_input = input("(column id to delete)~> ")
    try:
        header.remove_column(int(user_input))
    except:
        print("Column with id {0} not found.".format(user_input))

Этот прямолинейный. Пользователь указывает id, программа удаляет столбец с id, если не находит — выдает ошибку.

c — изменить столбец

# change column
elif user_input.lower().strip() == "c":
    # displaying all columns
    header.show_header()

    # getting id of column to change
    try:
        id_to_change = int(input("(column id to change)~> "))
    except:
        print("Invalid id.")

    what_to_change = input(
        "What'd you like to change?\n" "\t1) Name\n" "\t2) Type\n" "\t3) Both\n~> "
    )

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

Ниже мы добавляем код для каждого из этих 3 ответов.

Изменение имени.

 # changing only type
 elif what_to_change.strip() == "2":
     list_column_types()
     new_type = input("(column type)(1-7)~> ")
    
     try:
         new_type = ColumnType(int(new_type)).name
         header.change_column_type(id_to_change, new_type)
     except:
         print(
             "Incorrect data type. Enter type id from list of available options."
         )
         continue

Изменение имени и типа столбца.

 # changing name and type
 elif what_to_change.strip() == "3":
     # changing name
     new_name = input("(enter new name)~> ")
     header.change_column_name(id_to_change, new_name)

     # changing type
     list_column_types()
     new_type = input("(column type)(1-7)~> ")
     try:
         new_type = ColumnType(int(new_type)).name
         header.change_column_type(id_to_change, new_type)
     except:
         print(
             "Incorrect data type. Enter type id from list of available options."
         )
         continue

 else:
     print("Unknown option.")

г — генерировать данные

Наконец, самая важная часть кода, которая генерирует наши данные.

# generate dataset
elif user_input.lower().strip() == "g":
    file_name = input("(enter name of CSV file)~> ")

    try:
        amount_of_rows = int(input("(how many rows to generate?)~> "))
    except:
        print("Invalid value.")

    print("Generating data...")

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

 # writing header to file
 f = open("{0}.csv".format(file_name), "a")
 for column in header.columns:
     f.write("{0}, ".format(column.name))
 f.write("\n")
 f.close()

Здесь мы открываем созданный файл CSV и добавляем заголовок в начале.

 # generating rows of data, writing data to csv file
 f = open("{0}.csv".format(file_name), "a")
 for row_index in range(amount_of_rows):
     data_row = DataRow()
     data_row.generate()
     f.write(str(data_row))
 f.close()

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

 print("Generation complete.")
 answer = input("Do you want to continue working? (y/n): ").strip()
 if answer.lower() == "y":
     answer = input("Clean last table template? (y/n): ")
     if answer.lower() == "y":
         clear_table()
         continue
     else:
         continue

 elif answer.lower() == "n":
     running = False

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

перезагрузить

Добавить последнюю команду, чтобы очистить заголовок.

# clearing table template
elif user_input.lower().strip() == "reset":
    clear_table()
    print("Template cleared.")

Заключение

Поздравляем! Теперь у вас есть собственный генератор наборов данных.

Но это только генерация наборов данных CSV, вы можете добавить генерацию для других типов наборов данных, а также добавить все необходимые функции.

Если что-то не работает, есть вероятность, что вы допустили опечатки, вы можете сравнить то, что у вас есть, с исходным кодом — https://github.com/touhf/Dataset_Generator