python

Пишем генератор ip-адресов на Python со свистелками, дуделками и графической оболочкой Tkinter

  • среда, 4 октября 2017 г. в 03:13:51
https://habrahabr.ru/post/339124/
  • Python


Те, кто выбрал Python в качестве одного из первых изучаемых языков программирования общего назначения, нередко горят желанием написать внушительное количество мелких программ, наподобие UPD-клиентов, простеньких текстовых редакторов, чтобы отточить навыки на практике.

В этой статье я опишу процесс создания простенького генератора ip-адресов. Статья ориентирована на новичков. Среда разработки не так важна — вполне можно писать в IDLE, сложностей возникнуть не должно, однако я буду использовать Eclipse с плагином PyDev.

Что конкретно будет выполнять программа?


Представление адреса компьютера по протоколу IPv4 являет собой 32-битное число, разделенное на четыре блока для удобства записи. Такие блоки мы и будем генерировать. Следует отметить, что такая форма вывода удобна, поскольку зачастую списки ip-адресов, прокси-серверов и т. п. загружаются в сторонние программы именно текстовым файлом.

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

Пишем функцию generator()


Поскольку мы условились, что пользователь определяет количество, то создаём функцию generator(), которой передаём аргумент amount:

def generator(amount):
    for n in range(amount):
        #следуя Дзену Питона, явно создаю 4 переменных для наглядности
        a = randint(0,255)
        b = randint(0,255)
        c = randint(0,255)
        d = randint(0,255)
        #открываем файл в режиме редактирования
        f = open('ip-addresses.txt', 'a', encoding='utf-8')
        f.write(str(a)+'.'+str(b)+'.'+str(c)+'.'+str(d)+'\n')
        f.close()
    #ну и почему бы не выдать в консоль сообщение об успешной генерации?
    print('Success!')

Используем цикл for, так как он выполняется гораздо быстрее while'a. Не забываем импортировать модуль random (а лучше from random import randint, чтобы не засорять пространство имён).

GUI на Tkinter


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

Нам нужно создать поле ввода (виджет Entry), кнопку (виджет Button), желательно поле вывода логов, ну и добавить название программы, чтобы было с первого взгляда понятно, для чего она предназначена:

from tkinter import *
#создаём главное окно
root = Tk()

label1=Label(root, text="Генератор ip-адресов")
label1.grid()

#создаём поле Frame, далее упаковываем виджеты в него
frame = Frame(root)
frame.grid()

label2=Label(frame, text='Количество:')
label2.grid(row=1,column=1)
#поле ввода количества адресов
entry_amount = Entry(frame, width=4, borderwidth=5)
entry_amount.grid(row=1,column=2)
#кнопка генерации
button1 = Button(frame, text="Сгенерировать")
button1.grid(row=1, column=3, padx=(10,0))
#поле вывода логов
output = Text(frame, bg="lightblue", font="Arial 9", width=45, height=3)
output.grid(row=2, columnspan=8)

root.mainloop()

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

image

Обрабатываем введённые параметры


Теперь нам нужно написать функцию, которая будет считывать количество, введённое в поле ввода, при нажатии на кнопку «Сгенерировать», и передавать его в generator. При этом стоит сделать так, чтобы функция выдавала в поле логов сообщение об ошибке, если пользователь ввёл не число. Для этого будем пользоваться конструкцией try-except:

def handler():
    try:
        #считываем методом .get()
        amount = int(entry_amount.get())
        generator(amount)
    except ValueError:
        notif("Невозможно определить количество")

#создаём также функцию, передающую лог в поле вывода сообщений
def notif(value):
    output.delete("0.0","end") #очищаем поле перед следующим логом
    output.insert("0.0",value)

Здесь я создал отдельно функцию def notif(value), в которую передаётся аргумент-запись, чтобы упростить код при написании функционала, когда потребуется выводить другие ошибки. Также следует добавить в описание виджета button1 метод со значением нашей функции обработчика command=handler (без круглых скобок в конце).

Тогда описание виджета будет выглядеть так:

button1 = Button(frame, text="Сгенерировать", command=handler)

При добавлении метода command возможно потребуется прописать from distutils import command в импортах. Вот и всё, собственно, программа выполняет свою основную задачу.

Добавляем всякие полезности


Кнопка «Удалить файл»


