-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51

Habrahabr/New








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/new/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-new-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

Продвинутое конфигурирование Docker Compose (перевод)

Понедельник, 11 Сентября 2017 г. 19:02 + в цитатник
Tully вчера в 19:02 Администрирование

Продвинутое конфигурирование Docker Compose (перевод)

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



    Контроль порядка запуска


    Docker Compose запускает контейнеры в порядке зависимостей, используя опцию depends_on, чтобы указывать, когда запускается сервис. Для определения порядка запуска Compose применяет depends_on, links, volumes_from и network_mode: «service: ...».

    Если контейнер должен дождаться состояния “ready” другого контейнера, можно использовать инструменты wait-for-it или dockerize. Они будут проверять хосты и порты до тех пор, пока TCP соединение не будет подтверждено. Для включения принудительного ожидания в композицию необходимо добавить entrypoint:

    version: '2'
    
    services:
        web:
            build: .
            ports:
                - "80:8000"
            depends_on:
                - db
            entrypoint: "./wait-for-it.sh db:5432"
        db:
            image: postgres

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

    Запуск нескольких копий Compose проекта


    Если вам понадобится несколько копий окружений с одинаковой композицией (или docker-compose.yml файлом), просто запустите docker-compose up -p new_project_name.

    Переменные среды


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

    $ TAG="latest"
    $ echo $TAG
    latest
    $ DB="postgres"
    $ echo $DB
    postgres

    Использовать переменную среды в Docker Compose файле:

    db:
        image: "${DB}:$TAG"

    Docker Compose принимает и ${DB}, и $TAG. Также можно задать переменные среды в контейнерах:

    web:
        environment:
            - PRODUCTION=1

    Можно даже передать переменные среды внутрь контейнеров:

    $ PRODUCTION=1
    $ echo $PRODUCTION
    1

    Файл окружения


    Для гарантии передачи переменной среды, необходимо хранить её в файле среды. Назовите файл .env и сохраните в рабочей директории. Docker Compose игнорирует пустые строки (используйте их для лучшей читаемости) и код, начинающийся с # (то есть комментарии). Вы можете присвоить переменные для дальнейшей подстановки, а также задать переменные Compose CLI:

    COMPOSE_API_VERSION
    COMPOSE_FILE
    COMPOSE_HTTP_TIMEOUT
    COMPOSE_PROJECT_NAME
    DOCKER_CERT_PATH
    DOCKER_HOST
    DOCKER_TLS_VERIFY

    Пример файла среды:

    # ./.env 
    # для нашей промежуточной среды
    
    COMPOSE_API_VERSION=2
    COMPOSE_HTTP_TIMEOUT=45
    DOCKER_CERT_PATH=/mycerts/docker.crt
    EXTERNAL_PORT=5000

    Использование нескольких файлов Docker Compose


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

    Docker Compose по умолчанию читает два файла: docker-compose.yml и docker-compose.override.yml. В файле docker-compose-override.yml можно хранить переопределения для существующих сервисов или определять новые. Чтобы использовать несколько файлов (или файл переопределения с другим именем), необходимо передать -f в docker-compose up (порядок имеет значение):

    $ docker-compose up -f my-override-1.yml my-overide-2.yml

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

    В этом примере новое значение переписывает старое и command запускает my_new_app.py:

    # оригинальный сервис
    command: python my_app.py
    
    # новый сервис
    command: python my_new_app.py
    При использовании опции с несколькими значениями (ports, expose, external_links, dns, dns_search и tmpfs), Docker Compose объединяет значения (в примере ниже Compose открывает порты 5000 и 8000):
    
    # оригинальный сервис
    expose:
        - 5000
        
    # новый сервис
    expose:
        - 8000

    Если используются environment, labels, volumes, или devices, Docker Compose объединяет результаты. В следующем примере три переменные среды становятся FOO=Hello и BAR=«Python Dev!»:

    # оригинальный сервис
    environment:
        - FOO=Hello
        - BAR=World
    
    # новый сервис
    environment:
        - BAR="Python Dev!"
    
    Различные среды
    Начнём с базового Docker Compose файла для приложения (docker-compose.yml):
    web:
        image: "my_dockpy/my_django_app:latest"
        links:
            - db
            - cache
    
    db:
        image: "postgres:latest"
    
    cache:
        image: "redis:latest"
    

    На сервере разработки мы хотим открыть порты, смонтировать код как том и создать веб-изображение (docker-compose.override.yml):

    web:
        build: .
        volumes:
            - ".:/code"
        ports:
            - "8883:80"
        environment:
            DEBUG: "true"
    
    db:
        command: "-d"
        ports:
            - "5432:5432"
    
    cache:
        ports:
            - "6379:6379"

    docker-compose up автоматически читает файл переопределения и применяет его. Также понадобится продакшн версия Docker Compose приложения, которую назовём docker-compose.production.yml:

    web:
        ports:
            - "80:80"
        environment:
            PRODUCTION: "true"
    
    cache:
        environment:
            TTL: "500"
    

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

    $ docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d

    Примечание: Docker Compose читает docker-compose.production.yml, но не docker-compose.override.yml.

    Задачи администрирования


    Необходимо запустить административную копию приложения, чтобы иметь возможность выполнять определённые задачи, например, бэкапить базу данных. Используя уже упомянутый файл docker-compose.yml, создадим файл docker-compose.admin.yml:

    dbadmin:
        build: database_admin/
        links:
            - db

    А затем, выполним следующую команду:

    $ docker-compose -f docker-compose.yml -f docker-compose.admin.yml run dbadmin db-backup

    Расширение сервисов

    Совместно использовать конфигурации можно с помощью поля extends. Также оно позволяет делиться опциями между разными проектами.

    Создать common-services.yml (можно назвать его как угодно):

    webapp:
        build: .
        ports:
            - "8000:8000"
        volumes:
            - "/data"

    Создать базовый docker-compose.yml. Например:
    web:
        extends:
            file: common-services.yml
            service: webapp

    Кроме этого, определить (или переопределить) конфигурацию и добавить другие сервисы можно локально:

    web:
        extends:
            file: common-services.yml
            service: webapp
        environment:
            - DEBUG=1
        cpu_shares: 5
        links:
            - db
    
    important_web:
        extends: web
        cpu_shares: 10
    
    db: 
        image: postgres
    

    Распространённые проблемы


    Контроль порядка запуска


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

    Файл окружения


    Если вы определяете переменные среды в оболочке или через командную строку во время работы docker-compose, эти переменные будут иметь приоритет над .env файлом.

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

    COMPOSE_API_VERSION=    # должно быть 2
    COMPOSE_HTTP_TIMEOUT=   # мы используем 30 на продакшне и 120 в разработке
    DOCKER_CERT_PATH=       # храните путь сертификации здесь
    EXTERNAL_PORT=          # установите внешний порт здесь (запомните, 5000 для Flask и 8000 для Django)

    Использование нескольких файлов Docker Compose


    Обратите внимание, Docker Compose объединяет файлы в заданном вами порядке.

    Расширение сервисов


    Сервисы никогда не делят links, volumes_from или depends_on, используя extends; links и volumes_from всегда должны быть определены локально.

    The end


    С вопросами, предложениями и замечаниями — welcome в комментарии. А если хочется похоливарить в режиме реального времени — присоединяйтесь к нам на Дне открытых дверей курса.
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337688/


    Метки:  

    Oracle фактически ликвидирует Sun

    Понедельник, 11 Сентября 2017 г. 18:31 + в цитатник
    temujin сегодня в 18:31 Управление

    Oracle фактически ликвидирует Sun

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


      Похоже, что в Oracle приняли решение окончательно избавиться от трудовых ресурсов, составляющих костяк Sun Microsystems. Массовые увольнения затронули около 2500 сотрудников, работающих над операционной системой Solaris, платформой SPARC и системами хранения данных ZFS Storage Appliance.





      Это не рядовая трансформация — оптимизация, а настоящая бойня. По мнению создателя системы динамической отладки Dtrace Брайэна Кантрилла (Bryan Cantrill) на сей раз нанесен непоправимый ущерб, в результате потери 90% производственных кадров подразделения Solaris, включая все руководство.


      От Solaris до illumos


      В 2009 г. Oracle приобрел испытывающую серьезнейшие трудности на рынке Sun Microsystems за 5.6 млрд. долларов США. Компания теряла позиции на рынке вследствие лавинообразного распространения Linux в качестве серверной ОС, успеха платформы amd64 и невнятной стратегии по взаимоотношениям с сообществом открытого ПО. Solaris стал открытым слишком поздно — лишь в 2005 г., причем открытым не полностью, отдельные элементы ОС, такие как локализация и некоторые драйвера, оставались проприетарными. Затем появился OpenSolaris, однако точкой сбора сообщества он не сумел стать. То ли дело была в лицензии CDDL, то ли проблема была в том, что Sun пыталась манипулировать проектом. Трудно сказать почему именно, но не взлетел.


      Kicked butt, had fun, didn't cheat, loved our customers, changed computing forever. Scott McNealy

      Эпитафия Скота МакНили как нельзя лучше отражает жизненный путь компании — отлично развлекались, не дурили головы своим заказчикам и навсегда изменили ИТ. Довольно быстро стало очевидно, что Solaris Ораклу попросту не нужен, и развивать его он не намерен. Затем 13 августе 2010 г. случилось одно из самых позорных событий в истории открытого ПО — компания втихую закрыла исходный код OS Solaris. Никаких официальных заявлений на сей счет не последовало.


      We will distribute updates to approved CDDL or other open source-licensed code following full releases of our enterprise Solaris operating system. In this manner, new technology innovations will show up in our releases before anywhere else. We will no longer distribute source code for the entirety of the Solaris operating system in real-time while it is developed, on a nightly basis.

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


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





      О принципах руководства компании. Оставшись без мудрого руководства манагеров инженеры выдали гору инноваций: ZFS, DTrace, Zones и много других.


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

      О закрытии исходного кода OpenSolaris.


      Это ОТВРАТИТЕЛЬНАЯ выходка со стороны корпорации. Вот из-за такого поведения мы становимся циничными и подозрительными.

      О последствиях закрытия исходников OpenSolaris для нового проекта ОС illumos — полностью открытого форка OpenSolaris.


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

      Вскоре после этого из Oracle ушли все разработчики DTrace, создатели ZFS, команда zones и сетевики. Вся разработка и инновация на этих направлениях далее происходила операционной системе illumos, где осела диаспора программистов из Sun Solaris. Благодаря особенностям открытых лицензий, в том числе CDDL, в рамках которой шла разработка OpenSolaris, Oracle не может претендовать на все последующие улучшения в коде illumos. То есть может, но только в рамках своего же проекта с открытым кодом. Сценарии Судного Дня работают как надо.


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


      Факты россыпью


      • Главная цель: выдоить патентные отчисления и штрафы у Google за использование Java в ОС Андроид, ставки были крупные — $8 млрд. Не срослось.
      • В целом монетизация Java не удалась, Oracle передает NetBeans Apache Foundation, верное решение.
      • Также решено не запускать платформу Sun Cloud.
      • Из-за бюрократических проволочек вокруг заплаток безопасности для MySQL, появился форк MariaDB, куда перешло значительное число разработчиков и часть сообщества. Их оказалось достаточно для новой компании.

      Выводы


      Sun Microsystems еще недавно — живая легенда и лучшее, что когда-либо было в Unix. Вот лишь небольшая часть их наследия.


      • NFS
      • RPC
      • ZFS
      • DTrace
      • Zones
      • Fault Management Architecture
      • Service Management Facility

      Компания Oracle имела все возможности для того, чтобы развивать и поддерживать OpenSolaris, но вместо этого закрыла исходники и с тех пор Solaris уже не имел будущего. Когда тяжба с компанией Google за использования Java в мобильной ОС Андроид закончилась пшиком в Oracle потеряли к активам Sun Microsystems всякий интерес. Вместо этого компания будет продавать ПО на основе собственной операционной системы — Unbreakable Linux.


      Материалы по теме


      Original source: habrahabr.ru (comments, light).

      https://habrahabr.ru/post/337682/


      Метки:  

      Микросервисы — MIF на C++

      Понедельник, 11 Сентября 2017 г. 18:20 + в цитатник
      NYM вчера в 18:20 Разработка

      Микросервисы — MIF на C++

        Около трех лет назад у меня была идея создания небольшого каркаса для разработки небольших сервисов, которые могли бы как-то взаимодействовать между собой, предоставлять API во вне, работать с базами данных и кое-что по мелочи еще. Во время решения некоторых рабочих задач, окончательно сформировалась идея своего проекта, близкого к решению рабочих задач. Примерно год назад все это сформировалось в проект MIF (MetaInfo Framework). Предполагалось, что с его помощью можно будет решать такие задачи, как:

        • Разработка легковесных HTTP сервисов
        • Коммуникация микросервисов через передаваемые между процессами интерфейсы
        • Сериализация и десериализация на базе рефлексии структур данных в разные форматы
        • Работа с базами данных
        • Некоторые вспомогательные компоненты для создания каркасов сервисов

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

        Введение


        Выход нового стандарта C++, подготовка следующего. Проходят года, а рефлексии в C++ нет. Обсуждалась возможность появления рефлексии в C++, и казалось, что в очередной стандарт она будет включена. Нет… Может быть рефлексия и не нужна? Может быть… Но есть задачи, где она могла бы быть полезна: межпроцессное взаимодействие (IPC, RPC, REST API, микросервисы), работа с базами данных (ORM), различные (де)сериализаторы и т.д. — в общем есть пространство для применения.

        Компилятор имеет всю информацию о типах, и почему бы ему не поделиться ей с разработчиками? Попробую предположить, что всего лишь нет документа, по которому разработчики компиляторов должны отпускать честным разработчикам прочего программного обеспечения эту информацию. Такой документ — это стандарт C++, в котором никак не появится регламент «отоваривания».

        Во многих языках рефлексия есть. C++ считается языком для разработки программ, где важна производительность и, казалось бы, одна из его сфер применения — это веб-разработка. Разработка backend сервисов. В этой отрасли есть и REST API, и ORM и всевозможная сериализация во что угодно (часто угодно в json). Для решения этих задач приходится использовать готовые библиотеки или писать свои, в которых данные C++ связываются вручную с другими сущностями, например, отображение структур в C++ в формат json. Иногда с помощью макросов добавляется метаинформация о типе, которая используется в дальнейшем при построении REST API, ORM и т.д. Есть даже решения с плагинами для компилятора, например, odb.

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

        Предлагаемое решение основано на добавлении метаинформации к C++ типам с последующим ее использованием при решении разных задач. Можно сказать, что «тип нужно обуть в метаинформацию о нем» после чего он сможет смело шагать через границы процессов и датацентров и иметь возможность представляться по разному (binary, json, xml, text, etc) с минимальным вмешательством разработчика.

        Часто подобные задачи решаются с применением кодогенерации, например, thrift, gSOAP, protobuf и т.д. Мне хотелось получить свое решение, которое исключит внешнюю кодогенерацию, а все необходимое будет добавлено к типу в момент компиляции, и при этом есть желание максимально сохранить естественный синтаксис языка C++, не создавая новый язык в существующем языке.

        MIF в примерах


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

        Перечисленные в начале поста задачи предполагают, что будет какая-то возможность отображать C++ типы данных для сериализации, объектно-ориентированного межпроцессного взаимодействия, реализации REST API и т.д. С этого предлагаю и начать…

        Рефлексия


        Рефлексия — основа всего проекта, позволяющая решать мои прикладные задачи.

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

        struct Data
        {
            int field1 = 0;
            std::string field2;
        };
        

        Решение поставленной задачи могло выглядеть так:

        int main()
        {
            Data data;
            data.field1 = 100500;
            data.field2 = "Text";
            using Meta = Mif::Reflection::Reflect;
            std::cout << "Struct name: " << Meta::Name::GetString() << std::endl;
            std::cout << "Field count: " << Meta::Fields::Count << std::endl;
            std::cout << "Field1 value: " << data.field1 << std::endl;
            std::cout << "Field2 value: " << data.field2 << std::endl;
            std::cout << "Modify fields." << std::endl;
            data.*Meta::Fields::Field<0>::Access() = 500100;
            data.*Meta::Fields::Field<1>::Access() = "New Text.";
            std::cout << "Field1 value: " << data.field1 << std::endl;
            std::cout << "Field2 value: " << data.field2 << std::endl;
            return 0;
        }
        

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

        MIF_REFLECT_BEGIN(Data)
            MIF_REFLECT_FIELD(field1)
            MIF_REFLECT_FIELD(field2)
        MIF_REFLECT_END()
        
        MIF_REGISTER_REFLECTED_TYPE(Data)
        

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

        Код примера целиком
        // STD
        #include 
        #include 
        
        // MIF
        #include reflection/reflect_type.h>
        #include reflection/reflection.h>
        
        struct Data
        {
            int field1 = 0;
            std::string field2;
        };
        
        MIF_REFLECT_BEGIN(Data)
            MIF_REFLECT_FIELD(field1)
            MIF_REFLECT_FIELD(field2)
        MIF_REFLECT_END()
        
        MIF_REGISTER_REFLECTED_TYPE(Data)
        
        int main()
        {
            Data data;
            data.field1 = 100500;
            data.field2 = "Text";
            using Meta = Mif::Reflection::Reflect;
            std::cout << "Struct name: " << Meta::Name::GetString() << std::endl;
            std::cout << "Field count: " << Meta::Fields::Count << std::endl;
            std::cout << "Field1 value: " << data.field1 << std::endl;
            std::cout << "Field2 value: " << data.field2 << std::endl;
            std::cout << "Modify fields." << std::endl;
            data.*Meta::Fields::Field<0>::Access() = 500100;
            data.*Meta::Fields::Field<1>::Access() = "New Text.";
            std::cout << "Field1 value: " << data.field1 << std::endl;
            std::cout << "Field2 value: " << data.field2 << std::endl;
            return 0;
        }
        

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

        Пример обхода структуры
        // STD
        #include 
        #include 
        #include 
        #include 
        
        // MIF
        #include reflection/reflect_type.h>
        #include reflection/reflection.h>
        #include serialization/traits.h>
        
        struct Base1
        {
            int field1 = 0;
            bool field2 = false;
        };
        
        struct Base2
        {
            std::string field3;
        };
        
        struct Nested
        {
            int field = 0;
        };
        
        struct Data : Base1, Base2
        {
            int field4 = 0;
            std::string field5;
            std::map field6;
        };
        
        MIF_REFLECT_BEGIN(Base1)
            MIF_REFLECT_FIELD(field1)
            MIF_REFLECT_FIELD(field2)
        MIF_REFLECT_END()
        
        MIF_REFLECT_BEGIN(Base2)
            MIF_REFLECT_FIELD(field3)
        MIF_REFLECT_END()
        
        MIF_REFLECT_BEGIN(Nested)
            MIF_REFLECT_FIELD(field)
        MIF_REFLECT_END()
        
        MIF_REFLECT_BEGIN(Data, Base1, Base2)
            MIF_REFLECT_FIELD(field4)
            MIF_REFLECT_FIELD(field5)
            MIF_REFLECT_FIELD(field6)
        MIF_REFLECT_END()
        
        MIF_REGISTER_REFLECTED_TYPE(Base1)
        MIF_REGISTER_REFLECTED_TYPE(Base2)
        MIF_REGISTER_REFLECTED_TYPE(Nested)
        MIF_REGISTER_REFLECTED_TYPE(Data)
        
        class Printer final
        {
        public:
            template 
            static typename std::enable_if(), void>::type
            Print(T const &data)
            {
                using Meta = Mif::Reflection::Reflect;
                using Base = typename Meta::Base;
                PrintBase<0, std::tuple_size::value, Base>(data);
                std::cout << "Struct name: " << Meta::Name::GetString() << std::endl;
                Print<0, Meta::Fields::Count>(data);
            }
        
            template 
            static typename std::enable_if
                <
                    !Mif::Reflection::IsReflectable() && !Mif::Serialization::Traits::IsIterable(),
                    void
                >::type
            Print(T const &data)
            {
                std::cout << data << std::boolalpha << std::endl;
            }
        
            template 
            static typename std::enable_if
                <
                    !Mif::Reflection::IsReflectable() && Mif::Serialization::Traits::IsIterable(),
                    void
                >::type
            Print(T const &data)
            {
                for (auto const &i : data)
                    Print(i);
            }
        
        private:
            template 
            static typename std::enable_if::type
            Print(T const &data)
            {
                using Meta = Mif::Reflection::Reflect;
                using Field = typename Meta::Fields::template Field;
                std::cout << Field::Name::GetString() << " = ";
                Print(data.*Field::Access());
                Print(data);
            }
        
            template 
            static typename std::enable_if= N, void>::type
            Print(T const &)
            {
            }
        
            template 
            static void Print(std::pair const &p)
            {
                Print(p.first);
                Print(p.second);
            }
        
            template 
            static typename std::enable_if::type
            PrintBase(T const &data)
            {
                using Type = typename std::tuple_element::type;
                Print(static_cast(data));
                PrintBase(data);
            }
        
            template 
            static typename std::enable_if= N, void>::type
            PrintBase(T const &)
            {
            }
        };
        
        int main()
        {
            Data data;
            data.field1 = 1;
            data.field2 = true;
            data.field3 = "Text";
            data.field4 = 100;
            data.field5 = "String";
            data.field6["key1"].field = 100;
            data.field6["key2"].field = 200;
            Printer::Print(data);
            return 0;
        }
        

        Результат

        Struct name: Base1
        field1 = 1
        field2 = true
        Struct name: Base2
        field3 = Text
        Struct name: Data
        field4 = 100
        field5 = String
        field6 = key1
        Struct name: Nested
        field = 100
        key2
        Struct name: Nested
        field = 200
        

        Пример является прототипом для полноценного сериализатора, т.к. на основе добавленной метаинформации сериализует структуру в поток (в примере в стандартный поток вывода). Для определения является ли тип контейнером используется функция из пространства имен Serialization. В этом пространстве имен собраны готовые сериализаторы в json и boost.archives (xml, text, binary). Построены они по принципу близкому к приведенному в примере. Если нет необходимости расширить фреймворк своим сериализатором, то писать подобный код нет необходимости.

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

        #include reflection/reflect_type.h>
        #include serialization/json.h>
        
        // Data and meta
        
        int main()
        {
            Data data;
        
            // Fill data
        
            auto const buffer = Mif::Serialization::Json::Serialize(data); // Сериализация в json
        
            std::cout << buffer.data() << std::endl;
            return 0;
        }
        

        Результат работы
        {
        	"Base1" : 
        	{
        		"field1" : 1,
        		"field2" : true
        	},
        	"Base2" : 
        	{
        		"field3" : "Text"
        	},
        	"field4" : 100,
        	"field5" : "String",
        	"field6" : 
        	[
        		{
        			"id" : "key1",
        			"val" : 
        			{
        				"field" : 100
        			}
        		},
        		{
        			"id" : "key2",
        			"val" : 
        			{
        				"field" : 200
        			}
        		}
        	]
        }
        

        Для разнообразия можно попробовать воспользоваться сериализацией boost.archives в формате xml.

        Сериализация в xml с помощью boost.archives
        // BOOST
        #include archive/xml_oarchive.hpp>
        
        // MIF
        #include reflection/reflect_type.h>
        #include serialization/boost.h>
        
        // Data and meta
        
        int main()
        {
            Data data;
        
            // Fill data
        
            boost::archive::xml_oarchive archive{std::cout};
            archive << boost::serialization::make_nvp("data", data);
            return 0;
        }
        

        Результат работы
        
        
        
        
        	
        		1
        		1
        	
        	
        		Text
        	
        	100
        	String
        	
        		2
        		0
        		
        			key1
        			
        				100
        			
        		
        		
        			key2
        			
        				200
        			
        		
        	
        
        
        

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

        Сериализации и десериализаторы нескольких форматов реализованы в каркасе. А при необходимости добавить поддержку нового формата можно сделать на примере класса Printer или взяв за основу (де)сериализатор формата json, используя предложенное API для обхода C++ структур данных. Есть некоторые ограничения на типы (куда же без них), но об этом позже.

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

        Межпроцессное взаимодействие


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

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

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

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

        Решение подобных задач сводится к нескольким однотипным шагам

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

        Пример complex_type

        Общая часть


        Структуры данных

        // data.h
        
        namespace Service
        {
            namespace Data
            {
        
                using ID = std::string;
        
                struct Human
                {
                    std::string name;
                    std::string lastName;
                    std::uint32_t age = 0;
                };
        
                enum class Position
                {
                    Unknown,
                    Developer,
                    Manager
                };
        
                struct Employee
                    : public Human
                {
                    Position position = Position::Unknown;
                };
        
                using Employees = std::map;
        
            }   // namespace Data
        }   // namespace Service
        

        Метаинформация

        // meta/data.h
        
        namespace Service
        {
            namespace Data
            {
                namespace Meta
                {
        
                    using namespace ::Service::Data;
        
                    MIF_REFLECT_BEGIN(Human)
                        MIF_REFLECT_FIELD(name)
                        MIF_REFLECT_FIELD(lastName)
                        MIF_REFLECT_FIELD(age)
                    MIF_REFLECT_END()
        
                    MIF_REFLECT_BEGIN(Position)
                        MIF_REFLECT_FIELD(Unknown)
                        MIF_REFLECT_FIELD(Developer)
                        MIF_REFLECT_FIELD(Manager)
                    MIF_REFLECT_END()
        
                    MIF_REFLECT_BEGIN(Employee, Human)
                        MIF_REFLECT_FIELD(position)
                    MIF_REFLECT_END()
        
                }   // namespace Meta
            }   // namespace Data
        }   // namespace Service
        
        MIF_REGISTER_REFLECTED_TYPE(::Service::Data::Meta::Human)
        MIF_REGISTER_REFLECTED_TYPE(::Service::Data::Meta::Position)
        MIF_REGISTER_REFLECTED_TYPE(::Service::Data::Meta::Employee)
        

        Интерфейс

        // imy_company.h
        
        namespace Service
        {
        
            struct IMyCompany
                : public Mif::Service::Inherit
            {
                virtual Data::ID AddEmployee(Data::Employee const &employee) = 0;
                virtual void RemoveAccount(Data::ID const &id) = 0;
                virtual Data::Employees GetEmployees() const = 0;
            };
        
        }   // namespace Service
        

        Метаинформация

        // ps/imy_company.h
        
        namespace Service
        {
            namespace Meta
            {
        
                using namespace ::Service;
        
                MIF_REMOTE_PS_BEGIN(IMyCompany)
                    MIF_REMOTE_METHOD(AddEmployee)
                    MIF_REMOTE_METHOD(RemoveAccount)
                    MIF_REMOTE_METHOD(GetEmployees)
                MIF_REMOTE_PS_END()
        
            }   // namespace Meta
        }   // namespace Service
        
        MIF_REMOTE_REGISTER_PS(Service::Meta::IMyCompany)
        

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

        Определение интерфейса — это определение C++ структуры данных, содержащей только чисто виртуальные методы.

        Для интерфейсов было другое пожелание — возможность запрашивать из одного интерфейса другие, содержащиеся в реализации, и, возможно, не связанные в единую иерархию. Поэтому определяемый интерфейс всегда должен наследовать Mif::Service::IService или любой другой, наследуемый от Mif::Service::IService. Есть множественное наследование. Наследование делается через промежуточную сущность Mif::Service::Inherit. Это шаблон с переменным числом параметров. Его параметрами служат наследуемые интерфейсы или реализации (inheritance). Это необходимо для реализации механизма запроса интерфейсов такого же, как dynamic_cast, но работающего и за границами процесса.

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

        Указывать при добавлении метаинформации к интерфейсу его базовые интерфейсы не надо. Вся иерархия будет найдена в момент компиляции. Здесь небольшая вспомогательная сущность Mif::Service::Inherit играет свою основную роль в поиске наследников и связанной с ними метаинформации.

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

        Определив общие сущности, осталось реализовать серверное и клиентское приложения.

        У каждого интерфейса может быть множество реализаций. Их как-то нужно отличать. При создании объекта нужно явно указывать желаемую реализацию. Для этого нужны идентификаторы реализаций интерфейсов и их связь с реализациями.

        Для удобства и отказа от «магических значений в коде» идентификаторы реализаций лучше вынести в один или несколько заголовочных файлов. В проекте MIF в качестве идентификатора используются числа. Чтобы им как-то придать уникальность при этом не городить какие-то счетчики или все не помещать в один единый enum и иметь возможность логически разнести идентификаторы по разным файлам и пространствам имен, предлагается в качестве идентификаторов использовать crc32 от строки, с придумыванием уникальности которой проблем у разработчика должно быть меньше.

        Для реализации интерфейса IMyCompany нужен идентификатор

        // id/service.h
        
        namespace Service
        {
            namespace Id
            {
        
                enum
                {
                    MyCompany = Mif::Common::Crc32("MyCompany")
                };
        
            }   // namespace Id
        }   // namespace Service
        

        Серверное приложение


        Реализация IMyCompany

        // service.cpp
        
        // MIF
        #include common/log.h>
        #include reflection/reflection.h>
        #include service/creator.h>
        
        // COMMON
        #include "common/id/service.h"
        #include "common/interface/imy_company.h"
        #include "common/meta/data.h"
        
        namespace Service
        {
            namespace Detail
            {
                namespace
                {
        
                    class MyCompany
                        : public Mif::Service::Inherit
                    {
                    public:
                        // …
        
                    private:
                        // …
        
                        // IMyCompany
                        virtual Data::ID AddEmployee(Data::Employee const &employee) override final
                        {
                            // ...
                        }
        
                        virtual void RemoveAccount(Data::ID const &id) override final
                        {
                            // ...                }
                        }
        
                        virtual Data::Employees GetEmployees() const override final
                        {
                            // ...
                        }
                    };
        
                }   // namespace
            }   // namespace Detail
        }   // namespace Service
        
        MIF_SERVICE_CREATOR
        (
            ::Service::Id::MyCompany,
            ::Service::Detail::MyCompany
        )
        

        Есть несколько моментов, на которые хотелось бы обратить внимание:
        • Наследование в реализации так же через Mif::Service::Inherit. Это не обязательно, но можно считать хорошим тоном и будет полезно при реализации нескольких интерфейсов с наследованием части ранее уже реализованных интерфейсов.
        • Вся реализация может и, предпочтительно, должна быть сделана в одном cpp-файле, без разделения на h и cpp файлы. Что позволяет усилить инкапсуляцию и в больших проектах уменьшить время компиляции за счет того, что все необходимые включаемые файлы реализации находятся в файле с реализацией. При их модификации перекомпиляции подвергается меньшее число зависимых cpp-файлов.
        • Каждая реализация имеет точку входа — MIF_SERVICE_CREATOR, что является фабрикой реализации. Параметрами являются класс-реализация, идентификатор и при необходимости переменное количество параметров, передаваемых в конструктор реализации.

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

        // MIF
        #include application/tcp_service.h>
        
        // COMMON
        #include "common/id/service.h"
        #include "common/ps/imy_company.h"
        
        class Application
            : public Mif::Application::TcpService
        {
        public:
            using TcpService::TcpService;
        
        private:
            // Mif.Application.Application
            virtual void Init(Mif::Service::FactoryPtr factory) override final
            {
                factory->AddClass<::Service::Id::MyCompany>();
            }
        };
        
        int main(int argc, char const **argv)
        {
            return Mif::Application::Run(argc, argv);
        }
        

        При создании точки входа нужно реализовать свой класс приложения — наследник от базового класса приложений или от одного из предопределенных шаблонов приложений. В переопределенном методе Init нужно добавить к фабрике все существующие реализации интерфейсов, которые будет экспортировать сервис (factory->AddClass). В метод AddClass можно передавать параметры конструктора реализации.

        Сервис использует предопределенный транспорт tcp, сериализацию на базе boost.archive в бинарном формате со сжатием gzip данных для обмена информацией об интерфейсах, методах, параметрах, возвращаемых результатах, исключениях и экземплярах объектов.

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

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

        Клиентское приложение


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

        // MIF
        #include application/tcp_service_client.h>
        #include common/log.h>
        
        // COMMON
        #include "common/id/service.h"
        #include "common/ps/imy_company.h"
        
        class Application
            : public Mif::Application::TcpServiceClient
        {
        public:
            using TcpServiceClient::TcpServiceClient;
        
        private:
            void ShowEmployees(Service::Data::Employees const &employees) const
            {
                // ...
            }
        
            // Mif.Application.TcpServiceClient
            virtual void Init(Mif::Service::IFactoryPtr factory) override final
            {
                auto service = factory->Create(Service::Id::MyCompany);
                {
                    Service::Data::Employee e;
                    e.name = "Ivan";
                    e.lastName = "Ivanov";
                    e.age = 25;
                    e.position = Service::Data::Position::Manager;
                    auto const eId = service->AddEmployee(e);
                    MIF_LOG(Info) << "Employee Id: " << eId;
                }
                {
                    Service::Data::Employee e;
                    e.name = "Petr";
                    e.lastName = "Petrov";
                    e.age = 30;
                    e.position = Service::Data::Position::Developer;
                    auto const eId = service->AddEmployee(e);
                    MIF_LOG(Info) << "Employee Id: " << eId;
                }
                auto const &employees = service->GetEmployees();
                ShowEmployees(employees);
                if (!employees.empty())
                {
                    auto id = std::begin(employees)->first;
                    service->RemoveAccount(id);
                    MIF_LOG(Info) << "Removed account " << id;
                    auto const &employees = service->GetEmployees();
                    ShowEmployees(employees);
                    try
                    {
                        MIF_LOG(Info) << "Removed again account " << id;
                        service->RemoveAccount(id);
                    }
                    catch (std::exception const &e)
                    {
                        MIF_LOG(Warning) << "Error: " << e.what();
                    }
                }
            }
        };
        
        int main(int argc, char const **argv)
        {
            return Mif::Application::Run(argc, argv);
        }
        

        Результат


        Результат работы серверного приложения
        2017-08-09T14:01:23.404663 [INFO]: Starting network application on 0.0.0.0:55555
        2017-08-09T14:01:23.404713 [INFO]: Starting server on 0.0.0.0:55555
        2017-08-09T14:01:23.405442 [INFO]: Server is successfully started.
        2017-08-09T14:01:23.405463 [INFO]: Network application is successfully started.
        Press 'Enter' for quit.
        2017-08-09T14:01:29.032171 [INFO]: MyCompany
        2017-08-09T14:01:29.041704 [INFO]: AddEmployee. Name: Ivan LastName: Ivanov Age: 25 Position: Manager
        2017-08-09T14:01:29.042948 [INFO]: AddEmployee. Name: Petr LastName: Petrov Age: 30 Position: Developer
        2017-08-09T14:01:29.043616 [INFO]: GetEmployees.
        2017-08-09T14:01:29.043640 [INFO]: Id: 0 Name: Ivan LastName: Ivanov Age: 25 Position: Manager
        2017-08-09T14:01:29.043656 [INFO]: Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
        2017-08-09T14:01:29.044481 [INFO]: Removed employee account for Id: 0 Name: Ivan LastName: Ivanov Age: 25 Position: Manager
        2017-08-09T14:01:29.045121 [INFO]: GetEmployees.
        2017-08-09T14:01:29.045147 [INFO]: Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
        2017-08-09T14:01:29.045845 [WARNING]: RemoveAccount. Employee with id 0 not found.
        2017-08-09T14:01:29.046652 [INFO]: ~MyCompany
        
        2017-08-09T14:02:05.766072 [INFO]: Stopping network application ...
        2017-08-09T14:02:05.766169 [INFO]: Stopping server ...
        2017-08-09T14:02:05.767180 [INFO]: Server is successfully stopped.
        2017-08-09T14:02:05.767238 [INFO]: Network application is successfully stopped.
        

        Результат работы клиентского приложения
        2017-08-09T14:01:29.028821 [INFO]: Starting network application on 0.0.0.0:55555
        2017-08-09T14:01:29.028885 [INFO]: Starting client on 0.0.0.0:55555
        2017-08-09T14:01:29.042510 [INFO]: Employee Id: 0
        2017-08-09T14:01:29.043296 [INFO]: Employee Id: 1
        2017-08-09T14:01:29.044082 [INFO]: Employee. Id: 0 Name: Ivan LastName: Ivanov Age: 25 Position: Manager
        2017-08-09T14:01:29.044111 [INFO]: Employee. Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
        2017-08-09T14:01:29.044818 [INFO]: Removed account 0
        2017-08-09T14:01:29.045517 [INFO]: Employee. Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
        2017-08-09T14:01:29.045544 [INFO]: Removed again account 0
        2017-08-09T14:01:29.046357 [WARNING]: Error: [Mif::Remote::Proxy::RemoteCall] Failed to call remote method "IMyCompany::RemoveAccount" for instance with id "411bdde0-f186-402e-a170-4f899311a33d". Error: RemoveAccount. Employee with id 0 not found.
        2017-08-09T14:01:29.046949 [INFO]: Client is successfully started.
        2017-08-09T14:01:29.047311 [INFO]: Network application is successfully started.
        Press 'Enter' for quit.
         
        2017-08-09T14:02:02.901773 [INFO]: Stopping network application ...
        2017-08-09T14:02:02.901864 [INFO]: Stopping client ...
        2017-08-09T14:02:02.901913 [INFO]: Client is successfully stopped.
        2017-08-09T14:02:02.901959 [INFO]: Network application is successfully stopped.
        

        Да, исключения так же преодолевают границы процессов…

        [WARNING]: Error: [Mif::Remote::Proxy::RemoteCall] Failed to call remote method "IMyCompany::RemoveAccount" for instance with id "411bdde0-f186-402e-a170-4f899311a33d". Error: RemoveAccount. Employee with id 0 not found.
        

        Из сообщения видно, что был повторно вызван метод удаления информации о сотруднике с идентификатором 0. На стороне сервера уже такой записи нет, о чем сервер сообщил исключением с текстом «Employee with id 0 not found»/

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

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

        • Возможность запрашивать интерфейсы у реализации не объединенные в единую иерархию (не считая наследования от Mif::Service::IService).
        • Передавать указатели и умные указатели на интерфейсы между сервисами. Что может пригодиться для разных реализаций сервисов с обратными вызовами. Например, сервис на базе publish/subscribe. В качестве примера приведена реализация паттерна visitor, части которого находятся в разных процессах и взаимодействуют по tcp.

        HTTP


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

        Поддержка HTTP дает возможность строить сервисы, которые одновременно могут являться и web-сервером с каким-нибудь json REST API, и в то же время поддерживать ранее продемонстрированное взаимодействие через C++ интерфейсы.

        Комфорт часто связан с ограничениями. Таким ограничением при использовании HTTP-транспорта является отсутствие возможности обратного вызова методов, передаваемых интерфейсов. Решение на базе HTTP-транспорта не ориентировано на построение приложений publish / subscribe. Это связано с тем, что работа по HTTP предполагает подход «запрос-ответ». Клиент посылает запросы серверу и ждет ответа.

        Предложенный в MIF TCP-транспорт такого ограничения не имеет. Вызвать методы может как клиент, так и сервер. В этом случае размываются разграничения клиента и сервера. Появляется канал, по которому объекты могут общаться друг с другом, вызывая методы с любой его стороны. Это дает возможность двустороннего взаимодействия для построения архитектуры publish / subscribe, что было продемонстрировано в примере межпроцессным посетителем.

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

        Простой HTTP web-сервер


        // MIF
        #include application/http_server.h>
        #include common/log.h>
        #include net/http/constants.h>
        
        class Application
            : public Mif::Application::HttpServer
        {
        public:
            using HttpServer::HttpServer;
        
        private:
            // Mif.Application.HttpServer
            virtual void Init(Mif::Net::Http::ServerHandlers &handlers) override final
            {
                handlers["/"] = [] (Mif::Net::Http::IInputPack const &request,
                        Mif::Net::Http::IOutputPack &response)
                {
                    auto data = request.GetData();
        
                    MIF_LOG(Info) << "Process request \"" << request.GetPath()
                            << request.GetQuery() << "\"\t Data: "
                            << (data.empty() ? std::string{"null"} :
                            std::string{std::begin(data), std::end(data)});
        
                    response.SetCode(Mif::Net::Http::Code::Ok);
                    response.SetHeader(
                          Mif::Net::Http::Constants::Header::Connection::GetString(),
                          Mif::Net::Http::Constants::Value::Connection::Close::GetString());
        
                    response.SetData(std::move(data));
                };
            }
        };
        
        int main(int argc, char const **argv)
        {
            return Mif::Application::Run(argc, argv);
        }
        

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

        curl -iv -X POST "http://localhost:55555/" -d 'Test data'
        

        Всего около трех десятков строк кода и многопоточный HTTP эхо-сервер на базе каркаса приложений MIF готов. В качестве backend используется libevent. Простое тестирование с помощью утилиты ab выдает средний результат до 160K запросов в секунду. Конечно все зависит от «железа», на котором проводится тестирование, операционной системы, сети и т.д. Но для сравнения что-то подобное было сделано на Python и Go. Сервер на Python отработал в 2 раза медленнее, а на Go результат был лучше в среднем на 10% от результата сервера из примера. Так что, если Вы C++ разработчик и Python и Go Вам чужды или есть иные, возможно официально закрепленные внутренними распоряжениями в рамках проекта, причины любить только C++, то можно воспользоваться предложенным решением и получить неплохие результаты по скорости работы и срокам разработки …

        HTTP клиент


        Клиентская часть представлена классом Mif::Net::Http::Connection. Это основа HTTP-транспорта маршалинга C++ интерфейсы в MIF. Но пока не про маршалинг… Класс может использоваться отдельно от всей инфраструктуры межпроцессного взаимодействия MIF-микросервисов, например, для скачивания данных поставщиков, например, медиаконтента, прогноза погоды, биржевых котировок и т.д.

        Запустив приведенный выше эхо-сервис, можно к нему обратиться таким клиентом:

        // STD
        #include 
        #include 
        #include 
        #include 
        // MIF
        #include net/http/connection.h>
        #include net/http/constants.h>
        int main()
        {
            try
            {
                std::string const host = "localhost";
                std::string const port = "55555";
                std::string const resource = "/";
                std::promise promise;
                auto future = promise.get_future();
                Mif::Net::Http::Connection connection{host, port,
                        [&promise] (Mif::Net::Http::IInputPack const &pack)
                        {
                            if (pack.GetCode() == Mif::Net::Http::Code::Ok)
                            {
                                auto const data = pack.GetData();
                                promise.set_value({std::begin(data), std::end(data)});
                            }
                            else
                            {
                                promise.set_exception(std::make_exception_ptr(
                                    std::runtime_error{
                                        "Failed to get response from server. Error: "
                                        + pack.GetReason()
                                    }));
                            }
                        }
                    };
                auto request = connection.CreateRequest();
                request->SetHeader(Mif::Net::Http::Constants::Header::Connection::GetString(),
                        Mif::Net::Http::Constants::Value::Connection::Close::GetString());
                std::string data = "Test data!";
                request->SetData({std::begin(data), std::end(data)});
                connection.MakeRequest(Mif::Net::Http::Method::Type::Post,
                        resource, std::move(request));
                std::cout << "Response from server: " << future.get() << std::endl;
            }
            catch (std::exception const &e)
            {
                std::cerr << "Error: " << e.what() << std::endl;
                return EXIT_FAILURE;
            }
            return EXIT_SUCCESS;
        }
        

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

        HTTP web-сервер с двойным интерфейсом


        Пример подводит итог по поддержке HTTP в проекте MIF. В примере показан HTTP web-сервер, к которому можно обращаться из браузера или с помощью curl, а так же клиентом, работающим через C++ интерфейсы и не желающего знать ничего о транспорте и формате данных. Используется тот же каркас приложения Mif::Application::HttpServer, что и ранее. Ниже приведены необходимые для демонстрации фрагменты кода. Целиком пример доступен на github в примере http.

        Общая часть


        Интерфейс

        namespace Service
        {
            struct IAdmin
                : public Mif::Service::Inherit
            {
                virtual void SetTitle(std::string const &title) = 0;
                virtual void SetBody(std::string const &body) = 0;
                virtual std::string GetPage() const = 0;
            };
        }   // namespace Service
        

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

        Серверная часть


        Каркас приложения

        class Application
            : public Mif::Application::HttpServer
        {
            //...
        private:
            // Mif.Application.HttpService
            virtual void Init(Mif::Net::Http::ServerHandlers &handlers) override final
            {
                std::string const adminLocation = "/admin";
                std::string const viewLocation = "/view";
                auto service = Mif::Service::Create(viewLocation);
                auto webService = Mif::Service::Cast(service);
                auto factory = Mif::Service::Make();
                factory->AddInstance(Service::Id::Service, service);
                std::chrono::microseconds const timeout{10000000};
                auto clientFactory = Service::Ipc::MakeClientFactory(timeout, factory);
                handlers.emplace(adminLocation, Mif::Net::Http::MakeServlet(clientFactory));
                handlers.emplace(viewLocation, Mif::Net::Http::MakeWebService(webService));
            }
        };
        

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

        • Добавлять к ресурсу разные обработчики
        • Автоматически делать разбор параметров запроса и данных из тела запроса
        • Автоматически сериализовать ответ в выбранный формат
        • Объединять два подхода: работу по HTTP через, возможно, REST API и работу с клиентом с поддержкой маршалинга C++ интерфейсов

        Все эти возможности реализованы в базовом классе Mif::Net::Http::WebService. Пользовательский класс должен наследовать его. Т.к. это уже сервис, который содержит чисто виртуальные методы, реализация которых скрыта в сервисном коде каркаса, то экземпляры классов-наследников должны создаваться так же как и все сервисы — фабричным методом. Чтобы сервис можно было использовать как обработчик HTTP web-сервера нужно создать обертку функцией Mif::Net::Http::MakeServlet.

        Сервисы MIF и веб-сервисы — это разные сервисы. Под MIF сервисами стоит понимать только классы-реализации интерфейсов, которые не обязаны заниматься только обработкой HTTP-запросов или иных сетевых запросов. Веб-сервисы уже более привычное понятие. Но при необходимости в MIF это все легко объединяется в единое целое и приведенный пример этому подтверждение.

        Сервис-обработчик
        namespace Service
        {
            namespace Detail
            {
                namespace
                {
                class WebService
                    : public Mif::Service::Inherit
                        <
                            IAdmin,
                            Mif::Net::Http::WebService
                        >
                {
                public:
                    WebService(std::string const &pathPrefix)
                    {
                        AddHandler(pathPrefix + "/stat", this, &WebService::Stat);
                        AddHandler(pathPrefix + "/main-page", this, &WebService::MainPage);
                    }
        
                private:
                    // …
        
                    // IAdmin
                    virtual void SetTitle(std::string const &title) override final
                    {
                        // ...
                    }
                    // …
        
                    // Web hadlers
                    Result Stat()
                    {
                        // ...
                        std::map resp;
        
                        // Fill resp
        
                        return resp;
                    }
                    Result
                    MainPage(Prm const &format)
                    {
                        // ...
                    }
                };
                }   // namespace
            }   // namespace Detail
        }   // namespace Service
        
        MIF_SERVICE_CREATOR
        (
            ::Service::Id::Service,
            ::Service::Detail::WebService,
            std::string
        )
        

        Хотелось бы обратить внимание на несколько моментов:

        • Выше уже упоминалась необходимость использования Mif::Service::Inherit при написании реализаций. Здесь как раз роль Mif::Service::Inherit полностью оправдана. С одной стороны класс наследует интерфейс IAdmin и реализует все его методы, а с другой наследует реализацию интерфейса IWebService в виде базового класса Mif::Net::Http::WebService, которая делает работу с HTTP проще.
        • В конструкторе добавляются обработчики ресурсов. Полный путь строится из пути ресурса, с которым связан класс-наследник Mif::Net::Http::WebService и пути, который указан при вызове AddHandler. В примере путь для получения статистики по выполненным запросам будет выглядеть, как /view/stat
        • Возвращаемый тип обработчика может быть любым, который можно вывести в поток. Кроме того в качестве возвращаемого типа можно использовать обертку-сериализатор. Она сериализует переданный встроенный или пользовательский тип в свой формат. В этом случае для сериализации пользовательских типов используется добавленная к ним метаинформация.
        • Методы-обработчики могут принимать параметры. Для описания параметров используется сущность Prm. Класс Prm принимает тип параметра, его имя. При необходимости пользовательская реализация разбора параметров может быть передана в Prm. Поддерживается конвертация параметров запроса к интегральным типам, типам с плавающей точкой, множества параметров с разделителем в виде точки с запятой (некоторые stl контейнеры, например, list, vector, set). Так же дата, время и timestamp целиком, которые можно привести к типам boost::posix_time. Десериализация данных тела запроса производится классом Content. Класс использует метаинформацию и преданный десериализатор. При необходимости получить доступ к заголовкам запроса или ко всей карте параметров, в качестве параметра обработчика можно указать Headers и / или Params. Одновременно можно использовать все указанные классы. Подробная работа с параметрами показана в примере http_crud
        • При определении фабричного метода для создания класса реализации в макрос MIF_SERVICE_CREATOR передается дополнительный параметр, который должен быть передан в конструктор реализации. Использование макроса MIF_SERVICE_CREATOR упоминалось выше.

        Клиент

        Клиент не имеет особых отличий от ранее приведенных примеров. Исключением является отсутствие предопределенной цепочки обработки запросов. Она формируется разработчиком самостоятельно. Для работы по HTTP в MIF нет предопределенных цепочек обработки запросов. Особенности реализации… О причинах, возможно, что-то в последующих постах будет написано. Полный код клиента — это пример http.

        Результаты

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

        curl "http://localhost:55555/view/main-page?format=text"
        curl "http://localhost:55555/view/main-page?format=html"
        curl "http://localhost:55555/view/main-page?format=json"
        

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

        Работа с базами данных


        Backedn без БД? Как же без них? Не обязательно, но и не редко…

        На текущий момент работа с базами данных в MIF реализована без опоры на метаинформацию. Пока что почти классика работы с БД. Эта часть была нужна для разработки сервисов, которые что-то должны были хранить в БД. Но разработка этой части велась с прицелом на то, что она станет основой для ORM, где уже в полной мере будет использована вся метаинформация, добавляемая к типам. С ORM как-то пока не сложилось. Было несколько попыток реализовать. Получалось или сильно громоздко, или не гибко. Думаю, что компромисс между лаконичностью и гибкостью скоро будет достигнут и в ближайших версиях ORM все же появится, т.к. уж очень хочется иметь возможность какие-то проверки переложить на компилятор, а не ловить в момент исполнения программы ошибки, связанные с простейшими ошибками и опечатками в сырых строках SQL-запросов.

        Пока ORM нет, немного классики …

        Работа с БД содержит несколько шагов:

        • Создание объекта-подключения к БД
        • Выполнение запросов, через полученное подключение
        • Обработка результатов
        • Транзакции

        Кто работал с какими-нибудь обертками такими, как JDBC (в Java) или что-то подобное для C++ ничего нового для себя тут не откроют.

        В примере db_client показана работа с двумя СУБД: PostgreSQL и SQLite.

        HTTP CRUD сервер


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

        Класс web-сервера содержит обработчики всех базовых операций CRUD. Работа с БД ведется в обработчиках. Такой код можно нередко встретить в разных успешно работающих сервисах. Но не смотря на его изначальную простору, в нем есть недостаток — зависимость от СУБД. А при желании отказаться от реляционной БД и перейти к NoSQL базе данных код всех обработчиков почти полностью будет переписан. Во многих реальных проектах стараюсь избегать подобной работы с БД. Выносить в отдельные фасады высокоуровневые функции логики. Фасады — это как правило C++ интерфейсы с конкретными реализациями. А замена БД сводится к очередной реализации интерфейса и запросу у фабрики реализации с новым идентификатором. Играя капитана очевидности, можно сказать, что такой подход себя не раз оправдал.

        Заключение


        Все, что рассмотрено выше: рефлексия, сериализация, межпроцессное взаимодействие, маршалинг интерфейсов, работа с БД, поддержка HTTP — все нашло свое отражение в итоговом примере построения небольшой системы на микросервисах. Пример демонстрирует небольшой сервис, состоящий из двух микросервисов, взаимодействующих между собой. Один сервис является фасадом к БД, а второй предоставляет Json API для внешних клиентов. Пример является модифицированной версией рассмотренного примера http_crud и является логическим завершением (объединением) всему, о чем шла речь в рамках поста.

        Некоторые части MIF, не были рассмотрены, например, db_client, но работа с БД показана в других примера. Работа с сервисами (реализациями интерфейсов) так же частично затронута в примерах. Некоторые части MIF, такие как сериализация, работа с БД и поддержка HTTP уже неоднократно проверены. Межпроцессному взаимодействию пока было уделено меньше времени на проверку и отладку, несмотря на то, что некоторым частям в разработке было уделено много времени. Например, к таким можно отнести поддержку передачи интерфейсов как параметров в другой процесс и обратный вызов их методов. Что является по сути «эго-фичей». Хотелось попробовать реализовать подобный механизм, показанный в примере visitor, но в дальнейшем есть желание довести эту часть до полноценного проверенного и отлаженного решения.

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

        Подводя итог, продублирую ссылку на проект MIF on C++.

        Спасибо за внимание.
        Original source: habrahabr.ru (comments, light).

        https://habrahabr.ru/post/239571/


        Метки:  

        [Из песочницы] Простой WebScraping на R через API hh.ru

        Понедельник, 11 Сентября 2017 г. 18:06 + в цитатник
        pdepdepde вчера в 18:06 Разработка

        Простой WebScraping на R через API hh.ru

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


        Не так давно преподаватель дал задание: cкачать данные с некоторого сайта на выбор. Не знаю почему, но первое, что пришло мне в голову — это hh.ru.


        Далее встал вопрос: "А что же собственно будем выкачивать?", ведь на сайте порядка 5 млн. резюме и 100.000 вакансий.


        Решив посмотреть, какими навыками мне придется овладеть в будущем, я набрал в поисковой строке "data science" и призадумался. Может быть по синонимичному запросу найдется больше вакансий и резюме? Нужно узнать, какие формулировки популярны в данный момент. Для этого удобно использовать сервис GoogleTrends.


        image
        Отсюда видно, что выгоднее всего искать по запросу "machine learning". Кстати, это действительно так.


        Соберем вначале вакансии. Их довольно мало, всего 411. API hh.ru поддерживает поиск по вакансиям, поэтому задача становится тривиальной. Единственное, нам необходимо работать с JSON. Для этой цели я использовал пакет jsonlite и его метод fromJSON() принимающий на вход URL и возвращающий разобранную структуру данных.


         data <- fromJSON(paste0("https://api.hh.ru/vacancies?text=\"machine+learning\"&page=", pageNum) # Здесь pageNum - номер страницы. На странице отображается 20 вакансий.

        Полный код для выгрузки вакансий
        # Scrap vacancies
        vacanciesdf <- data.frame(
            Name = character(),  # Название компании
            Currency = character(), # Валюта
            From = character(), # Минимальная оплата
            Area = character(), # Город
            Requerement = character(), stringsAsFactors = T) # Требуемые навыки
        for (pageNum in 0:20) { # Всего страниц
            data <- fromJSON(paste0("https://api.hh.ru/vacancies?text=\"machine+learning\"&page=", pageNum))
            vacanciesdf <- rbind(vacanciesdf, data.frame(
                                            data$items$area$name, # Город
                                            data$items$salary$currency, # Валюта
                                            data$items$salary$from, # Минимальная оплата
                                            data$items$employer$name, # Название компании
                                            data$items$snippet$requirement)) # Требуемые навыки
            print(paste0("Upload pages:", pageNum + 1))
            Sys.sleep(3)
        }

        Записав все данные в DataFrame, давайте немного его почистим. Переведем все зарплаты в рубли и избавимся от столбца Currency, так же заменим NA значения в Salary на нулевые.


        Фильтрация DataFrame
        # Сделаем приличные названия столбцов
        names(vacanciesdf) <- c("Area", "Currency", "Salary", "Name", "Skills") 
        # Вместо зарплаты NA будет нулевая
        vacanciesdf[is.na(vacanciesdf$Salary),]$Salary <- 0 
        # Переведем зарплаты в рубли
        vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'USD',]$Salary <- vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'USD',]$Salary * 57
        vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'UAH',]$Salary <- vacanciesdf[!is.na(vacanciesdf$Currency) & vacanciesdf$Currency == 'UAH',]$Salary * 2.2
        vacanciesdf <- vacanciesdf[, -2] # Currency нам больше не нужна
        vacanciesdf$Area <- as.character(vacanciesdf$Area)

        После этого имеем DataFrame вида:


        image


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


        vacanciesdf %>% group_by(Area) %>% filter(Salary != 0) %>%
                   summarise(Count = n(), Median = median(Salary), Mean = mean(Salary)) %>% 
                            arrange(desc(Count))

        image


        Для scraping`a в R обычно используется пакет rvest, имеющий два ключевых метода read_html() и html_nodes(). Первый позволяет скачивать страницы из интернета, а второй обращаться к элементам страницы с помощью xPath и CSS-селектора. API не поддерживает возможность поиска по резюме, но дает возможность получить информацию о нем по id. Будем выгружать все id, а затем уже через API получать данные из резюме. Всего резюме на сайте по данному запросу — 1049.


        hhResumeSearchURL <- 'https://hh.ru/search/resume?exp_period=all_time&order_by=relevance&text=machine+learning&pos=full_text&logic=phrase&clusters=true&page=';
        # Загрузим очередную страницу с номером pageNum
        hDoc <- read_html(paste0(hhResumeSearchURL, as.character(pageNum)))
        # Выделим все аттрибуты ссылок на странице
            ids <- html_nodes(hDoc, css = 'a') %>% as.character() 
        # Выделим из ссылок необходимые id ( последовательности букв и цифр длины 38 )
            ids <- as.vector(ids) %>% `[`(str_detect(ids, fixed('/resume/'))) %>%
                    str_extract(pattern = '/resume/.{38}') %>% str_sub(str_count('/resume/') + 1)
            ids <- ids[4:length(ids)] # В первых 3х мусор

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


         resumes <- fromJSON(paste0("https://api.hh.ru/resumes/", id))

        Полный код для выгрузки резюме
        hhResumeSearchURL <- 'https://hh.ru/search/resume?exp_period=all_time&order_by=relevance&text=machine+learning&pos=full_text&logic=phrase&clusters=true&page=';
        for (pageNum in 0:51) { # Всего 51 страница
           #Вытащим id резюме 
            hDoc <- read_html(paste0(hhResumeSearchURL, as.character(pageNum)))
            ids <- html_nodes(hDoc, css = 'a') %>% as.character() 
           # Выделим все аттрибуты ссылок на странице
            ids <- as.vector(ids) %>% `[`(str_detect(ids, fixed('/resume/'))) %>%
            str_extract(pattern = '/resume/.{38}') %>% str_sub(str_count('/resume/') + 1)
            ids <- ids[4:length(ids)] # В первых 3х мусор
            Sys.sleep(1) # Подождем на всякий случай 
            for (id in ids) {
                resumes <- fromJSON(paste0("https://api.hh.ru/resumes/", id))
                skills <- if (is.null(resumes$skill_set)) "" else resumes$skill_set 
                buffer <- data.frame(
                  Age = if(is.null(resumes$age)) 0 else resumes$age, # Возраст
                  if (is.null(resumes$area$name)) "NoCity" else resumes$area$name,# Город
                  if (is.null(resumes$gender$id)) "NoGender" else resumes$gender$id, # Пол
                  if (is.null(resumes$salary$amount)) 0 else resumes$salary$amount, # Зарплата
                  if (is.null(resumes$salary$currency)) "NA" else resumes$salary$currency, # Валюта  
                 # Список навыков одной строкой через ,                 
                  str_c(if (!length(skills)) "" else skills, collapse = ",")) 
                write.table(buffer, 'resumes.csv', append = T, fileEncoding = "UTF-8",col.names = F)
                Sys.sleep(1) # Подождем на всякий случай 
            }   
            print(paste("Скачал страниц:", pageNum))
        }

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


        image


        Найдем топ — 15 навыков, чаще остальных встречающихся в резюме


        SkillNameDF <- data.frame(SkillName = str_split(str_c(
                               resumes$Skills, collapse = ','), ','), stringsAsFactors = F)
        names(SkillNameDF) <- 'SkillName'
        mostSkills <- head(SkillNameDF %>% group_by(SkillName) %>%
                                      summarise(Count = n()) %>% arrange(desc(Count)), 15 )

        image


        Посмотрим, сколько женщин и мужчин знают machine learning, а так же на какую зарплату претендуют


        resumes %>% group_by(Gender) %>% filter(Salary != 0)  %>% 
                  summarise(Count = n(), Median = median(Salary), Mean = mean(Salary)

        image


        И напоследок, топ — 10 самых популярных возрастов специалистов по машинному обучению


        resumes %>% filter(Age!=0) %>% group_by(Age) %>% 
                        summarise(Count = n()) %>% arrange(desc(Count))

        image

        Original source: habrahabr.ru (comments, light).

        https://habrahabr.ru/post/337684/


        Метки:  

        Blockchain Life 2017: от золотой пиццы к криптовалютной лихорадке

        Понедельник, 11 Сентября 2017 г. 18:05 + в цитатник
        О криптовалютах сейчас говорят на каждом углу. Новостные агентства мониторят курс биткойна и этериума, крупнейшие технологические гиганты активно интегрируют блокчейн-технологии в свои системы. Банковская система, презрительно фыркавшая на дерзкого новичка, взялась за голову и с осторожным интересом присматривается к нему. Читать далее

        https://habrahabr.ru/post/337396/


        Метки:  

        Архитектурная пирамида приложения

        Понедельник, 11 Сентября 2017 г. 17:25 + в цитатник
        bocharovf сегодня в 17:25 Разработка

        Архитектурная пирамида приложения

          Программирование — достаточно молодая область знаний, однако, в ней уже существуют базовые принципы «хорошего кода», рассматриваемые большинством разработчиков как аксиомы. Все слышали о SOLID, KISS, YAGNI и других трех- или четырех- буквенных аббревиатурах, делающих ваш код чище. Эти принципы влияют на архитектуру вашего приложения, но помимо них существуют архитектурные стили, методологии, фреймворки и много чего еще.

          Разбираясь со всем этим по отдельности, меня заинтересовал вопрос — как они взаимосвязаны?
          Пытаясь выстроить иерархию и вдохновившись небезызвестной пирамидой Маслоу, я построил свою пирамиду «архитектуры приложения».

          О том, что из этого вышло — читайте под катом.

          О пирамиде

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

          Уровни пирамиды


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

          Безусловные ограничения


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

          Бизнес требования


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

          Сложность: KISS, YAGNI


          Вряд ли кто-то будет спорить, что сложность системы является одним из самых важных факторов, влияющих на все остальные аспекты и, в конечном счете, на успех проекта.
          Принципами, направленными на борьбу со сложностью в разработке приложений, являются KISS (Keep it simple, stupid) и YAGNI (You aren't gonna need it).

          Принцип KISS призывает упрощать:
          большинство систем работают лучше всего, если они остаются простыми, а не усложняются
          А YAGNI не проектировать наперед сверх меры:
          Вам это не понадобится
          Оба они очень абстрактны и годятся для любого приложения, что делает их основополагающими.

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

          Связность: DRY, SRP, ISP, high cohesion


          Старая шутка про слона, которого нужно есть по частям в полной мере годится для любой сложной задачи. В том числе, и для разработки крупных приложений. За корректное разделение слона задачи на небольшие, изолированные и точно сформулированные подзадачи отвечают два принципа: DRY (Don't repeat yourself) и SRP (The Single Responsibility Principle).

          Принцип DRY гласит:
          Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы
          А принцип SRP:
          каждый объект должен иметь одну ответственность

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

          Принцип ISP (Interface segregation principle) утверждает, что:
          Клиенты не должны зависеть от методов, которые они не используют.
          Неожиданно, с этим принципом у меня возникли самые большие проблемы. Он чем-то похож на YAGNI — «клиенту могут и не понадобятся эти методы интерфейса». С другой стороны у него очень много и от SRP — «один интерфейс для одной задачи». Его можно было бы отнести к обобщению «Сложность» и поставить на один уровень с YAGNI и KISS, но эти два принципа более абстрактны.

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

          Сильная связанность или зацепление (high cohesion) — это метрика, показывающая, насколько хорошо код сгруппирован по функционалу. Выполнение принципов DRY и SRP ведет к коду с сильной связностью, в котором части, выполняющие одну и ту же задачу расположены «близко» друг к другу, а разные — изолированы. Выполнение ISP тоже ведет к сильной связности, хотя это и не так очевидно. Разделяя один интерфейс на более мелкие части вы группируете их по функционалу, оставляя в каждом интерфейсе только наиболее связанные между собой методы.
          GRASP: high cohesion, loose / low coupling
          Выше сильная связанность названа «метрикой», хотя ее в полной мере можно назвать принципом. High cohesion, наряду с Low coupling является частями GRASP (General responsibility assignment software patterns), который в этой статье не рассматривается.

          Зависимости: IoC, DIP, loose coupling


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

          IoC (Inversion of control) предполагает наличие некоторого фреймворка, который будет передавать управление компонентам нашей программы в нужный момент. При этом компоненты могут ничего не знать друг о друге.

          DIP (Dependency inversion principle) гласит:
          Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
          Слабая связность (loose coupling) — это не принцип, а метрика, показывающая, насколько компоненты системы независимы друг от друга. Слабо связанные компоненты не зависят от внешних изменений и легко могут быть использованы повторно. IoC и DIP являются средствами для достижения слабой связности компонентов в системе.

          Расширение: OCP, LSP


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

          Принципами, относящимися к расширению функционала, являются OCP (Open/closed principle) и LSP (Liskov substitution principle).

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

          Методологии: TDD, DDD, BDD


          Помимо принципов, существует еще довольно большой набор методологий, например BDD (Behavior-driven development) и TDD (Test-driven development). В общем случае методология, в отличие от принципа, определяет некоторый процесс, применяемый к разработке приложения.
          Методологии очень разнообразны, решают разные задачи и часто, при разработке приложения, используется их комбинация. Но какую бы из них вы ни выбрали, у вас возникнут серьезные проблемы, если попытаться применить их к коду, нарушающему предыдущие принципы. Например: легко ли будет применить TDD и писать тесты для кода, который нарушает DIP и содержит ссылки на конкретные реализации?

          Архитектурные стили и фреймворки


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

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

          Библиотеки и инструменты


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

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

          Зачем все это нужно?


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

          Если изменения вносятся в существующую систему, то он примерно таков:
          1. Реализуемы ли мои изменения с учетом отведенного мне бюджета, времени и прочих объективных ограничений?
          2. Не противоречат ли они бизнес-требованиям?
          3. Достаточно ли просто то, что я собираюсь сделать? Есть ли более простые способы сделать это?
          4. Как мне разделить мою задачу на компоненты (подзадачи) так, чтобы каждый из компонентов выполнял только одно действие? Не дублирую ли я уже существующий функционал?
          5. Как мои компоненты будут связаны с остальной программой? Как уменьшить количество связей? Буду ли я иметь возможность использовать мои компоненты повторно или заменить один компонент на другой в будущем?
          6. Смогу ли я расширить функционал моих компонентов, не изменяя их? Заложил ли я возможности расширения в те компоненты, вероятность изменения которых в будущем особенно велика?
          7. Не противоречат ли мои изменения выбранной мной методологии?
          8. Соотносятся ли мои изменения с лучшими практиками используемого мной фреймворка? Не нарушают ли они общий архитектурный стиль моего кода?
          9. Могут ли использованные мной библиотеки решить поставленную подзадачу?

          Каждый пункт списка соотносится с определенным уровнем пирамиды. При этом выбор, сделанный на каждом уровне не должен противоречить выбору, сделанному на предыдущих уровнях. Это особенно важно при проектировании системы с нуля, когда «неопределенность» в плане будущей архитектуры гораздо выше и шире возможный выбор.
          Так, например, выбранная вами библиотека не должна конфликтовать с используемым фреймворком. Если это происходит — вы меняете библиотеку, а не фреймворк. Фреймворк должен поддерживать (или хотя бы делать возможным) использование выбранной на предыдущем этапе методологии и так далее.

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

          Заключение


          Эта статья достаточно субъективна и не претендует на исчерпывающее описание всех существующих принципов и методологий. Более того, уже во время написания первого черновика, пара принципов переместилась со своих насиженных мест на другие уровни. Возможно, со временем пирамида будет дополнена новыми элементами или даже уровнями. Если вам кажется, что в ней что-то находится не на своем месте или вы хотите дополнить ее — буду рад конструктивной критике и предложениям в комментариях.
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/336496/


          [Из песочницы] Plugin for HANA Database project in Visual Studio

          Понедельник, 11 Сентября 2017 г. 17:06 + в цитатник

          Метки:  

          Как на Java c помощью КриптоПро подписать документ PDF

          Понедельник, 11 Сентября 2017 г. 17:00 + в цитатник
          vburmistrov сегодня в 17:00 Разработка

          Как на Java c помощью КриптоПро подписать документ PDF



            Привет! Я сотрудник Альфа-Банка и занимаюсь разработкой программного обеспечения со встроенными средствами криптографической защиты информации.

            В данной статье хочу рассказать о следующих вещах:

            • преимуществах формата PDF в качестве документа с электронной подписью;
            • платформе Java, библиотеке itextpdf и СКЗИ КриптоПро CSP, как инструментах подписи;
            • о том, с какими трудностями пришлось столкнуться, о доработке itextpdf;
            • привести пример кода, выполняющего несколько подписей;
            • поговорить о целесообразности использования формата PDF в качестве документа с подписью.

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

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

            Преимущества PDF


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

            Инструменты Adobe Systems (разработчика формата PDF) поддерживают использование электронной подписи. В отличие от сообщений с присоединенной усиленной электронной подписью стандарта PKCS#7 и его усовершенствования CAdES, для просмотра документа PDF с подписью не требуется дополнительное специальное ПО. Кроме криптографического провайдера, который требуется во всех случаях.

            Т.е. инструменты Adobe позволяют визуализировать электронную подпись в документе.

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

            При просмотре таких документов, кроме визуализации в теле документа, программы Acrobat и Adobe Reader отображают вкладку «Подписи» с сопутствующей информацией: значок, показывающий статус проверки подписи, сведения о том, был ли изменен документ, результат проверки сертификата открытого ключа, время последней проверки, страницу и поле, содержащие электронную подпись.

            Инструменты подписи


            Использовалась версия Java: «1.8.0_111» HotSpot(TM) 64-Bit Server VM (build 25.111-b14).

            В качестве сертифицированного средства защиты информации от лицензированного разработчика применяем криптографический провайдер КриптоПро CSP v4.0 и КриптоПро JCP – v.2.0, с установкой модуля КриптоПро Java CSP v.4.0

            Почему КриптоПро JCP – v.2.0, с модулем КриптоПро Java CSP v.4.0?

            Потому, что провайдер КриптоПро JCP после длительного несертифицированного периода получил сертификат соответствия от регулятора до 31.12.2018, а дальше, по информации от разработчика, с сертификацией может вновь возникнуть неопределенность. Модуль КриптоПро Java CSP v.4.0 не выполняет в себе криптографических преобразований и является по сути API к провайдеру КриптоПро CSP, с очередной сертификацией которого вопросов нет. Здесь нужно сказать, что действующий сертификат на СКЗИ не обязателен при условии использования криптографического провайдера исключительно для внутренних целей.

            В соответствии со спецификацией Java Cryptography Architecture (JCA), в своем приложении я указываю и использую функции криптографического провайдера: JCSP. После установки КриптоПро, данный провайдер отображается в списке всех доступных в файле ../java/jdk1.8.x_xxx/jre/lib/security/java.security, где можно настраивать, который из них предпочтительнее для использования по умолчанию, если явно не указано в приложении:
            #
            # List of providers and their preference orders (see above):
            #
            security.provider.1=ru.CryptoPro.JCSP.JCSP


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

            Для этого в ../java/jdk1.8.x_xxx/jre/lib/security необходимо заменить файлы local_policy.jar и US_export_policy.jar на предоставленные по адресу: Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 8 Download
            и содержащие в default_local.policy и default_US_export.policy:

            grant {
            // There is no restriction to any algorithms.
            permission javax.crypto.CryptoAllPermission;
            };


            Применение библиотеки iText itextpdf.com для работы с PDF документами на Java было продиктовано провайдером КриптоПро.

            В составе КриптоПро JCP поставляется файл Git patch для библиотеки iTextpdf версии 5.1.3 —
            jcp-2.0.xxxxx\Doc\itextpdf\ itextpdf_5.1.3.gost.user.patch



            Патч адаптирует itextpdf к работе с провайдером КриптоПро. Необходимо скачать исходный код библиотеки версии 5.1.3, затем с помощью командной строки Bash системы управления версиями Git применить патч: git apply --stat itextpdf_5.1.3.gost.user.patch



            Далее нужно собрать полученную библиотеку из обновленного исходного кода и подключить к приложению.

            В составе КриптоПро JCP можно найти много примеров в файле samples-sources.jar. В частности, там есть примеры подписи и проверки ЭП PDF-документов. (\PDF\SignVerifyPDFExample.java).

            Проблемы и трудности сборки


            После успешного обновления исходного кода itextpdf в нем появляются зависимости на пакеты ru.CryptoPro.JCP и ru.CryptoPro.reprov.x509.

            Без них проект с исходным кодом itextpdf_5.1.3.gost не соберется.

            [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project itextpdf: Compilation failure: Compilation failure:
            [ERROR] \github\iTextpdf_5.1.3_patched_cryptopro_bc1.50\src\main\java\com\itextpdf\text\pdf\PdfPKCS7.java:[138,23] error: package ru.CryptoPro.JCP does not exist
            [ERROR] \github\iTextpdf_5.1.3_patched_cryptopro_bc1.50\src\main\java\com\itextpdf\text\pdf\PdfPKCS7.java:[139,31] error: package ru.CryptoPro.reprov.x509 does not exist

            Нужно взять из поставки КриптоПро 2.0 файлы JCP.jar и JCPRevTools.jar и поместить в каталог JRE, которую использует Maven: Java\jdk1.8.0_111\jre\lib\ext. Само собой, они должны быть и в classPath приложения.

            Итак, библиотека собрана, подключаем ее в приложение. И тут возникает основная проблема. iTextpdf_5.1.3 содержит зависимость на Bouncy Castle версии 1.46 – библиотеку с открытым кодом, реализующую криптографический провайдер и поддержку ASN.1 структур.

                            
            			org.bouncycastle
            			bctsp-jdk15
            			1.46
            			jar
            			compile
            			true
            		
            

            Поставка КриптоПро JCP 2.0 в свою очередь имеет зависимости на Bouncy Castle версии 1.50 bcpkix-jdk15on-1.50 и bcprov-jdk15on-1.5, соответственно, они помещаются в jre/lib/ext при установке КриптоПро.

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

            Exception in thread "main" java.lang.NoClassDefFoundError: org/bouncycastle/asn1/DEREncodable
            at com.itextpdf.text.pdf.PdfSigGenericPKCS.setSignInfo(PdfSigGenericPKCS.java:97)
            at com.itextpdf.text.pdf.PdfSignatureAppearance.preClose(PdfSignatureAppearance.java:1003)
            at com.itextpdf.text.pdf.PdfSignatureAppearance.preClose(PdfSignatureAppearance.java:904)
            at com.itextpdf.text.pdf.PdfStamper.close(PdfStamper.java:194)
            at ru.alfabank.ccjava.trustcore.logic.SignatureProcessor.pdfSignature(SignatureProcessor.java:965)
            at ru.alfabank.ccjava.trustcore.logic.SignatureProcessor.main(SignatureProcessor.java:1363)
            Caused by: java.lang.ClassNotFoundException: org.bouncycastle.asn1.DEREncodable
            at java.net.URLClassLoader.findClass(Unknown Source)
            at java.lang.ClassLoader.loadClass(Unknown Source)
            at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
            at java.lang.ClassLoader.loadClass(Unknown Source)
            ... 6 more

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

            Что получается – библиотека iTextpdf_5.1.3 имеет зависимость от более старого провайдера Bouncy Castle, а для новых версий iTextpdf нет патча от КриптоПро.

            Конкретно в поставке КриптоПро JCP 2.0 зависимости на новую версию Bouncy Castle имеет библиотека CAdES.jar. Если удалить из JRE эту библиотеку или вовсе отказаться от поддержки формирования CAdES подписей при установке КриптоПро JCP 2.0, то проблема будет решена.

            Но что если поддержка CAdES должна остаться?

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

            • заменяем в исходном коде библиотеки iTextpdf_5.1.3_patched_cryptopro зависимость org.bouncycastle 1.46 на версию 1.50.

              	               
              				org.bouncycastle
              				bcprov-ext-jdk15on
              				1.50
              			
              			
              				org.bouncycastle
              				bcprov-jdk15on
              				1.50
              			
              			
              				org.bouncycastle
              				bcmail-jdk15on
              				1.50
              			
              	
            • вносим исправления в проект iTextpdf_5.1.3_patched_cryptopro, руководствуясь документом Bouncy Castle «Porting from earlier BC releases to 1.47 and later»
            • чтобы поправить те места iTextpdf_5.1.3_patched_cryptopro, где дело не ограничивалось новыми названиями классов и методов Bouncy Castle, а явно были изменены конструкции в методах, скачиваем исходный код itextpdf 5.5.5, который имеет зависимость на Bouncy Castle 1.50, вдумчиво переносим реализацию методов оттуда.

            Как итог, проект iTextpdf_5.1.3_patched_cryptopro_bc1.50 начинает собираться. Конфликт разрешен, КриптоПро и itextpdf ссылаются на одну версию org.bouncycastle 1.50.

            Исходный код iTextpdf_5.1.3_patched_cryptopro_bc1.50 выложен в GitHub: iTextpdf_5.1.3_patched_cryptopro_bc1.50

            Пример кода, несколько подписей PDF


            Пример использования КриптоПро и iTextpdf_5.1.3_patched_cryptopro_bc1.50 выглядит следующим образом:

             /**
                 * 
                 * @param aliases
                 *            - имена контейнеров с ключами ЭП
                 * @param data
                 *            - массив байтов с документом PDF
                 * @param pdfVersion
                 *            - номер версии формата PDF
                 * @return
                 * @throws SignatureProcessorException
                 */
                public static byte[] samplePDFSignature(String[] aliases, byte[] data, char pdfVersion) throws SignatureProcessorException {
                    ByteArrayOutputStream bais = new ByteArrayOutputStream();
                    HashMap currSignAttrMap = new HashMap();
                    for (String alias : aliases) {
                        X509Certificate certificate = (X509Certificate) signAttributesMap1.get(alias)[0];
                        PrivateKey privateKey = (PrivateKey) signAttributesMap1.get(alias)[1];
                        
                        currSignAttrMap.put(certificate, privateKey);
                        if (certificate == null) {
                            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + CERTIFICATE_NOT_FOUND_BY_ALIAS);
                        }
                        
                        if (privateKey == null) {
                            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + PRIVATE_KEY_NOT_FOUND_BY_ALIAS);
                        }
                    }
                    try {
                        FileInputStream fis = new FileInputStream(new File(FILE_PATH));
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        byte[] buf = new byte[1024];
                        int n = 0;
                        while ((n = fis.read(buf, 0, buf.length)) != -1) {
                            baos.write(buf, 0, n);
                        }
                        fis.close();
                        byte[] im = baos.toByteArray();
                        
                        X509Certificate innerCA = obtainCertFromTrustStoreJKS(false, INNER_CA);
                        PdfStamper stp = null;
                        PdfReader reader = null;
                        int pageNumber = 1;
                        for (Entry entry : currSignAttrMap.entrySet()) {
                            if (bais.toByteArray().length == 0) {
                                reader = new PdfReader(data);
                            } else {
                                reader = new PdfReader(bais.toByteArray());
                                bais = new ByteArrayOutputStream();
                            }
                            stp = PdfStamper.createSignature(reader, bais, pdfVersion); //'\0'
                            Certificate[] certPath = new Certificate[] {entry.getKey(), innerCA};
                            PdfSignatureAppearance sap = stp.getSignatureAppearance();
                            sap.setProvider("JCSP"); //JCP
                            sap.setCrypto(entry.getValue(), certPath, null,
                                    PdfSignatureAppearance.CRYPTOPRO_SIGNED);
                            Image image = Image.getInstance(im);
                            sap.setImage(image);
                            sap.setVisibleSignature(new Rectangle(150, 150), pageNumber, null);
                            pageNumber++;
                            stp.close();
                            bais.close();
                            reader.close();
                        }
                    } catch (RuntimeException e) {
                        throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));
                    } catch (IOException e) {
                        throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));
                    } catch (DocumentException e) {
                        throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));
                    } catch (CertificateEncodingException e) {
                        throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));
                    } catch (Exception e) {
                        throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));
                    }
                    return bais.toByteArray();
                }
            

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

            Создается объект с корневым сертификатом для формирования пути сертификации, который требуется методу setCrypto класса com.itextpdf.text.pdf.PdfSignatureAppearance.

            В цикле выполняется подпись страниц PDF-документа ключами, имена которых были переданы в метод.

            Для демонстрации выполнены две подписи — валидная и невалидная с недействительным сертификатом.





            Вкладка «Подписи» выглядит следующим образом:





            Применение электронной подписи PDF-документов


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

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

            Практичнее формировать электронную подпись в виде отдельного файла или оборачивать файл и подпись в один контейнер стандарта PKCS#7 (CAdES). Этот стандарт с присоединенной или отсоединенной подписью отлично подойдет для большого документооборота между информационными системами.

            Тот же документ PDF можно подписать по стандарту CAdES отсоединенной подписью, в итоге будет два файла — сам PDF и контейнер с подписью.

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

            В сфере деятельности, связанной с разработкой коммерческого программного обеспечения со встроенными СКЗИ, такой подход недопустим, патчи, обновления и доработки должны выполняться организацией-лицензиатом. Поэтому приведенный в посте способ подписи PDF-документа можно применять с целью демонстрации возможностей и для внутреннего использования.

            Полезные ссылки


            1. Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 8
            2. Сайт компании КриптоПро
            3. Разработчик iTextpdf
            4. Bouncy Castle [«Porting from earlier BC releases to 1.47 and later»
            5. Исходный код библиотеки iTextpdf_5.1.3_patched_cryptopro_bc1.50 в GitHub
            Original source: habrahabr.ru (comments, light).

            https://habrahabr.ru/post/337668/


            Apache Ignite 2.1 — теперь со вкусом Persistence

            Понедельник, 11 Сентября 2017 г. 16:04 + в цитатник

            Метки:  

            [Из песочницы] Получение текста запросов из SoapHttpClientProtocol

            Понедельник, 11 Сентября 2017 г. 15:50 + в цитатник
            XelMed сегодня в 15:50 Разработка

            Получение текста запросов из SoapHttpClientProtocol

            У .NET есть несколько вариантов создания SOAP клиента, одним из них является его генерация с помощью wsdl.exe. На выходе получаем файл (поскольку пишу я на C#, то генерировал cs, соответственно), основой которого является класс, унаследованный от SoapHttpClientProtocol. Подробнее тут.

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

            Впрочем, если очень хочется, то нужно сделать.

            Основная идея


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

            Переопределям SoapHttpClientProtocol


            Релизуем свой класс, пусть будет SoapHttpClientProtocolSpy, унаследованный от SoapHttpClientProtocol соответственно. Для перехвата запросов клиента переопределяем метод GetWriterForMessage, а для перехвата ответов сервера — GetReaderForMessage. Первый возвращает XmlWriter, второй — XmlReader; вместо них и вернем собственные реализации, которые будут позволять получить XMl, через них проходящий.

            Получаем следующий класс:

            SoapHttpClientProtocolSpy
               public class SoapHttpClientProtocolSpy: SoapHttpClientProtocol
                {
                    private XmlWriterSpy writer;
                    private XmlReaderSpy reader;
                    public SoapHttpClientProtocolSpy() : base(){}
            
                    protected override XmlWriter GetWriterForMessage(SoapClientMessage message, int bufferSize)
                    {
                        writer = new XmlWriterSpy(base.GetWriterForMessage(message, bufferSize));
                        return writer;
                    }
            
                    protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
                    {
                        reader = new XmlReaderSpy(base.GetReaderForMessage(message, bufferSize));
                        return reader;
                    }
            
                    public string XmlRequest => reader?.Xml;
                    public string XmlResponce => writer?.Xml;
                }


            XmlWriterSpy и XmlReaderSpy — это декораторы для XmlWriter и XmlReader.

            XmlWriterSpy


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

            Реализация
                public class XmlWriterSpy : XmlWriter
                {
                   //это же декоратор, поэтому просто пользуемся той реализацией, которая придет
                    private XmlWriter _me;
                    private XmlTextWriter _bu;
                    private StringWriter _sw;
            
                    public XmlWriterSpy(XmlWriter implementation)
                    {
                        _me = implementation;
                        _sw = new StringWriter();
                        _bu = new XmlTextWriter(_sw);
                        _bu.Formatting = Formatting.Indented;
                    }
            
                    public override void Flush()
                    {
                        _me.Flush();
                        _bu.Flush();
                        _sw.Flush();
                    }
                     public string Xml => _sw?.ToString();
            
                    public override void Close() { _me.Close(); _bu.Close(); }
                    public override string LookupPrefix(string ns) { return _me.LookupPrefix(ns); }
                    public override void WriteBase64(byte[] buffer, int index, int count) { _me.WriteBase64(buffer, index, count); _bu.WriteBase64(buffer, index, count); }
                    public override void WriteCData(string text) { _me.WriteCData(text); _bu.WriteCData(text); }
               //И так далее, в том же духе
            
                }


            Еще раз спасибо данной статье за это.

            XmlWriterSpy


            Здесь идея абсолютно та же. Но только с реализацией придется немного повозиться. С одной стороны, достаточно влезть только в 1 метод(Read), с другой стороны, там чтение из потока, и не так просто его не сломать. Идею взял отсюда.

            Реализация
                public class XmlReaderSpy : XmlReader
                {
                   //это же декоратор, поэтому просто пользуемся той реализацией, которая придет
                    private XmlReader _baseXmlReader;
                    StringWriter _sw;
                    public string Xml => _sw?.ToString();
                    public XmlReaderSpy(XmlReader xmlReader)
                    {
                        _sw = new StringWriter();
                        _baseXmlReader = xmlReader;
                    }
                 
            
                    public override bool Read()
                    {
            //получаем прочитанную ноду
                            var res = _baseXmlReader.Read();
            //каждый тип ноды придется обрабатывать немного по-разному
                            switch (_baseXmlReader.NodeType)
                            {
                                case XmlNodeType.Element:
                                    _sw.Write("<" + _baseXmlReader.Name);
                                       while (_baseXmlReader.MoveToNextAttribute())
                                            _sw.Write(" " + _baseXmlReader.Name + "='" + _baseXmlReader.Value + "'");
                                        _sw.Write(_baseXmlReader.HasValue || _baseXmlReader.IsEmptyElement ? "/>" : ">");
                                   //поскольку мы перемещались по элементу, надо вернуться на исходную
                                    _baseXmlReader.MoveToElement();
                                    break;
                             
                                case XmlNodeType.Text:
                                    _sw.Write(_baseXmlReader.Value);
                                    break;
                                case XmlNodeType.CDATA:
                                    _sw.Write(_baseXmlReader.Value);
                                    break;
                                case XmlNodeType.ProcessingInstruction:
                                    _sw.Write("");
                                    break;
                                case XmlNodeType.Comment:
                                    _sw.Write("");
                                    break;
                                case XmlNodeType.Document:
                                    _sw.Write("");
                                    break;
                                case XmlNodeType.Whitespace:
                                    _sw.Write(_baseXmlReader.Value);
                                    break;
                                case XmlNodeType.SignificantWhitespace:
                                    _sw.Write(_baseXmlReader.Value);
                                    break;
                                case XmlNodeType.EndElement:
                                    _sw.Write("");
                                    break;
                            }
                            return res;
                    }
                }


            Все остальные методы переопределяем на простой вызов того же метода у _baseXmlReader.

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


            Вспомним о сгенерированном классе, теперь нужно всего лишь унаследовать его от SoapHttpClientProtocolSpy. Стоит сказать, что такая реализация работает, даже если сервер вернет ошибку (то есть она будет тоже залогирована). Ну а далее вызов метода выглядит примерно так:

            
            using (var worker = new Service())
                            {
                                try
                                {
                                    res = worker.Metod(....);
                                    Log.Info((worker?.XmlRequest ?? "")+(worker?.XmlResponce ?? ""));
                                }
                                catch (System.Exception ex)
                                {
                                    Log.Error((worker?.XmlRequest ?? "")+(worker?.XmlResponce ?? ""));
                                    throw ex;
                                }
                            }
            

            PS. С моей точки зрения, достаточно удобным будет вынести это все в отдельную либу. Единственное, что будет постоянно напрягать, так это необходимость после обновления клиента лезть в файл и заменять там SoapHttpClientProtocol, но это мелочи.
            Original source: habrahabr.ru (comments, light).

            https://habrahabr.ru/post/337672/


            Метки:  

            Эзотерический язык, транслирующийся в шаблоны C++

            Понедельник, 11 Сентября 2017 г. 15:33 + в цитатник
            sekrasoft сегодня в 15:33 Разработка

            Эзотерический язык, транслирующийся в шаблоны C++

              КПДВ с примерами кода Шаблоны C++ — полный по Тьюрингу язык, на котором можно писать compile-time программы. Только вот синтаксис рассчитан на описание параметризованных типов и слабо приспособлен к ясному выражению чего-то более сложного. В этой статье рассмотрим, как типы и шаблоны становятся значениями и функциями, а также узнаем, к чему привела попытка автора создать свой функциональный язык, транслирующийся в шаблоны C++. Для прочтения текста знания в области функционального программирования почти не требуются.

              Как это работает?


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

              Честно говоря, пару лет назад я порывался написать сюда пост про объяснение принципов работы compile-time программ на примере алгоритма смены приоритетов операторов в выражениях (т.е. реализовал варианты для compile-time и run-time алгоритма, с помощью которого можно было выражение $2 + 2 * 2$ «пересобрать» с альтернативными приоритетами и посчитать как $8$, а не $6$).

              Я уже почти дописал длинную статью с картинками и UML-диаграммами, но вдруг понял, что на хабре настолько часто рассказывали про метапрограммы, что это уже попросту никому не интересно. Тем более, что для практического использования добавили constexpr, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template с constexpr) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.

              Если, несмотря на все мои старания, читателю всё ещё будет трудно воспринимать материал, рекомендую прочитать указанные статьи, а вдогонку — Мягкое введение в Haskell (часть 1, часть 2) от Пола Хьюдака и др. в переводе Дениса Москвина и Лямбда-исчисление на JavaScript от ibessonov. Всё это в какой-то степени повлияло на автора, может и на читателя повлияет?!

              Метазначения и метафункции


              Шаблоны классов — это, в некотором смысле, функции от типов, которые принимают и возвращают типы. Соответственно, шаблоны становятся метафункциями, а типы и целые числа — метазначениями. Например, метафункция std::vector принимает метазначение T и возвращает метазначение std::vector. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.

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

              template 
              struct square {
                static const int value = x * x;
              };
              

              Вызывается применяется, как говорят в ФП, метафункция следующим образом: square<5>::value.

              Но целые числа — это обычные значения, для работы с которыми достаточно и простого C++, а потому было бы несколько неспортивно использовать их в метавыражениях. Настоящее честное метазначение — это тип. Самый простой способ создать его — объявить структуру:

              struct One;
              struct Zero;
              

              С точки зрения C++ one и zero только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.

              Для нуля и единицы как булевых значений естественным будет написание функций NOT, AND и OR. Рассмотрим метафункцию отрицания:

              template 
              struct Not {
                typedef Zero value;
              };
              
              template <>
              struct Not  {
                typedef One value;
              };
              

              Not принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0> и fac<всё остальное>).

              Список


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

              template 
              struct Cons {
                typedef h head;
                typedef t tail;
              };
              
              struct Nil;
              

              Cons в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку $\{one,two,three\}$ будет соответствовать Cons>>. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons многозначной функцией, возвращающей голову (Cons<...,...>::head), и хвост (Cons<...,...>::tail). Любители ООП могут представить, что Cons — это конструктор, а head и tail — геттеры. В ФП все присваивания заменяются на вызов применение функции с изменёнными аргументами, поэтому сеттеров и их аналогов здесь не будет.

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

              // отрицание непустого списка
              template 
              struct negate {
              private: // инкапсулируем промежуточные вычисления
                typedef typename list::head h; // голова
                typedef typename Not::value not_head; // отрицание головы
                typedef typename list::tail t; // хвост
                typedef typename negate::value not_tail; // отрицание хвоста
              public:
                // список из отрицаний элементов - это отрицание головы,
                // соединённое с хвостом отрицаний
                typedef Cons value;
              };
              
              // отрицание пустого списка
              template <>
              struct negate  {
                // пустой список - сам себе отрицание
                typedef Nil value;
              };
              

              Тут оказывается, что Haskell не так страшен, как его малюют. Всё то же самое выглядит настолько просто, что не грех добавить для наглядности примеры на этом языке. Для неподготовленного читателя отметим основное отличие от C++ и школьной математики: в Haskell аргументы разделяются пробелами и не группируются в скобках. Т.е. $f(x+y, g(y), z)$ будет записано в Haskell как f (x+y) (g y) z.

              -- список - либо слитые голова и хвост (причём хвост - сам список),
              --          либо пустота
              data List a = Cons a List | Nil
              
              -- хелперы для получения головы и хвоста
              head (Cons x _) = x -- возвращает голову
              tail (Cons _ xs) = xs -- возвращает хвост
              
              -- отрицание списка
              negate (Cons x xs) = Cons (Not x) (negate xs)
              negate Nil = Nil
              

              В отличие от строго типизированных Haskell и C++, в языке шаблонов действует утиная типизация. negate::value, конечно, не сработает, но если One будет иметь метаполя head и tail, он вполне подойдёт. Впрочем, пока negate не «разыменовали» с помощью ::value, программа продолжает работать компилироваться.

              Функции высшего порядка


              В функциональных языках функции — такие же значения. Так, можно написать функцию высшего порядка — которая принимает функцию в качестве аргумента или возвращает функцию. Например, поэлементное преобразование списка осуществляется с помощью ФВП map:

              -- Преобразованная голова соединяется с преобразованным хвостом
              map f (Cons x xs) = Cons (f x) (map f xs)
              -- Пустой список преобразовывать не нужно
              map f Nil = Nil
              

              STL содержит такое преобразование под именем std::transform. В нашем же языке метафункция map объявляется с помощью параметризации шаблона шаблоном:

              // преобразование непустого списка
              // f - шаблон с одним аргументом - унарная метафункция
              template 

              В качестве f сюда можем подставить описанную ранее функцию Not и посчитать список отрицаний:

              typedef map>>>::value list;
              // list эквивалентно Cons>>
              

              Операции именования выражений


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

              Начиная с C++11 можно использовать псевдонимы типов и шаблонов, чтобы задавать значения и функции через известные выражения:

              using x = Not::value;
              
              template 
              using g = Cons;
              

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

              Программа просто может уйти в бесконечную рекурсию
              Вспомним, что negate компилируется, а negate::value — нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value, а его никто не трогал:
              // рассмотрим только одну из специализаций
              template 
              struct If  {
                // expr1, expr2 вычислены, а expr1::value и expr2::value - нет
                typedef typename expr1::value value;
              }
              
              // If, destroy>::value не разрушит мир
              

              В то же время, вариант с псевдонимом шаблона предусматривает, что некоторое g имеет смысл уже вычисленного f::value. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.

              template 
              struct If  {
                // expr1, expr2 вычислены
                typedef expr1 value;
              };
              
              // If::value>::value разрушит мир
              


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

              Замыкания


              Если в обычных C/C++ аргументы и локальные переменные умирают после завершения работы функции, то в функциональных языках функции могут возвращать функции, зависящие от аргументов и локальных переменных родительской функции. Соответственно, родительская функция уже завершилась, но её локальные переменные остаются в живых, «привязываясь» к дочерней функции. На практике это полезно, когда интерфейс требует, например, унарной функции, а в её реализации имеется некоторый контекст, от которого она тоже зависит.

              Например, f x возвращает унарную функцию g, неявно использующую аргумент x функции f.

              f x = g
                where g xs = Cons x xs
              
              -- использование: (f x) xs или, что полезнее, map (f x) list
              

              Можно было бы оставить и Cons, но g как унарную можно передать в уже написанную функцию map, а как бинарную — Cons нет!

              Впрочем, обычный C++ от отсутствия замыканий не страдает
              Для этого используют функциональный объект или лямбда-функцию. То есть если передача в функцию дополнительных параметров через глобальные переменные нарушает гибкость программы, вместо функции используют объект, this которого содержит весь нужный контекст, а operator () принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function предусмотрен!

              // до C++11: функциональный объект, захвативший переменную
              struct f {
                f(something& x) : x(x) {}
                something_else operator () (something_else& xs) { // xs - явный аргумент
                  return Cons(x, xs);
                }
              private:
                something x; // неявный аргумент
              };
              
              // C++11 и выше: лямбда-функция, захватившая переменную
              something x;
              auto f = [x](something_else& xs) { return Cons(x, xs); };
              


              При замене функций (например, int(int)) на функциональные объекты (например, std::function) программист C++ получает полноценный аналог замыканий.

              На уровне шаблонов потребности в подмене «функция -> объект», как оказывается, нет. Замыкания поддерживаются самим языком:

              template 
              struct f {
                template 
                struct g {
                  typedef Cons value;
                };
              };
              
              // использование: f::g::value или map::g, list>
              

              В отличие от C++ и Haskell, здесь наружу f «вылезает» символ g, но в остальном — честное замыкание. Выражение f::g может быть использовано вместо обычной унарной функции (например, Not).

              Неограниченное число аргументов или синтаксический сахар для списков


              Вариадические шаблоны позволяют записывать функции от списков. Опять же, в некоторых случаях это удобнее, но без них тоже можно спокойно жить с Cons и Nil. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f и передающий ей содержимое списка:

              typename 
              struct list_wrapper {
                // передаём в list_wrapper<...>::call функцию,
                // которой передадут наши аргументы xs
                struct call

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

              typename 
              struct negate {
                template 
                struct f {
                  typedef list_wrapper::value, ys> value;
                };
                
                typedef typename negate::template call::value;
              };
              
              typename <>
              struct negate<> {
                typedef list_wrapper<> value;
              };
              

              Вышло так, что за красивую запись вида $\{x1, x2, x3\}$ при объявлении, пришлось пострадать, запаковывая, передавая и распаковывая эти последовательности. negate выглядит более громоздко даже по сравнению с версией для Cons/Nil. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons/Nil, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.

              Выход во внешний мир


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

              По завершении вычислений в метапрограмме, результирующее выражение интерпретируется как сущность из реального мира и, либо сохраняется в переменную, либо выводится на экран. Для вывода можно использовать шаблонные классы. В конструкторе или других методах располагается императивный код. Он может быть как внутри самих метазначений (например, void One::print();), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>:

              template 
              struct print {
                print () {
                  std::cout << "unknown number" << std::endl;
                }
              };
              
              template <>
              struct print  {
                print () {
                  std::cout << "1" << std::endl;
                }
              };
              
              template <>
              struct print  {
                print () {
                  std::cout << "0" << std::endl;
                }
              };
              
              // print::value>() выведет "1"
              

              Аналогично, можно вывести список и всё, что угодно. Например, мы бы могли реализовать AND, NOT, XOR, двоичный сумматор, представить числа как списки из One и Zero и построить сложение, умножение,…

              До C++11 и появления decltype никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.

              sum, three> // тип - корректно
              sum, three>() // значение - компилируется
              
              do_something(sum(), three()) // значения - компилируется
              sum(), three()> // нельзя посчитать,
              // т.к. нет перехода от значений к типам
              
              sum()), decltype(three())> // C++11; компилируется
              

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

              До decltype типы были аналогией чистоты, а значения — сущностями императивного мира: параметром шаблона мог быть только тип, а тип мог быть получен только преобразованиями типа; один раз создав значение, нельзя было вернуться обратно к типам.

              В C++11 можно вычислить некий тип, создать значение этого типа, преобразовать его каким-либо образом и передать тип результата в выражение над типами с помощью decltype. Впрочем, decltype свой аргумент не исполняет, а лишь отвечает на вопрос «какой бы был тип выражения, если бы мы его начали считать», отчего не нарушает функциональной чистоты. Поэтому, пока выражение над переменными не покидает скобки decltype, чистота сохраняется. С точки зрения языка шаблонов, decltype выгодно использовать вместе с перегрузкой операторов. В следующем примере выражения эквивалентны, но нижнее выглядит менее громоздко:

              typedef sum, three> x;
              typedef decltype(one() + two() + three()) x;
              

              Выход за скобки нарушает чистоту:

              auto v = one() + two() + three(); // v выполнится как императивная конструкция
              typedef decltype(v) x;
              

              Эзотерический язык, транслирующийся в шаблоны C++


              Основная проблема шаблонов C++ как ФЯ времени компиляции — излишняя многословность. Все эти ::value, скобочки и typename сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.



              В общем, решил я создать свой язык, который будет транслироваться в шаблоны C++. Сделать это можно разными способами. Использовать можно даже наиболее хитроумные преобразования наподобие тех, что делает транслятор JSFuck. Но такой подход (а) породит ещё один ненужный язык, (б) оторванный от C++, (в) который ещё нужно придумать и (г) потратить силы на реализацию. А разработать и реализовать достаточно мощный и ненужный ФЯ — дело хлопотное и бесполезное. Особенно, когда в шаблонах C++ уже есть конструкции, эквивалентные функциям, замыканиям, паттерн-матчингу… Да и не спортивно это.

              Я избрал путь наибольшего соответствия. Код на моём языке должен был выглядеть просто, но оставаться идейно похожим на C++. То есть преобразование должно было наиболее дословно переводить мой язык в C++. Он должен был стать килограммом синтаксического сахара над шаблонами.

              Давайте перепишем приведённые выше куски кода на созданный язык, таким образом рассмотрев его особенности и их применение в деле. Будем двигаться в том же порядке от определения значений «ноль» и «один», функции отрицания числа, конструкторов списка к определению функции отрицания списка, ФВП map и т.д.

              Значения и функции


              Для объявления значения не требуется ключевое слово struct:

              One;
              Zero;
              

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

              Not (val x) = Zero; // общий случай
              Not (Zero) = One; // частный случай
              
              And(val x, val y); // для x,y != One,Zero And не определена
              And(val x, One) = x; // One - константа, val x - аргумент
              And(val x, Zero) = Zero;
              
              // использование: Not(One) или And(One, Zero)
              

              В C++ параметром шаблона может быть тип (typename), другой шаблон (template class) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->). Например, val -> val — унарная функция (шаблон, принимающий один параметр), (val, val) -> val — бинарная функция (шаблон, принимающий два параметра) и так далее.

              Здесь я немного отступил от дословности транслирования и ввёл псевдонимы типов, аналога которым (псевдонимов видов) в C++ нет. С помощью #type можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:

              #type number = val;
              #type list = val;
              #type unary = number -> number;
              #type map_t = (unary, list) -> list;
              // map_t раскроется в template 

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

              Список


              Множественный возврат как полезную возможность я не отменял. Это пригождается уже при реализации списка:

              Nil;
              
              Cons(val x, val y) {
                head = x;
                tail = y;
              }
              

              Применение функции к аргументам, транслируясь в C++, автоматически раскрывает ::value. То есть f(x) эквивалентно f::value. Но у Cons сгенерируется только два метаполя head и tail. Предотвращать раскрытие ::value требуется явно с помощью апострофа: Cons(x, xs)'.

              Я долго думал над этой проблемой. С одной стороны, ::value как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value:

              Cons(x, xs)'.head; // x
              Cons(x, xs)'.tail; // xs
              Not(Zero);         // One
              Not(Zero)'!;       // One
              

              Впрочем, метаполе ::value может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:

              // отрицание непустого списка:
              // список из отрицаний элементов - это отрицание головы,
              // соединённое с хвостом отрицаний
              negate (list list) = Cons(not_head, not_tail)' {
                h = list.head; // голова
                not_head = Not(h); // отрицание головы
                t = list.tail; // хвост
                not_tail = negate(t); // отрицание хвоста
              } // точку с запятой можно опустить!
              
              // пустой список - сам себе отрицание
              negate (Nil) = Nil; 
              

              Функции высшего порядка


              Вспомним, что мы объявили типы унарной функции и списка и напишем map:

              // преобразование непустого списка
              map (unary f, list list) = Cons(new_head, new_tail)' {
                h = list.head; // голова
                new_head = f(h); // преобразованная голова
                t = list.tail; // хвост
                new_tail = map(f, t); // преобразованный хвост
              }
              
              // преобразование пустого списка
              map (unary f, Nil) = Nil;
              

              Разумеется, вместо unary и list можно было сразу указать val -> val и val соответственно, но запись оказалась бы более длинной и менее наглядной.

              Замыкания


              Поскольку замыкания поддерживаются самими шаблонами C++, здесь нет чего-то необычного:

              f(val x) = {
                g(val xs) = Cons(x, xs)';
              }
              
              // использование: f(x)'.g(list) или map(f(x)'.g, list)
              

              Но требуется давать имя (например, g) возвращаемой функции. Возвращаемая безымянная функция должна была бы иметь имя value и возвращать value. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef приводит к ошибке компиляции:

              template 
              struct f {
                template 
                struct value {               // value!
                  typedef Cons value; // value!
                };
              };
              

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

              C++ также не позволяет переиспользовать имена параметров шаблона внутри шаблона. Поскольку эти имена в моём языке относятся к локальным метапеременным и за пределы шаблона не выходят, я автоматизировал их переименование:

              f (val x) = g(Not(x)) {
                g (val x) = x; // будет заменено на g (x1) = x1
                // т.к. x станет параметром template f и не допустит x как параметр template g
              }
              

              Такое преобразование, как мне кажется, не добавило «неспортивности» (из-за возможности тривиальной замены вручную), а жизнь несколько упростило.

              Выход во внешний мир


              На определённом этапе оказалось, что чисто функциональная часть более-менее успешно транслируется в C++, и надо как-то уметь хотя бы распечатать результат. Эта часть, выходила за рамки интересного мне чистого мира шаблонов (из-за чего в итоге продумана хуже) и, что более важно, достаточно хорошо описывалась обычным C++. В программах с компайл-тайм вычислениями на шаблонах за выход во внешний мир отвечает обычный C++ без шаблонов. То есть я должен был либо сделать мой язык надмножеством C++, либо создать свой аналог C++. Разумеется, я выбрал первый вариант.

              Оставалось только ввести тэги для вставки кода на C++ и удовлетворённо потирать руки. Но возникла логическая проблема: в исходниках нигде не упоминается, что для применения метафункций используется ::value, нигде не сказано, что f(x) — это f::value, а не call::result. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:

              f(val x) = One;
              f(Zero) = Zero;
              
              main {
                print::value>(); // почему так?
              }
              

              Идей насчёт малоинтересной императивной части было немного, и после прикидывания нескольких вариантов я решил ввести (а) императивные блоки с возможностью использования в них обычного C++ и (б) экспорт в них значений из чистого функционального мира. Блок impure { код } может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.

              В качестве демонстрации работы impure приготовим «распечатыватель» списка:

              print(val list) {
                head = list.head;
                tail = list.tail;
                impure {
                  // после typedef-ов head, tail расположится следующий блок кода:
                  print() {
                    impure print(head); // преобразуется в "print();"
                    std::cout << ", ";
                    impure print(tail);
                  }
                }
              }
              
              print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
                print() {
                  std::cout << "0";
                }
              }
              
              print(One) = impure {
                print() {
                  std::cout << "1";
                }
              }
              
              print(Nil) = impure {
                print() {
                  std::cout << std::endl;
                }
              }
              

              Пространства имён


              Поскольку на моём языке в теории можно было бы создавать библиотеки, которые использовались бы в обычном C++, я «прокинул» в него namespace и using namespace:

              namespace ns1 {
                namespace ns2 {
                  f(val x) = x;
                }
              }
              
              namespace ns3 {
                using namespace ns1.ns2;
                g(val x) = f(x);
              }
              

              using namespace также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y. Если f, x или z — не конкретные типы/шаблоны, а параметры шаблона, то ::y становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y — тип или шаблон (например, typename f::y или f::template y). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y оттранслируется во что-то некорректное вида typename typename f::value::typename y::value. В случае пространства имён это излишне, using namespace позволяет обойтись без вставки «typename».

              Лямбды


              Мне не хотелось оставлять функциональный язык без лямбда-функций. Полноценную поддержку реализовать не удалось, но вместо этого хотя бы можно перенести при объявлении функции её аргументы по ту сторону знака "="

              f          (val x) =  y(x) { y(x) = g(x); } // было
              f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
              

              Полноценная поддержка лямбд затрудняется тем, что в C++ (а) нельзя создать анонимный класс и (б) нельзя объявить нечто, эквивалентное шаблону, не раскрутив описание до эквивалентности типа типу. Т.е., выражаясь в математических терминах, $x = y$ написать можно, а вместо $g = f$ придётся использовать $g(x) = f(x)$. Пользователи императивного программирования уже привыкли к такому положению дел, но по меркам программирования функционального это довольно громоздко.

              template  struct f {}; // пусть была метафункция f
              
              using g = f; // нельзя описать эквивалентность шаблона шаблону
              
              // нужно описать эквивалентность выражений, соответствующих типам
              template  using g = f; // g - тип, f - тип
              

              Для реализации записей вида map(lambda(val x) -> y, xs) придётся модифицировать язык и генерировать шаблоны с временными именами.

              Как рассматривалось выше, value::value — это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f нужно использовать другие концепции и почти полностью переписать транслятор.

              Недостатки и возможные пути решения


              1. Нет сокрытия локальных переменных.
                Наивно решается вставкой impure { private: } в код или элементарной модификацией языка.
              2. При использовании метаполя автоматически подставляется «typename».
                Не знаю, можно ли избавиться от этого автоматически. В качестве варианта решения можно использовать обязательную венгерскую нотацию (скажем, fMap(fCurry(fCons).fApply(vXs), vObj.vYs) — при доступе к метаполю подставится template, typename или ничего, если первым символом имени будет «f», «v» или «n» соответственно).

                Второй путь — «динамическая типизация». То есть представление всех сущностей в виде структуры-обёртки с хранящимся типом и значением:

                // объявляем значение x
                struct x {
                  typedef internal::value type; // тип значения x
                };
                
                // объявляем функцию f(x) = x
                struct f {
                  typedef internal::function type; // тип функции f
                  template 
                  struct value {
                    typedef typename x::type type; // тип результата функции f
                    struct result { // обёртка для того, чтобы избежать value::value
                      typedef x value;
                    };
                  };
                };
                

                Транслятор автоматически сгенерирует пространство имён internal со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result можно будет избавиться от value::value и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.
              3. Есть ограничения на лямбды: они не могут возвращать функции и быть анонимными.
                Может помочь описанный выше подход.

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

                template 
                struct f {
                  typedef internal::True uses_value1; // использует value1
                  template 
                  struct value1 {
                    typedef internal::False uses_value1; // использует value2
                    typedef Cons value2;
                  };
                };
                

                Вызвать такие метафункции можно будет с помощью отдельной метафункции, которая в зависимости от обязательного метаполя uses_value1 будет выбирать, доступаться до value1 или до value2.
              4. Не учитываются вариадические шаблоны и целые числа как параметры шаблонов.
                Для реализации требуется поддержка кроме val дополнительных типов для чисел (int, uint, ...) и массивов ([val], [int], [uint], ...). При использовании метаполей придётся решить описанную выше проблему с указанием template и typename, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.

              Пример программы


              В качестве примера построим список $\{1, 1, 0\}$, возьмём его отрицание двумя способами и распечатаем все эти значения в main:

              // строим списки:
              my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
              negated_list = negate(my_list);
              negated_list2 = map(Not, my_list);
              
              impure {
                int main() {
                  // печатаем списки:
                  std::cout << "my list is ";
                  impure print(my_list);
                  
                  std::cout << "negated list is ";
                  impure print(negated_list);
                  
                  std::cout << "negated list is also ";
                  impure print(negated_list2);
                }
              }
              

              Программа выведет следующее:

              my list is 1, 1, 0,
              negated list is 0, 0, 1,
              negated list is also 0, 0, 1,
              

              Исходный код программы целиком
              impure {
                #include 
              }
              
              One;
              Zero;
              
              Not (val x) = Zero; // общий случай
              Not (Zero) = One; // частный случай
              
              And(val x, val y); // для x,y != One,Zero And не определена
              And(val x, One) = x; // One - константа, val x - аргумент
              And(val x, Zero) = Zero;
              
              #type number = val;
              #type list = val;
              #type unary = number -> number;
              #type map_t = (unary, list) -> list;
              
              Nil;
              
              Cons(val x, val y) {
                head = x;
                tail = y;
              }
              
              // отрицание непустого списка:
              negate (list list) = Cons(not_head, not_tail)' {
                h = list.head;
                not_head = Not(h);
                t = list.tail;
                not_tail = negate(t);
              }
              
              // пустой список - сам себе отрицание
              negate (Nil) = Nil; 
              
              // преобразование непустого списка
              map (unary f, list list) = Cons(new_head, new_tail)' {
                h = list.head;
                new_head = f(h);
                t = list.tail;
                new_tail = map(f, t);
              }
              
              // преобразование пустого списка
              map (unary f, Nil) = Nil;
              
              print(val list) {
                head = list.head;
                tail = list.tail;
                impure {
                  print() {
                    impure print(head);
                    std::cout << ", ";
                    impure print(tail);
                  }
                }
              }
              
              print(Zero) = impure {
                print() {
                  std::cout << "0";
                }
              }
              
              print(One) = impure {
                print() {
                  std::cout << "1";
                }
              }
              
              print(Nil) = impure {
                print() {
                  std::cout << std::endl;
                }
              }
              
              my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
              negated_list = negate(my_list);
              negated_list2 = map(Not, my_list);
              
              impure {
                int main() {
                  std::cout << "my list is ";
                  impure print(my_list);
                  
                  std::cout << "negated list is ";
                  impure print(negated_list);
                  
                  std::cout << "negated list is also ";
                  impure print(negated_list2);
                }
              }
              


              Код после трансляции в C++
              #include 
              struct One;
              struct Zero;
              template 
              struct Not {
                typedef Zero _value;
              };
              template <>
              struct Not {
                typedef One _value;
              };
              template 
              struct And;
              template 
              struct And {
                typedef x _value;
              };
              template 
              struct And {
                typedef Zero _value;
              };
              struct Nil;
              template 
              struct Cons {
                typedef x head;
                typedef y tail;
              };
              template 
              struct negate {
                typedef typename list::head h;
                typedef typename Not  ::_value not_head;
                typedef typename list::tail t;
                typedef typename negate  ::_value not_tail;
                typedef Cons   _value;
              };
              template <>
              struct negate {
                typedef Nil _value;
              };
              template 


              Транслятор


              Свой транслятор я написал на JavaScript (люблю этот язык, на нём мне легче думается). В качестве генератора парсеров использовал PEG.js.

              При запуске транслятор (см. страничку языка на GitHub) считывает файл, имя которого указано как параметр командной строки, и выдаёт в stdout результирующий текст программы на C++.

              node src/compile <исходник> # запуск

              Как только транслятор более-менее заработал и были написаны работающие программы, я разместил всё это добро на GitHub и, словно математик из анекдота, воскликнувший «Решение есть!», почти утратил интерес к развитию проекта. Порывался решить указанные выше проблемы чередованием/указанием типа/венгерской нотацией, но это требовало серьёзных изменений и повторного умственного напряжения. Думать, имея что-то готовое и работающее, было лень. Более того, мог нарушиться принцип наибольшего соответствия. Лишние обёртки и код по работе с ними убили бы красоту прямолинейности трансляции и приблизили бы программу к превышению лимита на вложенность шаблонов.

              Поэтому я остановился на достигнутом и буду рад, если кто-нибудь заинтересуется и, быть может, сделает свой вклад в развитие языка.
              Original source: habrahabr.ru (comments, light).

              https://habrahabr.ru/post/337590/


              [Перевод] JavaScript: методы асинхронного программирования

              Понедельник, 11 Сентября 2017 г. 15:20 + в цитатник
              ru_vds сегодня в 15:20 Разработка

              JavaScript: методы асинхронного программирования

              • Перевод
              Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.



              Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

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

              О синхронном и асинхронном коде


              Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

              console.log('1')
              console.log('2')
              console.log('3')

              Он, без особых сложностей, выводит в консоль числа от 1 до 3.

              Теперь — код асинхронный:

              console.log('1')
              setTimeout(function afterTwoSeconds() {
                console.log('2')
              }, 2000)
              console.log('3')

              Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds.

              Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

              Постановка задачи


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

              В плане интерфейса ограничимся чем-нибудь простым.


              Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

              В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

              // аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
              function request(url) {
                const xhr = new XMLHttpRequest();
                xhr.timeout = 2000;
                xhr.onreadystatechange = function(e) {
                  if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                     // Код обработки успешного завершения запроса
                    } else {
                     // Обрабатываем ответ с сообщением об ошибке
                    }
                  }
                }
                xhr.ontimeout = function () {
                  // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
                }
                xhr.open('get', url, true)
                xhr.send();
              }

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

              Функции обратного вызова


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

              // Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
              doThis(andThenThis)
              // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
              function andThenThis() {
                console.log('and then this')
              }
              // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
              function doThis(callback) {
                console.log('this first')
                
                // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
                callback()
              }

              Используя этот подход для решения нашей задачи, мы можем написать такую функцию request:

              function request(url, callback) {
                const xhr = new XMLHttpRequest();
                xhr.timeout = 2000;
                xhr.onreadystatechange = function(e) {
                  if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                     callback(null, xhr.response)
                    } else {
                     callback(xhr.status, null)
                    }
                  }
                }
                xhr.ontimeout = function () {
                 console.log('Timeout')
                }
                xhr.open('get', url, true)
                xhr.send();
              }

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

              const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
              request(userGet, function handleUsersList(error, users) {
                if (error) throw error
                const list = JSON.parse(users).items
                list.forEach(function(user) {
                  request(user.repos_url, function handleReposList(err, repos) {
                    if (err) throw err
                    //Здесь обработаем список репозиториев
                  })
                })
              })

              Разберём то, что здесь происходит:

              • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
              • После завершения запроса вызывается коллбэк handleUsersList;
              • Если не было ошибок, разбираем ответ сервера c помощью JSON.parse, преобразовываем его, для удобства, в объект;
              • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url — это URL для наших следующих запросов, и получили мы его из первого запроса.
              • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.

              Обратите внимание на то, что использование в качестве первого параметра объекта ошибки — это широко распространённая практика, в частности, для разработки с использованием Node.js.

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

              try {
                request(userGet, handleUsersList)
              } catch (e) {
                console.error('Request boom! ', e)
              }
              function handleUsersList(error, users) {
                if (error) throw error
                const list = JSON.parse(users).items
                list.forEach(function(user) {
                  request(user.repos_url, handleReposList)
                })
              }
              function handleReposList(err, repos) {
                if (err) throw err
                
                // Здесь обрабатываем список репозиториев
                console.log('My very few repos', repos)
              }

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


              Ад коллбэков во всей красе. Изображение взято отсюда.

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

              Промисы


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

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

              const myPromise = new Promise(function(resolve, reject) {
                
                // Здесь будет код
                
                if (codeIsFine) {
                  resolve('fine')
                } else {
                  reject('error')
                }
              })
              myPromise
                .then(function whenOk(response) {
                  console.log(response)
                  return response
                })
                .catch(function notOk(err) {
                  console.error(err)
                })

              Разберём этот пример:

              • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject;
              • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise. Если код будет выполнен успешно, вызывают метод resolve, если нет — reject;
              • Если функция вызовет resolve, будет исполнен метод .then для объекта Promise, аналогично, если будет вызван reject, будет исполнен метод .catch.

              Вот что стоит помнить, работая с промисами:

              • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works'), коллбэку .then будет передано лишь 'yey';
              • Если объединить в цепочку несколько вызовов .then, в конце соответствующих коллбэков следует всегда использовать return, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
              • При выполнении команды reject, если следующим в цепочке идёт .then, он будет выполнен (вы можете считать .then выражением, которое выполняется в любом случае);
              • Если в цепочке из вызовов .then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch;
              • У промисов есть три состояния: «pending» — состояние ожидания вызова resolve или reject, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve, и неуспешному, с вызовом reject, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.

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

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

              function request(url) {
                return new Promise(function (resolve, reject) {
                  const xhr = new XMLHttpRequest();
                  xhr.timeout = 2000;
                  xhr.onreadystatechange = function(e) {
                    if (xhr.readyState === 4) {
                      if (xhr.status === 200) {
                        resolve(xhr.response)
                      } else {
                        reject(xhr.status)
                      }
                    }
                  }
                  xhr.ontimeout = function () {
                    reject('timeout')
                  }
                  xhr.open('get', url, true)
                  xhr.send();
                })
              }

              При таком подходе, когда вы вызываете request, возвращено будет примерно следующее.


              Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

              Теперь, воспользовавшись новой функцией request, перепишем остальной код.

              const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
              const myPromise = request(userGet)
              console.log('will be pending when logged', myPromise)
              myPromise
                .then(function handleUsersList(users) {
                  console.log('when resolve is found it comes here with the response, in this case users ', users)
                  const list = JSON.parse(users).items
                  return Promise.all(list.map(function(user) {
                    return request(user.repos_url)
                  }))
                })
                .then(function handleReposList(repos) {
                  console.log('All users repos in an array', repos)
                })
                .catch(function handleErrors(error) {
                  console.log('when a reject is executed it will come here ignoring the then statement ', error)
                })

              Здесь мы оказываемся в первом выражении .then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch.

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

              const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
              const userRequest = request(userGet)
              // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
              userRequest
                .then(handleUsersList)
                .then(repoRequest)
                .then(handleReposList)
                .catch(handleErrors)
              function handleUsersList(users) {
                return JSON.parse(users).items
              }
              function repoRequest(users) {
                return Promise.all(users.map(function(user) {
                  return request(user.repos_url)
                }))
              }
              function handleReposList(repos) {
                console.log('All users repos in an array', repos)
              }
              function handleErrors(error) {
                console.error('Something went wrong ', error)
              }

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

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

              Генераторы


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

              Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова function. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:

              function* foo() {
                yield 1
                const args = yield 2
                console.log(args)
              }
              var fooIterator = foo()
              console.log(fooIterator.next().value) // выведет 1
              console.log(fooIterator.next().value) // выведет 2
              fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'

              Дело тут в том, что генераторы, вместо return, используют выражение yield, которое останавливает выполнение функции до следующего вызова .next итератора. Это похоже на выражение .then в промисах, которое выполняется при разрешении промиса.

              Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

              function request(url) {
                return function(callback) {
                  const xhr = new XMLHttpRequest();
                  xhr.onreadystatechange = function(e) {
                    if (xhr.readyState === 4) {
                      if (xhr.status === 200) {
                        callback(null, xhr.response)
                      } else {
                        callback(xhr.status, null)
                      }
                    }
                  }
                  xhr.ontimeout = function () {
                    console.log('timeout')
                  }
                  xhr.open('get', url, true)
                  xhr.send()
                }
              }

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

              Генератор будет выглядеть так:

              function* list() {
                const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
               
                const users = yield request(userGet)
                
                yield
                
                for (let i = 0; i<=users.length; i++) {
                  yield request(users[i].repos_url)
                }
              }

              Вот что здесь происходит:

              • Мы ожидаем подготовки первого запроса, возвращая ссылку на функцию и ожидая коллбэка для этого первого запроса (вспомните функцию request, которая она принимает url и возвращает функцию, которая ожидает коллбэк);
              • Ожидаем готовности списка пользователей, users, для отправки в следующий .next;
              • Проходимся по полученному массиву users и ожидаем, для каждого из них, .next, возвращая, для каждого, соответствующий коллбэк.

              Использование этого всего будет выглядеть так:

              try {
                const iterator = list()
                iterator.next().value(function handleUsersList(err, users) {
                  if (err) throw err
                  const list = JSON.parse(users).items
                  
                  // Отправляем список пользователей итератору
                  iterator.next(list)
                  
                  list.forEach(function(user) {
                    iterator.next().value(function userRepos(error, repos) {
                      if (error) throw repos
                      // Здесь обрабатываем информацию о репозиториях каждого пользователя
                      console.log(user, JSON.parse(repos))
                    })
                  })
                })  
              } catch (e) {
                console.error(e)
              }

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

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

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

              Async/await


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

              sumTwentyAfterTwoSeconds(10)
                .then(result => console.log('after 2 seconds', result))
              async function sumTwentyAfterTwoSeconds(value) {
                const remainder = afterTwoSeconds(20)
                return value + await remainder
              }
              function afterTwoSeconds(value) {
                return new Promise(resolve => {
                  setTimeout(() => { resolve(value) }, 2000);
                });
              }

              Здесь происходит следующее:

              • Имеется асинхронная функция sumTwentyAfterTwoSeconds;
              • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds, который может завершиться вызовом resolve или reject;
              • Выполнение кода заканчивается в .then, где завершается операция, отмеченная ключевым словом await, в данном случае — это всего одна операция.

              Подготовим функцию request к использовании в конструкции async/await:

              function request(url) {
                return new Promise(function(resolve, reject) {
                  const xhr = new XMLHttpRequest();
                  xhr.onreadystatechange = function(e) {
                    if (xhr.readyState === 4) {
                      if (xhr.status === 200) {
                        resolve(xhr.response)
                      } else {
                        reject(xhr.status)
                      }
                    }
                  }
                  xhr.ontimeout = function () {
                    reject('timeout')
                  }
                  xhr.open('get', url, true)
                  xhr.send()
                })
              }

              Теперь создаём функцию с ключевым словом async, в которой используем ключевое слово await:

              async function list() {
                const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
                
                const users = await request(userGet)
                const usersList = JSON.parse(users).items
                
                usersList.forEach(async function (user) {
                  const repos = await request(user.repos_url)
                  
                  handleRepoList(user, repos)
                })
              }
              function handleRepoList(user, repos) {
                const userRepos = JSON.parse(repos)
                
                // Обрабатываем тут репозитории для каждого пользователя
                console.log(user, userRepos)
              }

              Итак, у нас имеется асинхронная функция list, которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach, чтобы сформировать список репозиториев. Вызвать всё это очень просто:

              list()
                .catch(e => console.error(e))

              Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать здесь.

              Минус async/await, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.

              Итоги


              Здесь можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

              Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.

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

              Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?
              Original source: habrahabr.ru (comments, light).

              https://habrahabr.ru/post/337662/


              Метки:  

              6 сентября на Дизайн-заводе Flacon прошел неформальный митап для back-end разработчиков

              Понедельник, 11 Сентября 2017 г. 15:18 + в цитатник
              Prosfera сегодня в 15:18 Администрирование

              6 сентября на Дизайн-заводе Flacon прошел неформальный митап для back-end разработчиков

                6 сентября на Дизайн-заводе Flacon прошел неформальный митап для back-end разработчиков!


                Нас пугали ураганом, грозой и холодом! Не сработало, к нам присоединилось более 150 участников!

                Горячий глинтвейн и бургеры, теплая атмосфера! А еще горячие темы выступлений:

                — Как с пользой провести время на собеседовании
                — Почему Big Data бесполезна
                — Как внедрить Kubernetes и не сойти с ума
                — Почему архитектуру иногда нужно менять просто так
                — Про Docker




                Приглашенный спикер Владимир Ярцев, технический директор в компании Cloud Castle и создатель dockhero.io. В прошлом — системный программист и сисадмин. В настоящем — Ruby-on-Rails-разработчик и адепт Heroku. Верит в The Twelve Factors. Который рассказал все о Heroku!

                Было уютно и свежо! Много живого общения, много интересных вопросов и не менее интересных ответов!

                Ну и не обошлось без хайпа, точнее антихайпа! Нашим приглашенным гостем был российский хип-хоп-исполнитель, участник рэп-баттлов Гнойный, он же Слава КПСС, Соня Мармеладова, Валентин Дядька!





                Видеозапись митапа
                Фотоотчет 1
                Фотоотчет 2
                Original source: habrahabr.ru (comments, light).

                https://habrahabr.ru/post/337664/


                Метки:  

                Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО

                Понедельник, 11 Сентября 2017 г. 15:16 + в цитатник
                ARG89 сегодня в 15:16 Разработка

                Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО



                  После победы AlphaGo в марте 2016 года над одним из сильнейших игроков Go в мире Ли Седолем о методах глубокого обучения заговорили практически везде. И даже Google не упустил случая назвать себя компанией машинного обучения и искусственного интеллекта.

                  Что стоит за термином «глубокое обучение»? Какими бывают модели машинного обучения и на чём они пишутся? Ответить на эти и многие другие вопросы, связанные с МО и, в частности, с глубоким обучением (deep learning), мы попросили Алексея Потапова, профессора кафедры компьютерной фотоники и видеоинформатики ИТМО.

                  Алексей Потапов
                  Профессор кафедры компьютерной фотоники и видеоинформатики ИТМО. Победитель конкурса грантов для молодых научно-педагогических работников вузов Санкт-Петербурга. Победитель конкурса грантов Президента Российской Федерации для государственной поддержки молодых российских ученых — докторов наук в области «Информационно-телекоммуникационных систем и технологий».



                  — Сегодня мы практически отовсюду слышим о буме машинного обучения и особенно глубокого обучения. Как и в какой ситуации это может понадобиться простому IT-шнику?

                  Алексей Потапов: Честно говоря, у меня в голове нет образа «простого IT-шника», так что мне непросто ответить на этот вопрос. Можно ли считать «простым IT-шником» того, кто сможет применить методы глубокого обучения для автоматизации своей деятельности? Не уверен. Наверное, это всё же будет «сложный IT-шник». Тогда простому особо вникать, наверное, и не надо — за исключением того, чтобы поддерживать актуальную картину мира и в качестве демотиватора: рано или поздно «роботы отберут работу» не только у сотрудников Макдональдса, но и у IT-шников.

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

                  Алексей Потапов: Здесь очень сильно зависит от того, насколько глубоко и с какой целью человек решил погрузиться в тему, а также от уровня его подготовки. Одно дело — использовать имеющиеся предобученные сети или обучать известные модели с реализацией на высокоуровневых библиотеках, таких как Keras. Это может делать и не самый продвинутый школьник, не особо вникая в математику. Достаточно изучить документацию соответствующей библиотеки и на примерах научиться комбинировать разные слои, активационные функции и т.д. — работать как с «чёрными ящиками».

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

                  Если человеку не подходит такой путь или он хочет копнуть глубже, он может начать с поверхностной теории — с описания базовых моделей (автоэнкодеров, машин Больцмана, свёрточных сетей, рекуррентных блоков — tanh-RNN, LSTM, GRU и т.д.) AS-IS. Кому-то, может, придётся начать с логистической регрессии и перцептронов. Несложно обо всём этом почитать в интернете. На этом уровне погружения можно пользоваться более низкоуровневыми библиотеками (такими как Tensorflow и Theano) и реализовывать модели не из готовых блоков, а на уровне операций с тензорами.

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

                  Вот, например, несколько неплохих курсов:
                  Introduction into Deep Learning
                  Neural Networks and Deep Learning
                  Convolutional Neural Networks for Visual Recognition

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

                  Алексей Потапов: Если говорить широко, то дискриминантные и генеративные модели используются людьми повсеместно. Скажем, когда древние люди заметили разницу в движении планет и звёзд по небу, они построили дискриминантную модель: если у светила есть попятное движение, то это планета, а если оно движется по небесной сфере равномерно, то это звезда. А вот модель Кеплера — это генеративная модель: она не просто выделяет некоторый различительный признак из наблюдений, а позволяет воспроизводить траекторию.

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

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

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

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

                  — Почему глубокое обучение и вероятностный подход выстрелили именно сейчас, а не 20-30 лет назад?

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

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

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

                  Перейдём к техническим вопросам. Нейровероятностное программирование на Edward — каким образом две парадигмы объединены и что это позволяет моделировать?

                  Алексей Потапов: В Tensorflow всё строится на тензорах и операциях с ними и, главное, добавляется автоматическое дифференцирование, которое используется для оптимизации заданной функции потерь градиентным спуском. В Edward все операции с тензорами остаются, но поверх или внутрь тензоров добавляются распределения вероятностей, а также специальные функции потерь для распределений, скажем, дивергенция Кульбака-Лейблера, и методы их оптимизации. Это даёт возможность просто и компактно задавать и обучать генеративные глубокие сети, такие как Generative Adversarial Networks (вариант перевода: генеративные состязающиеся сети), вариационные автоэнкодеры, а также писать свои аналогичные модели и делать байесовский вывод над любыми сетями.

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

                  — При каких практических условиях Вы бы точно не рекомендовали использовать глубокое обучения?

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

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

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

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

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

                  Алексей Потапов: Можно построить более сложную метасеть, которая будет «понимать» более простую сеть, но это вычислительно слишком дорого. Вопрос, скорее, в том, что вы хотите узнать о сети. Скажем, «байесовизация» сети даст вам информацию о неопределённости. Есть техники для генерации «плохих» примеров. В случае генеративных сетей вы можете визуализировать выученное многообразие, меняя одну скрытую переменную. То есть отдельные техники для анализа есть. Но, конечно, всей картины они вам не покажут — если вы используете типичные сети глубокого обучения, а не какие-то специальные компактные модели, то просто описать работу сети вы не сможете, так как такая сеть в принципе сложная.

                  Классические алгоритмы машинного обучения с учителем (supervised), как, например SVM, пытаются определить класс объекта, а без учителя (unsupervised) — разбить набор объектов по классам без меток. С этой точки зрения автоэнкодеры (autoencoder) делают что-то очень странное: они пытаются воссоздать сами данные — зачем и почему это вообще может быть нужно?

                  Алексей Потапов: Обучение без учителя не обязательно сводится к кластеризации. Это лишь один возможный класс моделей. Да и вообще, разделение на обучение с учителем и без учителя несколько условно. Метку класса вы можете считать просто как дополнительный категориальный признак и тогда получите обычную постановку обучения без учителя. Вопрос, скорее, в целевой функции, в том, что вы хотите сделать в результате обучения. При обучении с учителем, как правило, вас интересует предсказание той самой метки и больше ничего. При обучении без учителя у вас такой конкретной цели нет, она гораздо более общая — вы хотите найти в данных закономерности. Возможно, это кластеры, в которые группируются образы. Возможно, гладкое малоразмерное многообразие, на котором эти образы расположены. Таким образом, при обучении без учителя вы пытаетесь построить модель того, как данные распределены. Обычно это генеративная модель, хотя не обязательно. Но как проверить правильность такой модели? Надо сравнить задаваемое ею распределение с реальным, то есть надо воссоздать данные из обучающей выборки. Таким образом, ничего странного автоэнкодеры не делают — они решают совершенно типичную задачу обучения без учителя. Именно это делают и все другие методы обучения без учителя, правда, не всегда явно.

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

                  Многие исследователи (например, Демис Хассабис и Андрэ Ын) отмечают, что машинное обучение началось с определения параметров (классическое машинное обучение SVM, C4.5), перешло к построению признаков (e.g., CNN) и сейчас переходит в стадию нахождения и построения архитектуры. Как технически такое метообучение вообще может быть реализовано и насколько результаты сравнимы с традиционными методами?

                  Алексей Потапов: Если же говорить про метаобучение в целом, то лично меня это направление очень интересует. Когда вы смотрите на многообразие имеющихся моделей, то становится очевидным, что ни одна из них не является фундаментальной, во всех есть какие-то эвристики, которые могут быть не очень применимы и т.д. Все эти модели придумываются людьми, а значит, можно автоматизировать и процесс разработки этих моделей и алгоритмов их обучения. И как вручную сконструированные признаки сейчас легко побеждаются обученными признаками, так и вручную сконструированные модели и алгоритмы обучения гипотетически будут проигрывать метаобучению. Сейчас есть разные отдельные эксперименты, показывающие, что это действительно возможно — в узких экспериментах метаобучение (и, говоря шире, AutoML) превосходит разработки человека, хотя для широкого использования пока технология недостаточно развита.



                  Если темы Big Data и машинного обучения вам близки так же, как и нам, хотим обратить ваше внимание на ряд ключевых докладов на грядущей конференции SmartData 2017, которая пройдёт 21 октября в Санкт-Петербурге:

                  Original source: habrahabr.ru (comments, light).

                  https://habrahabr.ru/post/337392/


                  Метки:  

                  AMD готовится потеснить Intel на рынке серверных решений

                  Понедельник, 11 Сентября 2017 г. 15:15 + в цитатник
                  it_man сегодня в 15:15 Разработка

                  AMD готовится потеснить Intel на рынке серверных решений

                    Этим летом AMD заявили о своем триумфальном возвращении в корпоративный сектор высокопроизводительных вычислений. С анонсом новых процессоров на архитектурных решениях семейства EPYC компания AMD собирается потеснить Intel в области оборудования для дата-центров.

                    / Фото / twin-loc.fr / CC BY-SA

                    Конкурентное преимущество


                    Главным оппонентом чипов EPYC на новом поле боя выступают процессоры Skylake от Intel. Компания доминирует на рынке процессоров, используемых крупными предприятиями в области облачных вычислений, а также компаниями с собственной инфраструктурой. Запуск линейки процессоров Xeon Skylake Навин Шеной (Navin Shenoy), руководитель группы по ЦОД в Intel, назвал «важнейшим объявлением для индустрии центров обработки данных за десятилетие».


                    / Фото / Kazuhisa OTSUBO / CC BY-SA

                    Несмотря на это, ряд участников IT-отрасли, таких как Курт Марко (Kurt Marko), заверяют, что век безраздельной власти Intel подходит к концу: облачные сервисы начали вытеснять локальную инфраструктуру, а двузначный рост финансовых показателей компании сократился вдвое за последние кварталы.

                    В то же время тестирование новинок от двух конкурентов показало, что чип EPYC обладает рядом неожиданных преимуществ в вопросе производительности для ЦОДов по сравнению с Xeon от Intel.

                    Intel интегрировала 512-битное расширение системы команд x86 (AVX-512) в свою новую масштабируемую платформу Xeon Skylake, что теоретически позволяет удвоить FLOPS и производительность по целочисленным расчетам по сравнению с предыдущей версией Xeon. В AMD же решили положиться на 128-битные векторы, что на первый взгляд ставит EPYC в невыгодное положение. Однако следует учитывать, что в большинстве высокопроизводительных вычислений на сегодняшний день не используется AVX-512.

                    Команда из Anandtech провела тестирование новинок с использованием программ C-ray, POV-Ray, NAMD и ряда других. Тестирование проводилось на двухпроцессорных серверах под управлением Ubuntu Linux.

                    Процессор EPYC опередил Xeon во всех трех тестах с плавающей точкой. В C-ray при рендеринге за заданный промежуток времени EPYC 7601 получил на 50% лучший результат, чем Xeon 8176. В тесте POV-Ray преимущество составило 16%. Для NAMD в Anandtech опробовали две методики: с AVX и без AVX. В обоих случаях процессор EPYC вышел вперед — на 22% и 41% соответственно.

                    Стоит отметить, что в Anandtech также был проведен BigData-тест, в котором разрыв между Xeon и EPYC составил чуть менее 5%. В этом случае процессоры прошли тесты, которые оценивали производительность при целочисленных расчетах и доступ к памяти.

                    Сравнивая Xeon 8176 и EPYC 7601, не следует упускать из внимания их стоимость. Здесь у AMD есть дополнительный потенциал по ослаблению позиций Intel на рынке.

                    Борьба за долю рынка


                    В ИТ-сообществе мнения по поводу возвращения AMD в корпоративный сектор разделились. Часть из них была не очень оптимистична. Например, 18 июля акции компании снизились на 3,2% из-за негативной оценки перспектив AMD в конкурентной борьбе с Intel. Аналитик Блэйн Кертис (Blayne Curtis) из Barclays заключил, что новый продукт будет «недостаточно конкурентоспособным».

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

                    В компании AMD это понимают, поэтому Скотт Айлор (Scott Aylor), корпоративный вице-президент AMD и руководитель в области корпоративных решений, подчеркнул, что компании недостаточно предоставить лишь один продукт, важно предложить клиентам перспективы на будущее.

                    Поэтому AMD взяли на себя ответственность за socket-совместимость между решениями EPYC 7000 и Rome. Последнее — это кодовое название нового процессора AMD следующего поколения, предназначенного для дата-центров. При этом Айлор убежден, что EPYC сможет конкурировать со Skylake за 50% рынка двухсокетных процессоров.


                    / Фото / AMD Markham Canada / CC BY-SA

                    Такое положение нашло отклик в других ИТ-компаниях. В поддержку AMD высказались Гириш Баблани (Girish Bablani), корпоративный вице-президент Microsoft Azure Compute, Чжан Я Цинь (Zhang Ya Qin), президент Baidu, и Эшли Горакхпурвала (Ashley Gorakhpurwalla), президент подразделения серверных решений Dell EMC. Они дали понять, что компании готовы сделать выбор в пользу новых архитектурных решений EPYC.

                    Что до смещения Intel с занятых позиций в сегменте дата-центров, даже сотрудники AMD пока скромны в прогнозах, однако, как отмечает Дэйв Альтавилла (Dave Altavilla) из Forbes, даже 10-процентная доля принесет AMD миллиарды в новом высокорентабельном бизнесе.

                    P.S. Другие статьи на тему производительности в нашем блоге:

                    Original source: habrahabr.ru (comments, light).

                    https://habrahabr.ru/post/337630/


                    Метки:  

                    Технокубок 2017-2018

                    Понедельник, 11 Сентября 2017 г. 15:01 + в цитатник
                    sat2707 сегодня в 15:01 Разработка

                    Технокубок 2017-2018

                      Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.


                      image


                      Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.


                      В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.


                      Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:


                      Технокубок 2015/16
                      1-й и 2-й отборочный раунд
                      Финал


                      Технокубок 2016/17
                      1-й отборочный раунд
                      2-й отборочный раунд
                      3-й отборочный раунд
                      Финал


                      image


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


                      Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.


                      image


                      Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.


                      Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.


                      image


                      В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.


                      image


                      Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!

                      Original source: habrahabr.ru (comments, light).

                      https://habrahabr.ru/post/337646/


                      Метки:  

                      Почему для 99% малого бизнеса бесполезен аудит сайта?

                      Понедельник, 11 Сентября 2017 г. 14:50 + в цитатник
                      wilelf сегодня в 14:50 Маркетинг

                      Почему для 99% малого бизнеса бесполезен аудит сайта?

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

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

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

                        Если заказчик обращается к экспертному мнению, это в большинстве случаев сигнал, что все уже плохо. И здесь требуется не аудит сайта, а анализ ситуации, в которой оказалась компания. Сайт это только «точка кипения», т.к. разработку и продвижение обычно отдают на аутсорс и контролировать эти процессы на 100% невозможно.

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

                        В чем основная беда полученных аудитов?

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

                        Дорогие и подробные документы либо некому внедрять, либо на 90% они содержат то, что даст только 10% результата. Это не только дорого, но и долго – от 2 недель работы специалиста (а мы помним, что все уже плохо). Копаться в запятых, когда у тебя продажи встали – смерти подобно.

                        Аудиты – это во многом про цифры, конверсию, динамику. Штука в том, что большая часть малого бизнеса получает на свой сайт весьма скромную аудиторию. Поэтому важно изучать не количество заказов, звонков, писем, а их содержание. Чем меньше у вас клиентов, тем выше ценность каждого из них. О конверсии здесь вообще говорить бесполезно, т.к. «потуги» ее повышения на 3%-5% при числе покупок 100 в месяц это смешно.

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

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

                        Два главных момента:
                        1. Не оправдавшиеся ожидания. Клиент видит проблему именно в сайте и хочет получить волшебную таблетку, а в итоге видит текст на 30-40 страниц, который для него бесполезен.
                        2. Сайт это только один из каналов коммуникаций. И не всегда самый главный. Зачастую нужен полный анализ тематики или альтернативное решение, в результате которого можно получить быстрый результат за вменяемые деньги (к примеру, можно не рвать задницу за топ-10, а дать рекламу на площадках, которые уже там).


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

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

                        Два дня аналитики вместо беготни и «текучки» ничего не ухудшат. Всего один свежий взгляд со стороны поможет избежать краха. И это будет сравнительно быстро и недорого.
                        Original source: habrahabr.ru (comments, light).

                        https://habrahabr.ru/post/337660/


                        Метки:  

                        [recovery mode] Считывание ввода пользователя (цифр) в приложении 3CX Call Flow Designer

                        Понедельник, 11 Сентября 2017 г. 14:35 + в цитатник
                        snezhko сегодня в 14:35 Администрирование

                        Считывание ввода пользователя (цифр) в приложении 3CX Call Flow Designer

                        Введение


                        В этой статье мы рассмотрим создание голосового приложения, которое считывает последовательность цифр, введенных пользователем через номеронабиратель, а затем подтверждает корректность ввода, проигрывая абоненту введенные цифры. Согласитесь, это довольно частая задача в порталах телефонного самообслуживания.
                         
                        Наше приложение будет выполнять эту задачу, используя 3CX Call Flow Designer. Приложение работает следующим образом:

                        1. 3CX CFD сообщает: Введите ваш номер пользователя
                        2. Пользователь набирает 1234
                        3. 3CX CFD сообщает: Введенный номер пользователя 1234. Для подтверждения нажмите 1, для повторного ввода нажмите 2.

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

                        Напомним, что в предыдущих статьях мы рассмотрели создание CFD приложения для исходящего обзвона (по завершении обзвона можно отсылать отчет, используя компонент Email Sender), работу CFD с базами данных (которые также можно использовать для получения номера абонента), маршрутизацию входящих вызовов в зависимости от времени суток (которую можно комбинировать с исходящим обзвоном) и отправку e-mail сообщения (например, для уведомления о заказе по телефону).
                         
                        Обратите внимание — среда разработки 3CX CFD поставляется бесплатно. Но голосовые приложения будут выполняться только на 3CX редакции Pro и Enterprise. Скачать CFD можно отсюда.     
                         
                        Демо-проект этого голосового приложения поставляется вместе с дистрибутивом 3CX CFD и находится в папке Documents\3CX Call Flow Designer Demos. Приступим.

                        Создание проекта


                        Для создания проекта CFD перейдите в File -> New -> Project, укажите папку размещения проекта и его имя, например, «PlayDigitsDemo».



                        Создание пользовательского компонента


                        Создаваемый компонент будет получать последовательность цифр от пользователя и последовательно проигрывать их.
                        Для создания компонента в окне Project Explorer кликните правой кнопкой мыши на имени проекта, выберите New Component и назовите его PlayDigits.



                        Затем кликните на компоненте PlayDigits.comp и в разделе Properties Window нажмите кнопку рядом с коллекцией Variables. Добавьте две переменные — Digits и Index. Переменная Digits — принимает цифры, которые компонент затем проигрывает. Переменная Index — внутренняя, она позволяет компоненту переходить от цифры к цифре.



                        Для добавления переменных нажмите кнопку Add, чтобы добавить переменную и измените ее имя (свойство Name) на Digits. Переменная Digits должна иметь следующие свойства: Accessibility: ReadWrite, Initial Value: , Scope: Public. Еще раз нажмите кнопку Add, добавьте новую переменную и измените ее имя на Index. Переменная Index должна иметь следующие свойства: Accessibility: ReadWrite, Initial Value: 0, Scope: Private

                        Теперь займемся архитектурой компонента. Мы будем использовать компонент Loop для перебора последовательности цифр (цикла). В компонент Loop добавим компонент Prompt Playback для воспроизведения очередной цифры в последовательности. В этот же компонент Loop добавим компонент Increment Variable, который будет увеличивать значение переменной Index. Такая конструкция позволит последовательно воспроизвести все цифры, введенные пользователем.

                        Создание цикла


                        1. Перетащите компонент Loop в основное окно среды разработки.
                        2. В окне Properties измените имя компонента на digitsLoop

                          Выберите компонент digitsLoop и в его свойстве Condition укажите строку

                          LESS_THAN(callflow$.Index,LEN(callflow$.Digits)).

                          Это выражение повторяет цикл до тех пор, пока переменная Index (callflow$.Index) меньше количества введенных пользователем цифр LEN(callflow$.Digits).

                        Воспроизведение сообщений


                        1. Прежде всего нам необходимы звуковые файлы, соответствующие каждой цифре. Подготовьте и скопируйте файлы 0.wav, 1.wav и т.д. до 9 в папку Audio вашего проекта.
                        2. Перетащите компонент Prompt Playback в основное поле приложения на компонент Loop.
                        3. Измените имя компонента на playDigit.
                        4. Выберите этот компонент и в окне свойств нажмите на кнопку возле коллекции Prompts, чтобы открыть редактор набора звуковых сообщений Prompt Collection Editor.
                        5. Нажмите кнопку Add и измените тип на Dynamic Audio File Prompt

                          В поле Audio File Expression введите строку

                          CONCATENATE(MID(callflow$.Digits,callflow$.Index,1),".wav").

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



                        Компонент увеличения номера переменной


                        1. Перетащите компонент Increment Variable на компонент Loop, ниже компонента playDigit

                          Измените имя компонента на incrementIndex

                          Для свойства VariableName укажите выражение
                          callflow$.Index


                        Цикл выполнения будет иметь следующий вид:



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

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


                        После того, как пользовательский компонент готов, задействуем его в нашем приложении:
                        1. В окне Project Explorer кликните Main.flow, чтобы открыть основное приложение
                        2. Добавьте компонент User Input, который будет запрашивать ввод от пользователя. Переименуйте его в requestInput и добавьте заранее подготовленное сообщение пользователю о необходимости ввода.
                        3. В ветвлении Valid Input из окна User Defined Components добавьте созданный пользовательский компонент PlayDigits

                          Переименуйте его в playEnteredDigits

                          Выберите его и в свойстве Digits укажите следующее выражение
                          requestInput.Buffer


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



                        Компиляция и установка приложения на сервер 3CX


                        Голосовое приложение готово! Теперь его следует скомпилировать и загрузить на сервер 3CX. Для этого:
                        • Перейдите в меню Build > Build All, и CFD создаст файл PlayDigitsDemo.tcxvoiceapp.

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



                        Заключение


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

                        По этому же принципу мы можем создавать пользовательские компоненты, проигрывающие время и даты, делающие валидацию ввода пользователя и т.п.
                        Original source: habrahabr.ru (comments, light).

                        https://habrahabr.ru/post/337654/


                        Машинное обучение руками «не программиста»: классификация клиентских заявок в тех.поддержку

                        Понедельник, 11 Сентября 2017 г. 14:22 + в цитатник
                        krubinshteyn сегодня в 14:22 Разработка

                        Машинное обучение руками «не программиста»: классификация клиентских заявок в тех.поддержку

                          Привет! Меня зовут Кирилл и я алкоголик более 10 лет был менеджером в сфере ИТ. Я не всегда был таким: во время учебы в МФТИ писал код, иногда за вознаграждение. Но столкнувшись с суровой реальностью (в которой необходимо зарабатывать деньги, желательно побольше) пошел по наклонной — в менеджеры.


                          image

                          Но не все так плохо! С недавнего времени мы с партнерами целиком и полностью ушли в развитие своего стартапа: системы учета клиентов и клиентских заявок Okdesk. С одной стороны — больше свободы в выборе направления движения. Но с другой — нельзя просто так взять и заложить в бюджет "3-х разработчиков на 6 месяцев для проведение исследований и разработки прототипа для…". Много приходится делать самим. В том числе — непрофильные эксперименты, связанные с разработкой (т.е. те эксперименты, что не относятся к основной функциональности продукта).

                          Одним из таких экспериментов стала разработка алгоритма классификации клиентских заявок по текстам для дальнейшей маршрутизации на группу исполнителей. В этой статье я хочу рассказать, как "не программист" может за 1,5 месяца в фоновом режиме освоить python и написать незамысловатый ML-алгоритм, имеющий прикладную пользу.


                          Как учиться?


                          В моём случае — дистанционное обучение на Coursera. Там довольно много курсов по машинному обучению и другим дисциплинам, связанным с искусственным интеллектом. Классикой считается курс основателя Coursera Эндрю Ына (Andrew Ng). Но минус этого курса (помимо того, что курс на английском языке: это не всем по силам) — редкостный инструментарий Octave (бесплатный аналог MATLAB-а). Для понимания алгоритмов это не главное, но лучше учиться на более популярных инструментах.

                          Я выбрал специализацию "Машинное обучение и анализ данных" от МФТИ и Яндекса (в ней 6 курсов; для того что написано в статье, достаточно первых 2-х). Главное преимущество специализации
                          — это живое сообщество студентов и менторов в Slack-е, где почти в любое время суток есть кто-то, к кому можно обратиться с вопросом.


                          Что такое "Машинное обучение"?


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

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

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


                          1. Имеется множество объектов с набором характеристик. Обозначим это множество буквой X;
                          2. Есть множество ответов. Обозначим это множество буквой Y;
                          3. Существует (неизвестная) зависимость между множеством объектов и множеством ответов. Т.е. такая функция, которая ставит в соответствие объекту множества X объект множества Y. Назовем её функцией y;
                          4. Имеется конечное подмножество объектов из X (обучающая выборка), для которой известны ответы из Y;
                          5. По обучающей выборке необходимо максимально хорошо приблизить функцию y какой-то функцией a. При помощи функции a мы хотим для любого объекта из X получить с хорошей вероятностью (или точностью — если речь о числовых ответах) верный ответ из Y. Поиск функции a — задача машинного обучения.

                          Вот пример из жизни. Банк выдает кредиты. У банка накопилось множество анкет заёмщиков, для которых уже известен исход: вернули кредит, не вернули, вернули с просрочкой и т.д. Объектом в этом примере является заемщик с заполненной анкетой. Данные из анкеты — параметры объекта. Факт возврата или невозврата кредита — это "ответ" на объекте (анкете заёмщика). Совокупность анкет с известными исходами является обучающей выборкой.

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

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


                          Постановка задачи


                          Напомним о том, что мы разрабатываем Okdesk — облачный сервис для обслуживания клиентов. Компании, которые используют Okdesk в своей работе, принимают клиентские заявки по разным каналам: клиентский портал, электронная почта, web-формы с сайта, мессенджеры и так далее. Заявка может относиться к той или иной категории. В зависимости от категории у заявки может быть тот или иной исполнитель. Например, заявки по 1С должны отправляться на решение к специалистам 1С, а заявки связанные с работой офисной сети — группе системных администраторов.

                          Для классификации потока заявок можно выделить диспетчера. Но, во-первых, это стоит денег (зарплата, налоги, аренда офиса). А во-вторых, на классификацию и маршрутизацию заявки будет потрачено время и заявка будет решена позже. Если бы можно было классифицировать заявку по её содержанию автоматически — было бы здорово! Попробуем решить эту задачу силами машинного обучения (и одного ИТ-менеджера).

                          Для проведения эксперимента была взята выборка из 1200 заявок с проставленными категориями. В выборке заявки распределены по 14-ти категориям. Цель эксперимента: разработать механизм автоматической классификации заявок по их содержанию, который даст в разы лучшее качество, по сравнению с random-ом. По результатам эксперимента необходимо принять решение о развитии алгоритма и разработке на его базе промышленного сервиса для классификации заявок.


                          Инструментарий


                          Для проведение эксперимента использовался ноутбук Lenovo (core i7, 8гб RAM), язык программирования Python 2.7 с библиотеками NumPy, Pandas, Scikit-learn, re и оболочкой IPython. Подробнее напишу об используемых библиотеках:


                          1. NumPy — библиотека, содержащая множество полезных методов и классов для проведения арифметических операций с большими многомерными числовыми массивами;
                          2. Pandas — библиотека, позволяющая легко и непринужденно анализировать и визуализировать данные и проводить операции над ними. Основные структуры данных (типы объектов) — Series (одномерная структура) и DataFrame (двумерная структура; по сути — набор Series одинаковой длины).
                          3. Scikit-learn — библиотека, в которой реализовано большинство методов машинного обучения;
                          4. Re — библиотека регулярных выражений. Регулярные выражения — незаменимый инструмент в задачах, связанных с анализом текстов.

                          Из библиотеки Scikit-learn нам понадобятся некоторые модули, о предназначение которых я напишу по ходу изложения материала. Итак, импортируем все необходимые библиотеки и модули:


                          import pandas as pd
                          import numpy as np
                          import re
                          from sklearn import neighbors, model_selection, ensemble
                          from sklearn.grid_search import GridSearchCV
                          from sklearn.metrics import accuracy_score

                          И переходим к подготовке данных.

                          (конструкция import xxx as yy означает, что мы подключаем библиотеку xxx, но в коде будем обращаться к ней через yy)


                          Подготовка данных


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

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

                          В задаче классификации текстов "фича" у объекта одна — текст. Скормить его алгоритму машинного обучения нельзя (допускаю, что мне не все известно :). Текст необходимо как-то оцифровать и формализовать.

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


                          Загрузка данных


                          Напомним, что в качестве исходных данных у нас есть выгрузка 1200 заявок (распределенных неравномерно по 14 категориям). Для каждой заявки есть поле "Тема", поле "Описание" и поле "Категория". Поле "Тема" — сокращенное содержание заявки и оно обязательно, поле "Описание" — расширенное описание и оно может быть пустым.

                          Данные загружаем из .xlsx файла в DataFrame. В .xlsx файле много столбцов (параметров реальных заявок), но нам нужны только "Тема", "Описание" и "Категория".

                          После загрузки данных объединяем поля "Тема" и "Описание" в одно поле для удобства дальнейшей обработки. Для этого предварительно необходимо заполнить чем-нибудь все пустые поля "Описание" (пустой строкой, например).


                          # Объявляем переменную issues типа DataFrame
                          issues = pd.DataFrame()
                          # Добавляем в issues столбцы Theme, Description и Cat, присваивая им значения полей Тема, Описание и Категория из .xlsx файла. u’...’ — потому что ‘…’ есть utf
                          issues[['Theme', 'Description','Cat']] = pd.read_excel('issues.xlsx')[[u'Тема', u'Описание', u'Категория']]
                          # Заполняем пустые ячейки столбца Description пустой строкой
                          issues.Description.fillna('', inplace = True)
                          # Объединяем столбцы Theme и Description (пробел используем в качестве разделителя) в столбец Content
                          issues['Content'] = issues.Theme + ' ' + issues.Description

                          Итак, у нас есть переменная issues типа DataFrame, в которой мы будем работать со столбцами Content (объединенное поле для полей "Тема" и "Описание") и Cat (категория заявки). Перейдем к формализации содержания заявки (т.е. столбца Content).


                          Формализация содержания заявок


                          Описание подхода к формализации


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


                          1. Разобьем содержание заявки на слова. Под словом понимаем последовательность из 2 или более символов, разделенную символами-разделителями (тире, дефис, точка, пробел, переход на новую строку и т.д.). В итоге для каждой заявки получим массив входящих в её содержание слов.
                          2. Из каждой заявки исключим "слова-паразиты", не несущие смысловую нагрузку (например, слова, входящие во фразы-приветствия: "здравствуйте", "добрый", "день" и т.д.);
                          3. Из полученных массивов составим словарь: набор слов, используемых для написания содержания всех заявок;
                          4. Далее составляем матрицу размером (количество заявок) x (количество слов в словаре), в которой i-я ячейка в j-м столбце соответствует количеству вхождений в i-ю заявку j-го слова из словаря.

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

                          Важный момент: п.3 осуществляем после того, как выделим из обучающей выборки случайную подвыборку для контроля качества алгоритма (тестовую выборку). Это необходимо для того, чтобы лучше понимать, какое качество алгоритм покажет "в бою" на новых данных (например, не сложно реализовать алгоритм, который на обучающей выборке будет давать идеально верные ответы, но на любых новых данных будет работать не лучше random-а: такая ситуация называется переобучением). Отделение тестовой выборки именно до составления словаря важно потому, что если бы мы составляли словарь в том числе и на тестовых данных, то обученный на выборке алгоритм получился бы уже знакомым с неизвестными объектами. Выводы о его качестве на неизвестных данных будут некорректными.

                          Теперь покажем, как п.п. 1-4 выглядят в коде.


                          Разбиваем содержание на слова и убираем слова-паразиты


                          Первым делом приведем все тексты к нижнему регистру (“принтер” и “Принтер” — одинаковые слова только для человека, но не для машины):


                          #объявляем функцию приведения строки к нижнему регистру
                          def lower(str):
                              return str.lower()
                          #применяем функцию к каждой ячейке столбца Content
                          issues['Content'] = issues.Content.apply(lower)

                          Далее определим вспомогательный словарь “слов-паразитов” (его наполнение производилось опытным итерационным путем для конкретной выборки заявок):


                          garbagelist = [u'спасибо', u'пожалуйста', u'добрый', u'день', u'вечер',u'заявка', u'прошу', u'доброе', u'утро']

                          Объявим функцию, которая разбивает текст каждой заявки на слова длиной 2 и более символов, а затем включает полученные слова, за исключением “слов-паразитов”, в массив:


                          def splitstring(str):
                              words = []
                              #разбиваем строку по символам из []
                              for i in re.split('[;,.,\n,\s,:,-,+,(,),=,/,«,»,@,\d,!,?,"]',str):
                                  #берём только "слова" длиной 2 и более символов
                                  if len(i) > 1:
                                      #не берем слова-паразиты
                                      if i in garbagelist:
                                          None
                                      else:
                                          words.append(i)
                              return words

                          Для разбиения текста на слова по символам-разделителям используется библиотека регулярных выражений re и её метод split. В метод split передается массив символов-разделителей (набор символов-разделителей пополнялся итерационно-опытным путем) и строка, подлежащая разбиению.

                          Применяем объявленную функцию к каждой заявке. На выходе получаем исходный DataFrame, в котором появился новый столбец Words с массивом слов (за исключением “слов-паразитов”), из которых состоит каждая заявка.


                          issues['Words'] = issues.Content.apply(splitstring)

                          Составляем словарь


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

                          Разделение выборки осуществляем методом train_test_split модуля model_selection библиотеки Scikit-learn. В метод передаем массив с данными (тексты заявок), массив с метками (категории заявок) и размер тестовой выборки (обычно выбирают 30% от всей). На выходе получаем 4 объекта: данные для обучения, метки для обучения, данные для контроля и метки для контроля:


                          issues_train, issues_test, labels_train, labels_test = model_selection.train_test_split(issues.Words, issues.Cat, test_size = 0.3)

                          Теперь объявим функцию, которая составит словарь по данным, оставленным на обучение (issues_train), и применим функцию в этим данным:


                          def WordsDic(dataset):
                              WD = []
                              for i in dataset.index:
                                  for j in xrange(len(dataset[i])):
                                      if dataset[i][j] in WD:
                                          None
                                      else:
                                          WD.append(dataset[i][j])
                              return WD
                          #применяем функцию к данным
                          words = WordsDic(issues_train)

                          Итак, мы составили словарь из слов, составляющих тексты всех заявок из обучающей выборки (за исключением заявок, оставленных на контроль). Словарь записали в переменную words. Размер массива words получился равным 12015-м элементов (т.е. слов).


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


                          Перейдем к заключительному шагу подготовки данных для обучения. А именно: составим матрицу размера (количество заявок в выборке) x (количество слов в словаре), где в i-й строке j-го столбца находится количество вхождений j-го слова из словаря в i-ю заявку из выборки.


                          #объявляем матрицу размером len(issues_train) на len(words), заполняем её нулями
                          train_matrix = np.zeros((len(issues_train),len(words)))
                          #заполняем матрицу, проставляя в [i][j] количество вхождений j-го слова из words в i-й объект обучающей выборки
                          for i in xrange(train_matrix.shape[0]):
                              for j in issues_train[issues_train.index[i]]:
                                  if j in words:
                                      train_matrix[i][words.index(j)]+=1

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


                          Обучение


                          Перейдем к обучению алгоритмов на размеченных данных (т.е. таких данных, для которых известны правильные ответы: матрица train_matrix с метками labels_train). В этом разделе будет мало кода, так как большинство методов машинного обучения реализовано в библиотеке Scikit-learn. Разработка своих методов может быть полезной для усвоения материала, но с практической точки зрения в этом нет потребности.

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


                          О принципах выбора лучшего алгоритма


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

                          Что считать качеством алгоритма, зависит от задачи, которую вы решаете. Выбор метрики качества — отдельная большая тема. В рамках классификации заявок была выбрана простая метрика: точность (accuracy). Точность определяется как доля объектов в выборке, для которых алгоритм дал верный ответ (поставил верную категорию заявки). Таким образом, мы выберем тот алгоритм, который будет давать бОльшую точность в предсказании категорий заявок.

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

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

                          Для определения качества алгоритма на каждой комбинации гиперпараметров используется кросс-валидация. Поясню, что это такое. Обучающая выборка разбивается на N равных частей. Алгоритм последовательно обучается на подвыборке из N-1 частей, а качество считается на одной отложенной. В итоге каждая из N частей 1 раз используется для подсчета качества и N-1 раз для обучения алгоритма. Качество алгоритма на комбинации параметров считается как среднее между значениями качества, полученными при кросс-валидации. Кросс-валидация необходима для того, чтобы мы могли больше доверять полученному значению качества (при усреднении мы нивелируем возможные “перекосы” конкретного разбиения выборки). Чуть подробнее сами знаете где.

                          Картинка из Википедии, которая иллюстрирует понятие кросс-валидации

                          Итак, для выбора наилучшего алгоритма для каждого алгоритма:


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

                          С точки зрения программирования описанного выше алгоритма нет ничего сложного. Но и в этом нет потребности. В библиотеке Scikit-learn есть готовый метод подбора параметров по сетке (метод GridSearchCV модуля grid_search). Все что нужно — передать в метод алгоритм, сетку параметров и число N (количество частей, на которые разбиваем выборку для кросс-валидации; их ещё называют "folds").

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


                          k ближайших соседей (kNN)


                          Метод k ближайших соседей наиболее прост в понимании. Заключается он в следующем.

                          Есть обучающая выборка, данные в которой уже формализованы (подготовлены к обучению). Т.е объекты представлены в виде векторов какого-либо пространства. В нашем случае заявки представлены в виде векторов пространства словаря. Для каждого вектора обучающей выборки известен правильный ответ.

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


                          Иллюстрация для метода k ближайших соседей


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

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

                          Выполняем незамысловатый код:


                          %%time
                          #задаем сетку для подбора гиперпараметров
                          param_grid = {'n_neighbors': np.arange(1,8), 'p': [1,2]}
                          #задаем количество fold-ов для кросс-валидации
                          cv = 3
                          #объявляем классификатор
                          estimator_kNN = neighbors.KNeighborsClassifier()
                          #передаем сетку, классификатор и количество fold-ов в метод подбора параметров по сетке
                          optimazer_kNN = GridSearchCV(estimator_kNN, param_grid, cv = cv)
                          #запускаем подбор параметров по сетке на обучающей выборке
                          optimazer_kNN.fit(train_matrix, labels_train)
                          #смотрим лучшие параметры и качество алгоритма на лучших параметрах
                          print optimazer_kNN.best_score_
                          print optimazer_kNN.best_params_

                          Через 2 минуты 40 секунд узнаем, что лучшее качество в 53,23% показывает алгоритм на 3 ближайших соседях, определенных по манхэттенскому расстоянию.


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


                          Решающие деревья


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

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

                          В решающей вершине проверяется простое условие: соответствие какого-то (об этом далее) j-го признака объекта условию xj больше или равно какому-то t. Объекты, удовлетворяющие условию, отправляются в одну ветку, а не удовлетворяющие — в другую.

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

                          Новые (требующие предсказания) объекты прогоняются по обученному дереву и попадают в соответствующий им лист. Для объектов, попавших в лист, даем следующий ответ:


                          1. Для задач классификации возвращаем самый распространенный в этом листе класс объектов обучающей выборки;
                          2. Для задач регрессии (т.е. таких, где ответом является число) возвращаем среднее значение на объектах обучающей выборки из этого листа.

                          Осталось поговорить о том, как выбирать для каждой вершины признак j (по какому признаку разбиваем выборку в конкретной вершине дерева) и соответствующий этому признаку порог t. Для этого вводят так называемый критерий ошибки Q(Xm,j,t). Как видно, критерий ошибки зависит от выборки Xm (та часть обучающей выборки, что дошла до рассматриваемой вершины), параметра j, по которому будет разбиваться выборка Xm в рассматриваемой вершине и порогового значения t. Необходимо подобрать такое j и такое t, при котором критерий ошибки будет минимальным. Так как возможный набор значений j и t для каждой обучающей выборки ограничен, задача решается перебором.

                          Что такое критерий ошибки? В черновой версии статьи на этом месте было много формул и сопровождающих объяснений, рассказ про критерий информативности и его частные случаи (критерий Джинни и энтропийный критерий). Но статья получилась и так раздутой. Желающие разобраться в формальностях и математике смогут почитать обо всем в интернете (например, здесь). Ограничусь "физическим смыслом" на пальцах. Критерий ошибки показывает уровень "разнообразия" объектов в получающихся после разбиения подвыборках. Под "разнообразием" в задачах классификации подразумевается разнообразие классов, а в задачах регрессии (где предсказываем числа) — дисперсия. Таким образом, при разбиении выборки мы хотим максимальным образом уменьшить "разнообразие" в получающихся подвыборках.

                          С деревьями разобрались. Перейдем к композиции деревьев.


                          Композиция решающих деревьев


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

                          Для построения композиции деревьев берется N обученных деревьев и их результат “усредняется”. Для задач регрессии (там, где предсказываем число) берется среднее значение предсказанных чисел, а для задач классификации — самый популярный (из N предсказаний) предсказанный класс.

                          Таким образом, для построения леса необходимо сначала обучить N решающих деревьев. Но делать это на одной и той же обучающей выборке бессмысленно: мы получим N одинаковых алгоритмов и результат усреднения будет равен результату любого из них. Поэтому для обучения базовых деревьев используется рандомизация: обучение каждого базового дерева производится по случайной подвыборке объектов исходной обучающей выборки и/или по случайному набору параметров (т.е. для обучения каждого базового алгоритма берутся не все параметры объектов, а только случайный набор определенного размера). Но и этого бывает недостаточно для построения независимых алгоритмов — применяют рандомизацию признаков для разбиения на каждой вершине (т.е. на каждой вершине каждого дерева признак выбирают не из всего набора, а из случайной подвыборки признаков определенного размера; важно, что для каждой вершины каждого дерева случайная подвыборка — своя). Поэтому алгоритм и называется композицией случайных деревьев.

                          Перейдем наконец-то к практике! При обучении алгоритма будем искать лучшие значения для 2-х гиперпараметров: минимального количества объектов в узле (гиперпараметр для дерева) и количества базовых алгоритмов (гиперпараметр для леса).

                          Запускаем код:


                          %%time
                          #задаем сетку для подбора гиперпараметров
                          param_grid = {'n_estimators': np.arange(20,101,10), 'min_samples_split': np.arange(4,11, 1)}
                          #задаем количество fold-ов для кросс-валидации
                          cv = 3
                          #объявляем классификатор
                          estimator_tree = ensemble.RandomForestClassifier()
                          #передаем сетку, классификатор и количество fold-ов в метод подбора параметров по сетке
                          optimazer_tree = GridSearchCV(estimator_tree, param_grid, cv = cv)
                          #запускаем подбор параметров по сетке на обучающей выборке
                          optimazer_tree.fit(train_matrix, labels_train)
                          #смотрим лучшие параметры и качество алгоритма на лучших параметрах
                          print optimazer_tree.best_score_
                          print optimazer_tree.best_params_

                          Через 3 минуты 30 секунд узнаем, что лучшее качество в 65,82% показывает алгоритм из 60 деревьев, для которых минимальное количество объектов в узле равно 4.


                          Результат


                          Результаты на отложенной выборке


                          Теперь проверим работу двух обученных алгоритмов на отложенной (тестовой, контрольной — как её только не называют) выборке.

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


                          #Создаем матрицу размером len(issues_test) на len(words)
                          test_matrix = np.zeros((len(issues_test),len(words)))
                          #заполняем матрицу, проставляя в [i][j] количество вхождений j-го слова из words в i-й объект тестовой выборки
                          for i in xrange(test_matrix.shape[0]):
                              for j in issues_test[issues_test.index[i]]:
                                  if j in words:
                                      test_matrix[i][words.index(j)]+=1

                          Для получения качества алгоритмов используем метод accuracy_score модуля metrics библиотеки Scikit-learn. Передаем в него предсказания на отложенной выборке каждого из алгоритмов и известные для отложенной выборки значения меток:


                          print u'Случайный лес:', accuracy_score(optimazer_tree.best_estimator_.predict(test_matrix), labels_test)
                          print u'kNN:', accuracy_score(optimazer_kNN.best_estimator_.predict(test_matrix), labels_test)

                          Получаем 51,39% на алгоритме k ближайших соседей и 73,46% на композиции случайных деревьев.


                          Сравнение с "глупыми" алгоритмами


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

                          Сравним полученное качество с 3-мя видами “глупых” алгоритмов:


                          1. Равномерный random;
                          2. Всегда выдаем самый популярный класс из обучающей выборки;
                          3. Random, пропорциональный плотности классов в обучающей выборке.

                          Равномерный random на 14 классах дает примерно 100/14 * 100% = 7,14% качество. Алгоритм, который всегда выдает самый популярный класс даст качество в 14,5% (такая доля у самого популярного класса в обучающей выборке). Для получения качества на пропорциональном random-е напишем простой код. Каждому объекту отложенной выборки алгоритм будет присваивать класс объекта из обучающей выборки, выбранный равномерным random-ом:


                          #импортируем библиотеку random
                          import random
                          #объявляем массив, куда будем складывать random-ные предсказания
                          rand_ans = []
                          #предсказываем
                          for i in xrange(test_matrix.shape[0]):
                              rand_ans.append(labels_train[labels_train.index[random.randint(0,len(labels_train))]])
                          #смотрим качество
                          print u'Пропорциональный random:', accuracy_score(rand_ans, labels_test)

                          Получаем 14,52%.

                          Итак, используя не самые сложные методы мы получили качество классификации в разы выше, чем дают “глупые” алгоритмы. Ура!


                          Что дальше?


                          Для промышленного применения машинного обучения при классификации заявок, необходимо получить качество не ниже 90% — такой ориентир дают потенциальные потребители. Результаты эксперимента показывают, что такое качество может быть достигнуто. Ведь даже при подготовке данных к обучению мы не использовали такие “классические” методы подготовки текстов как стемминг и ранжирование слов при попадании в словарь (если слово встречается пару раз в выборке, оно скорее всего мусорное и его можно исключить). К тому-же, в словаре для одного и того же слова было несколько написаний (например: "печать", "пичать", "печять" и т.д.) — с этим тоже можно бороться (желательно в школе) и повышать качество алгоритма.

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

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

                          "Отлично! Работаем дальше!" (с)

                          Original source: habrahabr.ru (comments, light).

                          https://habrahabr.ru/post/337278/


                          Метки:  

                          Сегментация лица на селфи без нейросетей

                          Понедельник, 11 Сентября 2017 г. 14:08 + в цитатник
                          mephistopheies сегодня в 14:08 Разработка

                          Сегментация лица на селфи без нейросетей

                            Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.


                            Алгоритм


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



                            Далее делаем следующие шаги:


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

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


                            import
                            %matplotlib inline
                            import matplotlib
                            import numpy as np
                            import matplotlib.pyplot as plt
                            import seaborn as sns
                            sns.set_style("dark")
                            plt.rcParams['figure.figsize'] = 16, 12
                            import pandas as pd
                            from PIL import Image
                            from tqdm import tqdm_notebook
                            from skimage import transform
                            import itertools as it
                            from sklearn.neighbors.kde import KernelDensity
                            import matplotlib.cm as cm
                            import queue
                            from skimage import morphology
                            import dlib
                            import cv2
                            from imutils import face_utils
                            from scipy.spatial import Delaunay




                            Автоматизируем штрихи


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



                            img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
                            print(img_input.shape)
                            plt.imshow(img_input)


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


                            # инстанцируем класс для детекции лиц (рамка)
                            detector = dlib.get_frontal_face_detector()
                            # инстанцируем класс для детекции ключевых точек
                            predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
                            
                            # конвертируем изображение в много оттенков серого
                            img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
                            # вычисляем список рамок на каждое найденное лицо
                            rects = detector(img_gray, 0)
                            # вычисляем ключевые точки
                            shape = predictor(img_gray, rects[0])
                            shape = face_utils.shape_to_np(shape)

                            отрисуем ключевые точки
                            img_tmp = img_input.copy()
                            for x, y in shape:
                                cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
                            plt.imshow(img_tmp)


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


                            # оригинальная рамка
                            face_origin = sorted([(t.width()*t.height(), 
                                                   (t.left(), t.top(), t.width(), t.height())) 
                                                  for t in rects], 
                                                 key=lambda t: t[0], reverse=True)[0][1]
                            
                            # коэффициенты расширения рамки
                            rescale = (1.3, 2.2, 1.3, 1.3)
                            # расширение рамки, так чтобы она не вылезла за края
                            (x, y, w, h) = face_origin
                            cx = x + w/2
                            cy = y + h/2
                            w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
                            h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
                            fx = max(0, int(x + w/2*(1 - rescale[0])))
                            fy = max(0, int(y + h/2*(1 - rescale[1])))
                            fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
                            fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
                            
                            face = (fx, fy, fw, fh)

                            отрисовываем рамки
                            img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]), 
                                                    (face[0] + face[2], face[1] + face[3]),
                                                    (255, 0, 0), thickness=3, lineType=8, shift=0)
                            img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]), 
                                                    (face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
                                                    (0, 255, 0), thickness=3, lineType=8, shift=0)
                            plt.imshow(img_tmp)
                            


                            Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси $y$, тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.


                            # выбираем вышеописанные пять точек
                            points = [shape[0].tolist(), shape[16].tolist()]
                            for ix in [4, 12, 8]:
                                x, y = shape[ix].tolist()
                                points.append((x, y))
                                points.append((x, points[0][1] + points[0][1] - y))
                            
                            # я не особо в прототипе запариваюсь над производительностью
                            # так что вызываю триангуляцию Делоне,
                            # чтобы использовать ее как тест на то, что точка внутри полигона
                            # все это можно делать быстрее, т.к. точный тест не нужен
                            # для прототипа :good-enough: 
                            hull = Delaunay(points)
                            xy_fg = []
                            for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
                                if hull.find_simplex([y, x]) >= 0:
                                    xy_fg.append((x, y))
                            print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
                            
                            # вычисляем количество точек для фона
                            # примерно равно что бы было тому, что на лице
                            r = face[1]*face[3]/np.prod(img_input.shape[:2])
                            print(r)
                            k = 0.1
                            xy_bg_n = int(k*np.prod(img_input.shape[:2]))
                            print(xy_bg_n)
                            
                            # накидываем случайные точки
                            xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
                                        np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
                            xy_bg = list(xy_bg)
                            xy_bg = [(x, y) for (x, y) in xy_bg 
                                     if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
                            print(len(xy_bg)/np.prod(img_input.shape[:2]))

                            отрисовываем штрихи
                            img_tmp = img_input/255
                            for x, y in xy_fg:
                                img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
                            
                            for x, y in xy_bg:
                                img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
                            
                            plt.imshow(img_tmp)





                            Нечеткое разделение фона и объекта


                            Теперь у нас есть два набора данных: точки объекта $D_f$ и точки фона $D_b$.


                            points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
                            points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])

                            Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.


                            отрисовка распределений
                            fig, axes = plt.subplots(nrows=2, ncols=1)
                            sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
                            sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
                            sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
                            sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
                            sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
                            sns.distplot(points_bg[:, 2], ax=axes[1], color='b')


                            Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки $x$ следующим образом (для простоты пример для одномерного распределения):


                            $F\left(x\right) = \frac{1}{h\cdot\left|D\right|} \sum_{i =1}^{\left|D\right|} K\left(\frac{x - x_i}{h}\right)$


                            где:


                            • $h$ — параметр сглаживания
                            • $K$ — некоторое ядро

                            Мы для простоты будем использовать Гауссово ядро:


                            $K(u)={\frac {1}{\sqrt {2\pi }}}e^{-{\frac {1}{2}}u^{2}}$


                            Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.


                            # инстанцируем классы KDE для объекта и фона
                            kde_fg = KernelDensity(kernel='gaussian', 
                                                   bandwidth=1, 
                                                   algorithm='kd_tree', 
                                                   leaf_size=100).fit(points_fg)
                            kde_bg = KernelDensity(kernel='gaussian', 
                                                   bandwidth=1, 
                                                   algorithm='kd_tree', 
                                                   leaf_size=100).fit(points_bg)
                            
                            # инициализируем и вычисляем маски
                            score_kde_fg = np.zeros(img_input.shape[:2])
                            score_kde_bg = np.zeros(img_input.shape[:2])
                            likelihood_fg = np.zeros(img_input.shape[:2])
                            coodinates = it.product(range(score_kde_fg.shape[0]), 
                                                    range(score_kde_fg.shape[1]))
                            for x, y in tqdm_notebook(coodinates, 
                                                      total=np.prod(score_kde_fg.shape)):
                                score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
                                score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
                                n = score_kde_fg[x, y] + score_kde_bg[x, y]
                                if n == 0:
                                    n = 1
                                likelihood_fg[x, y] = score_kde_fg[x, y]/n

                            В итоге у нас есть несколько масок:


                            • score_kde_fg — оценка вероятности быть точкой объекта $p\left(x \mid D_f\right)$
                            • score_kde_bg — оценка вероятности быть точкой объекта $p\left(x \mid D_b\right)$
                            • likelihood_fg — нормализированная вероятность быть точкой объекта $p_f\left(x\right) = \frac{p\left(x \mid D_f\right)}{p\left(x \mid D_f\right) + p\left(x \mid D_b\right)}$
                            • 1 - likelihood_fg нормализированная вероятность быть точкой фона $p_b\left(x\right) = 1 - p_f\left(x\right)$

                            Посмотрим на следующие распределения.


                            Распределение значений score_kde_fg
                            sns.distplot(score_kde_fg.flatten())
                            plt.show()


                            Распределение значений score_kde_bg
                            sns.distplot(score_kde_bg.flatten())
                            plt.show()


                            Распределение значений likelihood_fg:


                            sns.distplot(likelihood_fg.flatten())
                            plt.show()


                            Вселяет надежду то, что на $p_f\left(x\right)$ есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.


                            маска score_kde_fg
                            plt.matshow(score_kde_fg, cmap=cm.bwr)
                            plt.show()


                            маска score_kde_bg
                            plt.matshow(score_kde_bg, cmap=cm.bwr)
                            plt.show()


                            plt.matshow(likelihood_fg, cmap=cm.bwr)
                            plt.show()


                            маска 1 - likelihood_fg
                            plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
                            plt.show()


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





                            Бинарная маска объекта


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


                            $d\left(a, b\right) = \left| p\left(a\right) - p\left(b\right) \right|$


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


                            def dijkstra(start_points, w):
                                d = np.zeros(w.shape) + np.infty
                                v = np.zeros(w.shape, dtype=np.bool)
                                q = queue.PriorityQueue()
                                for x, y in start_points:
                                    d[x, y] = 0
                                    q.put((d[x, y], (x, y)))
                            
                                for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
                                    if np.isinf(d[x, y]):
                                        q.put((d[x, y], (x, y)))
                            
                                while not q.empty():
                                    _, p = q.get()
                                    if v[p]:
                                        continue
                            
                                    neighbourhood = []
                                    if p[0] - 1 >= 0:
                                        neighbourhood.append((p[0] - 1, p[1]))
                                    if p[0] + 1 <= w.shape[0] - 1:
                                        neighbourhood.append((p[0] + 1, p[1]))
                                    if p[1] - 1 >= 0:
                                        neighbourhood.append((p[0], p[1] - 1))
                                    if p[1] + 1 < w.shape[1]:
                                        neighbourhood.append((p[0], p[1] + 1))
                            
                                    for x, y in neighbourhood:
                                        # тут вычисляется расстояние
                                        d_tmp = d[p] + np.abs(w[x, y] - w[p])
                                        if d[x, y] > d_tmp:
                                            d[x, y] = d_tmp
                                            q.put((d[x, y], (x, y)))
                            
                                    v[p] = True
                            
                                return d
                            
                            # вызываем алгоритм для двух масок
                            d_fg = dijkstra(xy_fg, likelihood_fg)
                            d_bg = dijkstra(xy_bg, 1 - likelihood_fg)

                            новая нечеткая маска объекта
                            plt.matshow(d_fg, cmap=cm.bwr)
                            plt.show()


                            новая нечеткая маска фона
                            plt.matshow(d_bg, cmap=cm.bwr)
                            plt.show()


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


                            margin = 0.0
                            mask = (d_fg < (d_bg + margin)).astype(np.uint8)
                            
                            plt.matshow(mask)
                            plt.show()


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


                            img_fg = img_input/255.0
                            img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                            
                            x = int(img_bg.shape[0] - img_fg.shape[0])
                            y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                            
                            img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                            mask_3d = np.dstack([mask, mask, mask])
                            img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                            
                            plt.imshow(img_bg)





                            Сглаживание маски


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



                            Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.


                            • эрозия: прикладываем к каждой точке объекта на оригинальном изображении СЭ так, чтобы совпадал центр СЭ и точка на изображении; если СЭ полностью принадлежит в объекту, то такая точка объекта остается; получается, что удаляются детали, которые меньше чем СЭ, и объект "худеет"; в примере из синего квадрата сделали голубой
                            • наращивание (dilation): на каждую точку объекта накладывается СЭ, и недостающие точки дорисовываются; таким образом закрашиваются дырки меньшие чем СЭ, а объект в целом "толстеет"; на примере из синего квадрата сделали голубой, углы получились закругленные
                            • размыкание (opening): сначала эрозия, потом наращивание тем же СЭ
                            • замыкание (closing): сначала наращивание, потом эрозия тем же СЭ

                            Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).


                            mask = morphology.opening(mask, morphology.disk(11))
                            plt.imshow(mask)


                            После применения такой маски результат станет поприятнее:


                            код применения маски
                            img_fg = img_input/255.0
                            img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                            
                            x = int(img_bg.shape[0] - img_fg.shape[0])
                            y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                            
                            img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                            mask_3d = np.dstack([mask, mask, mask])
                            img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
                                mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                            
                            plt.imshow(img_bg)





                            Накладываем маску


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


                            Подопытный экземпляр
                            img_target = np.array(Image.open('./../data/target.jpg'))
                            img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
                            print(img_target.shape)
                            plt.imshow(img_target)


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


                            img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
                            rects_target = detector(img_gray, 0)
                            shape_target = predictor(img_gray, rects_target[0])
                            shape_target = face_utils.shape_to_np(shape_target)

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


                            $\left({\begin{array}{cc} x^1_1, x^2_1, 1 \\ x^1_2, x^2_2, 1 \\ \cdots \\ x^1_{62}, x^2_{62}, 1 \end{array} } \right) \times \left({\begin{array}{cc} a_1^1, a_2^1, a_3^1 \\ a_1^2, a_2^2, a_3^2 \\ a_1^3, a_2^3, a_3^3 \\ \end{array} } \right) = \left({\begin{array}{cc} y^1_1, y^2_1, 1 \\ y^1_2, y^2_2, 1 \\ \cdots \\ y^1_{62}, y^2_{62}, 1 \end{array} } \right) $


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


                            $\large X\cdot A = Y \Rightarrow A = \left(X^T X\right)^{-1}X^T Y$


                            Так и сделаем:


                            # добавим справа к матрицам колонку единиц,
                            # иначе будет только масштабирование и поворот, без переноса
                            X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
                            Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
                            # учим оператор
                            A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
                            
                            # выбираем точки лица по искомой маске
                            X = np.array([(y, x, 1) for (x, y) in 
                                it.product(range(mask.shape[0]), 
                                               range(mask.shape[1])) if mask[x, y] == 1.0])
                            # вычисляем новые координаты маски на целевом изображении
                            Y = np.dot(X, A).astype(np.int)

                            Накладываем маску
                            img_tmp = img_target.copy()
                            for y, x, _ in Y:
                                if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
                                    continue
                                img_tmp[x, y, :] = np.array([0, 0, 0])
                            
                            plt.imshow(img_tmp)


                            Переносим лицо
                            img_trans = img_target.copy().astype(np.uint8)
                            points_face = {}
                            for ix in range(X.shape[0]):
                                y1, x1, _ = X[ix, :]
                                y2, x2, _ = Y[ix, :]
                                if x2 < 0 or x2 >= img_target.shape[0] or 
                                   y2 < 0 or y2 >= img_target.shape[1]:
                                    continue
                                points_face[(x2, y2)] = img_input[x1, y1, :]
                            
                            for (x, y), c in points_face.items():
                                img_trans[x, y, :] = c
                            plt.imshow(img_trans)





                            Заключение


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


                            • добавить прозрачности по краям, чтобы был плавный переход маски в целевое изображение (matting);
                            • пофиксить перенос на изображения большего размера — точек оригинального лица не хватит, чтобы покрыть все точки целевого лица, таким образом, образуются пропуски между точками; исправить это можно увеличением размера исходного лица;
                            • сделать какую-либо маску, например бурундука Дейла, нанести вручную 62 точки и осуществить перенос (вот вам и маскарад);
                            • использовать какой-нибудь модный нейросетевой детектор точек, который умеет искать большее количество точек.

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


                            Как обычно спс bauchgefuehl за редактуру.

                            Original source: habrahabr.ru (comments, light).

                            https://habrahabr.ru/post/336594/



                            Поиск сообщений в rss_rss_hh_new
                            Страницы: 1437 ... 1138 1137 [1136] 1135 1134 ..
                            .. 1 Календарь