Русский
Русский
English
Статистика
Реклама

Python

Пара слов об именовании переменных и методов

01.07.2020 14:11:33 | Автор: admin


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


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


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


Не будем затягивать и, пожалуй, начнем.


Переменные


Один из самых раздражающих видов переменных это такие переменные, что дают ложное представление о природе данных, которые они хранят. Эдакие переменные-мошенники.


В среде Python-разработчиков крайне популярна библиотека requests и, если вы когда-либо искали что-то связанное с requests, то наверняка натыкались на подобный код:


import requestsreq = requests.get('https://api.example.org/endpoint')req.json()

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


Когда вы делаете запрос (requests.Request), то получаете ответ (requests.Response), так отразите это у себя в коде:


response = requests.get('https://api.example.org/endpoint')response.json()

Не r, не res, не resp и уж точно не req, а именно response. res, r, resp (про req и вовсе молчу) это все переменные, содержание которых можно понять только взглянув на их объявление, а зачем прыгать к объявлению, когда можно изначально дать подходящее название?


Давайте рассмотрим еще один пример, но теперь из Django:


users_list = User.objects.filter(age__gte=22)

Когда вы видите где-то в коде users_list, то вы совершенно справедливо ожидаете, что сможете сделать так:


users_list.append(User.objects.get(pk=3))

Но нет, вы этого сделать не можете, так как .filter() возвращает QuerySet, а QuerySet далеко не list:


Traceback (most recent call last):# ...# ...AttributeError: 'QuerySet' object has no attribute 'append'

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


users_queryset = User.objects.all()users_queryset.order_by('-age')

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


users_list = list(User.objects.all())

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


users = User.objects.all()

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


users_seq = [1, 2, 3]# или users_seq = (1, 2, 3)# илиusers_seq = {1, 2, 3}

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


Еще одним видом раздражающих переменных являются переменные с сокращенными именами.


Вернемся к requests и рассмотрим этот код:


s = requests.Session()# ... # ... s.close()

Это яркий пример совершенно неоправданного сокращения названия переменной. Это ужасная практика и ее ужасы становятся еще более очевидны, когда такой код занимает больше 10-15 строк кода.


Конкретно в случае requests, со скрежетом в зубах можно простить подобное сокращение, когда код занимает не более 5-10 строк и записывается вот так:


with requests.Session() as s:    # ...    # ...    s.get('https://api.example.org/endpoint')

Тут контекстный менеджер позволяет дополнительно выделить объемлющий блок для переменной s.


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


session = requests.Session()# ...# ...session.get('https://api.example.org/endpoint')# ...# ...session.close()

или


with requests.Session() as session:    session.get('https://api.example.org/endpoint')

Вы можете возразить, ведь это более многословный вариант, но я вам отвечу, что это окупается, когда вы читаете код и сразу понимаете, что session это Session. Поймете ли вы это по переменной s, не взглянув на ее определение?


Рассмотрим еще один пример:


info_dict = {'name': 'Isaak', 'age': 25}# ...# ... info_dict = list(info_dict)# ...# ...

Вы видите dict и можете захотеть сделать так:


for key, value in info_dict.items():    print(key, value)

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


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


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


info_dict = {'name': 'Isaak', 'age': 25}# ...# ... info_keys = list(info_dict)# ...# ...

или даже так, что более идиоматично:


info_dict = {'name': 'Isaak', 'age': 25}# ...# ... info_keys = info_dict.keys()# ...# ...

Комментарии-кэпы


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


Возьмем небольшой пример из JavaScript:


// Remove first five lettersconst errorCode = errorText.substr(5)

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


Давайте попробуем сделать этот комментарий полезным:


// Remove "net::" from error textconst errorCode = errorText.substr(5)

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


const errorCode = errorText.replace('net::', '')

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


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


Методы


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


Рассмотрим пример с методом:


>>> person = Person()>>> person.has_publications()['Post 1', 'Post 2', 'Post 3']

В коде мы выразили весьма однозначный вопрос: "Имеются ли у этого человека публикации?", но какой ответ мы получили?


Мы не спрашивали, какие у человека есть публикаций. Название этого метода подразумевает, что возвращаемое значение должно иметь булевый тип, а именно True или False:


>>> person = Person()>>> person.has_publications()True

А для получения постов вы можете использовать более подходящее название:


>>> person.get_publications()['Post 1', 'Post 2', 'Post 3']

или


>>> person.publications()['Post 1', 'Post 2', 'Post 3']

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


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


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


  1. Robert Martin Clean Code
  2. Robert Martin Clean Architecture
  3. Robert Martin The Clean Coder: A Code of Conduct for Professional Programmers
  4. Martin Fowler Refactoring: Improving the Design of Existing Code
  5. Colin J. Neill Antipatterns: Managing Software Organizations and People
Подробнее..

Магия WebPush в Mozilla Firefox. Взгляд изнутри

07.07.2020 10:10:24 | Автор: admin

Безусловно одной из самых популярных технологий доставки оповещений на устройства пользователей являются Push уведомления. Технология такова, что для её работы необходим постоянный доступ к интернету, а именно доступ к серверам, на которых регистрируются устройства пользователя для получения уведомлений. В данной статье мы рассмотрим весь спектр механизмов технологии WebPush уведомлений, спрятанных за словами WebSocket, ServiceWorker, vapid, register, broadcast, message encryption и т.д. Основной причиной побудившей меня к реверсу и изучению механизма, являлась необходимость доставки уведомлений мониторинга на рабочие места техподдержки, находящиеся в закрытом сегменте сети без доступа в интернет. И да, это возможно! Подробности под катом.


Disclaimer


В статье рассматривается режим доставки уведомлений пользователям в рамках использования браузера Mozilla Firefox. Это связано с тем, что на данный момент это единственный продукт позволяющий менять настройки push серверов используемых по умолчанию. Настройки браузеров Google Chrome, Chromium и производных в целях безопасности жёстко "зашиты" производителем в коде продукта.


Статья делится на две части


  • Теоретическая информация
  • Практические заметки для реализации механизма WebPush уведомлений

Используемые технологии и термины


WebSocket


Транспортным ядром системы Push уведомлений является протокол WebSocket, позволяющий в рамках стандартного HTTP/HTTPS подключения к Web серверу установить постоянный двусторонний канал связи между клиентом и сервером. В рамках установленного канала связи могут использоваться любые, в том числе бинарные, протоколы клиент-серверного взаимодействия заложенные разработчиками сервиса.


ServiceWorker


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


VAPID


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


WebPush


Механизм доставки сообщений до получателя.
Набор документов и спецификаций по WebPush


Workflow


Документации по WebPush довольно много (см. спойлер), но она существует только в парадигме
Client <-> Push Service <-> Application


Подробные спецификации по работе механизма в продуктах Google и Mozilla

Модель взаимодействия предполагает следующую схему.
image


Таким образом создаётся впечатление, что для работы механизма как минимум нужны специализированные сервисы регистрации и приёма/доставки уведомлений, а для отправки сообщений обязателен VAPID, спецзаголовки и ключи. Документация не описывает некоторых внутренних механизмов взаимодействия Push сервера и клиента, которые реально влияют на работу.
Попробуем рассмотреть все процессы подробно.


Фаза обработки сообщения


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


Блок кода расставил все точки над И
    try {      reply = JSON.parse(message);    } catch (e) {      console.warn("wsOnMessageAvailable: Invalid JSON", message, e);      return;    }    // If we receive a message, we know the connection succeeded. Reset the    // connection attempt and ping interval counters.    this._retryFailCount = 0;    let doNotHandle = false;    if (      message === "{}" ||      reply.messageType === undefined ||      reply.messageType === "ping" ||      typeof reply.messageType != "string"    ) {      console.debug("wsOnMessageAvailable: Pong received");      doNotHandle = true;    }    // Reset the ping timer.  Note: This path is executed at every step of the    // handshake, so this timer does not need to be set explicitly at startup.    this._startPingTimer();    // If it is a ping, do not handle the message.    if (doNotHandle) {      return;    }    // A whitelist of protocol handlers. Add to these if new messages are added    // in the protocol.    let handlers = [      "Hello",      "Register",      "Unregister",      "Notification",      "Broadcast",    ];    // Build up the handler name to call from messageType.    // e.g. messageType == "register" -> _handleRegisterReply.    let handlerName =      reply.messageType[0].toUpperCase() +      reply.messageType.slice(1).toLowerCase();    if (!handlers.includes(handlerName)) {      console.warn(        "wsOnMessageAvailable: No whitelisted handler",        handlerName,        "for message",        reply.messageType      );      return;    }    let handler = "_handle" + handlerName + "Reply";

Ни одно сообщение отправленное через websocket в сторону браузера не будет обработано, если оно не является системным сообщением проверки доступности конечной стороны "{}" или ответом на запрос от Push сервера. Это означает, что Push сервер не имеет никакого способа воздействия на работу клиентской стороны, кроме проверки её доступности. Аналогично, кроме 5 типов ответных сообщений, ничего обработано не будет.


Фаза инициализации


При запуске браузера Firefox, его внутренний механизм автоматически инициирует соединение с WebSocket(WS) сервером находящимся в системной настройке dom.push.serverURL с сообщением следующего формата.


{  "messageType": "hello",  "broadcasts":    {      "remote-settings/monitor_changes": "v923"    },    "use_webpush": True}

При первичной инициализации соединения(первый запуск браузера после установки/запуск нового профиля), поле "uaid" отсутствует, что является сигналом Push серверу о необходимости регистрации нового идентификатора. Как мы видим в разделе "broadcasts" присутствует некая пара "remote-settings/monitor_changes": "v923". Данная пара используется как буфер для хранения информации, отправляемой в сторону сервера при установлении соединения. В продукте Mozilla autopush, промышленной версии webpush сервера используемого на стороне серверов Mozilla, данная переменная используется как идентификатор последнего полученного пользователем сообщения из глобальной очереди сервера. Об изменении данного идентификатора мы поговорим позже. Итак, после принятия сообщения от клиента, сервер отвечает сообщением следующего вида


{  "messageType": "hello",  "status": 200,  "uaid": "b4ab795089784bbb978e6c894fe753c0",  "use_webpush": True}

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


Фаза регистрации


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


  • Проверка разрешения пользователя на получение информации
  • Регистрация ServiceWorker
  • Получение параметров подписки
  • Формирование ключей шифрования для обслуживания подписки

Проверка разрешений пользователя на получение информации


На данном этапе браузер перед установкой ServiceWorker, запрашивает пользователя и системные настройки: "готов ли пользователь получать сообщения о подписке?"
В случае одного из отказов, установка ServiceWorker прерывается


Регистрация ServiceWorker


Как мы ранее оговаривали, ServiceWorker это автономная страница с обработчиками событий в отдельном пространстве браузера недоступном пользователю.
На работу с этим компонентом накладываются довольно серъёзные ограничения:


  • Загрузка компонента ServiceWorker должна производиться через защищённое соединение(HTTPS), либо в целях отладки с localhost. Возможно включение флагов на "небезопасное" использование внешних ресурсов, но это не рекомендуется
  • соединение WebSocket должно устанавливаться по защищённому соединению(WSS), либо в целях отладки по обычному WS соединению с localhost
  • если в локальной сети имя сервера(ресурса) с которого происходит регистрация ServiceWorker, отличается от полного fqdn ресурса на котором находится ServiceWorker, то будет вызвано исключение о небезопасном вызове
    Жизненный цикл ServiceWorker от Google
    Жизненный цикл ServiceWorker от Mozilla

Процесс подписки


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


  • получение публичного ключа шифрования с Web сервера, являющегося промежуточным звеном между Push сервером и приложением осуществляющем отправку сообщений
  • формирование браузером ключей шифрования для защиты сообщений
  • вызов механизма подписки через канал WebSocket и получение точки для отправки сообщений

Получение публичного ключа


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


Запуск процесса генерации ключей шифрования


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


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


После формирования ключей шифрования вызывается процесс внутри браузера называемый register. В сторону Push сервера через WebSocket браузер отправляет запрос вида


{  "channelID": "f9cb8f1c-05e0-403f-a09b-dd7864a03eb7",  "messageType": "register",  "key": "BO_C-Ou.......zKu2U4HZ9XeElUIdRfc6EBbRudAjq4="}

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


{  "messageType": "register",  "channelID": "f9cb8f1c-05e0-403f-a09b-dd7864a03eb7",  "status": 200,  "pushEndpoint": "https://webpush.example.net/wpush/f9cb8f1c-05e0-403f-a09b-dd7864a03eb7/",  "scope": "https://webpush.example.net/"}

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


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


Браузер:


  • приватный ключ шифрования сообщений
  • публичный ключ шифрования сообщений
  • ключ авторизации(DH)
  • конечная точка для доставки сообщений получателю
  • номер канала зарегистрированный на WebSocket сервере
  • идентификатор клиента внутри WS соединения
  • публичный ключ WebPush сервера

WebPush сервер:


  • публичный ключ WebPush сервера
  • приватный ключ WebPush сервера

Push(WebSocket) сервер:


  • публичный ключ WebPush сервера
  • адрес конечной точки клиента
  • номер канала клиента привязанный к конечной точке
  • идентификатор клиента внутри WS соединения

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


  • некто хочет отправить сообщение в браузер пользователя
  • для защиты сообщения необходимо извлечь из браузера настройки текущей подписки к Push серверу (конечную точку для отправки сообщения, публичный ключ шифрования сообщения, ключ авторизации)
  • полученные настройки передаются на промежуточный WebPush сервер вместе с текстом сообщения
  • промежуточный WebPush сервер формирует авторизационный JWT токен, содержащий время создания сообщения, адрес администратора WebPush сервера, время действия сообщения и подписывает его при помощи своего приватного ключа
  • промежуточный WebPush сервер производит шифрование сообщения при помощи публичного ключа и ключа авторизации из браузера
  • промежуточный WebPush сервер вызывает конечную точку полученную из браузера, передавая в неё связку JWT токен+публичный ключ для их проверки в заголовке Authorization, а также бинарный массив зашифрованного сообщения в теле запроса
  • Push сервер по вызываемой конечной точке производит привязку запроса к каналу получателя
  • Push сервер проверяет валидность JWT токена
  • Push сервер конвертирует бинарный массив принятых данных в base64, формирует сообщение типа "notification" с каналом получателя, ставит сообщение в очередь, после чего механизм контроля очереди отправляет сообщение по WebSocket каналу в сторону клиента

Здесь мы прервём процесс для описания формата сообщения типа "notification".
Дело в том, что формат сообщения типа "notification" имеет два варианта. От того, что получил браузер и передал в ServiceWorker зависит логика работы по получению и отображению сообщения. Первый вариант, это "пустое" сообщение:


{  "messageType": "notification",  "channelID": "f7dfeed8-f868-47ca-a066-fbe629879fbf",  "version": "bf82eea1-69fd-4be0-b943-da96ff0041fb"}

"Пустое" сообщение как бы говорит браузеру "Эй, тебя тут ждут данные, приходи за ними". Браузер по логике работы должен сделать GET запрос на URL конечной точки и получить первую запись из очереди для отображения её пользователю. Схема конечно хорошая, только совсем небезопасная. В большинстве случаев она не применяется.
Вторым вариантом является передача данных совместно с сообщением.


{  "messageType": "notification",  "channelID": "f7dfeed8-f868-47ca-a066-fbe629879fbf",  "version": "bf82eea1-69fd-4be0-b943-da96ff0041fb",  "data": "I_j8p....eMlYK6jxE2-pHv-TRhqQ",  "headers":  {    "encoding": "aes128gcm"  }}

Браузер реагирует на поле headers в структуре сообщения типа "notification". При наличии этого поля автоматически включается механизм обработки зашифрованных данных из поля "data". На основании номера канала, событийная машина браузера выбирает набор ключей шифрования и пытается расшифровать полученные данные. После расшифровки расшифрованные данные передаются в обработчик "push" сообщений ServiceWorker. Как вы успели заметить, сообщение типа "notification" имеет поле "version", которое представляет собой уникальный номер сообщения. Уникальный номер сообщения используется в системе доставки и отображения сообщений для дедупликации данных.
Она работает следующим образом:


  • любое принятое сообщение имеющее поле "version" заносится во внутренний реестр исключений
  • корректно принятые и обработанные сообщения остаются в реестре исключений
  • некорректно принятые и не обработанные сообщения из рееста исключений удаляются
    Информация о причинах такого поведения будет ниже.

Продолжим разбор процесса.


  • Если сообщение принято и расшифровано, от браузера в сторону Push сервера формируется новое сообщение с типом "ack", включающее в себя номер канала и номер обработанного сообщения. Это является сигналом удаления сообщения из очереди сообщений для данного канала
  • Если сообщение по какой-то причине не может быть обработано, от браузера в сторону Push сервера формируется новое сообщение с типом "noack", включающее в себя номер канала и номер отвергнутого сообщения. Это является сигналом постановки сообщения на повторную доставку через 60 секунд

Вернёмся к сообщениям с типом "broadcast". Продукт "autopush" от Mozilla использует их в качестве хранилища на стороне клиента, для определения последнего отправленного клиенту сообщения. Дело в том, что отправка сообщения типа "broadcast" со сменой значения ключа "remote-settings/monitor_changes", приводит к срабатыванию механизма, сохраняющего полученное значение в хранилище браузера. При потере соединения или каком-то программном сбое, сохранённое значение будет автоматически передано на сторону Push сервера в момент инициализации соединения и будет являться начальной точкой для последующей переотправки пропущенных сообщений из очереди.


Описывать сообщения типа "unregister" смысла не имеет, т.к. оно ни на что, кроме удаления сессии не влияет.


К чему же было приведено подробное описание всех процессов происходящих при Push оповещениях?
Смысл в том, что на основании этих данных можно довольно быстро построить свой Push сервер с необходимым функционалом. Продукт "autopush" от Mozilla является продуктом промышленного масштаба, который рассчитан на многомилионные подключения клиентов. В его составе присутствует TornadoDB, PyPy, CPython. К сожалению движок написан на Python 2.7, который массово выводится из эксплуатации.
Нам же нужен небольшой сервер с простым, желательно асинхронным кодом. А именно, без промежуточного WebPush сервера, VAPID, лишних межсерверных проверок и прочего. Сервер должен уметь привязывать клиентские подключения Push сервера к именам пользователей, а также иметь возможность организации эндпоинтов и webhook'ов для отправки сообщений этим пользователям.


Пишем свой сервер


У нас есть следующие данные:


  • Пользователь с браузером Mozilla Firefox;
  • Точка регистрации пользователя на сервере уведомлений для получения этих самых уведомлений;
  • WebSocket сервер, обслуживающий подключения движка уведомлений, встроенного в браузер;
  • Web сервер, формирующий интерфейс для пользователя и обслуживающий точки для отправки уведомлений;

Шаг 1
Первым делом мы должны подготовить WebSocket сервер, обслуживающий описанную ранее логику работы и подключения к нему клиентов.
В качестве фреймворка для реализации логики сервера используется AsyncIO Python.
Изначально стоит сразу разделить понятие "регистрация" для WebSocket движка браузера и понятие "регистрация" на сервере уведомлений. Разница заключается в том, что "регистрация" WebSocket движка браузера происходит автоматически без участия пользователя, в то время как разрешение на "регистрацию" на сервере уведомлений это осознанное действие со стороны пользователя.
Первичной задачей WebSocket сервера является принятие входящего соединения и его контроль на протяжении всего времени подключения браузера к серверу. Поэтому мы должны принять внешнее соединение, сделать его привязку к каналу и сохранить для дальнейшей работы.


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


WebSocket Handler
# внешнее имя сервераSERVERNAME='webpush.example.net'# вебсокетыWS = set()# каналыCHANNELS = dict()async def register(websocket):    try:        WS.add(websocket)        websocket.handler = PushConnectionHandler(websocket)    except Exception as ex:        logger.error('Register exception: %s' % ex)async def unregister(websocket):    try:        CHANNELS.remove(websocket.handler.channel_id)        WS.remove(websocket)        logger.debug('UnregisterWebsocket[websocket]: %s'%websocket)    except Exception as ex:        logger.error('Unregister exception: %s' % ex)async def pushserver(websocket, path):    await register(websocket)    try:        await websocket.send(json.dumps({}))        async for message in websocket:            data = json.loads(message)            logger.info('Incoming message[data]: %s => %s '%(message, data))            if message == '{}':                await websocket.send(json.dumps({}))            elif 'messageType' in data:                logger.info('Processing WebSocket Data')                # Подключение к вебсокету из браузера                if data['messageType'] == 'hello':                    # Если это первичное подключение, то нужно задать идентификатор подключения и вернуть его браузеру                    if 'uaid' not in data:                        data['uaid'] = '%s' % uuid.uuid4()                    # Принудительно включить webpush                    if 'use_webpush' not in data:                        data['use_webpush'] = True                    helloreturn = {                        "messageType": "hello",                        "status": 200,                        "uaid": data['uaid'],                        "use_webpush": data['use_webpush']                        }                    websocket.handler.uaid = data['uaid']                    if 'broadcasts' in data:                        websocket.handler.register_broadcasts(data['broadcasts'])                    logger.debug('Hello websocket: %s' % vars(websocket.handler))                    CHANNELS.update({ data['uaid'] : websocket.handler })                    await websocket.send(json.dumps(helloreturn))                elif data['messageType'] == 'register':                    # Регистрация serviceWorker                    logger.debug('Register[data]: %s'%data)                    registerreturn = {                        "messageType": "register",                        "channelID": data['channelID'],                        "status": 200,                        "pushEndpoint": "https://%s/wpush/%s/" % (SERVERNAME,data['channelID']),                        "scope": "https://%s/" % SERVERNAME                    }                    websocket.handler.channel_id = data['channelID']                    if 'key' in data:                        websocket.handler.server_public_key = data['key']                    logger.debug('Register[registerreturn]: %s'%registerreturn)                    CHANNELS.update({ data['channelID'] : websocket.handler })                    await websocket.send(json.dumps(registerreturn))                elif data['messageType'] == 'unregister':                    unregisterreturn = {                        "messageType": "unregister",                        "channelID": data['channelID'],                        "status": 200                    }                    if data['channelID'] in CHANNELS:                        del CHANNELS[data['channelID']]                    logger.debug('Unregister[unregisterreturn]: %s'%unregisterreturn)                    logger.debug('Unregister[CHANNELS]: %s'%CHANNELS)                    await websocket.send(json.dumps(unregisterreturn))                elif data['messageType'] == 'ack':                    logger.debug('Ack: %s' % data)                    for update in data['updates']:                        if CHANNELS[update['channelID']].mqueue.count(update['version']) > 0:                            CHANNELS[update['channelID']].mqueue.remove(update['version'])                    logger.debug('Mqueue for channel %s is %s' % (websocket.handler.channel_id, websocket.handler.mqueue))                    await websocket.send('{}')                elif data['messageType'] == 'nack':                    await websocket.send('{}')            else:                logger.error("unsupported event: {}", data)    finally:        await unregister(websocket)

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


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