При проверке корректности программы стало неудобно удалять текстовый файл вручную, так почему бы не добавить кнопку «Удалить файл»? Создаём функцию def delete():

def delete():
    try:
        remove('ip-addresses.txt')
        inserter("Файл ip-addresses.txt успешно удален")
    except:
        inserter("Невозможно удалить несуществующий файл")

Особо с названием файла я заморачиваться не стал, но при желании можно реализовать пользовательское имя файла на выводе (и удалять соответствующий файл). Теперь пишем кнопку, которая будет вызывать delete:

button2 = Button(frame, text="Удалить файл", command=delete)
button2.grid(row=1, column=4, padx=(10,0))

Упаковщиком grid мы разместили эту кнопку в первой строке, четвёртом столбце поля frame. Не забываем добавить в импорты строчку from os import remove. Всё, с кнопкой «Удалить» разобрались.

Прогресс-бар


При генерации списка ip с 500 и более адресов становится заметно, что программе требуется некоторое время, чтобы обработать цикл for. Чтобы пользователь не подумал, что наша программа зависла, реализуем простенький прогресс-бар.

Сразу хочу сказать: Tkinter не подойдёт для этой цели. Мы не сможем реализовать вывод сообщения о прогрессе в окно Tkinter'а, поскольку компилятор не в состоянии обрабатывать окна графического интерфейса во время хода цикла for, даже если мы будем выводить сообщение не каждый ход, а, допустим, через 10, хоть через 100 ходов. Всё равно компилятор в состоянии вывести сообщение только последнего хода цикла (проще говоря, Tkinter таким образом будет выводить только 99%). То есть, создать кошерный прогресс-бар в том же окне, к сожалению, обычными средствами не получится. Так что логичнее всего на фоне основного интерфейса программы открыть терминал логов (обычную консоль), и в него выводить прогресс генерации. Заметка: консоль в привычном для пользователя виде откроется только при запуске программы из проводника; при запуске из компилятора сообщения выводятся в консоль компилятора.

Дорабатываем функцию generator:

def generator(amount, port=''):
    for n in range(amount):
        a = randint(0,255)
        b = randint(0,255)
        c = randint(0,255)
        d = randint(0,255)
        f = open('ip-addresses.txt', 'a', encoding='utf-8')
        f.write(str(a)+'.'+str(b)+'.'+str(c)+'.'+str(d)+port+'\n')
        f.close()
        #для каждого хода цикла присваиваем переменной prc целочисленное значение, которое n составляет от amount при условии, что amount — 100%
        prc = int(n//(amount/100))
        print(str(prc)+'%')
    print('Success!')
    
    #выводим уже в GUI сообщение об успешной генерации txt-файла
    notif("IP-адреса успешно сгенерированы и записаны в\nip-addresses.txt")

Компилируем, генерируем список в 500, например, адресов. В консоль выводится следующее:

image

Здорово. А как убрать лишние повторяющиеся проценты? Создаём небольшой велосипед и сохраняем предыдущее значение:

def generator(amount, port=''):
    prc_bfr=0 #здесь будем запоминать прогресс предыдущего хода цикла
    for n in range(amount):
        a = randint(0,255)
        b = randint(0,255)
        c = randint(0,255)
        d = randint(0,255)
        f = open('ip-addresses.txt', 'a', encoding='utf-8')
        f.write(str(a)+'.'+str(b)+'.'+str(c)+'.'+str(d)+port+'\n')
        f.close()
        prc = int(n//(amount/100))
        #проверяем, равняется ли предыдущее значение переменной prc текущему
        if(prc!=prc_bfr):
            print(str(prc)+'%')
        prc_bfr = prc        
    print('Success!')

    notif("IP-адреса успешно сгенерированы и записаны в\nip-addresses.txt")

Готово! С прогресс-баром, будем считать, разобрались.

Добавляем порты к ip-адресам


А что если я захочу ip-лист с портом 8080, к примеру? Добавим и эту функцию. Начнём с GUI: добавим два виджета Radiobutton, которые будут определять два режима: генерация с портами и без портов. Также следует добавить виджет Entry для ввода порта:

label2 = Label(frame, text='Указать порт:')
label2.grid(row=2,column=1)
entry_port = Entry(frame, width=4, borderwidth=5, state=DISABLED)
entry_port.grid(row=2,column=2)
#объявляем var1 для работы с radiobutton'ами
var1 = IntVar()
check_port1 = Radiobutton(frame, text='С портами', variable=var1, value=1, command=lock)
check_port1.grid(row=2,column=3)
check_port2 = Radiobutton(frame, text="Без портов", variable=var1, value=0, command=lock)
check_port2.grid(row=2,column=4)

entry_port по умолчанию у нас скрыт. Каждый check_port выполняет функцию lock() при активации, которая соответственно скрывает/активирует поле ввода портов. Опишем функцию lock:
def lock():
    #если активирован check_port2:
    if var1.get() == 1:
        entry_port.configure(state=NORMAL)
    #если активирован check_port1:
    elif var1.get() == 0:
        entry_port.configure(state=DISABLED)

Теперь наш GUI имеет вид:

image

Добавляем в функцию-обработчик handler конструкцию if-else, которая выполняется в зависимости от режима генерации (от значения var1):

def handler():
    try:
        amount = int(entry_amount.get())
        #проверяем режим генерации
        if var1.get() == 1:
            port = ':'+str(int(entry_port.get())) #выполняем явное преобразование в int для проверки целочисленности данных, и снова в str, чтобы добавить двоеточие к порту
            generator(amount, port)
        else:
            #иначе передаём генератору один аргумент — количество
            generator(amount)
    except ValueError:
        notif("Невозможно определить количество/порт")

Стоп, функция-генератор принимает только один аргумент. Тогда вновь возвращаемся к ней и объявляем её следующим образом:

def generator(amount, port='')


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

f.write(str(a)+'.'+str(b)+'.'+str(c)+'.'+str(d)+port+'\n')

В конечном счёте, исходный код программы:

import tkinter
import random
from tkinter import *
from random import randint
from os import remove
from distutils import command

print('Logs terminal:')

def generator(amount, port=''):
    prc_bfr=0
    for n in range(amount):
        a = randint(0,255)
        b = randint(0,255)
        c = randint(0,255)
        d = randint(0,255)
        f = open('ip-addresses.txt', 'a', encoding='utf-8')
        f.write(str(a)+'.'+str(b)+'.'+str(c)+'.'+str(d)+port+'\n')
        f.close()
        
        prc = int(n//(amount/100))
        
        if(prc!=prc_bfr):
            print(str(prc)+'%')
        prc_bfr = prc        
    print('Success!')

    notif("IP-адреса успешно сгенерированы и записаны в\nip-addresses.txt")
    
def notif(value):
    output.delete("0.0","end")
    output.insert("0.0",value)

def handler():
    try:
        amount = int(entry_amount.get())
        if var1.get() == 1:
            port = ':'+str(int(entry_port.get()))
            generator(amount, port)
        else:
            generator(amount)
    except ValueError:
        notif("Невозможно определить количество/порт")

def delete():
    try:
        remove('ip-addresses.txt')
        notif("Файл ip-addresses.txt успешно удален")
    except:
        notif("Невозможно удалить несуществующий файл")

def lock():
    if var1.get() == 1:
        entry_port.configure(state=NORMAL)
    elif var1.get() == 0:
        entry_port.configure(state=DISABLED)
        
root = Tk()

label1=Label(root, text="Генератор ip-адресов")
label1.grid()

frame = Frame(root)
frame.grid()

label2=Label(frame, text='Количество:')
label2.grid(row=1,column=1)

entry_amount = Entry(frame, width=4, borderwidth=5)
entry_amount.grid(row=1,column=2)


button1 = Button(frame, text="Сгенерировать", command=handler)
button1.grid(row=1, column=3, padx=(10,0))

button2 = Button(frame, text="Удалить файл", command=delete)
button2.grid(row=1, column=4, padx=(10,0))

label2=Label(frame, text='Указать порт:')
label2.grid(row=2,column=1)

entry_port = Entry(frame, width=4, borderwidth=5, state=DISABLED)
entry_port.grid(row=2,column=2)

var1 = IntVar()

check_port1 = Radiobutton(frame, text='С портами', variable = var1, value=1, command=lock)
check_port1.grid(row=2,column=3)
check_port2 = Radiobutton(frame, text="Без портов", variable = var1, value=0, command=lock)
check_port2.grid(row=2,column=4)

output = Text(frame, bg="lightblue", font="Arial 9", width=45, height=3)
output.grid(row=3, columnspan=8)

root.mainloop()

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