main.js
'use strict';let isSubscribed = false;let swRegistration = null;var wait = ms => new Promise((r, j)=>setTimeout(r, ms));function urlB64ToUint8Array(base64String) {    const padding = '='.repeat((4 - base64String.length % 4) % 4);    const base64 = (base64String + padding)        .replace(/\-/g, '+')        .replace(/_/g, '/');    const rawData = window.atob(base64);    const outputArray = new Uint8Array(rawData.length);    for (let i = 0; i < rawData.length; ++i) {        outputArray[i] = rawData.charCodeAt(i);    }    return outputArray;}function subscribeUser() {    const applicationServerPublicKey = localStorage.getItem('applicationServerPublicKey');    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);    swRegistration.pushManager.subscribe({            userVisibleOnly: true,            applicationServerKey: applicationServerKey        })        .then(function(subscription) {            console.log('User is subscribed.', JSON.stringify(subscription));            localStorage.setItem('sub_token',JSON.stringify(subscription));            isSubscribed = true;            fetch(subscription.endpoint, {                method: 'POST',                cache: 'no-cache',                body: JSON.stringify(subscription)            })            .then(function(response) {                console.log('Push keys Update Response: ' + JSON.stringify(response));            })        })        .catch(function(err) {            console.log('Failed to subscribe the user: ', err);        });}function unsubscribeUser() {    swRegistration.pushManager.getSubscription()        .then(function(subscription) {            if (subscription) {                return subscription.unsubscribe();            }        })        .catch(function(error) {            console.log('Error unsubscribing', error);        })        .then(function() {            console.log('User is unsubscribed.');            isSubscribed = false;        });}function initializeUI() {    // Set the initial subscription value    swRegistration.pushManager.getSubscription()        .then(function(subscription) {            isSubscribed = !(subscription === null);            if (isSubscribed) {                console.log('User IS subscribed. Unsubscribing.');                subscription.unsubscribe();            } else {                console.log('User is NOT subscribed. Subscribing.');                subscribeUser();            }        });    (async () => {    await wait(2000);    console.warn('Wait for operation is ok');         swRegistration.pushManager.getSubscription()                .then(function(subscription) {                        isSubscribed = !(subscription === null);                        if (!isSubscribed) {                                console.log('ReSubscribe user');                                subscribeUser();                        }                })    })()}console.log(navigator);console.log(window);if ('serviceWorker' in navigator && 'PushManager' in window) {    console.log('Service Worker and Push is supported');    navigator.serviceWorker.register("/sw.js")        .then(function(swReg) {            console.log('Service Worker is registered', swReg);            swRegistration = swReg;            initializeUI();        })        .catch(function(error) {            console.error('Service Worker Error', error);        });} else {    console.warn('Push messaging application ServerPublicKey is not supported');}$(document).ready(function(){    $.ajax({        type:"GET",        url:'/subscription/',        success:function(response){            console.log("response",response);            localStorage.setItem('applicationServerPublicKey',response.public_key);        }    })});

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


sw.js
'use strict';/* eslint-disable max-len *//* eslint-enable max-len */function urlB64ToUint8Array(base64String) {  const padding = '='.repeat((4 - base64String.length % 4) % 4);  const base64 = (base64String + padding)    .replace(/\-/g, '+')    .replace(/_/g, '/');  const rawData = window.atob(base64);  const outputArray = new Uint8Array(rawData.length);  for (let i = 0; i < rawData.length; ++i) {    outputArray[i] = rawData.charCodeAt(i);  }  return outputArray;}function getEndpoint() {  return self.registration.pushManager.getSubscription()  .then(function(subscription) {    if (subscription) {      return subscription.endpoint;    }    throw new Error('User not subscribed');  });}self.popNotification = function(title, body, tag, icon, url) {  console.debug('Popup data:', tag, body, title, icon, url);  self.registration.showNotification(title, {      body: body,      tag: tag,      icon: icon    });  self.onnotificationclick = function(event){      console.debug('On notification click: ', event.notification.tag);      event.notification.close();      event.waitUntil(        clients.openWindow(url)      );  };}var wait = ms => new Promise((r, j)=>setTimeout(r, ms));self.addEventListener('push', function(event) {   console.log('[Push]', event);  if (event.data) {    var data = event.data.json();    var evtag = data.tag || 'notag';    self.popNotification(data.title || 'Default title', data.body || 'Body is not present', evtag, data.icon || '/static/images/default.svg', data.url || '/getevent?tag='+evtag);  }  else {    event.waitUntil(      getEndpoint().then(function(endpoint) {        return fetch(endpoint);      }).then(function(response) {          return response.json();      }).then(function(payload) {          console.debug('Payload',JSON.stringify(payload), payload.length);          var evtag = payload.tag || 'notag';          self.popNotification(payload.title || 'Default title', payload.body || 'Body is not present', payload.tag || 'notag', payload.icon || '/static/images/default.svg', payload.url || '/getevent?tag='+evtag);      })    );  }});self.addEventListener('pushsubscriptionchange', function(event) {  console.log('[Service Worker]: \'pushsubscriptionchange\' event fired.');  const applicationServerPublicKey = localStorage.getItem('applicationServerPublicKey');  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);  event.waitUntil(    self.registration.pushManager.subscribe({      userVisibleOnly: true,      applicationServerKey: applicationServerKey    })    .then(function(newSubscription) {      // TODO: Send to application server      console.log('[Service Worker] New subscription: ', newSubscription);    })  );});

Согласно представленного кода, Javascript файл main.js инициирует при своём запуске получение публичного VAPID ключа и принудительно вызывает подписку браузера на оповещения.
Для простоты отладки WebSocket сервер во время регистрации подписки отдаёт URL вида: https://webpush.example.net/wpush/ChannelGuid.
Откуда же берётся имя пользователя в сервере уведомлений. Вся суть в том, что инициирование подписки /subscription/ происходит полуавтоматически. Соответственно в зависимости от того, что вы хотите увидеть в качестве идентификатора пользователя, вы можете передать после оформления подписки в момент передачи ключей.
Это происходит путём вызова метода POST по адресу WebPush endpoint присланного сервером из модуля ServiceWorker.


function subscribeUser() {    const applicationServerPublicKey = localStorage.getItem('applicationServerPublicKey');    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);    swRegistration.pushManager.subscribe({            userVisibleOnly: true,            applicationServerKey: applicationServerKey        })        .then(function(subscription) {            console.log('User is subscribed.', JSON.stringify(subscription));            localStorage.setItem('sub_token',JSON.stringify(subscription));            isSubscribed = true;            fetch(subscription.endpoint, {                method: 'POST',                cache: 'no-cache',                body: JSON.stringify(subscription)            })            .then(function(response) {                console.log('Push keys Update Response: ' + JSON.stringify(response));            })        })        .catch(function(err) {            console.log('Failed to subscribe the user: ', err);        });}

Как было написано ранее, в сервере используется обработчик точек подключения. Это отдельная часть кода в скрипте сервера, но обрабатывающая вместо WebSocket, клиентский WEB трафик от браузера.
В качестве обрабатываемого заголовка, содержащего идентификатор пользователя, в базовом варианте сервиса использовался basiclogin полученный при авторизации пользователя в LDAP.


        location ~ /subscription|/pushdata|/getdata|/wpush|/notify {            proxy_pass http://localhost:8090;            proxy_set_header LDAP-AuthUser $remote_user;            proxy_set_header 'X-Remote-Addr' $remote_addr;            add_header "Access-Control-Allow-Origin" "*";            add_header Last-Modified $date_gmt;        proxy_hide_header "Authorization";            add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';            if_modified_since off;            expires off;            etag off;        }

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


USERIDHEADERNAME='X-Remote-Addr'async def update_channel_keys(request, data):    channel = request.path.replace('wpush','').replace('/','')    logger.debug('update channel keys data: %s'%data)    logger.debug('Update Channel keys Headers: %s' % request.headers)    if USERIDHEADERNAME not in set(request.headers):        return False    basiclogin = request.headers[USERIDHEADERNAME]    logger.debug('Login %s' % basiclogin)    if basiclogin not in LOGINS_IN_CHANNELS:        LOGINS_IN_CHANNELS.update({ '%s'%basiclogin : {} })    LOGINS_IN_CHANNELS['%s'%basiclogin].update({'%s' % channel : {} })    logger.debug('LOGINS_IN_CHANNELS: %s' % LOGINS_IN_CHANNELS)    try:        jdata = json.loads(data)        if 'endpoint' in jdata and 'keys' in jdata:            logger.debug('Saving Keys for Channel: %s => %s' % (channel, jdata))            CHANNELS[channel].register_keys(jdata['keys'])            logger.debug('Registered channel keys %s:' % vars(CHANNELS[channel]))        return True    except Exception as ex:        logger.error('Exception %s'%ex)        return False

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


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


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

При корректном формировании сообщения необходимо передать поле tag содержащее уникальный идентификатор сообщения. Если сервер имеет сессионные ключи для клиента в сторону которого передаётся уведомление, то он может зашифровать это уведомление. Если нет, то сервер отправит пустое уведомление и клиент сам придёт за этим уведомлением по полю tag. Логику получения браузером сообщения реализует следущий блок кода в ServiceWorker:


self.addEventListener('push', function(event) {   console.log('[Push]', event);  if (event.data) {    var data = event.data.json();    var evtag = data.tag || 'notag';    self.popNotification(data.title || 'Default title', data.body || 'Body is not present', evtag, data.icon || '/static/images/default.svg', data.url || '/getevent?tag='+evtag);  }  else {    event.waitUntil(      getEndpoint().then(function(endpoint) {        return fetch(endpoint);      }).then(function(response) {          return response.json();      }).then(function(payload) {          console.debug('Payload',JSON.stringify(payload), payload.length);          var evtag = payload.tag || 'notag';          self.popNotification(payload.title || 'Default title', payload.body || 'Body is not present', payload.tag || 'notag', payload.icon || '/static/images/default.svg', payload.url || '/getevent?tag='+evtag);      })    );  }});

В случае отсутствия ключей шифрования для уведомления, необходимо организовать очередь сообщений, через которую ServiceWorker сможет получить все ожидающие его сообщения. Так как мы строим простую реализацию сервера, всё что нам нужно это сохранить сообщения до определённого момента, а потом их удалить. Т.к. сообщения мониторинга ждущие получения больше 10 минут, в большинстве случаев уже не актуальны. При этом можно не хранить в очереди "пустые" сообщения для индикации наличия уведомлений на сервере.


Блок шифрования сообщений передаваемых был взят из кода сервера "autopush", дабы не нарушать совместимости.


Блок шифрования сообщения
    def encrypt_message(self, data, content_encoding="aes128gcm"):        """Encrypt the data.        :param data: A serialized block of byte data (String, JSON, bit array,            etc.) Make sure that whatever you send, your client knows how            to understand it.        :type data: str        :param content_encoding: The content_encoding type to use to encrypt            the data. Defaults to RFC8188 "aes128gcm". The previous draft-01 is            "aesgcm", however this format is now deprecated.        :type content_encoding: enum("aesgcm", "aes128gcm")        """        # Salt is a random 16 byte array.        if not data:            logger.error("PushEncryptMessage: No data found...")            return        if not self.auth_key or not self.receiver_key:            raise WebPushException("No keys specified in subscription info")        logger.debug("PushEncryptMessage: Encoding data...")        salt = None        if content_encoding not in self.valid_encodings:            raise WebPushException("Invalid content encoding specified. "                                   "Select from " +                                   json.dumps(self.valid_encodings))        if content_encoding == "aesgcm":            logger.debug("PushEncryptMessage: Generating salt for aesgcm...")            salt = os.urandom(16)        # The server key is an ephemeral ECDH key used only for this        # transaction        server_key = ec.generate_private_key(ec.SECP256R1, default_backend())        crypto_key = server_key.public_key().public_bytes(            encoding=serialization.Encoding.X962,            format=serialization.PublicFormat.UncompressedPoint        )        if isinstance(data, str):            data = bytes(data.encode('utf8'))        if content_encoding == "aes128gcm":            logger.debug("Encrypting to aes128gcm...")            encrypted = http_ece.encrypt(                data,                salt=salt,                private_key=server_key,                dh=self.receiver_key,                auth_secret=self.auth_key,                version=content_encoding)            reply = CaseInsensitiveDict({                'data': base64.urlsafe_b64encode(encrypted).decode()            })        else:            logger.debug("Encrypting to aesgcm...")            crypto_key = base64.urlsafe_b64encode(crypto_key).strip(b'=')            encrypted = http_ece.encrypt(                data,                salt=salt,                private_key=server_key,                keyid=crypto_key.decode(),                dh=self.receiver_key,                auth_secret=self.auth_key,                version=content_encoding)            reply = CaseInsensitiveDict({                'crypto_key': crypto_key,                'data': base64.urlsafe_b64encode(encrypted).decode()            })            if salt:                reply['salt'] = base64.urlsafe_b64encode(salt).strip(b'=')        reply['headers'] = { 'encoding': content_encoding }        return reply

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


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


WebPush AsyncIO server


Для развёртывания сервера необходимо:


  • Установить необходимые Python модули, а также настроить nginx по примеру приложенного конфигурационного файла.
  • Поместить содержимое директории web в корень ранее настроенного виртуального сервера
  • Перезапустить/перечитать конфиг nginx
  • В браузере через about:config поменять параметр dom.push.serverURL на адрес wss://ваш.сервер/ws
  • Перед сменой адреса push сервера можно очистить поле dom.push.userAgentID, которое автоматически заполнится если ваш Push сервер работает корректно и принимает соединения.
  • Для тестирования оповещений необходимо зайти на страницу https://ваш.сервер/indexpush.html и открыв окно отладки удостовериться в корректной регистрации ServiceWorker
  • Нажать кнопку "Check Push Notify"
  • Если всё правильно настроено, появится всплывающее сообщение

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


var req = new CurlHttpRequest();req.AddHeader('Content-Type: application/x-www-form-urlencoded');var jv = JSON.parse(value);if (jv.recovery_nstatus == '{EVENT.RECOVERY.VALUE}') {jv.icon = '/static/images/problem/' + jv.event_severity + '.svg';}else{jv.icon = '/static/images/recovery/' + jv.event_severity + '.svg';}value = JSON.stringify(jv);Zabbix.Log(2, 'webhook request value='+value);req.Post('https://webpush.server.net/pushdata/',  value);Zabbix.Log(2, 'response code: '+req.Status());return JSON.stringify({  'tags': {    'endpoint': 'webpush'  }});

c параметрами


Ключ Значение
url /zabbix/tr_events.php?triggerid={TRIGGER.ID}&eventid={EVENT.ID}
recipient {ALERT.SENDTO}
title {ALERT.SUBJECT}
body {ALERT.MESSAGE}
event_severity {EVENT.NSEVERITY}
recovery_nstatus {EVENT.RECOVERY.VALUE}

Если добавить красивых картинок из FontAwesome, то получится вот так


WebPush сервер поддерживает следующие вызовы :


  • POST https://webpush.example.net/wpush/channelId сохранение ключей шифрования и имени пользователя
  • GET https://webpush.example.net/wpush/channelId получение тестового сообщения
  • GET https://webpush.example.net/subscription получение публичного VAPID ключа
  • POST https://webpush.example.net/pushdata отправка JSON структуры передаваемой в качестве сообщения в браузер
    {        "url": "http://personeltest.ru/away/habr.com/", // URL на который необходимо перейти при клике        "recipient": login, // Логин или идентификатор пользователя        "title": "Заголовок сообщения",        "body": "Тело сообщения",         "icon": "/static/images/new-notification.png", // путь к иконке сообщения        "version": uuid, // идентификатор сообщения        "tag": uuid, // тег сообщения для получения        "mtime": parseInt(new Date().getTime()/1000) //Время }
    
  • GET https://webpush.example.net/getdata Получение очереди сообщений
  • POST https://webpush.example.net/notify/login Отправка пустого оповещения пользователю
  • POST https://webpush.example.net/notifychannel/channelId Отправка пустого оповещения в канал

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


Aborche 2020
Aborche

Подробнее..

Перевод Учебный проект на Python интерфейс в 40 строк кода (часть 2)

03.07.2020 08:04:12 | Автор: admin
image

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

Streamlit


Streamlit это веб-фреймворк, предназначенный для исследователей данных для простого развертывания моделей и визуализаций с использованием Python. Это быстро и минималистично, а также красиво и удобно. Есть встроенные виджеты для пользовательского ввода, такие как загрузка изображений, ползунки, ввод текста и другие знакомые элементы HTML, такие как флажки и переключатели. Всякий раз, когда пользователь взаимодействует с потоковым приложением, сценарий python перезапускается сверху вниз, что важно учитывать при рассмотрении различных состояний вашего приложения.
Вы можете установить Streamlit с помощью pip:

pip install streamlit


И запустите streamlit в скрипте Python:

streamlit run app.py


Варианты использования


В предыдущей статье мы создали программу на Python, которая будет проходить лабиринт, учитывая файл изображения и начальное/конечное местоположения. Мы хотели бы превратить эту программу в одностраничное веб-приложение, где пользователь может загрузить изображение лабиринта (или использовать изображение лабиринта по умолчанию), настроить начальное и конечное местоположение лабиринта и увидеть пройденный лабиринт.

Во-первых, давайте создадим пользовательский интерфейс для загрузчика изображений и возможность использовать изображение по умолчанию. Мы можем добавить вывод текста, используя такие функции, как st.write() или st.title(). Мы храним динамически загруженный файл, используя функцию st.file_uploader(). Наконец, st.checkbox() вернет логическое значение в зависимости от того, установил ли пользователь флажок.

import streamlit as stimport cv2import matplotlib.pyplot as pltimport numpy as npimport mazest.title('Maze Solver')uploaded_file = st.file_uploader("Choose an image", ["jpg","jpeg","png"]) #image uploaderst.write('Or')use_default_image = st.checkbox('Use default maze')


Результат:

image

Затем мы можем вывести наше изображение по умолчанию или загруженное изображение в пригодный для использования формат изображения OpenCV.

if use_default_image:    opencv_image = cv2.imread('maze5.jpg')elif uploaded_file is not None:    file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)    opencv_image = cv2.imdecode(file_bytes, 1)


Как только изображение загружено, мы хотим показать изображение, размеченное с начальной и конечной точками. Мы будем использовать ползунки, чтобы позволить пользователю переместить эти точки. Функция st.sidebar() добавляет боковую панель на страницу, а st.slider() принимает числово в пределах определенного минимума и максимума. Мы можем определить минимальное и максимальное значения слайдера динамически в зависимости от размера нашего изображения лабиринта.

if opencv_image is not None:    st.subheader('Use the sliders on the left to position the start and end points')    start_x = st.sidebar.slider("Start X", value= 24 if use_default_image  else 50, min_value=0, max_value=opencv_image.shape[1], key='sx')    start_y = st.sidebar.slider("Start Y", value= 332 if use_default_image  else 100, min_value=0, max_value=opencv_image.shape[0], key='sy')    finish_x = st.sidebar.slider("Finish X", value= 309 if use_default_image  else 100, min_value=0, max_value=opencv_image.shape[1], key='fx')    finish_y = st.sidebar.slider("Finish Y", value= 330 if use_default_image  else 100, min_value=0, max_value=opencv_image.shape[0], key='fy')    marked_image = opencv_image.copy()    circle_thickness=(marked_image.shape[0]+marked_image.shape[0])//2//100 #circle thickness based on img size    cv2.circle(marked_image, (start_x, start_y), circle_thickness, (0,255,0),-1)    cv2.circle(marked_image, (finish_x, finish_y), circle_thickness, (255,0,0),-1)    st.image(marked_image, channels="RGB", width=800)


image

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

После того, как пользователь определил начальную и конечную позиции, мы хотим кнопку, чтобы решить лабиринт и отобразить решение. Элемент st.spinner () отображается только во время работы его дочернего процесса, а вызов st.image () используется для отображения изображения.

if marked_image is not None:    if st.button('Solve Maze'):        with st.spinner('Solving your maze'):            path = maze.find_shortest_path(opencv_image,(start_x, start_y),(finish_x, finish_y))        pathed_image = opencv_image.copy()        path_thickness = (pathed_image.shape[0]+pathed_image.shape[0])//200        maze.drawPath(pathed_image, path, path_thickness)        st.image(pathed_image, channels="RGB", width=800)


image

Кнопка

image

Вывод решения

Вывод


Менее чем в 40 строк кода мы создали простой пользовательский интерфейс для приложения обработки изображений Python. Нам не нужно было писать какой-либо традиционный интерфейсный код. Помимо способности Streamlit переваривать простой код Python, Streamlit интеллектуально перезапускает необходимые части вашего скрипта сверху вниз при каждом взаимодействии пользователя со страницей или при изменении скрипта. Это обеспечивает прямой поток данных и быструю разработку.

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

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


Подробнее..

Перевод 8 трюков в Python, используемых опытными программистами

30.06.2020 18:10:31 | Автор: admin
image

Вот восемь изящных приемов Python, которые, я уверен, вы еще не видели. Примените эти приемы в своем коде на Python, чтобы сделать его более лаконичным и производительным!

1. Сортировка объектов по нескольким ключам


Предположим, мы хотим отсортировать следующий список словарей:

people = [{ 'name': 'John', "age": 64 },{ 'name': 'Janet', "age": 34 },{ 'name': 'Ed', "age": 24 },{ 'name': 'Sara', "age": 64 },{ 'name': 'John', "age": 32 },{ 'name': 'Jane', "age": 34 },{ 'name': 'John', "age": 99 },]


Но мы не просто хотим сортировать их по имени или возрасту, мы хотим отсортировать их по обоим полям. В SQL это будет такой запрос:

SELECT * FROM people ORDER by name, age


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

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

import operatorpeople.sort(key=operator.itemgetter('age'))people.sort(key=operator.itemgetter('name'))


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

Это дает нам результат, который мы хотели:

[ {'name': 'Ed',   'age': 24}, {'name': 'Jane', 'age': 34}, {'name': 'Janet','age': 34}, {'name': 'John', 'age': 32}, {'name': 'John', 'age': 64}, {'name': 'John', 'age': 99}, {'name': 'Sara', 'age': 64}]


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

Источник вдохновения вопрос со StackOverflow.

2. Списковые включения (Генератор списка)


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

[ expression for item in list if conditional ]


Очень простой пример для заполнения списка последовательностью чисел:

mylist = [i for i in range(10)]print(mylist)# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


И поскольку вы можете использовать выражение, вы также можете сделать некоторую математику:

squares = [x**2 for x in range(10)]print(squares)# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


Или даже вызвать внешнюю функцию:

def some_function(a):    return (a + 5) / 2    my_formula = [some_function(i) for i in range(10)]print(my_formula)# [2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0]


И, наконец, вы можете использовать если для фильтрации списка. В этом случае мы сохраняем только те значения, которые делятся на 2:

filtered = [i for i in range(20) if i%2==0]print(filtered)# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]


3. Проверьте использование памяти ваших объектов


С помощью sys.getsizeof() вы можете проверить использование памяти объектом:

import sysmylist = range(0, 10000)print(sys.getsizeof(mylist))# 48


Вау подождите почему этот огромный список весит всего 48 байтов?
Это потому, что функция range возвращает класс, который только ведет себя как список. Диапазон намного менее нагружает память, чем фактический список чисел.
Вы можете убедиться сами, используя списковые включения, чтобы создать фактический список чисел из того же диапазона:

import sysmyreallist = [x for x in range(0, 10000)]print(sys.getsizeof(myreallist))# 87632


Итак, поиграв с sys.getsizeof(), вы можете больше узнать о Python и использовании вашей памяти.

4. Классы данных


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

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


Вот пример класса данных в работе:

from dataclasses import dataclass@dataclassclass Card:    rank: str    suit: str    card = Card("Q", "hearts")print(card == card)# Trueprint(card.rank)# 'Q'print(card)Card(rank='Q', suit='hearts')


Подробное руководство можно найти здесь.

5. Пакет attrs


Вместо классов данных вы можете использовать attrs. Есть две причины, чтобы выбрать attrs:

  • Вы используете версию Python старше 3.7
  • Вы хотите больше возможностей


Пакет attrs поддерживает все основные версии Python, включая CPython 2.7 и PyPy. Некоторые из дополнительных атрибутов, предлагаемых attrs по сравнению с обычными классами данных, являются валидаторами и конвертерами. Давайте посмотрим на пример кода:

@attrsclass Person(object):    name = attrib(default='John')    surname = attrib(default='Doe')    age = attrib(init=False)    p = Person()print(p)p = Person('Bill', 'Gates')p.age = 60print(p)# Output: #   Person(name='John', surname='Doe', age=NOTHING)#   Person(name='Bill', surname='Gates', age=60)


Авторы attrs фактически работали в PEP, которые ввели классы данных. Классы данных намеренно хранятся проще (легче для понимания), в то время как attrs предлагает полный набор функций, которые вам могут понадобиться!

Дополнительные примеры можно найти на странице примеров attrs.

6. Объединение словарей (Python 3.5+)


Начиная с Python 3.5, легче объединять словари:

dict1 = { 'a': 1, 'b': 2 }dict2 = { 'b': 3, 'c': 4 }merged = { **dict1, **dict2 }print (merged)# {'a': 1, 'b': 3, 'c': 4}


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

В Python 3.9 объединение словарей становится еще чище. Вышеупомянутое слияние в Python 3.9 может быть переписано как:

merged = dict1 | dict2


7. Поиск наиболее часто встречающегося значение


Чтобы найти наиболее часто встречающееся значение в списке или строке:

test = [1, 2, 3, 4, 2, 2, 3, 1, 4, 4, 4]print(max(set(test), key = test.count))# 4


Вы понимаете, почему это работает? Попробуйте разобраться в этом сами, прежде чем читать дальше.

Вы даже попытались, не так ли? Я все равно скажу вам:

  • max() вернет самое большое значение в списке. Аргумент key принимает функцию единственного аргумента для настройки порядка сортировки, в данном случае это test.count. Функция применяется к каждому элементу итерируемого.
  • test.count встроенная функция списка. Она принимает аргумент и будет подсчитывать количество вхождений для этого аргумента. Таким образом, test.count(1) вернет 2, а test.count(4) вернет 4.
  • set(test) возвращает все уникальные значения из test, поэтому {1, 2, 3, 4}


Итак, в этой единственной строке кода мы принимаем все уникальные значения теста, который равен {1, 2, 3, 4}. Далее max применит к ним функцию list.count и вернет максимальное значение.
И нет я не изобрел этот однострочник.

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

from collections import CounterCounter(test).most_common(1)# [4: 4]


8. Возврат нескольких значений


Функции в Python могут возвращать более одной переменной без словаря, списка или класса. Это работает так:

def get_user(id):    # fetch user from database    # ....    return name, birthdatename, birthdate = get_user(4)


Это нормально для ограниченного числа возвращаемых значений. Но все, что превышает 3 значения, должно быть помещено в (data) класс.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


Подробнее..

Функциональные тесты в Циан

02.07.2020 18:23:23 | Автор: admin


Привет!


Меня зовут Тимофей, я Python-разработчик в команде Платформа компании Циан. Наша команда занимается разработкой инструментов для продуктовых разработчиков. Это и библиотеки: HTTP-клиент, веб-сервер, библиотеки доступа к базам данных, и средства мониторинга микросервисов и сайта в целом, и интеграция с CI/CD и многое другое.


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


Но для начала...


Для чего нужны тесты


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


Сначала любой код прост, на него может быть всего пара-тройка тест-кейсов. Такое количество кейсов можно проверить и руками. Но когда в проект придёт другой разработчик, он может не знать об этих кейсах и что-то сломать. А кодовая база только увеличивается, кейсов становится всё больше и больше, проверять всё руками уже не вариант.


Code-coverage


У нас в Циан принята политика покрывать код тестами. На ревью мы измеряем diff-coverage: процент затронутых в pull-request строк, которые покрыты тестами. На данный момент наш санитарный минимум 80%, и мы готовимся к тому, чтобы автоматически отклонять pull-request с diff-coverage ниже этого числа. Таким образом новый код всегда почти полностью покрыт тестами.


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



Также в Циан разработчики пишут API-тесты, тестирующие микросервис в реальном окружении: dev, beta или даже prod, но на них мы не будем заострять внимание в этой статье.


Юнит-тесты


До недавнего времени у разработчиков Циан был лишь один инструмент получения заветного процента coverage юнит-тесты. Но удобно ли это?


При всей нашей любви к юнит-тестам, они не являются серебряной пулей и имеют неприятные недостатки:


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


Ломаются при рефакторинге. Стоит нам разделить 2 класса на 3 приходится переписывать тесты. Если не можем проверить рефакторинг существующими тестами, значит мы не можем быть уверены в том, что всё будет работать как и раньше. Даже можно сказать, что юнит-тесты замораживают архитектуру и не дают её менять.


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


Решение


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


На деле это изолированные API тесты для микросервисов. Под API здесь понимается в прямом его смысле Application Programming Interface, то есть любые интерфейсы микросервиса, будь то HTTP API, кроны или RabbitMQ / Kafka консюмеры.


Для тестирования в докере поднимаются все нужные базы данных, брокер сообщений, HTTP Mock Server, а микросервис автоматически запускается с настройками, указывающими на них.



Такие тесты призваны:


  • Улучшить качество, за счет тестирования многошаговых сценариев с вызовами API, обработкой сообщений RabbitMQ, запуском кронов.
  • Увеличить скорость разработки, за счет уменьшения количества ручных проверок.
  • Упростить рефакторинг, за счет проверки всего микросервиса целиком как black box, не вдаваясь в подробности, как он реализован внутри.
  • Упростить покрытие кода, за счет сбора покрытия во время работы микросервиса и объединения его с покрытием юнит-тестов.

Инструмент решили делать кроссплатформенным, с возможностью тестировать как микросервисы на Python, так и на C#, а в будущем и на frontend микросервисы на NodeJS в интеграции с браузером. Для реализации выбрали язык Python и известный фреймворк pytest. Python-разработчикам он уже известен по юнит-тестами, C#-разработчики пишут на нём API-тесты. К тому же pytest позволяет писать довольно мощные плагины, чем мы и воспользовались.


Выкидываем юнит-тесты?


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


  • Хоть такие тесты и довольно быстры, они всё равно медленней юнит-тестов. Особенно эта проблема заметна в параметризованных тестах.
  • Труднее анализировать падение теста. Если бы мы реализовали какое-нибудь бинарное дерево внутри нашей программы, находить ошибки в нём по отчету функциональных тестов нетривиальная задача.
  • Не так стабильны, как юнит-тесты, что правда не мешает нам ронять CI-pipeline если хотя бы один тест упал.
  • Некоторые сценарии невозможно, а если и возможно, то не рационально, проверять функциональными тестами, например конкурентный доступ к объектам в многопоточной среде.

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


Например в гипотетическом API регистрации пользователя можно покрыть все базовые сценарии функциональными тестами, а функцию проверки пароля на безопасность покрыть параметризованными юнит-тестами.


Возможности фреймворка


Конечно же для запуска тестов сначала нужно запустить все нужные базы данных и другие сервисы. Для этого мы используем собственный формат конфигурации микросервиса, называемый app.toml. В нём мы в унифицированном для всех используемых языков программирования уже описывали конфигурацию деплоя микросервиса, а теперь описываем и конфигурацию для тестов:


[[dependency]]type = "postgres"alias = "users"[[dependency]]type = "rabbitmq"

Запускаем command line утилитой фреймворка:


cian-functional-test-utils deps up

Под капотом этой команды считывается конфиг микросервиса, формируется docker-compose.yml и запускается привычный всем docker-compose up -d. Не советуем давать разработчикам голый docker-compose, фреймворк обычно лучше знает как запустить тот же Elasticsearch, чтобы он сносно работал и не съел всю оперативную память. Также свой формат позволяет описать нужные фреймворку метаданные контейнера, как например alias в нашем примере.


Далее в файле conftest.py опишем подготовку базы данных и процесс запуска микросервиса:


@pytest.fixture(scope='session', autouse=True)async def start(runner, pg):    # Так как все микросервисы Циан имеют один и тот же интерфейс,    # фреймворк знает как их запускать и куда идти за health-check.    await runner.start_background_python_web()    # Можно запускать и тестировать не только HTTP API, но и RabbitMQ консюмеры, кроны    await runner.start_background_python_command('save-users-consumer')@pytest.fixture(scope='session')async def pg(postgres_service):    db = await postgres_service.create_database_by_alias('users')    # Используем `pathlib.Path` для кроссплатформенности.    await db.execute_scripts(Path('database_schemas') / 'postgres.sql')    return db

Подготовка завершена! А вот и первый тест:


async def test_v1_get_user(http, pg):  # тот самый pg из conftest.py    # arrange    await pg.execute('INSERT INTO users (id, name) VALUES (1, "Bart")')    # act    response = await http.request('GET', '/v1/get-user/', params={'id': 1})    # assert    assert response.status == 200    assert response.data == {'id': 1, 'name': 'Bart'}

Аналогично PostgreSQL есть поддержка MsSQL, Cassandra, Redis, Elasticsearch.


С HTTP API разобрались, теперь посмотрим как можно проверить работу консюмера:


async def test_save_users_consumer(pg, queue_service):    # arrange    # Перед каждым тестов все очереди RabbitMQ удаляются,     # нужно подождать, пока консюмер пересоздаст её.    await queue_service.wait_consumer(queue='save-users')    # act    await queue_service.publish(        exchange='users',        routing_key='user.created',        payload={'id':1, 'name': 'Bart'},    )    await asyncio.sleep(0.5)  # Подождем немного, чтобы консюмер обработал сообщение    # assert    row = await pg.fetchrow('SELECT name FROM users WHERE id = 1')    assert row['name'] == 'Bart'

Перед каждым тестом мы удаляем все очереди в RabbitMQ (наши консюмеры настроены переподключаться в таких случаях), чистим все таблицы в базах данных. Почему перед тестом? Чтобы можно было зайти в базу данных или в админку RabbitMQ и посмотреть что там лежит на момент падения теста.


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


Да, иногда разница есть. К примеру у нас микросервисы могут иметь локальный кеш в оперативной памяти. Сбросить такой кеш без предоставления какого-то внешнего API невозможно. Мы выбрали довольно грязное решение: если микросервис запущен с определенной переменной окружения, на переданном в ней порту поднимается дополнительный HTTP-сервер, мы его зовём Management API. Так как все микросервисы для кеша используют нашу собственную библиотеку, нет никакой сложности сделать API, которая чистит его. И вся эта логика зашита в наших библиотеках, разработчикам ничего для этого делать не надо.


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


HTTP моки


Для HTTP-моков мы выбрали инструмент mountebank. Он умеет слушать несколько портов (по порту на подменяемый сервис) и настраивается полностью по HTTP. Работать с ним напрямую не очень удобно, поэтому мы сделали небольшую обертку, которая на практике выглядит так:


@pytest.fixture(scope='session')async def users_mock(http_mock_service):    # Нужно лишь указать имя микросервиса, который мы хотим замокать,     # фреймворк автоматически добавит URL мока в настройки микросервиса.    return await http_mock_service.make_microservice_mock('users')def test_something(users_mock):    # arrange    stub = await users_mock.add_stub(        method='GET',        path='/v1/get-user/',        response=MockResponse(body={'firstName': 'Bart', 'lastName': 'Simpson'}),    )    # act    # do something    # assert    # Проверяем, что запрос в мок был сделан с ?userId=234    request = (await stub.get_requests())[0]    assert request.params['userId'] == '234'

Под капотом при создании мока микросервиса автоматически создаётся еще и stub, который на все запросы отвечает кодом 404. Стабы у mountebank хранятся в списке и приоретизируются порядком в нём. Если, к примеру, наш стаб с 404 будет первым в списке, то мы всегда будем получать 404, независимо от наличия других стабов, до них дело просто не дойдет. Так дело не пойдёт, поэтому создание стаба у нас всегда помещает его на предпоследнюю позицию в списке (перед 404). Чем раньше в коде объявлен стаб, тем он приоритетней.


Ещё интересной особенностью mountebank является то, что по-умолчанию запросы в моки не сохраняются. Реализовать с таким поведением тест выше было бы невозможно. Есть два решения:


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

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


Как можно заметить, разработчику перед написанием самих тестов нужно написать session фикстуры на каждую используемую базу данных, HTTP-мок. Фикстура start, описывающая процесс запуска приложения, зависит от всех них и имеет параметр autouse=True. Таким образом перед запуском каких-либо тестов, инициализируются все базы данных и http-моки, запускаются процессы приложения.


Фреймворк имеет еще много разных возможностей, например:


  • сбор логов приложения, для проверки логирования;
  • сбор телеметрии statsd и graphite для её проверки;
  • перехват отправляемых в RabbitMQ сообщений, для тестирования продюсеров.

Останавливаться на них не хочется, так как фреймворк проприетарный, и вам, скорее всего, это не очень интересно.


Документация


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


Мы проанализировали существующие инструменты и не нашли ничего лучше, чем старый добрый Sphinx. reStructuredText сначала ломал мозг, но, уже после пары страниц, начинаешь осознавать всю мощь этих инструментов.


Наша документация содержит:


  • краткое описание, что вообще за тесты предстоит написать разработчику;
  • статью по настройке окружения для их написания, что особенно актуально нашим C#-разработчикам;
  • простые и более сложные примеры использования;
  • API Reference, где можно посмотреть детальное описание всех классов и методов.

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


Type annotations


Python язык с динамической типизацией. Лезть в документацию каждый раз, когда забыл имя нужного метода неприятно и непродуктивно. Поэтому мы покрыли код фреймворка аннотациями типов по PEP 484.


Благодаря этому и поддержке pytest у PyCharm, IDE автодополняет методы фикстур как и в обычном коде:



Поддержка pytest есть и в других IDE от компании Jetbrains с установленным плагином Python Community Edition. Наши C#-разработчики используют Rider, для них, так привыкших к статической типизации и подсказкам IDE, это особенно важно.


Вывод


Как вы уже могли заметить, получившийся фреймворк сильно завязан на архитектуру микросервисов в Циан. Его невозможно использовать в других компаниях, поэтому нет никакого смысла выкладывать его в Open Source.


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


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

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


На этом, собственно, всё, задавайте вопросы в комментариях.


Спасибо за внимание.

Подробнее..

Перевод - recovery mode Учебный проект на Python алгоритм Дейкстры, OpenCV и UI ( часть 1)

02.07.2020 20:17:59 | Автор: admin
Лабиринты это распространенная головоломка для людей, но они представляют из себя интересную задачу для программирования, которую мы можем решить, используя методы кратчайшего пути, такие как алгоритм Дейкстры.

Вспоминаем алгоритм Дейкстры


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

Сначала мы присваиваем значение расстояния от источника всем узлам. Узел s получает значение 0, потому что это источник; остальные получают значения для начала.

image

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

image

Узел s теперь завершен (черный), а его соседи a и b приняли новые значения. Новый интересующий узел b, поэтому мы повторяем процесс ослабления соседних узлов b и финализации значения кратчайшего пути для b.

image

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

image

image

image

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

Концептуализация изображений лабиринта


image

Мы можем представить себе изображение как матрицу пикселей. Каждый пиксель (для простоты) имеет значение RGB 0,0,0 (черный) или 255,255,255 (белый). Наша цель создать кратчайший путь, который начинается на белом и не переходит на чёрные границы. Чтобы представить эту цель, мы можем рассматривать каждый пиксель как узел и рисовать ребра между соседними пикселями с длиной ребер, основанной на разнице значений RGB. Мы будем использовать формулу евклидова квадратного расстояния и добавим 0,1, чтобы гарантировать отсутствие длины пути 0 (требование для алгоритма Дейкстры):

image

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

image

Реализация


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

import cv2import matplotlib.pyplot as pltimport numpy as npimg = cv2.imread('maze.png') # read an image from a file usingcv2.circle(img,(5,220), 3, (255,0,0), -1) # add a circle at (5, 220)cv2.circle(img, (25,5), 3, (0,0,255), -1) # add a circle at (5,5)plt.figure(figsize=(7,7))plt.imshow(img) # show the imageplt.show()


image


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

class Vertex:    def __init__(self,x_coord,y_coord):        self.x=x_coord        self.y=y_coord        self.d=float('inf') #current distance from source node        self.parent_x=None        self.parent_y=None        self.processed=False        self.index_in_queue=None


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

def find_shortest_path(img,src,dst):    pq=[] #min-heap priority queue    imagerows,imagecols=img.shape[0],img.shape[1]    matrix = np.full((imagerows, imagecols), None)     #access matrix elements by matrix[row][col]    #fill matrix with vertices    for r in range(imagerows):        for c in range(imagecols):            matrix[r][c]=Vertex(c,r)            matrix[r][c].index_in_queue=len(pq)            pq.append(matrix[r][c])    #set source distance value to 0    matrix[source_y][source_x].d=0    #maintain min-heap invariant (minimum d Vertex at list index 0)    pq = bubble_up(pq, matrix[source_y][source_x].index_in_queue)


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

#Implement euclidean squared distance formuladef get_distance(img,u,v):    return 0.1 + (float(img[v][0])-float(img[u][0]))**2+(float(img[v][1])-float(img[u][1]))**2+(float(img[v][2])-float(img[u][2]))**2#Return neighbor directly above, below, right, and leftdef get_neighbors(mat,r,c):    shape=mat.shape    neighbors=[]    #ensure neighbors are within image boundaries    if r > 0 and not mat[r-1][c].processed:         neighbors.append(mat[r-1][c])    if r < shape[0] - 1 and not mat[r+1][c].processed:            neighbors.append(mat[r+1][c])    if c > 0 and not mat[r][c-1].processed:        neighbors.append(mat[r][c-1])    if c < shape[1] - 1 and not mat[r][c+1].processed:            neighbors.append(mat[r][c+1])    return neighbors


Теперь мы можем реализовать алгоритм Дейкстры и присвоить значения расстояния (d) всем вершинам пикселей в изображении лабиринта:

while len(pq) > 0:    u=pq[0] #smallest-value unprocessed node    #remove node of interest from the queue    pq[0]=pq[-1]     pq[0].index_in_queue=0    pq.pop()    pq=bubble_down(pq,0) #min-heap function, see source code         u.processed=True    neighbors = get_neighbors(matrix,u.y,u.x)    for v in neighbors:        dist=get_distance(img,(u.y,u.x),(v.y,v.x))        if u.d + dist < v.d:            v.d = u.d+dist            v.parent_x=u.x #keep track of the shortest path            v.parent_y=u.y            idx=v.index_in_queue            pq=bubble_down(pq,idx)             pq=bubble_up(pq,idx)


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

img = cv2.imread('maze.png') # read an image from a file using opencv (cv2) libraryp = find_shortest_path(img, (25,5), (5,220))drawPath(img,p)plt.figure(figsize=(7,7))plt.imshow(img) # show the image on the screen plt.show()


image

image

Давайте попробуем другие лабиринты со всего Интернета.

image

image

image

image

Полный исходный код доступен на GitHub здесь.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


Подробнее..

Восходящая сортировка кучей

03.07.2020 00:15:20 | Автор: admin

Это заключительная статья из серии про сортировки кучей. В предыдущих лекциях мы рассмотрели весьма разнообразные кучные структуры, показывающих отличные результаты по скорости. Напрашивается вопрос: а какая куча наиболее эффективна, если речь идёт о сортировке? Ответ таков: та, которую мы рассмотрим сегодня.
EDISON Software - web-development
Мы в EDISON в наших проектах используем только лучшие методологии разработки.


Когда мы дорабатывали приложения и сайты Московского ювелирного завода мы сделали полный аудит имеющихся веб-ресурсов, переписали их на Python и Django, внедрили SDK для обращения к видеосервису и рассылки SMS-оповещений, произвели интеграцию с системой электронного документооборота и API 2ГИС.

Мы работаем с ювелирной точностью ;-)
Необычные кучи, которые мы рассматривали ранее это, конечно, прекрасно, однако самая эффективная куча стандартная, но с улучшенной просейкой.

Что такое просейка, зачем она нужна в куче и как она работает описано в самой первой части серии статей.

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



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

Итак, как это работает, давайте посмотрим на конкретном примере.

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

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



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

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



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



Теперь всё. Восходящая просейка сформировала из массива сортирующее дерево, в котором любой родитель больше чем его потомки.

Итоговая анимация:



Реализация на Python 3.7


Основной алгоритм сортировки ничем не отличается от обычной heapsort:

# Основной алгоритм сортировки кучейdef HeapSortBottomUp(data):    # Формируем первоначальное сортирующее дерево    # Для этого справа-налево перебираем элементы массива    # (у которых есть потомки) и делаем для каждого из них просейку    for start in range((len(data) - 2) // 2, -1, -1):        HeapSortBottomUp_Sift(data, start, len(data) - 1)     # Первый элемент массива всегда соответствует корню сортирующего дерева    # и поэтому является максимумом для неотсортированной части массива.    for end in range(len(data) - 1, 0, -1):         # Меняем этот максимум местами с последним         # элементом неотсортированной части массива        data[end], data[0] = data[0], data[end]        # После обмена в корне сортирующего дерева немаксимальный элемент        # Восстанавливаем сортирующее дерево        # Просейка для неотсортированной части массива        HeapSortBottomUp_Sift(data, 0, end - 1)    return data

Спуск до нижнего листа удобно/наглядно вынести в отдельную функцию:

# Спуск вниз до самого нижнего листа# Выбираем бОльших потомковdef HeapSortBottomUp_LeafSearch(data, start, end):        current = start        # Спускаемся вниз, определяя какой    # потомок (левый или правый) больше    while True:        child = current * 2 + 1 # Левый потомок        # Прерываем цикл, если правый вне массива        if child + 1 > end:             break         # Идём туда, где потомок больше        if data[child + 1] > data[child]:            current = child + 1        else:            current = child        # Возможна ситуация, если левый потомок единственный    child = current * 2 + 1 # Левый потомок    if child <= end:        current = child            return current

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

# Восходящая просейкаdef HeapSortBottomUp_Sift(data, start, end):        # По бОльшим потомкам спускаемся до самого нижнего уровня    current = HeapSortBottomUp_LeafSearch(data, start, end)        # Поднимаемся вверх, пока не встретим узел    # больший или равный корню поддерева    while data[start] > data[current]:        current = (current - 1) // 2        # Найденный узел запоминаем,    # в этот узел кладём корень поддерева    temp = data[current]    data[current] = data[start]        # всё что выше по ветке вплоть до корня    # - сдвигаем на один уровень вниз    while current > start:        current = (current - 1) // 2        temp, data[current] = data[current], temp  

На просторах Интернета также обнаружен код на C
/*----------------------------------------------------------------------*//*                         BOTTOM-UP HEAPSORT                           *//* Written by J. Teuhola <teuhola@cs.utu.fi>; the original idea is      *//* probably due to R.W. Floyd. Thereafter it has been used by many      *//* authors, among others S. Carlsson and I. Wegener. Building the heap  *//* bottom-up is also due to R. W. Floyd: Treesort 3 (Algorithm 245),    *//* Communications of the ACM 7, p. 701, 1964.                           *//*----------------------------------------------------------------------*/#define element float/*-----------------------------------------------------------------------*//* The sift-up procedure sinks a hole from v[i] to leaf and then sifts   *//* the original v[i] element from the leaf level up. This is the main    *//* idea of bottom-up heapsort.                                           *//*-----------------------------------------------------------------------*/static void siftup(v, i, n) element v[]; int i, n; {  int j, start;  element x;  start = i;  x = v[i];  j = i << 1;  /* Leaf Search */  while(j <= n) {    if(j < n) if v[j] < v[j + 1]) j++;    v[i] = v[j];    i = j;    j = i << 1;  }  /* Siftup */  j = i >> 1;  while(j >= start) {    if(v[j] < x) {      v[i] = v[j];      i = j;      j = i >> 1;    } else break;  }  v[i] = x;} /* End of siftup *//*----------------------------------------------------------------------*//* The heapsort procedure; the original array is r[0..n-1], but here    *//* it is shifted to vector v[1..n], for convenience.                    *//*----------------------------------------------------------------------*/void bottom_up_heapsort(r, n) element r[]; int n; {  int k;   element x;  element *v;  v = r - 1; /* The address shift */  /* Build the heap bottom-up, using siftup. */  for (k = n >> 1; k > 1; k--) siftup(v, k, n);  /* The main loop of sorting follows. The root is swapped with the last  */  /* leaf after each sift-up. */  for(k = n; k > 1; k--) {    siftup(v, 1, k);    x = v[k];    v[k] = v[1];    v[1] = x;  }} /* End of bottom_up_heapsort */

Чисто эмпирически по моим замерам восходящая сортировка кучей работает в 1,5 раза быстрее, чем обычная сортировка кучей.

По некоторой информации (на странице алгоритма в Википедии, в приведённых PDF в разделе Ссылки) BottomUp HeapSort в среднем опережает даже быструю сортировку для достаточно крупных массивов размером от 16 тысяч элементов.

Ссылки


Bottom-up heapsort

A Variant of Heapsort with Almost Optimal Number of Comparisons

Building Heaps Fast

A new variant of heapsort beating, on an average, quicksort(if n is not very small)

Статьи серии:



В приложение AlgoLab добавлена сегодняшняя сортировка, кто пользуется обновите excel-файл с макросами.
Подробнее..

Перевод Введение в асинхронное программирование на Python

03.07.2020 14:20:11 | Автор: admin
Всем привет. Подготовили перевод интересной статьи в преддверии старта базового курса Разработчик Python.



Введение


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



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

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

Асинхронность это одна из основных причин популярности выбора Node.js для реализации бэкенда. Большое количество кода, который мы пишем, особенно в приложениях с тяжелым вводом-выводом, таком как на веб-сайтах, зависит от внешних ресурсов. В нем может оказаться все, что угодно, от удаленного вызова базы данных до POST-запросов в REST-сервис. Как только вы отправите запрос в один из этих ресурсов, ваш код будет просто ожидать ответа. С асинхронным программированием вы позволяете своему коду обрабатывать другие задачи, пока ждете ответа от ресурсов.

Как Python умудряется делать несколько вещей одновременно?




1. Множественные процессы

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

from multiprocessing import Processdef print_func(continent='Asia'):    print('The name of continent is : ', continent)if __name__ == "__main__":  # confirms that the code is under main function    names = ['America', 'Europe', 'Africa']    procs = []    proc = Process(target=print_func)  # instantiating without any argument    procs.append(proc)    proc.start()    # instantiating process with arguments    for name in names:        # print(name)        proc = Process(target=print_func, args=(name,))        procs.append(proc)        proc.start()    # complete the processes    for proc in procs:        proc.join()


Вывод:

The name of continent is :  AsiaThe name of continent is :  AmericaThe name of continent is :  EuropeThe name of continent is :  Africa


2. Множественные потоки

Еще один способ запустить несколько работ параллельно это использовать потоки. Поток это очередь выполнения, которая очень похожа на процесс, однако в одном процессе вы можете иметь несколько потоков, и у всех них будет общий доступ к ресурсам. Однако из-за этого написать код потока будет сложно. Аналогично, все тяжелую работу по выделению памяти процессора сделает операционная система, но глобальная блокировка интерпретатора (GIL) позволит только одному потоку Python запускаться в одну единицу времени, даже если у вас есть многопоточный код. Так GIL на CPython предотвращает многоядерную конкурентность. То есть вы насильно можете запуститься только на одном ядре, даже если у вас их два, четыре или больше.

import threading def print_cube(num):    """    function to print cube of given num    """    print("Cube: {}".format(num * num * num)) def print_square(num):    """    function to print square of given num    """    print("Square: {}".format(num * num)) if __name__ == "__main__":    # creating thread    t1 = threading.Thread(target=print_square, args=(10,))    t2 = threading.Thread(target=print_cube, args=(10,))     # starting thread 1    t1.start()    # starting thread 2    t2.start()     # wait until thread 1 is completely executed    t1.join()    # wait until thread 2 is completely executed    t2.join()     # both threads completely executed    print("Done!")


Вывод:

Square: 100Cube: 1000Done!


3. Корутины и yield:

Корутины это обобщение подпрограмм. Они используются для кооперативной многозадачности, когда процесс добровольно отдает контроль (yield) с какой-то периодичностью или в периоды ожидания, чтобы позволить нескольким приложениям работать одновременно. Корутины похожи на генераторы, но с дополнительными методами и небольшими изменениями в том, как мы используем оператор yield. Генераторы производят данные для итерации, в то время как корутины могут еще и потреблять данные.

def print_name(prefix):    print("Searching prefix:{}".format(prefix))    try :         while True:                # yeild used to create coroutine                name = (yield)                if prefix in name:                    print(name)    except GeneratorExit:            print("Closing coroutine!!") corou = print_name("Dear")corou.__next__()corou.send("James")corou.send("Dear James")corou.close()


Вывод:

Searching prefix:DearDear JamesClosing coroutine!!


4. Асинхронное программирование

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

Ответ: asyncio

Asyncio модуль асинхронного программирования, который был представлен в Python 3.4. Он предназначен для использования корутин и future для упрощения написания асинхронного кода и делает его почти таким же читаемым, как синхронный код, из-за отсутствия callback-ов.

Asyncio использует разные конструкции: event loop, корутины и future.

  • event loop управляет и распределяет выполнение различных задач. Он регистрирует их и обрабатывает распределение потока управления между ними.
  • Корутины (о которых мы говорили выше) это специальные функции, работа которых схожа с работой генераторов в Python, с помощью await они возвращают поток управления обратно в event loop. Запуск корутины должен быть запланирован в event loop. Запланированные корутины будут обернуты в Tasks, что является типом Future.
  • Future отражает результат таска, который может или не может быть выполнен. Результатом может быть exception.


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

Переключение контекста в asyncio представляет собой event loop, который передает поток управления от одной корутины к другой.

В следующем примере, мы запускаем 3 асинхронных таска, которые по-отдельности делают запросы к Reddit, извлекают и выводят содержимое JSON. Мы используем aiohttp клиентскую библиотеку http, которая гарантирует, что даже HTTP-запрос будет выполнен асинхронно.

import signal  import sys  import asyncio  import aiohttp  import jsonloop = asyncio.get_event_loop()  client = aiohttp.ClientSession(loop=loop)async def get_json(client, url):      async with client.get(url) as response:        assert response.status == 200        return await response.read()async def get_reddit_top(subreddit, client):      data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')    j = json.loads(data1.decode('utf-8'))    for i in j['data']['children']:        score = i['data']['score']        title = i['data']['title']        link = i['data']['url']        print(str(score) + ': ' + title + ' (' + link + ')')    print('DONE:', subreddit + '\n')def signal_handler(signal, frame):      loop.stop()    client.close()    sys.exit(0)signal.signal(signal.SIGINT, signal_handler)asyncio.ensure_future(get_reddit_top('python', client))  asyncio.ensure_future(get_reddit_top('programming', client))  asyncio.ensure_future(get_reddit_top('compsci', client))  loop.run_forever()


Вывод:

50: Undershoot: Parsing theory in 1965 (http://personeltest.ru/away/jeffreykegler.github.io/Ocean-of-Awareness-blog/individual/2018/07/knuth_1965_2.html)12: Question about best-prefix/failure function/primal match table in kmp algorithm (http://personeltest.ru/aways/www.reddit.com/r/compsci/comments/8xd3m2/question_about_bestprefixfailure_functionprimal/)1: Question regarding calculating the probability of failure of a RAID system (http://personeltest.ru/aways/www.reddit.com/r/compsci/comments/8xbkk2/question_regarding_calculating_the_probability_of/)DONE: compsci336: /r/thanosdidnothingwrong -- banning people with python (http://personeltest.ru/aways/clips.twitch.tv/AstutePluckyCocoaLitty)175: PythonRobotics: Python sample codes for robotics algorithms (http://personeltest.ru/aways/atsushisakai.github.io/PythonRobotics/)23: Python and Flask Tutorial in VS Code (http://personeltest.ru/aways/code.visualstudio.com/docs/python/tutorial-flask)17: Started a new blog on Celery - what would you like to read about? (http://personeltest.ru/aways/www.python-celery.com)14: A Simple Anomaly Detection Algorithm in Python (http://personeltest.ru/aways/medium.com/@mathmare_/pyng-a-simple-anomaly-detection-algorithm-2f355d7dc054)DONE: python1360: git bundle (http://personeltest.ru/aways/dev.to/gabeguz/git-bundle-2l5o)1191: Which hashing algorithm is best for uniqueness and speed? Ian Boyd's answer (top voted) is one of the best comments I've seen on Stackexchange. (http://personeltest.ru/aways/softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed)430: ARM launches Facts campaign against RISC-V (http://personeltest.ru/aways/riscv-basics.com/)244: Choice of search engine on Android nuked by Anonymous Coward (2009) (http://personeltest.ru/aways/android.googlesource.com/platform/packages/apps/GlobalSearch/+/592150ac00086400415afe936d96f04d3be3ba0c)209: Exploiting freely accessible WhatsApp data or Why does WhatsApp web know my phones battery level? (http://personeltest.ru/aways/medium.com/@juan_cortes/exploiting-freely-accessible-whatsapp-data-or-why-does-whatsapp-know-my-battery-level-ddac224041b4)DONE: programming


Использование Redis и Redis Queue RQ


Использование asyncio и aiohttp не всегда хорошая идея, особенно если вы пользуетесь более старыми версиями Python. К тому же, бывают моменты, когда вам нужно распределить задачи по разным серверам. В этом случае можно использовать RQ (Redis Queue). Это обычная библиотека Python для добавления работ в очередь и обработки их воркерами в фоновом режиме. Для организации очереди используется Redis база данных ключей/значений.

В примере ниже мы добавили в очередь простую функцию count_words_at_url с помощью Redis.

from mymodule import count_words_at_urlfrom redis import Redisfrom rq import Queueq = Queue(connection=Redis())job = q.enqueue(count_words_at_url, 'http://nvie.com')******mymodule.py******import requestsdef count_words_at_url(url):    """Just an example function that's called async."""    resp = requests.get(url)    print( len(resp.text.split()))    return( len(resp.text.split()))


Вывод:

15:10:45 RQ worker 'rq:worker:EMPID18030.9865' started, version 0.11.015:10:45 *** Listening on default...15:10:45 Cleaning registries for queue: default15:10:50 default: mymodule.count_words_at_url('http://nvie.com') (a2b7451e-731f-4f31-9232-2b7e3549051f)32215:10:51 default: Job OK (a2b7451e-731f-4f31-9232-2b7e3549051f)15:10:51 Result is kept for 500 seconds


Заключение


В качестве примера возьмем шахматную выставку, где один из лучших шахматистов соревнуется с большим количеством людей. У нас есть 24 игры и 24 человека, с которыми можно сыграть, и, если шахматист будет играть с ними синхронно, это займет не менее 12 часов (при условии, что средняя игра занимает 30 ходов, шахматист продумывает ход в течение 5 секунд, а противник примерно 55 секунд.) Однако в асинхронном режиме шахматист сможет делать ход и оставлять противнику время на раздумья, тем временем переходя к следующему противнику и деля ход. Таким образом, сделать ход во всех 24 играх можно за 2 минуты, и выиграны они все могут быть всего за один час.

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

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

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



Узнать о курсе подробнее.


Подробнее..

Перевод О нет! Моя Data Science ржавеет

04.07.2020 14:10:42 | Автор: admin
Привет, Хабр!

Предлагаем вашему вниманию перевод интереснейшего исследования от компании Crowdstrike. Материал посвящен использованию языка Rust в области Data Science (применительно к malware analysis) и демонстрирует, в чем Rust на таком поле может посоперничать даже с NumPy и SciPy, не говоря уж о чистом Python.


Приятного чтения!

Python один из самых популярных языков программирования для работы с data science, и неслучайно. В индексе пакетов Python (PyPI) найдется огромное множество впечатляющих библиотек для работы с data science, в частности, NumPy, SciPy, Natural Language Toolkit, Pandas и Matplotlib. Благодаря изобилию высококачественных аналитических библиотек в доступе и обширному сообществу разработчиков, Python очевидный выбор для многих исследователей данных.

Многие из этих библиотек реализованы на C и C++ из соображений производительности, но предоставляют интерфейсы внешних функций (FFI) или привязки Python, так, чтобы из функции можно было вызывать из Python. Эти реализации на более низкоуровневых языках призваны смягчить наиболее заметные недостатки Python, связанные, в частности, с длительностью выполнения и потреблением памяти. Если удается ограничить время выполнения и потребление памяти, то сильно упрощается масштабируемость, что критически важно для сокращения расходов. Если мы сможем писать высокопроизводительный код, решающий задачи data science, то интеграция такого кода с Python станет серьезным преимуществом.

При работе на стыке data science и анализа вредоносного ПО требуется не только скоростное выполнение, но и эффективное использование разделяемых ресурсов, опять же, для масштабирования. Проблема масштабирования является одной из ключевых в области больших данных, как, например, эффективная обработка миллионов исполняемых файлов на множестве платформ. Для достижения хорошей производительности на современных процессорах требуется параллелизм, обычно реализуемый при помощи многопоточности; но также необходимо повышать эффективность выполнения кода и расхода памяти. При решении подобных задач бывает сложно сбалансировать ресурсы локальной системы, а правильно реализовать многопоточные системы даже сложнее. Суть C и C++ такова, что потокобезопасность в них не предоставляется. Да, существуют внешние платформо-специфичные библиотеки, но обеспечение потокобезопасности, это, очевидно, долг разработчика.

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

Но есть Rust. Язык Rust во многом позиционируется как идеальное решение всех потенциальных проблем, обрисованных выше: время выполнения и потребление памяти сравнимы с C и C++, а также предоставляется обширная типобезопасность. Также в языке Rust предоставляются дополнительные приятности, в частности, серьезные гарантии безопасности памяти и никаких издержек времени исполнения. Поскольку таких издержек нет, упрощается интеграция кода Rust с кодом других языков, в частности, Python. В этой статье мы сделаем небольшую экскурсию по Rust, чтобы понять, достоин ли он связанного с ним хайпа.

Пример приложения для Data Science


Data science очень широкая предметная область со множеством прикладных аспектов, и обсудить их все в одной статье невозможно. Простая задача для data science вычисление информационной энтропии для байтовых последовательностей. Общая формула для расчета энтропии в битах приводится в Википедии:



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

Давайте испробуем Rust и посмотрим, как он справляется с вычислением энтропии по сравнению с чистым Python, а также с некоторыми популярнейшими библиотеками Python, упомянутыми выше. Это упрощенная оценка потенциальной производительности Rust в области data science; данный эксперимент не является критикой Python или отличных библиотек, имеющихся в нем. В этих примерах мы сгенерируем собственную библиотеку C из кода Rust, который сможем импортировать из Python. Все тесты проводились на Ubuntu 18.04.

Чистый Python


Начнем с простой функции на чистом Python (в entropy.py) для расчета энтропии bytearray, воспользуемся при этом только математическим модулем из стандартной библиотеки. Эта функция не оптимизирована, возьмем ее в качестве отправной точки для модификаций и измерения производительности.

import mathdef compute_entropy_pure_python(data):    """Compute entropy on bytearray `data`."""    counts = [0] * 256    entropy = 0.0    length = len(data)    for byte in data:        counts[byte] += 1    for count in counts:        if count != 0:            probability = float(count) / length            entropy -= probability * math.log(probability, 2)    return entropy

Python с NumPy и SciPy


Неудивительно, что в SciPy предоставляется функция для расчета энтропии. Но сначала мы воспользуемся функцией unique() из NumPy для расчета частот байтов. Сравнивать производительность энтропийной функции SciPy с другими реализациями немного нечестно, так как в реализации из SciPy есть дополнительный функционал для расчета относительной энтропии (расстояния Кульбака-Лейблера). Опять же, мы собираемся провести (надеюсь, не слишком медленный) тест-драйв, чтобы посмотреть, какова будет производительность скомпилированных библиотек Rust, импортированных из Python. Будем придерживаться реализации из SciPy, включенной в наш скрипт entropy.py.

import numpy as npfrom scipy.stats import entropy as scipy_entropydef compute_entropy_scipy_numpy(data):    """Вычисляем энтропию bytearray `data` с SciPy и NumPy."""    counts = np.bincount(bytearray(data), minlength=256)    return scipy_entropy(counts, base=2)

Python с Rust


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

cargo new --lib rust_entropyCargo.toml

Начинаем с обязательного файла манифеста Cargo.toml, в котором определяем пакет Cargo и указываем имя библиотеки, rust_entropy_lib. Используем общедоступный контейнер cpython (v0.4.1), доступный на сайте crates.io, в реестре пакетов Rust Package Registry. В статье мы используем Rust v1.42.0, новейшую стабильную версию, доступную на момент написания.

[package] name = "rust-entropy"version = "0.1.0"authors = ["Nobody <nobody@nowhere.com>"] edition = "2018"[lib] name = "rust_entropy_lib"crate-type = ["dylib"][dependencies.cpython] version = "0.4.1"features = ["extension-module"]

lib.rs


Реализация библиотеки Rust весьма проста. Как и в случае с нашей реализацией на чистом Python, мы инициализируем массив counts для каждого возможного значения байт и перебираем данные для наполнения counts. Для завершения операции вычисляем и возвращаем отрицательную сумму вероятностей, умноженную на вероятностей.

use cpython::{py_fn, py_module_initializer, PyResult, Python};/// вычисляем энтропию массива байтfn compute_entropy_pure_rust(data: &[u8]) -> f64 {    let mut counts = [0; 256];    let mut entropy = 0_f64;    let length = data.len() as f64;    // collect byte counts    for &byte in data.iter() {        counts[usize::from(byte)] += 1;    }    // вычисление энтропии    for &count in counts.iter() {        if count != 0 {            let probability = f64::from(count) / length;            entropy -= probability * probability.log2();        }    }    entropy}

Все, что нам остается взять из lib.rs это механизм для вызова чистой функции Rust из Python. Мы включаем в lib.rs функцию, приспособленную к работе с CPython (compute_entropy_cpython()) для вызова нашей чистой функции Rust (compute_entropy_pure_rust()). Поступая таким образом, мы только выигрываем, так как будем поддерживать единственную чистую реализацию Rust, а также предоставим обертку, удобную для работы с CPython.

/// Функция Rust для работы с CPython fn compute_entropy_cpython(_: Python, data: &[u8]) -> PyResult<f64> {    let _gil = Python::acquire_gil();    let entropy = compute_entropy_pure_rust(data);    Ok(entropy)}// инициализируем модуль Python и добавляем функцию Rust для работы с CPython py_module_initializer!(    librust_entropy_lib,    initlibrust_entropy_lib,    PyInit_rust_entropy_lib,    |py, m | {        m.add(py, "__doc__", "Entropy module implemented in Rust")?;        m.add(            py,            "compute_entropy_cpython",            py_fn!(py, compute_entropy_cpython(data: &[u8])            )        )?;        Ok(())    });

Вызов кода Rust из Python


Наконец, вызываем реализацию Rust из Python (опять же, из entropy.py). Для этого сначала импортируем нашу собственную динамическую системную библиотеку, скомпилированную из Rust. Затем просто вызываем предоставленную библиотечную функцию, которую ранее указали при инициализации модуля Python с использованием макроса py_module_initializer! в нашем коде Rust. На данном этапе у нас всего один модуль Python (entropy.py), включающий функции для вызова всех реализаций расчета энтропии.

import rust_entropy_libdef compute_entropy_rust_from_python(data):    ""Вычисляем энтропию bytearray `data` при помощи Rust."""    return rust_entropy_lib.compute_entropy_cpython(data)

Мы собираем вышеприведенный библиотечный пакет Rust на Ubuntu 18.04 при помощи Cargo. (Эта ссылка может пригодиться пользователям OS X).

cargo build --release

Закончив со сборкой, мы переименовываем полученную библиотеку и копируем ее в тот каталог, где находятся наши модули Python, так, чтобы ее можно было импортировать из сценариев. Созданная при помощи Cargo библиотека называется librust_entropy_lib.so, но ее потребуется переименовать в rust_entropy_lib.so, чтобы успешно импортировать в рамках этих тестов.

Проверка производительности: результаты


Мы измеряли производительность каждой реализации функции при помощи контрольных точек pytest, рассчитав энтропию более чем для 1 миллиона случайно выбранных байт. Все реализации показаны на одних и тех же данных. Эталонные тесты (также включенные в entropy.py) показаны ниже.

# ### КОНТРОЛЬНЕ ТОЧКИ #### генерируем случайные байты для тестирования w/ NumPyNUM = 1000000VAL = np.random.randint(0, 256, size=(NUM, ), dtype=np.uint8)def test_pure_python(benchmark):    """тестируем чистый Python."""    benchmark(compute_entropy_pure_python, VAL)def test_python_scipy_numpy(benchmark):    """тестируем чистый Python со SciPy."""    benchmark(compute_entropy_scipy_numpy, VAL)def test_rust(benchmark):    """тестируем реализацию Rust, вызываемую из Python."""    benchmark(compute_entropy_rust_from_python, VAL)

Наконец, делаем отдельные простые драйверные скрипты для каждого метода, нужного для расчета энтропии. Далее идет репрезентативный драйверный скрипт для тестирования реализации на чистом Python. В файле testdata.bin 1 000 000 случайных байт, используемых для тестирования всех методов. Каждый из методов повторяет вычисления по 100 раз, чтобы упростить захват данных об использовании памяти.

import entropywith open('testdata.bin', 'rb') as f:    DATA = f.read()for _ in range(100):    entropy.compute_entropy_pure_python(DATA)

Реализации как для SciPy/NumPy, так и для Rust показали хорошую производительность, легко обставив неоптимизированную реализацию на чистом Python более чем в 100 раз. Версия на Rust показала себя лишь немного лучше, чем версия на SciPy/NumPy, но результаты подтвердили наши ожидания: чистый Python гораздо медленнее скомпилированных языков, а расширения, написанные на Rust, могут весьма успешно конкурировать с аналогами на C (побеждая их даже в таком микротестировании).

Также существуют и другие методы повышения производительности. Мы могли бы использовать модули ctypes или cffi. Могли бы добавить подсказки типов и воспользоваться Cython для генерации библиотеки, которую могли бы импортировать из Python. При всех этих вариантах требуется учитывать компромиссы, специфичные для каждого конкретного решения.



Мы также измерили расход памяти для каждой реализации функции при помощи приложения GNU time (не путайте со встроенной командой оболочки time). В частности, мы измерили максимальный размер резидентной части памяти (resident set size).

Тогда как в реализациях на чистом Python и Rust максимальные размеры этой части весьма схожи, реализация SciPy/NumPy потребляет ощутимо больше памяти по данному контрольному показателю. Предположительно это связано с дополнительными возможностями, загружаемыми в память при импорте. Как бы то ни было, вызов кода Rust из Python, по-видимому, не привносит серьезных накладных расходов памяти.



Итоги


Мы крайне впечатлены производительностью, достигаемой при вызове Rust из Python. В ходе нашей откровенно краткой оценки реализация на Rust смогла потягаться в производительности с базовой реализацией на C из пакетов SciPy и NumPy. Rust, по-видимому, отлично подходит для эффективной крупномасштабной обработки.

Rust показал не только отличное время выполнения; следует отметить, что и накладные расходы памяти в этих тестах также оказались минимальными. Такие характеристики времени выполнения и использования памяти представляются идеальными для целей масштабирования. Производительность реализаций SciPy и NumPy C FFI определенно сопоставима, но с Rust мы получаем дополнительные плюсы, которых не дают нам C и C++. Гарантии по безопасности памяти и потокобезопасности это очень привлекательное преимущество.

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

Мы не призываем портировать SciPy или NumPy на Rust, так как эти библиотеки Python уже хорошо оптимизированы и поддерживаются классными сообществами разработчиков. С другой стороны, мы настоятельно рекомендуем портировать с чистого Python на Rust такой код, который не предоставляется в высокопроизводительных библиотеках. В контексте приложений для data science, используемых для анализа безопасности, Rust представляется конкурентоспособной альтернативой для Python, учитывая его скорость и гарантии безопасности.
Подробнее..

Из песочницы Как скомпилировать декоратор C, Python и собственная реализация. Часть 2

29.06.2020 22:07:32 | Автор: admin

Декораторы одна из самых необычных особенностей Python. Это инструмент, который полноценно может существовать только в динамически типизированном, интерпретируемом языке. В первой части статьи мой товарищ Witcher136 показал, как в С++ реализовать наиболее приближенную к эталонной (питоновской) версию декораторов.


Я же расскажу про то, как решил попытаться реализовать декораторы в компилируемом языке программирования, для чего в итоге написал написал собственный небольшой компилятор на Haskell на основе LLVM.



Оглавление


  1. Как работают декораторы в Python
  2. Haskell и LLVM собственный компилятор
  3. Так как же скомпилировать декоратор?


Как работают декораторы в Python


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


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


Как работают декораторы в Python, в общем-то интуитивно понятно любому человеку, знакомому с этим языком:


Функции decorator, принимающей другую функцию как свой аргумент func, в момент применения декоратора в качестве данного аргумента передается декорируемая функция old. Результатом является новая функция new и с этого момента она привязывается к имени old

def decorator(func):    def new(*args, **kwargs):        print('Hey!')        return func(*args, **kwargs)    return new@decoratordef old():    pass# old() выведет "Hey!" - к имени old теперь приязан вызов new

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


Про интерпретатор CPython

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


Благодаря этому, интерпретатору не надо знать о типах объектов, соответстующих символам в коде, вплоть до момент выполнения операций над ними когда очередь дойдет до выполнения какой-либо "конкретной" инструкции тогда он и проверит тип. Сильно упрощая можно объяснить это так: BINARY_SUBSTRACT (вычитание) упадет с TypeError, если дальше на стэке лежат число 1 и строка 'a'. В тоже время, выполнение STORE_FAST для одного и того же имени (запись в одну и ту же переменную), когда один раз на стэке лежит число, а в другой раз строка, не приведет к TypeError, т.к. в инструкцию по выполнению команды STORE_FAST не входит проверка типа только связывание имени с новым объектом.


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


Проблема 1. Декораторы это просто функции


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


name = input('Введите имя декоратора')def first(func):    ...  # тело декоратораdef second (func):    ...  # тело декоратораif name == 'first':    decorator = firstelif name == 'second':    decorator = secondelse:    decorator = lambda f: f   # оставляем функцию в покое@decorator def old():    pass

С точки зрения нашего умозрительного компилятора, значение функции old может поменяться на что угодно в процессе выполнения программы. В некоторых языках (например, C++) замыкания реализованы так, что даже при одинаковой сигнатуре они будут разного типа (из-за разницы в захваченном ими окружении), что не позволит провернуть такой трюк. В Python же каждое замыкание носит всё свое окружение с собой в питоне всё, включая функции, это объекты, так что замыкания "могут себе это позволить", тем более потребление памяти и быстродействие не являются приоритетом для этого языка.


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


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


Проблема 2. Python мало интересуют типы


def decorator(func):    def two_args(x, y):        ...    return two_args@decoratordef one_arg(x):    ...

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


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


Это также подводит нас к обратной проблеме какой тип должен быть у аргумента декоратора в наших примерах это аргумент с названием func? Чаще всего этот аргумент, представляющий декорируемую функцию, вызвается внутри замыкания значит нам хотелось бы знать хотя бы тип возвращаемого значения, не говоря уже об аргументах. Если мы его строго зафиксируем с помощью объявления func как функции типа A, мы ограничили область применения декоратора функциями типа A. Если же мы и это объявляем как void* func, и предлагаем программисту самому везде приводить нужные типы, то проще писать на питоне.


Один из вариантов выводить тип func автоматически, например с помощью шаблонов так и сделал Witcher136 в предыдущей статье. Однако это не решает проблему с разными сигнатурыми возвращаемых замыканий, а так же делает определения декораторов совершенно нечитаемыми для простых смертных (см. примеры на C++ в предыдущей статье).




Подведем итоги. Реализация декораторов в компилируемом языке создает следующие сложности:


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

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

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


Про этот компилятор я и расскажу далее.



Haskell и LLVM собственный компилятор


Для создания компилятора я выбрал Haskell, как язык для написания фронтенда, и LLVM в качестве компиляторного бэкенда. Для Haskell есть замечательная библиотека llvm-hs, предоставляющая все необходимые биндинги к LLVM. В поставку самого языка также входит библиотека Parsec, предназначенная для создания парсеров, путем комбинации различных парсер-функций этой библиотеки (я думаю, что на этом моменте читатель догадался, что Parsec сокращение от parser combinators).


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


Grit expression-oriented (фразированный) язык


Любое выражение в Grit, будь то сложение, блок кода или if-else, возвращает результат, который можно использовать внутри любого другого выражения например, присвоить переменной или передать функции как аргумент.


int main() = {    int i = 0;    i = i + if(someFunction() > 0) {        1;    }    else {        0;    };};

В данном примере, i будет равен 1, если функция someFunction вернет положительное значение, и нулю, если вернется 0 или меньше.


Нет ключевого слова return


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


Это обсуловлено тем, что язык фразированный тело функции это блок кода, который является валидным выражением Grit, а значит он уже должен возвращать значение я решил, что разумнее всего сделать этим значением результат последнего выражения в блоке. А ключевое слово returns, описанное в примерах ниже просто синтаксический сахар, эквивалентный выражению переменная; в конце блока.


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


int simple(int x) = {    /*       Данная фукция вернет результат сложения      своего аргумента x и переменной y    */    int y = someOtherFunction();    x + y;};/*  Для функций, состоящих из одного выражения, фигурные скобки можно опустить.  Эта функция вернет свой аргумент, увеличенный на единицу*/int incr(int x) = x + 1;int main() returns statusCode {    /*      В объявлении функции с помощью ключевого слова returns      можно указать название переменной, значение которой      будет возвращено после окончания работы функции.      Это переменная будет "объявлена" автоматически      с таким же типом, как у возвращаемого значения функции    */    int statusCode = 0;    int result = someFunction();    if (someFunction < 0) {        statusCode = 1;    };};

Auto компилятор Grit обладает базовой возможностью выведения типов


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


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


auto half (int x) = x / 2;   // для функции incr будет выведен тип float



Фразированность (expression-oriented), отсутствие return из произвольного места (тело функции тоже выражение) и базовое выведение типов это самые интересные для нас особенности Grit. Я выделил их потому, что они напрямую используются в реализации декораторов в этом языке.

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


А для нас теперь пришло время наконец ответить на главный вопрос этой серии статей как скомпилировать декоратор?



Так как же скомпилировать декоратор?


Как было указано ранее, при разработке декораторов для компилируемого языка программирования, нужно определиться с двумя вещами будут они применяться в runtime или compile-time, и как определять типы аргументов и результата итоговой функции.


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


Во-первых, декораторы в Grit применяются на этапе компиляции это позволяет, после перестройки синтаксического дерева (оно же AST, abstract syntax tree), вывести тип возвращаемого значения и скопировать объявления аргументов. Во-вторых, декораторы не являются функциями, а лишь похожими на них конструкциями.


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


@auto flatten = {    auto result = @target;    if (result < 0) {        0;    }    else {         result;    };};

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


@auto flatten объявление декоратора с именем flatten и типом @auto то есть тип будет выведен в зависимости от функции, к которой он применен (@ указатель но то, что это объявление декоратора, а не функции).


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


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


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

Декораторы в виде, реализованном в Grit, позволяют делать множество различных вещей большинство из них те же, что и в Python.


Например, можно ожидать захвата ресурса:


@auto lockFunction = {    mutex.lock();    @target};

Или вызывать функцию, только если установлен какой-либо флаг:


@auto optional = if (checkCondition()) {    @target;}else {    someDefaultValue;};

И так далее


Рассмотрим этот механизм на примере сгенерированного компилятором Grit синтаксического дерева для простой программы с декораторами:


@auto flatten = {    auto result = @target;    if (result < 0) {        0;    }    else {         result;    };};@flattenint incr(int x) = x+1;

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


Так выглядит "сырое" внутреннее представление, до какой-либо обработки вообще:


Decorator "flatten" auto {  BinaryOp = (Def auto "result") (DecoratorTarget)  If (BinaryOp < (Var "result") (Int 0)) {    Int 0  }  else {    Var "result"  }}Function "incr" int ; args [Def int "x"] ; modifiers [Decorator "flatten"] ; returns Nothing {  BinaryOp + (Var "x") (Int 1)}

Не вдаваясь в конкретные обозначения, сразу видно две вещи определение декоратора это самостоятельная конструкция с типом Decorator, и функция incr на данном этапе существует в своем исходном виде, и хранит информацию о том что к ней применен модификатор Decorator "flatten". В теле декоратора же мы видим метку DecoratorTarget сюда будет вставленно тело функции incr.


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


Посмотрим на аннотированное, полностью обработанное и готовое к кодогенерации предсавление той же программы:


Function (int -> int) incr ["x"] {  BinaryOp i= (Def int "result") (    Block int {      BinaryOp i+ (Var int "x") (int 1)    }  )  If int (BinaryOp b< (Var int "result") (int 0)) {    int 0  }  else {    Var int "result"  }}

Здесь мы можем заметить несколько важных вещей:


  • Определение декоратора было удалено после его применения на этапе обработки AST, он больше не нужен.
  • Тело функции incr изменилось теперь оно такое же, каким было тело декоратора flatten, но на месте DecoratorTarget теперь Block {...} выражение вида "блок кода", совпадающее с исходным телом функции. Внутри этого выражения есть обращения к аргументам функции, и оно возвращает то же значение, которое вернула бы исходная функция это значение присваивается новой переменной int "result", с которой декоратор и работает дальше. BinaryOp i= это операция присваивания int-а, но в исходном коде тип result был указан как auto значит тип возвращаемого значения и переменных в теле функции, работающих с ним, был выведен правильно.

Таким образом, в процессе разработки этого компилятора, удалось вывести общий алгоритм применения декораторов для компилируемого, статически типизированного языка. Он не обладает всеми возможностями декораторов в Python, но приближается к ним настолько, насколько это возможно в таком языке, как Grit.


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


@auto lockF(mutex M) {    M.lock();    @target;};@lockF(мьютексКоторыйФункцияДолжнаЗахватить)int someFunction(...)

Это вполне сработало бы при текущем подходе самый простой вариант это при применении декоратора убрать аргумент mutex M, и в теле конкретного инстанса декоратора заменить обращения к этому аргументу обращениями к "мьютексКоторыйФункцияДолжнаЗахватить", который должен существовать в области видимости декорируемой функции исходя из объявления (кстати, такой способ создавать декораторы с аргументами выглядит гораздо привлекательнее того, как они реализованы в Python замыкание внутри замыкания внутри функции).


Кроме того, я экспереминтировал с меткой @args, дающей внутри декоратора доступ к аргументам целевой функции, и так же разварачивающейся в "обычный код" на этапе обработки синтаксического дерева. Например, @args.length количество аргументов, @args.1 ссылка на первый аргумент и так далее. Что-то из этого работает, что-то пока не совсем но принципиально сделать это возможно.


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


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


P.S. Это был очень интересный и необычный для меня опыт надеюсь, что и вы смогли вынести для себя что-нибудь полезное из этого рассказа. Если нужна отдельная статья про написание компилятора на Haskell на основе LLVM пишите в комментарии.
На любые вопросы постараюсь ответить в комментариях, или в телеграмме @nu11_pointer_exception

Подробнее..

Подключение к session в Java и Python. HttpURLConnection и CookieManager (Java). Requests(Python)

29.06.2020 22:07:32 | Автор: admin
Допустим, что нам надо подключиться к серверу, авторизоваться и поддерживать сессию. В браузере это выглядит следующим образом:
  1. На адрес http://localhost:8080/login отправляется пустой GET запрос.
  2. Сервер присылает формочку для заполнения логина и пароля, а также присылает Cookie вида JSESSIONID=094BC0A489335CF8EE58C8E7846FE49B.
  3. Заполнив логин и пароль, на сервер отправляется POST запрос с полученной ранее Cookie, со строкой в выходном потоке username=Fox&password=123. В Headers дополнительно указывается Content-Type: application/x-www-form-urlencoded.
  4. В ответ сервер нам присылает новую cookie c новым JSESSIONID=. Сразу же происходит переадресация на http://localhost:8080/ путём GET запроса с новой Cookie.
  5. Далее можно спокойно использовать остальное API сервера, передавая последнее Cookie в каждом запросе.


Рассмотрим, как это можно реализовать на Java и на Python.



Содержание:




Реализация на Python. Requests.



При выборе библиотеки для работы с сетью на Python большинство сайтов будет вам рекомендовать библиотеку requests , которая полностью оправдывает свой лозунг:
HTTP for Humans

Вся задача решается следующим скриптом:
import requestssession = requests.session()  #создаём сессиюurl = "http://localhost:8080/login"session.get(url)   #получаем cookiedata = {"username": "Fox", "password": "123"} response = session.post(url, data=data) #логинимся


Заметим, что махинации с Cookie и переадресацией происходят под капотом, прямо как в браузере. Так же можно отметить, что если завести ещё одну переменную session2, то можно держать активными сразу два подключения.

Реализация на Java, HttpURLConnection и CookieManager.



Поиски библиотеки для работы с сетью на Java приводят сразу к нескольким библиотекам. Например, java.net, Apache HttpClient и OkHttp3.

Я остановился на HttpURLConnection (java.net). Плюсами данной библиотеки является то, что это библиотека "из-под коробки", а так же, если надо написать приложение под android, на официальном сайте есть документация. Минусом является очень большой объём кода. (После Python это просто боль).

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

CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);CookieHandler.setDefault(cookieManager);


Что нужно отметить, используя такой подход:
  • CookiePolicy.ACCEPT_ALL указывает, что надо работать со всеми cookie.
  • Переменная cookieManager далее нигде не будет использоваться. Она контролирует все подключения, и, если необходимо поддерживать несколько активных сессий, необходимо будет в этой одной переменной руками менять Cookie


Учтя это, можно записать и в одну строчку:
 CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));


Пункт 1 и 2. Выполним GET запрос для получения первой Cookie
URL url = new URL("http://localhost:8080/login");HttpURLConnection con = (HttpURLConnection) url.openConnection();con.setRequestMethod("GET");BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));String inputLine;final StringBuilder content = new StringBuilder();while ((inputLine = in.readLine()) != null) {    content.append(inputLine);}


После этого наш cookieManager будет содержать Cookie с сервера и автоматически подставит её в следующий запрос.

Веселье начинается с POST запросом.
url = new URL("http://localhost:8080/login");con = (HttpURLConnection) url.openConnection();con.setRequestMethod("POST");


Нужно записать в Headers Content-Type: application/x-www-form-urlencoded.
Почему метод называется setRequestProperty, а не setHeaders (или addHeaders) при наличии метода getHeaderField, остаётся загадкой.
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");


Далее идёт код, который непонятно по каким причинам не засунут под капот библиотеки.
con.setDoOutput(true);

Нужна эта строчка кода для открытия исходящего потока. Забавно, что без этой строки мы получим следующее сообщение:
Exception in thread main java.net.ProtocolException: cannot write to a URLConnection if doOutput=false call setDoOutput(true)

Открываем исходящий поток и записываем туда логин и пароль:
final DataOutputStream out = new DataOutputStream(con.getOutputStream());out.writeBytes("username=Fox&password=123");out.flush();out.close();


Остаётся считать ответ с уже перенаправленного запроса.

Реализация на Java, HttpURLConnection без CookieManager.



Можно реализовать и без CookieManager и самому контролировать перемещение cookie.
Пункт 1 и 2. Вынимаем cookie.
URL url = new URL("http://localhost:8080/login");HttpURLConnection con = (HttpURLConnection) url.openConnection();con.setRequestMethod("GET");BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));String inputLine;final StringBuilder content = new StringBuilder();while ((inputLine = in.readLine()) != null) {    content.append(inputLine);String cookie = con.getHeaderField("Set-Cookie").split(";")[0];}


Далее отправляем POST запрос, только на этот раз вставив cookie и отключив автоматическое перенаправление, т.к. перед ним надо успеть вытащить новое cookie:

// создаём запросurl = new URL("http://localhost:8080/login");con = (HttpURLConnection) url.openConnection();con.setRequestMethod("POST");//указываем headers и cookiecon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");con.setRequestProperty("Cookie", cookie);//отключаем переадресациюcon.setInstanceFollowRedirects(false);//отправляем логин и парольcon.setDoOutput(true);final DataOutputStream out = new DataOutputStream(con.getOutputStream());out.writeBytes("username=Fox&password=123");out.flush();out.close();//считываем и получаем второе cookieBufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));String inputLine;final StringBuilder content = new StringBuilder();while ((inputLine = in.readLine()) != null) {    content.append(inputLine);String cookie2 = con.getHeaderField("Set-Cookie").split(";")[0];


Далее во все запросы просто добавляем следующую строку:
con.setRequestProperty("Cookie", cookie2);


Надеюсь было полезно. В комментариях приветствуются варианты попроще.
Подробнее..

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

30.06.2020 18:10:31 | Автор: admin

Привет, Хабр! Как известно, топливом для машинного обучения являются наборы данных. В качестве источников для получения датасетов, которыми люди обычно пользуются и которые у всех на слуху, являются такие сайты как Kaggle, ImageNet, Google Dataset Search и Visual Genom, но довольно редко встречаю людей, которые для поиска данных используют такие сайты как Bing Image Search и Instagram. Поэтому в этой статье я покажу как легко получить данные с этих источников, написав две небольшие программы на Python.


Bing Image Search


Первое, что нужно сделать это перейти по ссылке нажать кнопку Get API Key и зарегистрироваться с помощью любой из предложенных социальных сетей(Microsoft, Facebook, LinkedIn или GitHub). После того, как процесс регистрации завершится вас перенаправят на страницу Your APIs, которая должна выглядеть подобным образом( то, что замазано, это и есть ваши API ключи) :


1


Переходим к написанию кода. Импортируем необходимые библиотеки :


from requests import exceptionsimport requestsimport cv2import os

Дальше нужно указать некоторые параметры: API ключ( нужно выбрать из двух предложенных ключей один), указать условия поиска, максимальное количество изображений на запрос, а также установим конечный URL :


subscription_key = "YOUR_API_KEY"search_terms = ['girl', 'man']number_of_images_per_request = 100search_url = "https://api.cognitive.microsoft.com/bing/v7.0/images/search"

Теперь напишем три небольшие функции, которые:
1) Создают для каждого условия поиска отдельную папку :


def create_folder(name_folder):    path = os.path.join(name_folder)    if not os.path.exists(path):        os.makedirs(path)        print('------------------------------')        print("create folder with path {0}".format(path))        print('------------------------------')    else:        print('------------------------------')        print("folder exists {0}".format(path))        print('------------------------------')        return path

2) Возвращает содержимое ответа сервера в JSON :


def get_results():    search = requests.get(search_url, headers=headers,                           params=params)    search.raise_for_status()    return search.json()

3) Записывает изображения на диск :


def write_image(photo):    r = requests.get(v["contentUrl"], timeout=25)    f = open(photo, "wb")    f.write(r.content)    f.close()

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


for category in search_terms:    folder = create_folder(category)    headers = {"Ocp-Apim-Subscription-Key": subscription_key}    params = {"q": category, "offset": 0,              "count": number_of_images_per_request}    results = get_results()    total = 0    for offset in range(0, results["totalEstimatedMatches"],                        number_of_images_per_request):        params["offset"] = offset        results = get_results()        for v in results["value"]:            try:                ext = v["contentUrl"][v["contentUrl"].                                          rfind("."):]                photo = os.path.join(category, "{}{}".                                     format('{}'.format(category)                                     + str(total).zfill(6), ext))                write_image(photo)                print("saving: {}".format(photo))                image = cv2.imread(photo)                if image is None:                    print("deleting: {}".format(photo))                    os.remove(photo)                    continue                total += 1            except Exception as e:                if type(e) in EXCEPTIONS:                    continue

Instagram


Импортируем библиотеки:


from selenium import webdriverfrom time import sleepimport pyautoguifrom bs4 import BeautifulSoupimport requestsimport shutil

Как вы можете видеть, я использую библиотеку selenium, поэтому нужно скачать geckodriver. В инстаграме поиск изображений будем осуществлять по хэштегу, допустим, возьмём хэштег #bird. По данному хэштегу можно найти около 26млн публикаций. Копируем ссылку, которая сформировалась в результате запроса и путь к geckodriver, и вставляем это соответственно в две строчки, которые представлены ниже:


browser=webdriver.Firefox(executable_path='/path/to/geckodriver')browser.get('https://www.instagram.com/explore/tags/bird/')

Дальше напишем 6 функций, которые:
1) Входит в инстаграм аккаунт. В строчках login.send_keys(' ') и password.send_keys(' ') необходимо вставить свой логин и пароль соответственно:


def enter_in_account():    button_enter = browser.find_element_by_xpath("//*[@class='sqdOP  L3NKy   y3zKF     ']")    button_enter.click()    sleep(2)    login = browser.find_element_by_xpath("//*[@class='_2hvTZ pexuQ zyHYP']")    login.send_keys('')    sleep(1)    password = browser.find_element_by_xpath("//*[@class='_2hvTZ pexuQ zyHYP']")    password.send_keys('')    enter = browser.find_element_by_xpath(        "//*[@class='                    Igw0E     IwRSH      eGOV_         _4EzTm                                                                                                              ']")    enter.click()    sleep(4)    not_now_button = browser.find_element_by_xpath("//*[@class='sqdOP yWX7d    y3zKF     ']")    not_now_button.click()    sleep(2)

2) Находит первый пост и нажимаем на него:


def find_first_post():    sleep(3)    pyautogui.moveTo(450, 800, duration=0.5)    pyautogui.click()

Тут необходимо отметить, что, возможно, из-за того, что у всех разрешение экрана монитора разное, то первый пост может находиться по другим координатам, поэтому в методе moveTo() нужно будет поменять первых два параметра.


3) Получаем ссылку на публикацию и нажимаем на кнопку далее:


def get_url():    sleep(0.5)    pyautogui.moveTo(1740, 640, duration=0.5)    pyautogui.click()    return browser.current_url

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


4)Получаем html-код исходной страницы:


def get_html(url):    r = requests.get(url)    return r.text

5) Получаем URL изображения:


def get_src(html):    soup = BeautifulSoup(html, 'lxml')    src = soup.find('meta', property="og:image")    return src['content']

6) Скачиваем и сохраняем текущее изображение. В переменной filename нужно указать по какому пути будет сохраняться ваше изображение:


def download_image(image_name, image_url):    filename = 'bird/bird{}.jpg'.format(image_name)    r = requests.get(image_url, stream=True)    if r.status_code == 200:        r.raw.decode_content = True        with open(filename, 'wb') as f:            shutil.copyfileobj(r.raw, f)        print('Image sucessfully Downloaded')    else:        print('Image Couldn\'t be retreived')

Заключение


В заключении хотелось бы сказать про недостаток источников и реализации. Что касается самих ресурсов, то изображений с них можно собрать большое количество, но эти данные придётся сортировать, так как изображения не всегда подходят под те критерии поиска, которые вы задали. Что касается реализации, то в получении данных с инстаграма была задействована библиотека pyautogui, которая эмулирует действия пользователя, в результате чего, при выполнении программы вы не сможете задействовать ваш компьютер для решения других задач. Если будут предложения, как написать код лучше, прошу писать в комментарии.
Что касается написания кода, то всё было сделано на Ubuntu 18.04. Исходный код выложил на GitHub .

Подробнее..

FAISS Быстрый поиск лиц и клонов на многомиллионных данных

02.07.2020 12:23:15 | Автор: admin


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

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

Добавили Telegram-бота для удобства, и всё было отлично. С точки зрения алгоритмов распознавания лиц всё работало на ура, но конференция завершилась, а расставаться с опробованными технологиями не хотелось. От нескольких тысяч лиц хотелось перейти к сотням миллионов, но конкретной бизнес-задачи у нас не было. Через некоторое время у наших коллег появилась задача, которая требовала работы с такими большими объемами данных.

Вопрос был в том, чтобы написать умную систему мониторинга ботов внутри сети Instagram. Тут наша мысль породила простой и сложный подходы:

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

Сложный путь: Так как современные боты стали намного умнее, и теперь они выкладывают посты, спят, да еще и контент пишут, встаёт вопрос: как таких поймать? Во-первых, внимательно следить за их друзьями, так как часто они тоже бывают ботами, и отслеживать дублирующие фотки. Во-вторых, редко какой бот умеет генерировать свои фотки (хотя и такое возможно), а значит, дубли фото людей в разных аккаунтах в Инстаграме являются хорошим триггером для поиска сети ботов.

Что дальше?


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

  1. Скорость и точность поиска
  2. Размер занимаемого данными места на диске
  3. Размер используемой RAM памяти.

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

Существуют известные и зарекомендовавшие себя технологии, такие как Annoy, FAISS, HNSW. Быстрый алгоритм поиска соседей HNSW , доступный в библиотеках nmslib и hnswlib, показывает state-of-the-art результаты на CPU, что видно по тем же бенчмаркам. Но его мы отсекли сразу, так как нас не устраивает количество используемой памяти при работе с действительно большими объемами данных. Мы стали выбирать между Annoy и FAISS и в итоге выбрали FAISS из-за удобства, меньшего использования памяти, потенциальной возможности использования на GPU и бенчмарков по результативности (посмотреть можно, например, здесь). К слову, в FAISS алгоритм HNSW реализован как опция.

Что такое FAISS?


Facebook AI Research Similarity Search разработка команды Facebook AI Research для быстрого поиска ближайших соседей и кластеризации в векторном пространстве. Высокая скорость поиска позволяет работать с очень большими данными до нескольких миллиардов векторов.

Основное преимущество FAISS state-of-the-art результаты на GPU, при этом его реализация на CPU незначительно проигрывает hnsw (nmslib). Нам хотелось иметь возможность вести поиск как на CPU, так и на GPU. Кроме того, FAISS оптимизирован в части использования памяти и поиска на больших батчах.

Source

FAISS позволяет быстро осуществлять операцию поиска k ближайших векторов для данного вектора x. Но как же этот поиск устроен под капотом?

Индексы


Главное понятие в FAISS это index, и, по сути, это просто набор параметров и векторов. Наборы параметров бывают совершенно разные и зависят от нужд пользователя. Векторы могут оставаться неизменными, а могут перестраиваться. Некоторые индексы доступны для работы сразу после добавления в них векторов, а некоторые требуют предварительного обучения. Имена векторов хранятся в индексе: либо в нумерации от 0 до n, либо в виде числа, влезающего в тип Int64.

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

Пример:

import numpy as npdim = 512  # рассмотрим произвольные векторы размерности 512nb = 10000  # количество векторов в индексеnq = 5 # количество векторов в выборке для поискаnp.random.seed(228)vectors = np.random.random((nb, dim)).astype('float32')query = np.random.random((nq, dim)).astype('float32')

Создаем Flat индекс и добавляем векторы без обучения:

import faissindex = faiss.IndexFlatL2(dim)print(index.ntotal)  # пока индекс пустойindex.add(vectors)print(index.ntotal)  # теперь в нем 10 000 векторов

Теперь найдем 7 ближайших соседей для первых пяти векторов из vectors:

topn = 7D, I = index.search(vectors[:5], topn)  # Возвращает результат: Distances, Indicesprint(I)print(D)

Output
[[0 5662 6778 7738 6931 7809 7184] [1 5831 8039 2150 5426 4569 6325] [2 7348 2476 2048 5091 6322 3617] [3  791 3173 6323 8374 7273 5842] [4 6236 7548  746 6144 3906 5455]][[ 0.  71.53578  72.18823  72.74326  73.2243   73.333244 73.73317 ] [ 0.  67.604805 68.494774 68.84221  71.839905 72.084335 72.10817 ] [ 0.  66.717865 67.72709  69.63666  70.35903  70.933304 71.03237 ] [ 0.  68.26415  68.320595 68.82381  68.86328  69.12087  69.55179 ] [ 0.  72.03398  72.32417  73.00308  73.13054  73.76181  73.81281 ]]


Видим, что самые близкие соседи с расстоянием 0 это сами векторы, остальные отранжированы по увеличению расстояния. Проведем поиск по нашим векторам из query:

D, I = index.search(query, topn) print(I)print(D)

Output
[[2467 2479 7260 6199 8640 2676 1767] [2623 8313 1500 7840 5031   52 6455] [1756 2405 1251 4136  812 6536  307] [3409 2930  539 8354 9573 6901 5692] [8032 4271 7761 6305 8929 4137 6480]][[73.14189  73.654526 73.89804  74.05615  74.11058  74.13567  74.443436] [71.830215 72.33813  72.973885 73.08897  73.27939  73.56996  73.72397 ] [67.49588  69.95635  70.88528  71.08078  71.715965 71.76285  72.1091  ] [69.11357  69.30089  70.83269  71.05977  71.3577   71.62457  71.72549 ] [69.46417  69.66577  70.47629  70.54611  70.57645  70.95326  71.032005]]


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

Индекс можно сохранить на диск и затем загрузить с диска:

faiss.write_index(index, "flat.index")index = faiss.read_index("flat.index")

Казалось бы, всё элементарно! Несколько строчек кода и мы уже получили структуру для поиска по векторам высокой размерности. Но такой индекс всего с десятком миллионов векторов размерности 512 будет весить около 20Гб и занимать при использовании столько же RAM.

В проекте для конференции мы использовали именно такой базовый подход с flat index, всё было замечательно благодаря относительно маленькому объему данных, однако сейчас речь идет о десятках и сотнях миллионов векторов высокой размерности!

Ускоряем поиск с помощью Inverted lists



Source

Основная и наикрутейшая особенность FAISS IVF index, или Inverted File index. Идея Inverted files лаконична, и красиво объясняется на пальцах:

Давайте представим себе гигантскую армию, состоящую из самых разношерстных воинов, численностью, скажем, в 1 000 000 человек. Командовать всей армией сразу будет невозможно. Как и принято в военном деле, нужно разделить нашу армию на подразделения. Давайте разделим на $\sqrt{1 000 000} = 1000$ примерно равных частей, выбрав на роли командиров по представителю из каждого подразделения. И постараемся отправить максимально похожих по характеру, происхождению, физическим данным и т.д. воинов в одно подразделение, а командира выберем таким, чтобы он максимально точно представлял свое подразделение был кем-то средним. В итоге наша задача свелась от командования миллионом воинов к командованию 1000-ю подразделениями через их командиров, и мы имеем отличное представление о составе нашей армии, так как знаем, что из себя представляют командиры.

В этом и состоит идея IVF индекса: сгруппируем большой набор векторов по частям с помощью алгоритма k-means, каждой части поставив в соответствие центроиду, вектор, являющийся выбранным центром для данного кластера. Поиск будем осуществлять через минимальное расстояние до центроид, и только потом искать минимальные расстояния среди векторов в том кластере, что соответствует данной центроиде. Взяв k равным $\sqrt{n}$, где $n$ количество векторов в индексе, мы получим оптимальный поиск на двух уровнях сначала среди $\sqrt{n}$ центроид, затем среди $\sqrt{n}$ векторов в каждом кластере. Поиск по сравнению с полным перебором ускоряется в разы, что решает одну из наших проблем при работе с множеством миллионов векторов.


Пространство векторов разбивается методом k-means на k кластеров. Каждому кластеру в соответствие ставится центроида

Пример кода:

dim = 512k = 1000  # количество командировquantiser = faiss.IndexFlatL2(dim) index = faiss.IndexIVFFlat(quantiser, dim, k)vectors = np.random.random((1000000, dim)).astype('float32')  # 1 000 000 воинов

А можно это записать куда более элегантно, воспользовавшись удобной штукой FAISS для построения индекса:

index = faiss.index_factory(dim, IVF1000,Flat)Запускаем обучение:print(index.is_trained)   # False.index.train(vectors)  # Train на нашем наборе векторов # Обучение завершено, но векторов в индексе пока нет, так что добавляем их в индекс:print(index.is_trained)  # Trueprint(index.ntotal)   # 0index.add(vectors)print(index.ntotal)   # 1000000

Рассмотрев такой тип индекса после Flat, мы решили одну из наших потенциальных проблем скорость поиска, которая становится в разы меньше по сравнению с полным перебором.

D, I = index.search(query, topn) print(I)print(D)

Output
[[19898 533106 641838 681301 602835 439794 331951] [654803 472683 538572 126357 288292 835974 308846] [588393 979151 708282 829598  50812 721369 944102] [796762 121483 432837 679921 691038 169755 701540] [980500 435793 906182 893115 439104 298988 676091]][[69.88127  71.64444  72.4655   72.54283  72.66737  72.71834  72.83057] [72.17552  72.28832  72.315926 72.43405  72.53974  72.664055 72.69495] [67.262115 69.46998  70.08826  70.41119  70.57278  70.62283  71.42067] [71.293045 71.6647   71.686615 71.915405 72.219505 72.28943  72.29849] [73.27072  73.96091  74.034706 74.062515 74.24464  74.51218  74.609695]]


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

print(index.nprobe)  # 1  заходим только в один кластер и ведем поиск только в нёмindex.nprobe = 16  # Проходим по топ-16 центроид для поиска top-n ближайших соседейD, I = index.search(query, topn) print(I)print(D)

Output
[[ 28707 811973  12310 391153 574413  19898 552495] [540075 339549 884060 117178 878374 605968 201291] [588393 235712 123724 104489 277182 656948 662450] [983754 604268  54894 625338 199198  70698  73403] [862753 523459 766586 379550 324411 654206 871241]][[67.365585 67.38003  68.17187  68.4904   68.63618  69.88127  70.3822] [65.63759  67.67015  68.18429  68.45782  68.68973  68.82755  69.05] [67.262115 68.735535 68.83473  68.88733  68.95465  69.11365  69.33717] [67.32007  68.544685 68.60204  68.60275  68.68633  68.933334 69.17106] [70.573326 70.730286 70.78615  70.85502  71.467674 71.59512  71.909836]]


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

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

Ведем поиск по диску On Disk Inverted Lists


Отлично, первую проблему мы решили, теперь на десятках миллионов векторов мы получаем приемлемую скорость поиска! Но всё это бесполезно до тех пор, пока наш огромный индекс не умещается в RAM.

Конкретно для нашей задачи основное преимущество FAISS в возможности хранить Inverted Lists IVF индекса на диске, загружая в RAM только метаданные.

Как мы создаем такой индекс: обучаем indexIVF с нужными параметрами на максимально возможном объеме данных, который влезает в память, затем в обученный индекс по частям добавляем векторы (побывавшие в обучении и не только) и записываем на диск индекс для каждой из частей.

index = faiss.index_factory(512, ,IVF65536, Flat, faiss.METRIC_L2)

Обучение индекса на GPU осуществляем таким образом:

res = faiss.StandardGpuResources()index_ivf = faiss.extract_index_ivf(index)index_flat = faiss.IndexFlatL2(512)clustering_index = faiss.index_cpu_to_gpu(res, 0, index_flat)  #  0  номер GPUindex_ivf.clustering_index = clustering_index

faiss.index_cpu_to_gpu(res, 0, index_flat) можно заменить на faiss.index_cpu_to_all_gpus(index_flat), чтобы использовать все GPU вместе.

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

train_vectors = ...  # предварительно сформированный датасет для обученияindex.train(train_vectors)# Сохраняем пустой обученный индекс, содержащий только параметры:faiss.write_index(index, "trained_block.index") # Поочередно создаем новые индексы на основе обученного# Блоками добавляем в них части датасета:for bno in range(first_block, last_block+ 1):    block_vectors = vectors_parts[bno]    block_vectors_ids = vectors_parts_ids[bno]  # id векторов, если необходимо    index = faiss.read_index("trained_block.index")    index.add_with_ids(block_vectors, block_vectors_ids)    faiss.write_index(index, "block_{}.index".format(bno))

После этого объединяем все Inverted Lists воедино. Это возможно, так как каждый из блоков, по сути, является одним и тем же обученным индексом, просто с разными векторами внутри.

ivfs = []for bno in range(first_block, last_block+ 1):    index = faiss.read_index("block_{}.index".format(bno), faiss.IO_FLAG_MMAP)    ivfs.append(index.invlists)    # считать index и его inv_lists независимыми    # чтобы не потерять данные во время следующей итерации:    index.own_invlists = False# создаем финальный индекс:index = faiss.read_index("trained_block.index")# готовим финальные invlists# все invlists из блоков будут объединены в файл merged_index.ivfdatainvlists = faiss.OnDiskInvertedLists(index.nlist, index.code_size, "merged_index.ivfdata")ivf_vector = faiss.InvertedListsPtrVector() for ivf in ivfs:     ivf_vector.push_back(ivf)ntotal = invlists.merge_from(ivf_vector.data(), ivf_vector.size())index.ntotal = ntotal  # заменяем листы индекса на объединенныеindex.replace_invlists(invlists)  faiss.write_index(index, data_path + "populated.index")  # сохраняем всё на диск

Итог: теперь наш индекс это файлы populated.index и merged_blocks.ivfdata.

В populated.index записан первоначальный полный путь к файлу с Inverted Lists, поэтому, если путь к файлу ivfdata по какой-то причине изменится, при чтении индекса потребуется использовать флаг faiss.IO_FLAG_ONDISK_SAME_DIR, который позволяет искать ivfdata файл в той же директории, что и populated.index:

index = faiss.read_index('populated.index', faiss.IO_FLAG_ONDISK_SAME_DIR)

За основу был взят demo пример из Github проекта FAISS.

Мини-гайд по выбору индекса можно посмотреть в FAISS Wiki. Например, мы смогли поместить в RAM тренировочный датасет из 12 миллионов векторов, поэтому выбрали IVFFlat индекс на 262144 центроидах, чтобы затем масштабироваться до сотен миллионов. Также в гайде предлагается использовать индекс IVF262144_HNSW32, в котором принадлежность вектора к кластеру определяется по алгоритму HNSW с 32 ближайшими соседями (иными словами, используется quantizer IndexHNSWFlat), но, как нам показалось при дальнейших тестах, поиск по такому индексу менее точен. Кроме того, следует учитывать, что такой quantizer исключает возможность использования на GPU.

Спойлер:
Даже при использовании on disk inverted lists FAISS по возможности загружает данные в оперативную память. Так как RAM памяти на этапах тестов нам хватало, пусть и с трудом, а для масштабных тестов было необходимо иметь значительно больший запас данных здесь и сейчас, тесты на объемах свыше объема RAM не проводились. Но FAISS wiki и обсуждения данного подхода на Github говорят, что всё должно работать корректно.


Значительно уменьшаем использование дискового пространства с Product Quantization


Благодаря методу поиска с диска удалось снять нагрузку с RAM, но индекс с миллионом векторов всё равно занимал около 2 ГБ дискового пространства, а мы рассуждаем о возможности работы с миллиардами векторов, что потребовало бы более двух ТБ! Безусловно, объем не такой большой, если задаться целью и выделить дополнительное дисковое пространство, но нас это немного напрягало.

И тут приходит на помощь кодирование векторов, а именно Scalar Quantization (SQ) и Product Quantization (PQ). SQ кодирование каждой компоненты вектора n битами (обычно 8, 6 или 4 бит). Мы рассмотрим вариант PQ, ведь идея кодирования одной компоненты типа float32 восемью битами выглядит уж слишком удручающе с точки зрения потерь в точности. Хотя в некоторых случаях сжатие SQfp16 до типа float16 будет почти без потерь в точности.

Суть Product Quantization состоит в следующем: векторы размерности 512 разбиваются на n частей, каждая из которых кластеризуется по 256 возможным кластерам (1 байт), т.е. мы представляем вектор с помощью n байт, где n обычно не превосходит 64 в реализации FAISS. Но применяется такая квантизация не к самим векторам из датасета, а к разностям этих векторов и соответствующих им центроид, полученным на этапе генерации Inverted Lists! Выходит, что Inverted Lists будут представлять из себя кодированные наборы расстояний между векторами и их центроидами.

index = faiss.index_factory(dim, "IVF262144,PQ64", faiss.METRIC_L2)

Выходит, что теперь нам не обязательно хранить все векторы достаточно выделять n байт на вектор и 2048 байт на каждый вектор центроиды. В нашем случаем мы взяли $n = 64$, то есть $\frac{512}{64} = 8$ длина одного субвектора, который определяется в один из 256 кластеров.



При поиске по вектору x сначала обычным Flat квантайзером будут определяться ближайшие центроиды, а затем x так же разделяется на суб-векторы, каждый из которых кодируется номером одной из 256 соответствующей центроиды. И расстояние до вектора определяется как сумма из 64 расстояний между суб-векторами.

Что в итоге?


Мы остановили свои эксперименты на индексе IVF262144, PQ64, так как он полностью удовлетворил все наши нужды по скорости и точности поиска, а также обеспечил разумное использование дискового пространства при дальнейшем масштабировании индекса. Если говорить конкретнее, на данный момент при 315 миллионах векторов индекс занимает 22 Гб дискового пространства и около 3 Гб RAM при использовании.

Еще одна интересная деталь, которую мы не упоминали ранее, метрика, используемая индексом. По умолчанию расстояния между любыми двумя векторами считаются в евклидовой метрике L2, или более понятным языком, расстояния считаются как квадратный корень из суммы квадратов покоординатных разностей. Но задать метрику можно и другую, в частности, мы тестировали метрику METRIC_INNER_PRODUCT, или метрику косинусных расстояний между векторами. Косинусная она потому, что косинус угла между двумя векторами в Евклидовой системе координат выражается как отношение скалярного (покоординатного) произведения векторов к произведению их длин, а если все векторы в нашем пространстве имеют длину 1, то косинус угла будет в точности равен покоординатному произведению. В таком случае чем ближе расположены векторы в пространстве, тем ближе к единице будет их скалярное произведение.

Метрика L2 имеет непосредственный математический переход к метрике скалярных произведений. Однако при экспериментальном сравнении двух метрик сложилось впечатление, что метрика скалярных произведений помогает нам анализировать коэффициенты похожести изображений более удачным образом. К тому же эмбеддинги наших фотографий были получены с помощью InsightFace, в котором реализована архитектура ArcFace, использующая косинусные расстояния. Также есть и другие метрики в индексах FAISS, о которых можно почитать здесь.

пару слов о GPU
Обучение и использование индексов FAISS на GPU весьма ограничено в выборе параметров индекса, а при работе с гигантскими объемами данных высокой размерности использование если и будет возможным, то вызовет трудности, несопоставимые с полученным результатом. К тому же на GPU реализована только метрика L2.

Однако, стоит заметить, что для использования индекса с PQ квантилизацией на GPU требуется ограничить размер кода 56-ю байтами, либо в случае большего размера сменить float32 на float16, связано это с ограничениями на используемую память.

В итоге о FAISS на GPU мы думать перестали, потому что CPU реализация нас полностью устроила по скорости, и хотелось сохранить работу с метрикой скалярных произведений, что на GPU недоступно. Чтобы регулировать нагрузку на процессоры при работе с индексами, можно задать максимальное число используемых процессоров:

faiss.omp_set_num_threads(N)


Заключение и любопытные примеры


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

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

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


Наш коллега попал на фотографию посетительницы Comic-Con, оказавшись на заднем фоне в толпе. Источник


Пикник в многочисленной компании друзей, фотография из аккаунта подруги. Источник


Просто проходили мимо. Неизвестный фотограф запечатлел ребят для своего тематического профиля. Они не знали, куда попала их фотография, а спустя 5 лет и вовсе забыли, как их фотографировали. Источник


В этом случае и фотограф неизвестен, и сфотографировали тайно!
Сразу вспомнилась подозрительная девушка с зеркальным фотоаппаратом, сидевшая в тот момент напротив:) Источник


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

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


Некоторые из клонов автора.
Источники фото: 1, 2,3


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


Source

Благодарим за внимание и надеемся что этот материал будет полезен читателям Хабра!

Статья написана при поддержке моих коллег Артёма Королёва (korolevart), Тимура Кадырова и Арины Решетниковой.

R&D Dentsu Aegis Network Russia.
Подробнее..

Ищем фильмы, книги и подкасты с помощью Python

02.07.2020 12:23:15 | Автор: admin

Пакет podsearch для поиска подкастов


Apple неособо афиширует, что у iTunes Store идругих каталогов есть кривенькое, нопростое поисковое API поэтому ярешил онём написать. Изэтой заметки выузнаете, что API умеет икак его использовать.


Поиск в каталогах Apple


API ищет посодержимому iTunes Store, iBooks Store, Apple Podcasts и App Store. Соответственно, можно найти песни, фильмы, книги, подкасты и приложения.


API работает подавно забытому принципу JSONP, тоесть возвращает Content-Type: text/javascript вместо нормального application/json.


GET /search?media=podcast&term=python HTTP/1.1Host: itunes.apple.comAccept: application/jsonHTTP/2 200content-type: text/javascript; charset=utf-8{...}

Конечно, эту особенность можно просто игнорировать:


import requestsdef search(term, media):    url = "https://itunes.apple.com/search"    payload = {"term": term, "media": media}    response = requests.get(url, params=payload)    response.raise_for_status()    results = response.json().get("results", [])    return results

>>> results = search("python", media="podcast")>>> results[0]["collectionName"]'Talk Python To Me'

Параметры запроса


Поиск можно настроить:


  • term поисковый запрос, единственный обязательный параметр;
  • media тип произведения (movie, podcast, music, audiobook, software, ebook, all), поумолчанию all;
  • country страна произведения, двухсимвольный ISO-код (us, ru, ...), по умолчанию us;
  • limit максимальное количество результатов, по умолчанию 50.

Пример функции, которая поддерживает все параметры:


import requestsdef search(term, media="all", country="us", limit=10):    url = "https://itunes.apple.com/search"    payload = {"term": term, "media": media, "country": country, "limit": limit}    response = requests.get(url, params=payload)    response.raise_for_status()    results = response.json().get("results", [])    return results

Ответ


Общая структура ответа:


{    "resultCount": 10,    "results": [        {            "wrapperType": "track",            "kind": "song",            "artistId": 1495668306,            "collectionId": 527039271,            "trackId": 527039276,            "artistName": "Dodge & Fuski",            "collectionName": "Never Say Die (Deluxe Edition)",            "trackName":"Python",            ...        },         {            "wrapperType": "track",            "kind": "podcast",            "collectionId": 979020229,            "trackId": 979020229,            "artistName": "Michael Kennedy (@mkennedy)",            "collectionName": "Talk Python To Me"            ...        },        ...    ]}

Конкретный набор полей зависит оттипа произведения (kind) иполноценно нигде неописан, насколько мне известно. Часто заполнены:


  • artistId идентификатор автора произведения;
  • artistName автор;
  • artistViewUrl ссылка на автора на сайте Apple;
  • collectionId идентификатор альбома;
  • collectionName название альбома;
  • collectionViewUrl ссылка на альбом на сайте Apple;
  • trackId идентификатор композиции;
  • trackName название композиции;
  • artworkUrl100 ссылка на превьюшку произведения 100x100 пикселей;
  • country страна произведения;
  • primaryGenreName жанр произведения.

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


Запрос поидентификатору


Если известен идентификатор объекта (artistId, collectionId, trackId), можно вызвать метод lookup понему:


import requestsdef lookup(object_id):    url = "https://itunes.apple.com/lookup"    payload = {"id": object_id}    response = requests.get(url, params=payload)    response.raise_for_status()    results = response.json().get("results", [])    return results

>>> results = lookup(979020229)>>> results[0]["collectionName"]'Talk Python To Me'

Нюансы


Про необычный Content-Type ответа я уже упомянул. Кроме того:


  • Страна произведения (country) есть как впараметрах запроса, так и вответе. Но впараметрах это двухсимвольный ISO-код (ru), а вответетрёхсимвольный (RUS).
  • Схемы ответа несуществует.
  • Apple ограничивает частоту вызова API 20 запросами вминуту.

Описание API на сайте Apple
Пакет для поиска по подкастам


Если хотите больше интересных штук наPython подписывайтесь на канал @ohmypy

Подробнее..

MLOps Cook book, chapter 1

03.07.2020 10:08:54 | Автор: admin


Всем привет! Я CV-разработчик в КРОК. Уже 3 года мы реализуем проекты в области CV. За это время чего мы только не делали, например: мониторили водителей, чтобы во время движения они не пили, не курили, по телефону не разговаривали, смотрели на дорогу, а не сны или в облака; фиксировали любителей ездить по выделенным полосам и занимать несколько мест на парковке; следили за тем, чтобы работники носили каски, перчатки и т.п.; идентифицировали сотрудника, который хочет пройти на объект; подсчитывали всё, что только можно.


Я все это к чему?


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


Моделируем ситуацию


Представим, что мы устроились в молодую компанию N, деятельность которой связана с ML. Работаем мы над ML (DL, CV) проектом, потом по каким-либо причинам переключаемся на другую работу, в общем делаем перерыв, и возвращаемся к своей или чужой нейроночке.


  1. Наступает момент истины, нужно как-то вспомнить на чем ты остановился, какие гиперпараметры пробовал и, самое главное, к каким результатам они привели. Может быть множество вариантов, кто как хранил информацию по всем запускам: в голове, конфигах, блокноте, в рабочей среде в облаке. Мне довелось видеть вариант, когда гиперпараметры хранились в виде закомментированных строк в коде, в общем полет фантазии. А теперь представьте, что вы вернулись не к своему проекту, а к проекту человека, который покинул компанию и в наследство вам достался код и модель под названием model_1.pb. Для полноты картины и передачи всей боли, представим, что вы еще и начинающий специалист.
  2. Идем дальше. Для запуска кода нам и всем кто будет с ним работать необходимо создать окружение. Часто бывает, что и его нам в наследство также по каким-то причинам не оставили. Это тоже может стать нетривиальной задачей. На этот шаг не хочется тратить время, не так ли?
  3. Тренируем модель (например, детектор автомобилей). Доходим до момента, когда она становится очень даже ничего самое время сохранить результат. Назовем ее car_detection_v1.pb. Потом тренируем еще одну car_detection_v2.pb. Некоторое время спустя наши коллеги или мы сами обучаем ещё и ещё, используя различные архитектуры. В итоге формируется куча артефактов, информацию о которых нужно кропотливо собирать (но, делать мы это будем позже, у нас ведь пока есть более приоритетные дела).
  4. Ну вот и всё! У нас есть модель! Мы можем приступать к обучению следующей модели, к разработке архитектуры для решения новой задачи или можем пойти попить чай? А деплоить кто будет?

Выявляем проблемы


Работа над проектом или продуктом это труд многих человек. А с течением времени люди уходят и приходят, проектов становится больше, сами проекты становятся сложнее. Так или иначе, ситуации из описанного выше цикла (и не только) в тех или иных комбинациях будут встречаться от итерации к итерации. Все это выливается в трату времени, путаницу, нервы, возможно в недовольство заказчика, и в конечном счете в упущенные деньги. Хоть все мы обычно ходим по старым граблям, но полагаю, что никто при этом не хочет раз за разом переживать эти моменты.



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


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

Видимо необходимо придумать workflow, который бы позволял легко и удобно управлять этим жизненным циклом? У такой практики есть название MLOps


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


Можете почитать, что обо всем этом думают ребята из Google. Из статьи понятно, что MLOps, довольно, объемная штука.



Далее в своей статье я опишу лишь часть процесса. Для реализации я воспользуюсь инструментом MLflow, т.к. это open-source проект, для подключения необходимо небольшое количество кода и есть интеграция с популярными ml-фреймворками. Вы можете поискать на просторах интернета другие инструменты, например Kubeflow, SageMaker, Trains и т.д., и возможно, подобрать тот, который лучше подходит под ваши нужды.


"Cтроим" MLOps на примере использования инструмента MLFlow


MLFlow это платформа с открытым исходным кодом для управления жизненным циклом ml моделей (https://mlflow.org/).


MLflow включает четыре компонента:


  • MLflow Tracking закрывает вопросы фиксации результатов и параметров, которые к этому результату привели;
  • MLflow Project позволяет упаковывать код и воспроизвести его на любой платформе;
  • MLflow Models отвечает за деплой моделей в прод;
  • MLflow Registry позволяет хранить модели и управлять их состоянием в централизованном хранилище.

MLflow оперирует двумя сущностями:


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

Все шаги примера реализованы на операционной системе Ubuntu 18.04.


1. Разворачиваем сервер


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


  • backend store отвечает за хранение информации о зарегистрированных моделях (поддерживает 4 СУБД: mysql, mssql, sqlite, and postgresql);
  • artifact store отвечает за хранение артефактов (поддерживает 7 вариантов хранения: Amazon S3, Azure Blob Storage, Google Cloud Storage, FTP server, SFTP Server, NFS, HDFS).

В качестве artifact store для простоты возьмем sftp сервер.


  • создаем группу
    $ sudo groupadd sftpg
    
  • добавляем пользователя и устанавливаем ему пароль
    $ sudo useradd -g sftpg mlflowsftp$ sudo passwd mlflowsftp 
    
  • корректируем пару настроек доступа
    $ sudo mkdir -p /data/mlflowsftp/upload$ sudo chown -R root.sftpg /data/mlflowsftp$ sudo chown -R mlflowsftp.sftpg /data/mlflowsftp/upload
    
  • добавляем несколько строк в /etc/ssh/sshd_config
    Match Group sftpg ChrootDirectory /data/%u ForceCommand internal-sftp
    
  • перезапускаем службу
    $ sudo systemctl restart sshd
    

В качестве backend store возьмем postgresql.


$ sudo apt update$ sudo apt-get install -y postgresql postgresql-contrib postgresql-server-dev-all$ sudo apt install gcc$ pip install psycopg2$ sudo -u postgres -i# Create new user: mlflow_user[postgres@user_name~]$ createuser --interactive -PEnter name of role to add: mlflow_userEnter password for new role: mlflowEnter it again: mlflowShall the new role be a superuser? (y/n) nShall the new role be allowed to create databases? (y/n) nShall the new role be allowed to create more new roles? (y/n) n# Create database mlflow_bd owned by mlflow_user$ createdb -O mlflow_user mlflow_db

Для запуска сервера необходимо установить следующие python пакеты (советую создать отдельное виртуальное окружение):


pip install mlflowpip install pysftp

Запускаем наш сервер


$ mlflow server  \                 --backend-store-uri postgresql://mlflow_user:mlflow@localhost/mlflow_db \                 --default-artifact-root sftp://mlflowsftp:mlflow@sftp_host/upload  \                --host server_host \                --port server_port

2. Добавляем трекинг


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


Для примера я создал небольшой проект на github на Keras по сегментации всего, что есть в COCO датасете. Для добавления трекинга я создал файл mlflow_training.py.


Вот строки, в которых происходит самое интересное:


def run(self, epochs, lr, experiment_name):        # getting the id of the experiment, creating an experiment in its absence        remote_experiment_id = self.remote_server.get_experiment_id(name=experiment_name)        # creating a "run" and getting its id        remote_run_id = self.remote_server.get_run_id(remote_experiment_id)        # indicate that we want to save the results on a remote server        mlflow.set_tracking_uri(self.tracking_uri)        mlflow.set_experiment(experiment_name)        with mlflow.start_run(run_id=remote_run_id, nested=False):            mlflow.keras.autolog()            self.train_pipeline.train(lr=lr, epochs=epochs)        try:            self.log_tags_and_params(remote_run_id)        except mlflow.exceptions.RestException as e:            print(e)

Здесь self.remote_server это небольшая обвязка над методами mlflow.tracking. MlflowClient (я сделал для удобства), с помощью которых я создаю эксперимент и запуск на сервере. Далее указываю куда должны сливаться результаты запуска (mlflow.set_tracking_uri(self.tracking_uri)). Подключаю автоматическое логирование mlflow.keras.autolog(). На данный момент MLflow Tracking поддерживает автоматическое логирование для TensorFlow, Keras, Gluon XGBoost, LightGBM, Spark. Если вы не нашли своего фреймворка или библиотеки, то вы всегда можете логировать в явном виде. Запускаем обучение. Регистрируем теги и входные параметры на удаленном сервере.


Пара строк и вы, как и все желающие, имеете доступ к информации о всех запусках. Круто?


3. Оформляем проект


Теперь сделаем так, чтобы запустить проект было проще простого. Для этого добавим в корень проекта файл MLproject и conda.yaml.
MLproject


name: flow_segmentationconda_env: conda.yamlentry_points:  main:    parameters:        categories: {help: 'list of categories from coco dataset'}        epochs: {type: int, help: 'number of epochs in training'}        lr: {type: float, default: 0.001, help: 'learning rate'}        batch_size: {type: int, default: 8}        model_name: {type: str, default: 'Unet', help: 'Unet, PSPNet, Linknet, FPN'}        backbone_name: {type: str, default: 'resnet18', help: 'exampe resnet18, resnet50, mobilenetv2 ...'}        tracking_uri: {type: str, help: 'the server address'}        experiment_name: {type: str, default: 'My_experiment', help: 'remote and local experiment name'}    command: "python mlflow_training.py \            --epochs={epochs}            --categories={categories}            --lr={lr}            --tracking_uri={tracking_uri}            --model_name={model_name}            --backbone_name={backbone_name}            --batch_size={batch_size}            --experiment_name={experiment_name}"

MLflow Project имеет несколько свойств:


  • Name имя вашего проекта;
  • Environment в моем случае conda_env указывает на то, что для запуска используется Anaconda и описание зависимостей находится в файле conda.yaml;
  • Entry Points указывает какие файлы и с какими параметрами мы можем запустить (все параметры при запуске обучения автоматически логируются)

conda.yaml


name: flow_segmentationchannels:  - defaults  - anacondadependencies:  - python==3.7  - pip:    - mlflow==1.8.0    - pysftp==0.2.9    - Cython==0.29.19    - numpy==1.18.4    - pycocotools==2.0.0    - requests==2.23.0    - matplotlib==3.2.1    - segmentation-models==1.0.1    - Keras==2.3.1    - imgaug==0.4.0    - tqdm==4.46.0    - tensorflow-gpu==1.14.0

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


4. Запускаем обучение


Клонируем проект и переходим в директорию проекта:


git clone https://github.com/simbakot/mlflow_example.gitcd mlflow_example/

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


pip install mlflowpip install pysftp

Т.к. в примере я использую conda_env на вашем компьютере должна быть установлена Anaconda (но и это можно обойти установив все необходимые пакеты самостоятельно и поигравшись с параметрами запуска).


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


$ mlflow run -P epochs=10 -P categories=cat,dog -P tracking_uri=http://server_host:server_port .

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


5. Оцениваем результаты обучения


После окончания обучения мы можем перейти в браузере по адресу нашего сервера http://server_host:server_port



Здесь мы видим список всех экспериментов (слева вверху), а также информацию по запускам (посередине). Мы можем посмотреть более подробную информацию (параметры, метрики, артефакты и какую-то доп. информацию) по каждому запуску.



По каждой метрике мы можем наблюдать историю изменения



Т.е. на данный момент мы можем анализировать результаты в "ручном" режиме, также вы можете настроить и автоматическую валидацию при помощи MLflow API.


6. Регистрируем модель


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



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



Для каждой модели мы можем добавить описание и выбрать одно из трех состояний (Staging, Production, Archived), впоследствии мы при помощи api можем обращаться к этим состояниям, что на ряду с версионированием дает дополнительную гибкость.



У нас также имеется удобный доступ ко всем моделям



и их версиям



Как и в предыдущем пункте все операции можно сделать при помощи API.


7. Деплоим модель


На данном этапе у нас уже есть натренированная (keras) модель. Пример, как можно её использовать:


class SegmentationModel:    def __init__(self, tracking_uri, model_name):        self.registry = RemoteRegistry(tracking_uri=tracking_uri)        self.model_name = model_name        self.model = self.build_model(model_name)    def get_latest_model(self, model_name):        registered_models = self.registry.get_registered_model(model_name)        last_model = self.registry.get_last_model(registered_models)        local_path = self.registry.download_artifact(last_model.run_id, 'model', './')        return local_path    def build_model(self, model_name):        local_path = self.get_latest_model(model_name)        return mlflow.keras.load_model(local_path)    def predict(self, image):        image = self.preprocess(image)        result = self.model.predict(image)        return self.postprocess(result)    def preprocess(self, image):        image = cv2.resize(image, (256, 256))        image = image / 255.        image = np.expand_dims(image, 0)        return image    def postprocess(self, result):        return result

Здесь self.registry это опять небольшая обвязка над mlflow.tracking.MlflowClient, для удобства. Суть в том, что я обращаюсь к удаленному серверу и ищу там модель с указанным именем, причем, самую последнюю production версию. Далее скачиваю артефакт локально в папку ./model и собираю модель из этой директории mlflow.keras.load_model(local_path). Всё теперь мы можем использовать нашу модель. CV (ML) разработчики могут спокойно заниматься улучшением модели и публиковать новые версии.


В заключение


Я представил систему которая позволяет:


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

Данный пример является игрушечным и служит точкой старта для выстраивания вашей собственной системы, которая, возможно, будет включать в себя автоматизацию оценки результатов и регистрации моделей (п.5 и п.6 соответственно) или вы добавите версионирование датасетов, или может ещё что-то? Я пытался донести мысль, что вам нужен MLOps в целом, MLflow лишь средство достижения цели.


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


P.S. Оставлю пару ссылок:
github проект https://github.com/simbakot/mlflow_example
MLflow https://mlflow.org/
Моя рабочая почта, для вопросов ikryakin@croc.ru


У нас в компании периодически проводятся различные мероприятия для ИТ-специалистов, например: 8-го июля в 19:00 по МСК будет проходить митап по CV в онлайн-формате, если интересно, то можете принять участие, регистрация здесь .

Подробнее..

Перевод Повелевать Webом с помощью Python

03.07.2020 20:16:17 | Автор: admin
Путешествие в простую веб-автоматизацию

image


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

Решение: Используйте Python для автоматической отправки выполненных заданий! В идеале я мог бы сохранить задание, набрать несколько клавиш и загрузить свою работу за считанные секунды. Сначала это звучало слишком хорошо, чтобы быть правдой, но потом я обнаружил Selenium, инструмент, который можно использовать с Python для навигации по сети.

image


Каждый раз, когда мы повторяем утомительные действия в Интернете с одинаковой последовательностью шагов, это отличный шанс написать программу для автоматизации процесса. С Selenium и Python нам просто нужно написать сценарий один раз, и затем мы сможем запустить его сколько угодно раз и уберечь себя от повторения однообразных задач (и в моем случае исключается возможность отправки задания в неправильное место)!

Здесь я рассмотрю решение, разработанное мной для автоматической (и правильной) отправки моих заданий. Попутно мы рассмотрим основы использования Python и selenium для программного управления вебом. Хотя эта программа работает (я использую ее каждый день!), она довольно индивидуальна, поэтому вы не сможете скопировать и вставить код для своего приложения. Тем не менее, общие методы здесь могут быть применены к неограниченному числу ситуаций. (Если вы хотите увидеть полный код, он доступен на GitHub).

Подход


Прежде чем мы перейдем к интересной части автоматизации, нам необходимо выяснить общую структуру нашего решения. Начинать программировать без плана отличный способ потратить много часов и разочароваться. Я хочу написать программу для отправки выполненных заданий по классу в правильное место в Canvase (система управления обучением моего университета). Начнем сначала, мне нужен способ сообщить программе название задания для отправки и класс. Я использовал простой подход и создал папку для хранения выполненных заданий с дочерними папками для каждого класса. В дочерние папки я помещаю готовый документ, названный для конкретного задания. Программа может выяснить имя класса по папке и название задания по названию документа.
Вот пример, где название класса EECS491, а задание Задание 3 Вывод в больших графических моделях.

image

File structure (слева) и Complete Assignment (справа).

Первая часть программы представляет собой цикл, который проходит по папкам, чтобы найти задание и класс, которые мы храним в кортеже Python:

# os for file managementimport os# Build tuple of (class, file) to turn insubmission_dir = 'completed_assignments'dir_list = list(os.listdir(submission_dir))for directory in dir_list:    file_list = list(os.listdir(os.path.join(submission_dir, directory)))    if len(file_list) != 0:        file_tup = (directory, file_list[0])    print(file_tup)


('EECS491', 'Assignment 3 Inference in Larger Graphical Models.txt')

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

Веб-контроль с Selenium


Чтобы начать работу с Selenium, мы импортируем библиотеку и создаем веб-драйвер, который является браузером, управляемым нашей программой. В этом случае я буду использовать Chrome в качестве браузера и отправлять драйвер на веб-сайт Canvas, где я отправляю задания.

import selenium# Using Chrome to access webdriver = webdriver.Chrome()# Open the websitedriver.get('https://canvas.case.edu')


Когда мы открываем веб-страницу Canvas, нас встречает первое препятствие поле для входа! Чтобы обойти это, нам нужно будет ввести идентификатор и пароль и нажать кнопку входа.

image

Представьте, что веб-драйвер это человек, который никогда раньше не видел веб-страницу: нам нужно точно сказать, куда нажимать, что печатать и какие кнопки нажимать. Есть несколько способов сообщить нашему веб-драйверу, какие элементы найти, и все они используют селекторы. Селектор это уникальный идентификатор элемента на веб-странице. Чтобы найти селектор для определенного элемента, скажем, поле CWRU ID, нам нужно посмотреть код веб-страницы. В Chrome это можно сделать, нажав Ctrl + Shift + I или щелкнув правой кнопкой мыши на любом элементе и выбрав Посмотреть код. Это открывает инструменты разработчика Chrome, чрезвычайно полезное приложение, которое показывает HTML, лежащий в основе любой веб-страницы.

Чтобы найти селектор для поля CWRU ID, я щелкнул правой кнопкой мыши в поле, нажал Посмотреть код и увидел следующее в инструментах разработчика. Выделенная строка соответствует элементу id_box (эта строка называется тегом HTML).

image

Этот HTML может выглядеть подавляющим, но мы можем игнорировать большую часть информации и сосредоточиться на частях id = "username" и name = "username". (они известны как атрибуты тега HTML).
Чтобы выбрать поле id с помощью нашего веб-драйвера, мы можем использовать атрибут id или name, который мы нашли в инструментах разработчика. Веб-драйверы в Selenium имеют много разных способов выбора элементов на веб-странице, и часто есть несколько способов выбрать один и тот же элемент:

# Select the id boxid_box = driver.find_element_by_name('username')# Equivalent Outcome! id_box = driver.find_element_by_id('username')


Наша программа теперь имеет доступ к id_box, и мы можем взаимодействовать с ним различными способами, такими как ввод ключей или нажатие (если мы выбрали кнопку).

# Send id informationid_box.send_keys('my_username')


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

# Find password boxpass_box = driver.find_element_by_name('password')# Send passwordpass_box.send_keys('my_password')# Find login buttonlogin_button = driver.find_element_by_name('submit')# Click loginlogin_button.click()

Как только мы вошли в систему, нас приветствует эта слегка пугающая панель инструментов:

image

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

# Find and click on list of coursescourses_button = driver.find_element_by_id('global_nav_courses_link')courses_button.click()# Get the name of the folderfolder = file_tup[0]    # Class to select depends on folderif folder == 'EECS491':    class_select = driver.find_element_by_link_text('Artificial Intelligence: Probabilistic Graphical Models (100/10039)')elif folder == 'EECS531':    class_select = driver.find_element_by_link_text('Computer Vision (100/10040)')# Click on the specific classclass_select.click()


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

image

Этот рабочий процесс может показаться немного утомительным, но помните, что мы должны сделать это только один раз, когда пишем нашу программу! После этого мы можем нажать Выполнить столько раз, сколько захотим, и программа зайдет за нас на все эти страницы.
Мы используем один и тот же процесс проверки страницы выбора элемента взаимодействия с элементом, чтобы пройти еще пару экранов. Наконец, мы достигаем страницы отправки задания:

image

В этот момент я мог видеть финишную черту, но изначально этот экран меня озадачил. Я мог бы довольно легко нажать на поле Выбрать файл, но как я должен был выбрать нужный файл для загрузки? Ответ оказывается невероятно простым! Мы находим поле Choose File с помощью селектора и используем метод send_keys для передачи точного пути к файлу (называемого file_location в приведенном ниже коде) к блоку:

# Choose File buttonchoose_file = driver.find_element_by_name('attachments[0][uploaded_data]')# Complete path of the filefile_location = os.path.join(submission_dir, folder, file_name)# Send the file location to the buttonchoose_file.send_keys(file_location)


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

image

Теперь мы выбираем кнопку Отправить задание, нажимаем, и наше задание отправлено!

# Locate submit button and clicksubmit_assignment = driver.find_element_by_id('submit_file_button')submit_assignent.click()


image


Уборка


Управление файлами это всегда критический шаг, и я хочу быть уверенным, что не отправлю повторно или не потеряю старые задания. Я решил, что лучшим решением будет сохранить файл, который будет помещен в папку completed_assignments, и переместить файлы в папку submitted_assignments, как только они будут загружены. Последний бит кода использует модуль os для перемещения завершенного задания в нужное место.

# Location of files after submissionsubmitted_file_location = os.path.join(submitted_dir, submitted_file_name)# Rename essentially copies and pastes filesos.rename(file_location, submitted_file_location)


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

Вот как это выглядит, когда я запускаю программу:

image

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

image

Пока программа работает, я могу наблюдать, как Python работает на меня:

image

Выводы


Техника автоматизации с помощью Python отлично подходит для многих задач, как общих, так и в моей области науки о данных. Например, мы могли бы использовать Selenium для автоматической загрузки новых файлов данных каждый день (при условии, что у веб-сайта нет API). Хотя на первый взгляд создание сценария может показаться трудоемким, преимущество заключается в том, что мы можем заставить компьютер повторять эту последовательность столько раз, сколько захотим, точно таким же образом. Программа никогда не потеряет фокус и зайдет в Twitter. Она будет точно выполнять шаги с идеальной последовательностью (алгоритм будет прекрасно работать, пока сайт не изменится).

Я должен упомянуть, что вы должны быть осторожными, прежде чем автоматизировать критические задачи. Этот пример является относительно низким риском, так как я всегда могу вернуться и повторно отправить задания, и я обычно перепроверяю работу программы. Веб-сайты меняются, и если вы не измените программу в ответ, вы можете получить скрипт, который делает что-то совершенно иное, чем вы изначально предполагали!

В плане окупаемости эта программа экономит мне около 30 секунд на каждое задание, а на ее написание уходит 2 часа. Так что, если я использую ее для сдачи 240 заданий, я выйду в плюс по времени! Тем не менее, отдача от этой программы заключается в разработке классного решения проблемы и многому учит в процессе. Хотя мое время могло бы быть более эффективно потрачено на выполнение заданий, а не на выяснение того, как автоматически сдавать их, я полностью наслаждался этой задачей.Есть несколько вещей, приносящих такое удовлетворение, как решение проблем, и Python оказывается довольно хорошим инструментом для этого.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


Подробнее..

Перевод Python для начинающих как повелевать Webом

03.07.2020 22:11:07 | Автор: admin
Путешествие в простую веб-автоматизацию

image


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

Решение: Используйте Python для автоматической отправки выполненных заданий! В идеале я мог бы сохранить задание, набрать несколько клавиш и загрузить свою работу за считанные секунды. Сначала это звучало слишком хорошо, чтобы быть правдой, но потом я обнаружил Selenium, инструмент, который можно использовать с Python для навигации по сети.

image


Каждый раз, когда мы повторяем утомительные действия в Интернете с одинаковой последовательностью шагов, это отличный шанс написать программу для автоматизации процесса. С Selenium и Python нам просто нужно написать сценарий один раз, и затем мы сможем запустить его сколько угодно раз и уберечь себя от повторения однообразных задач (и в моем случае исключается возможность отправки задания в неправильное место)!

Здесь я рассмотрю решение, разработанное мной для автоматической (и правильной) отправки моих заданий. Попутно мы рассмотрим основы использования Python и selenium для программного управления вебом. Хотя эта программа работает (я использую ее каждый день!), она довольно индивидуальна, поэтому вы не сможете скопировать и вставить код для своего приложения. Тем не менее, общие методы здесь могут быть применены к неограниченному числу ситуаций. (Если вы хотите увидеть полный код, он доступен на GitHub).

Подход


Прежде чем мы перейдем к интересной части автоматизации, нам необходимо выяснить общую структуру нашего решения. Начинать программировать без плана отличный способ потратить много часов и разочароваться. Я хочу написать программу для отправки выполненных заданий по классу в правильное место в Canvase (система управления обучением моего университета). Начнем сначала, мне нужен способ сообщить программе название задания для отправки и класс. Я использовал простой подход и создал папку для хранения выполненных заданий с дочерними папками для каждого класса. В дочерние папки я помещаю готовый документ, названный для конкретного задания. Программа может выяснить имя класса по папке и название задания по названию документа.
Вот пример, где название класса EECS491, а задание Задание 3 Вывод в больших графических моделях.

image

File structure (слева) и Complete Assignment (справа).

Первая часть программы представляет собой цикл, который проходит по папкам, чтобы найти задание и класс, которые мы храним в кортеже Python:

# os for file managementimport os# Build tuple of (class, file) to turn insubmission_dir = 'completed_assignments'dir_list = list(os.listdir(submission_dir))for directory in dir_list:    file_list = list(os.listdir(os.path.join(submission_dir, directory)))    if len(file_list) != 0:        file_tup = (directory, file_list[0])    print(file_tup)


('EECS491', 'Assignment 3 Inference in Larger Graphical Models.txt')

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

Веб-контроль с Selenium


Чтобы начать работу с Selenium, мы импортируем библиотеку и создаем веб-драйвер, который является браузером, управляемым нашей программой. В этом случае я буду использовать Chrome в качестве браузера и отправлять драйвер на веб-сайт Canvas, где я отправляю задания.

import selenium# Using Chrome to access webdriver = webdriver.Chrome()# Open the websitedriver.get('https://canvas.case.edu')


Когда мы открываем веб-страницу Canvas, нас встречает первое препятствие поле для входа! Чтобы обойти это, нам нужно будет ввести идентификатор и пароль и нажать кнопку входа.

image

Представьте, что веб-драйвер это человек, который никогда раньше не видел веб-страницу: нам нужно точно сказать, куда нажимать, что печатать и какие кнопки нажимать. Есть несколько способов сообщить нашему веб-драйверу, какие элементы найти, и все они используют селекторы. Селектор это уникальный идентификатор элемента на веб-странице. Чтобы найти селектор для определенного элемента, скажем, поле CWRU ID, нам нужно посмотреть код веб-страницы. В Chrome это можно сделать, нажав Ctrl + Shift + I или щелкнув правой кнопкой мыши на любом элементе и выбрав Посмотреть код. Это открывает инструменты разработчика Chrome, чрезвычайно полезное приложение, которое показывает HTML, лежащий в основе любой веб-страницы.

Чтобы найти селектор для поля CWRU ID, я щелкнул правой кнопкой мыши в поле, нажал Посмотреть код и увидел следующее в инструментах разработчика. Выделенная строка соответствует элементу id_box (эта строка называется тегом HTML).

image

Этот HTML может выглядеть подавляющим, но мы можем игнорировать большую часть информации и сосредоточиться на частях id = "username" и name = "username". (они известны как атрибуты тега HTML).
Чтобы выбрать поле id с помощью нашего веб-драйвера, мы можем использовать атрибут id или name, который мы нашли в инструментах разработчика. Веб-драйверы в Selenium имеют много разных способов выбора элементов на веб-странице, и часто есть несколько способов выбрать один и тот же элемент:

# Select the id boxid_box = driver.find_element_by_name('username')# Equivalent Outcome! id_box = driver.find_element_by_id('username')


Наша программа теперь имеет доступ к id_box, и мы можем взаимодействовать с ним различными способами, такими как ввод ключей или нажатие (если мы выбрали кнопку).

# Send id informationid_box.send_keys('my_username')


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

# Find password boxpass_box = driver.find_element_by_name('password')# Send passwordpass_box.send_keys('my_password')# Find login buttonlogin_button = driver.find_element_by_name('submit')# Click loginlogin_button.click()

Как только мы вошли в систему, нас приветствует эта слегка пугающая панель инструментов:

image

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

# Find and click on list of coursescourses_button = driver.find_element_by_id('global_nav_courses_link')courses_button.click()# Get the name of the folderfolder = file_tup[0]    # Class to select depends on folderif folder == 'EECS491':    class_select = driver.find_element_by_link_text('Artificial Intelligence: Probabilistic Graphical Models (100/10039)')elif folder == 'EECS531':    class_select = driver.find_element_by_link_text('Computer Vision (100/10040)')# Click on the specific classclass_select.click()


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

image

Этот рабочий процесс может показаться немного утомительным, но помните, что мы должны сделать это только один раз, когда пишем нашу программу! После этого мы можем нажать Выполнить столько раз, сколько захотим, и программа зайдет за нас на все эти страницы.
Мы используем один и тот же процесс проверки страницы выбора элемента взаимодействия с элементом, чтобы пройти еще пару экранов. Наконец, мы достигаем страницы отправки задания:

image

В этот момент я мог видеть финишную черту, но изначально этот экран меня озадачил. Я мог бы довольно легко нажать на поле Выбрать файл, но как я должен был выбрать нужный файл для загрузки? Ответ оказывается невероятно простым! Мы находим поле Choose File с помощью селектора и используем метод send_keys для передачи точного пути к файлу (называемого file_location в приведенном ниже коде) к блоку:

# Choose File buttonchoose_file = driver.find_element_by_name('attachments[0][uploaded_data]')# Complete path of the filefile_location = os.path.join(submission_dir, folder, file_name)# Send the file location to the buttonchoose_file.send_keys(file_location)


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

image

Теперь мы выбираем кнопку Отправить задание, нажимаем, и наше задание отправлено!

# Locate submit button and clicksubmit_assignment = driver.find_element_by_id('submit_file_button')submit_assignent.click()


image


Уборка


Управление файлами это всегда критический шаг, и я хочу быть уверенным, что не отправлю повторно или не потеряю старые задания. Я решил, что лучшим решением будет сохранить файл, который будет помещен в папку completed_assignments, и переместить файлы в папку submitted_assignments, как только они будут загружены. Последний бит кода использует модуль os для перемещения завершенного задания в нужное место.

# Location of files after submissionsubmitted_file_location = os.path.join(submitted_dir, submitted_file_name)# Rename essentially copies and pastes filesos.rename(file_location, submitted_file_location)


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

Вот как это выглядит, когда я запускаю программу:

image

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

image

Пока программа работает, я могу наблюдать, как Python работает на меня:

image

Выводы


Техника автоматизации с помощью Python отлично подходит для многих задач, как общих, так и в моей области науки о данных. Например, мы могли бы использовать Selenium для автоматической загрузки новых файлов данных каждый день (при условии, что у веб-сайта нет API). Хотя на первый взгляд создание сценария может показаться трудоемким, преимущество заключается в том, что мы можем заставить компьютер повторять эту последовательность столько раз, сколько захотим, точно таким же образом. Программа никогда не потеряет фокус и зайдет в Twitter. Она будет точно выполнять шаги с идеальной последовательностью (алгоритм будет прекрасно работать, пока сайт не изменится).

Я должен упомянуть, что вы должны быть осторожными, прежде чем автоматизировать критические задачи. Этот пример является относительно низким риском, так как я всегда могу вернуться и повторно отправить задания, и я обычно перепроверяю работу программы. Веб-сайты меняются, и если вы не измените программу в ответ, вы можете получить скрипт, который делает что-то совершенно иное, чем вы изначально предполагали!

В плане окупаемости эта программа экономит мне около 30 секунд на каждое задание, а на ее написание уходит 2 часа. Так что, если я использую ее для сдачи 240 заданий, я выйду в плюс по времени! Тем не менее, отдача от этой программы заключается в разработке классного решения проблемы и многому учит в процессе. Хотя мое время могло бы быть более эффективно потрачено на выполнение заданий, а не на выяснение того, как автоматически сдавать их, я полностью наслаждался этой задачей.Есть несколько вещей, приносящих такое удовлетворение, как решение проблем, и Python оказывается довольно хорошим инструментом для этого.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


Подробнее..

Чтобы я хотел знать когда начинал изучать Django? очень общий взгляд

05.07.2020 20:11:11 | Автор: admin
Здесь на хабре много различных инструкций по использованию Django. Эти инструкции часто включают много кода и представляют последовательность шагов, которые нужно сделать, чтобы создать проект.
Когда я начинал изучать Django и Wagtail по таким инструкциям, меня часто смущало, что пара команд создает кучу непонятных файлов (особенно на самом старте). Последующее описание этих файлов в инструкциях содержало слишком много деталей, которые трудно было усвоить за раз.
В этом посте я бы хотел посмотреть на Django с очень философского вида минимум кода, максимум общих фактов. Думаю, что такой взгляд поможет тем, кто хочет начать изучать Django но теряется на старте.
image

Хочу также сказать, что не являюсь профессионалом по части веб-программирования я в этой области скорее любитель, которого интересуют исключительно личные проекты один из них сайт по расшифровке данных ДНК тестов https://ru.bezoder.com написан на Wagtail.
Сначала давайте вспомним, что сайт в интернете это просто программа, которая, возможно, работает почти на таком же компьютере, что находится перед вами.
Ваш компьютер (телефон и т.п.) посылает какой-то запрос к чужому компьютеру в интернет, тот его обрабатывает и отвечает. При обработке чужой компьютер, возможно, делает запрос или производит запись в базу данных. Теперь представим, что необходимо запрограммировать компьютер в интернете, чтобы он правильно обрабатывал каждый запрос.
Это можно сделать вообще на каком угодно языке программирования вы получаете запрос и на его основе что-то выполняете. Но представьте сколько может быть вариантов как запрограммировать этот компьютер их может быть бесконечно много! Например, можно написать функцию что-то вроде:
Если запрос == google.ru:
ответ "Привет"
Если запрос == google.de:
ответ "Hallo"
...

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

Концепция Django


Django предлагает все разделить на "слои". Слои отвечают за разные составляющие вашей программы. Между слоями есть связь, но она не затрудняет разработку каждого слоя изолированно (без большого внимания к другим слоям) в Django это называется loose coupling. Вот несколько важных слоев Django:
  • Модели (Models) это слой ваших данных и то как вы их храните их в базе данных
  • Виды (views) этот слой собирает все данные которые необходимо для создания веб страниц или обработки данных, отправленных через формы
  • Шаблоны (Templates) этот слой получает данные из видов и отображает их на вебстранице (в этом слое вы работаете уже с html)
  • Ссылки (url) этот слой организует работу ссылок на вашем сайте на какую ссылку нужно создавать какой вид и какой шаблон
  • Формы (Forms) этот слой помогает создавать и обрабатывать веб формы для данных от пользователей
  • ...

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

Слой модели


Первый и, наверно, самый важный слой это модели(models) отвечает за базу данных. База данных это много всяких таблиц например, может быть таблица пользователи такого вида:
ID
name
surname
karma
1
Михаил
Трунов
2

Как видите, в базе каждая строка это запись, относящаяся к пользователю сайта. В строке есть данные различного типа в нашем случае числа и текст.
Распространенным языком баз данных является SQL определенными командами вы можете создавать новые таблицы в базе или вносить и получать данные в и из существующих таблиц.
У SQL есть уязвимости подробнее. Вкратце если определенным образом расставить кавычки и точки с запятой в данных, которые отправляются в SQL команду, часть этих данных может быть интерпретирована как составляющая SQL команды.
Django берет всю головную боль, связанную с проблемами SQL на себя вам даже не надо знать SQL, чтобы пользоваться Django, от вас нужен только python Django сам сформирует SQL команды для создания таблиц, поиска и записи данных в таблицы и все это будет безопасно.
Идея Django в том, что классы на python повторяют структуру таблиц вашей базы данных.
То есть, для таблицы выше я могу создать класс в python что-то вроде:
class User:    def __init__(id, name, surname, karma)        self.id = id        self.name = name        ...

но как связать такой класс с базой данных? Вот тут начинается магия Django:
# мы просто импортируем модуль models из Djangofrom django.db import models# создаем класс, который наследует models.Modelclass CustomUser(models.Model):    # создаем поля для базы данных в классе    name = models.CharField(max_length = 20)    ...    karma = models.FloatField(...)            ...# Еще одна таблица в базе данных - статьяclass Article(models.Model):    # создаем название и содержание статьи    title = models.CharField(...)    content = models.TextField(...)            ...

Вы просто используете django.db.models.Model чтобы создать класс, далее каждое поле в вашем классе это также поле, взятое из django.db.models. В моем случае поле name это текстовое поле CharField, поле karma это число float. Список всех полей (Field types) есть в официальной документации.
У каждого поля есть опции (Field options) в коде выше опция это max_length = 20. Опции зависят от полей, которые вы создаете в базе например, max_length = 20 это максимальная длина в символах поля name в базе. В документации по ссылке выше также описаны опции для всех полей.
На основе этого кода Django сам создаст таблицу в базе данных и то, что я назвал полями в классе будут столбцами в этой таблице. Django дает вам также удобные команды в python как получать или записывать значения в базу данных. Все делается с помощью методов models.Model а также абстракции Manager, отвечающей в Django за коммуникацию с базой данных (в данном посте я эти абстракции детально не рассматриваю). Например, CustomUser.objects.filter(name=Михаил) вернет всех пользователей с именем Михаил.
Такая связь между строками в базе данных и объектами (экземплярами, инстансами) в Python называется Object-relational mapping в нашем случае Django-ORM.
А наши модели повторяют структуру базы данных и при этом являются классами в Python. Это значит, что к моделям (классы в Python) можно добавить методы. Например, продолжая логику сайта хабр, я могу добавить метод для изменения кармы:
from django.db import modelsclass CustomUser(models.Model):    ...    # пример метода в модели Django    def change_karma(self, other):                  ....         if ...:              self.karma = self.karma +1              ...         else:         ...

Тут other это другой пользователь. Как вы знаете здесь определенная логика добавления кармы. Всю эту логику я могу, например, создать в указанном методе.
В Django вы думаете какие таблицы хотите создать в своей базе и потом просто создаете классы python по примеру выше.

Слой виды


Следующим важным, на мой взгляд, слоем является слой видов (views). Ваши модели это некоторые абстракции, с которыми вам удобно работать или они интуитивно понятны. Но, когда вы хотите что-то показать пользователям, то, возможно, вас будут интересовать иные абстракции.
Например, вы создали три модели в Django: CustomUser, Article и Advertisement с разными полями. Модель Article это статья сайта, Advertisement это реклама, которую вы показываете на сайте, CustomUser зарегистрированный пользователь сайта.
Когда вы захотите создать вебстраницу со статьей, то вам понадобятся данные сразу из нескольких ваших моделей разумеется вы хотите показать все поля в самой статье (название, содержание и т.д.), вы, скорее всего, также хотите показать какую-то рекламу рядом с этой статьей. Причем реклама зависит не от содержания статьи а от поведения пользователя CustomUser. При таком подходе будет нужна какая-то логика как собирать данные. Так, слой view в данном случае и будет подходящим местом для этой логики. Тут можно собрать все данные, которые будут относиться к тому, что вы хотите показать.
Есть два типа видов view в Django функциональный и классовый.
Функциональный вид это просто Python функция с аргументом request это запрос к вашему сайту. В нем содержится информация о пользователе, типе запроса и многом другом. На основе этой информации вы формируете ответ и возвращаете его в своей функции.
Еще один тип view классовый. Он позволяет создавать виды не на основе функций, а виды как экземпляры классов. Тут Django предоставляет также кучу всяких облегчающих жизнь классов и функций. Предположим, вы хотите создать вид на основе статьи Article:
# импорт полезного классаfrom django.views.generic import DetailView# импорт созданной в другом файле модели Articlefrom .models import Article# создание классового вида class ArticleDetailView(DetailView):    # модель на основе которой мы хотим создать вид    model = Article    # имя, которое будет использовано в html шаблоне (это другой слой - рассмотрим далее)    context_object_name = 'article'    # имя html шаблона, на основе которого будет создана веб страница    template_name = 'article/article_detail.html'

Классовый вид на основе DetailView автоматически соберет всю информацию модели Article и затем отправит ее в следующий слой Django:

Слой шаблоны


В коде выше template_name это переменная для названия html шаблона, который будет использован для формирования веб страницы, которая и будет показана пользователю. Вот пример кода из такого шаблона:
  <h1>{{ article.title }}</h1>   <div>{{ article.content }}</div>       

{{ article.title }} и {{ article.content }} это название статьи и ее содержание, заключенные в html теги. title и content повторяют название полей модели Article, которую вы создали в слое Модели. Слово article мы указали в context_object_name в виде. В результате обработки Django вставит соответствующие поля из Article в шаблон.

Резюме


Это общий взгляд на некоторые Django слои. Описанная концепция позволяет разделить отдельные блоки программы. В слое модели вы создаете удобные абстракции вашей базы данных, в слое виды вы решаете, какие данные вы хотите показать, и в слое шаблоны вы создаете уже дизайн ваших страниц на основе html и добавляете в шаблоны немного логики с помощью языка Jinja это из примера с фигурными скобками {{ article.name }}.

Я тут не затронул довольно много важных тем например связи между вашими моделями. У каждой статьи может быть один автор или несколько авторов Django с легкостью справится с любым из перечисленных вариантов, и с помощью одной строки в Python вы сможете получить автора статьи или же коллекцию авторов в виде экземпляров класса Автор, созданного на основе models.Model.

Но откуда столько файлов?


Если вы создаете какое-то сложное приложение с кучей Моделей, видов и т.п. то это огромное количество кода надо как-то разбить на отдельные файлы. И файлы желательно организовать по папкам так, чтобы файл с моделями статьи был в той же папке что и виды статьи.
Вот тут приходит еще одна ключевая идея Django приложения, которые заслуживают отдельного поста.
Подробнее..
Категории: Python , Django

Что бы я хотел знать когда начинал изучать Django? очень общий взгляд

06.07.2020 00:12:26 | Автор: admin
Здесь на хабре много различных инструкций по использованию Django. Эти инструкции часто включают много кода и представляют последовательность шагов, которые нужно сделать, чтобы создать проект.
Когда я начинал изучать Django и Wagtail по таким инструкциям, меня часто смущало, что пара команд создает кучу непонятных файлов (особенно на самом старте). Последующее описание этих файлов в инструкциях содержало слишком много деталей, которые трудно было усвоить за раз.
В этом посте я бы хотел посмотреть на Django с очень философского вида минимум кода, максимум общих фактов. Думаю, что такой взгляд поможет тем, кто хочет начать изучать Django но теряется на старте.
image

Хочу также сказать, что не являюсь профессионалом по части веб-программирования я в этой области скорее любитель, которого интересуют исключительно личные проекты один из них сайт по расшифровке данных ДНК тестов https://ru.bezoder.com написан на Wagtail.
Сначала давайте вспомним, что сайт в интернете это просто программа, которая, возможно, работает почти на таком же компьютере, что находится перед вами.
Ваш компьютер (телефон и т.п.) посылает какой-то запрос к чужому компьютеру в интернет, тот его обрабатывает и отвечает. При обработке чужой компьютер, возможно, делает запрос или производит запись в базу данных. Теперь представим, что необходимо запрограммировать компьютер в интернете, чтобы он правильно обрабатывал каждый запрос.
Это можно сделать вообще на каком угодно языке программирования вы получаете запрос и на его основе что-то выполняете. Но представьте сколько может быть вариантов как запрограммировать этот компьютер их может быть бесконечно много! Например, можно написать функцию что-то вроде:
Если запрос == google.ru:
ответ "Привет"
Если запрос == google.de:
ответ "Hallo"
...

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

Концепция Django


Django предлагает все разделить на "слои". Слои отвечают за разные составляющие вашей программы. Между слоями есть связь, но она не затрудняет разработку каждого слоя изолированно (без большого внимания к другим слоям) в Django это называется loose coupling. Вот несколько важных слоев Django:
  • Модели (Models) это слой ваших данных и то как вы их храните их в базе данных
  • Виды (views) этот слой собирает все данные которые необходимо для создания веб страниц или обработки данных, отправленных через формы
  • Шаблоны (Templates) этот слой получает данные из видов и отображает их на вебстранице (в этом слое вы работаете уже с html)
  • Ссылки (url) этот слой организует работу ссылок на вашем сайте на какую ссылку нужно создавать какой вид и какой шаблон
  • Формы (Forms) этот слой помогает создавать и обрабатывать веб формы для данных от пользователей
  • ...

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

Слой модели


Первый и, наверно, самый важный слой это модели(models) отвечает за базу данных. База данных это много всяких таблиц например, может быть таблица пользователи такого вида:
ID
name
surname
karma
1
Михаил
Трунов
2

Как видите, в базе каждая строка это запись, относящаяся к пользователю сайта. В строке есть данные различного типа в нашем случае числа и текст.
Распространенным языком баз данных является SQL определенными командами вы можете создавать новые таблицы в базе или вносить и получать данные в и из существующих таблиц.
У SQL есть уязвимости подробнее. Вкратце если определенным образом расставить кавычки и точки с запятой в данных, которые отправляются в SQL команду, часть этих данных может быть интерпретирована как составляющая SQL команды.
Django берет всю головную боль, связанную с проблемами SQL на себя вам даже не надо знать SQL, чтобы пользоваться Django, от вас нужен только python Django сам сформирует SQL команды для создания таблиц, поиска и записи данных в таблицы и все это будет безопасно.
Идея Django в том, что классы на python повторяют структуру таблиц вашей базы данных.
То есть, для таблицы выше я могу создать класс в python что-то вроде:
class User:    def __init__(id, name, surname, karma)        self.id = id        self.name = name        ...

но как связать такой класс с базой данных? Вот тут начинается магия Django:
# мы просто импортируем модуль models из Djangofrom django.db import models# создаем класс, который наследует models.Modelclass CustomUser(models.Model):    # создаем поля для базы данных в классе    name = models.CharField(max_length = 20)    ...    karma = models.FloatField(...)            ...# Еще одна таблица в базе данных - статьяclass Article(models.Model):    # создаем название и содержание статьи    title = models.CharField(...)    content = models.TextField(...)            ...

Вы просто используете django.db.models.Model чтобы создать класс, далее каждое поле в вашем классе это также поле, взятое из django.db.models. В моем случае поле name это текстовое поле CharField, поле karma это число float. Список всех полей (Field types) есть в официальной документации.
У каждого поля есть опции (Field options) в коде выше опция это max_length = 20. Опции зависят от полей, которые вы создаете в базе например, max_length = 20 это максимальная длина в символах поля name в базе. В документации по ссылке выше также описаны опции для всех полей.
На основе этого кода Django сам создаст таблицу в базе данных и то, что я назвал полями в классе будут столбцами в этой таблице. Django дает вам также удобные команды в python как получать или записывать значения в базу данных. Все делается с помощью методов models.Model а также абстракции Manager, отвечающей в Django за коммуникацию с базой данных (в данном посте я эти абстракции детально не рассматриваю). Например, CustomUser.objects.filter(name=Михаил) вернет всех пользователей с именем Михаил.
Такая связь между строками в базе данных и объектами (экземплярами, инстансами) в Python называется Object-relational mapping в нашем случае Django-ORM.
А наши модели повторяют структуру базы данных и при этом являются классами в Python. Это значит, что к моделям (классы в Python) можно добавить методы. Например, продолжая логику сайта хабр, я могу добавить метод для изменения кармы:
from django.db import modelsclass CustomUser(models.Model):    ...    # пример метода в модели Django    def change_karma(self, other):                  ....         if ...:              self.karma = self.karma +1              ...         else:         ...

Тут other это другой пользователь. Как вы знаете здесь определенная логика добавления кармы. Всю эту логику я могу, например, создать в указанном методе.
В Django вы думаете какие таблицы хотите создать в своей базе и потом просто создаете классы python по примеру выше.

Слой виды


Следующим важным, на мой взгляд, слоем является слой видов (views). Ваши модели это некоторые абстракции, с которыми вам удобно работать или они интуитивно понятны. Но, когда вы хотите что-то показать пользователям, то, возможно, вас будут интересовать иные абстракции.
Например, вы создали три модели в Django: CustomUser, Article и Advertisement с разными полями. Модель Article это статья сайта, Advertisement это реклама, которую вы показываете на сайте, CustomUser зарегистрированный пользователь сайта.
Когда вы захотите создать вебстраницу со статьей, то вам понадобятся данные сразу из нескольких ваших моделей разумеется вы хотите показать все поля в самой статье (название, содержание и т.д.), вы, скорее всего, также хотите показать какую-то рекламу рядом с этой статьей. Причем реклама зависит не от содержания статьи а от поведения пользователя CustomUser. При таком подходе будет нужна какая-то логика как собирать данные. Так, слой view в данном случае и будет подходящим местом для этой логики. Тут можно собрать все данные, которые будут относиться к тому, что вы хотите показать.
Есть два типа видов view в Django функциональный и классовый.
Функциональный вид это просто Python функция с аргументом request это запрос к вашему сайту. В нем содержится информация о пользователе, типе запроса и многом другом. На основе этой информации вы формируете ответ и возвращаете его в своей функции.
Еще один тип view классовый. Он позволяет создавать виды не на основе функций, а виды как экземпляры классов. Тут Django предоставляет также кучу всяких облегчающих жизнь классов и функций. Предположим, вы хотите создать вид на основе статьи Article:
# импорт полезного классаfrom django.views.generic import DetailView# импорт созданной в другом файле модели Articlefrom .models import Article# создание классового вида class ArticleDetailView(DetailView):    # модель на основе которой мы хотим создать вид    model = Article    # имя, которое будет использовано в html шаблоне (это другой слой - рассмотрим далее)    context_object_name = 'article'    # имя html шаблона, на основе которого будет создана веб страница    template_name = 'article/article_detail.html'

Классовый вид на основе DetailView автоматически соберет всю информацию модели Article и затем отправит ее в следующий слой Django:

Слой шаблоны


В коде выше template_name это переменная для названия html шаблона, который будет использован для формирования веб страницы, которая и будет показана пользователю. Вот пример кода из такого шаблона:
  <h1>{{ article.title }}</h1>   <div>{{ article.content }}</div>       

{{ article.title }} и {{ article.content }} это название статьи и ее содержание, заключенные в html теги. title и content повторяют название полей модели Article, которую вы создали в слое Модели. Слово article мы указали в context_object_name в виде. В результате обработки Django вставит соответствующие поля из Article в шаблон.

Резюме


Это общий взгляд на некоторые Django слои. Описанная концепция позволяет разделить отдельные блоки программы. В слое модели вы создаете удобные абстракции вашей базы данных, в слое виды вы решаете, какие данные вы хотите показать, и в слое шаблоны вы создаете уже дизайн ваших страниц на основе html и добавляете в шаблоны немного логики с помощью языка Jinja это из примера с фигурными скобками {{ article.name }}.

Я тут не затронул довольно много важных тем например связи между вашими моделями. У каждой статьи может быть один автор или несколько авторов Django с легкостью справится с любым из перечисленных вариантов, и с помощью одной строки в Python вы сможете получить автора статьи или же коллекцию авторов в виде экземпляров класса Автор, созданного на основе models.Model.

Но откуда столько файлов?


Если вы создаете какое-то сложное приложение с кучей Моделей, видов и т.п. то это огромное количество кода надо как-то разбить на отдельные файлы. И файлы желательно организовать по папкам так, чтобы файл с моделями статьи был в той же папке что и виды статьи.
Вот тут приходит еще одна ключевая идея Django приложения, которые заслуживают отдельного поста.
Подробнее..
Категории: Python , Django

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

06.07.2020 18:16:50 | Автор: admin


Здесь вас ждёт список вышедших за июнь материалов на английском языке. Все они написаны без лишнего академизма, содержат примеры кода и ссылки на непустые репозитории. Большинство упомянутых технологий находятся в открытом доступе и не требуют сверхмощного железа для тестирования.

Image GPT

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



Face depixelizer

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


DeepFaceDrawing

Если работы с пиксельными изображениями недостаточно, и нужно составить фотографию с портретом человека по примитивному наброску, то и для этого уже появился инструмент на основе DNN. По замыслу создателей нужны только общие начертания, а не профессиональные эскизы модель далее сама восстановит лицо человека, которое будет совпадать с наброском. Система создана с использованиям фремворка Jittor, как обещают создатели, исходный код на Pytorch скоро тоже будет добавлен в репозиторий проекта.



PIFuHD

С реконструкциями лиц разобрались, что насчет остального тела? Благодаря развитию DNN, стало возможным 3D-моделирование фигуры человека на основе двухмерного фото. Основное ограничение было связано с тем, что точные прогнозы требуют анализировать широкий контекст и исходные данные в высоком разрешении. Многоуровневая архитектура модели и способность к сквозному обучению помогут решить эту проблему. На первом уровне для экономии ресурсов изображение анализируется целиком в низком разрешении. После этого формируется контекст, и на более детальном уровне модель оценивает геометрию, анализируя изображение с высоким разрешением.



RepNet

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



SPICE model

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

Детектор социального дистанцирования

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



Распознавание типовых документов

На сегодняшний день существуют тысячи вариаций самых распространенных шаблонных документов вроде квитанций, счетов и чеков. Существующие автоматизированные системы, которые рассчитаны на работу с очень ограниченным типом шаблонов. Google предлагает использовать для этого машинное обучение. В статье рассматривается архитектура модели и результаты полученных данных. Вскоре инструмент станет частью сервиса Document AI.

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

Израильский стартап Trigo делится опытом применения машинного обучения и компьютерного зрения для take-and-go ритейла. Компания является поставщиком системы, которая позволяет магазинам работать без кассы. Авторы рассказывают какие задачи перед ними стояли и объясняют, почему выбрали PyTorch в качестве фреймворка для машинного обучения, а Allegro AI Trains для инфраструктуры и как им удалось наладили процесс разработки.

На этом все, спасибо за внимание!
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru