Некоторое время назад, листая просторы хабра, я наткнулся на вакансию «Python Backend Разработчик». В ней больше всего меня подкупило расположение офиса — он был рядом с домом, и я написал отклик. Ответ пришел быстро с вопросом о том, не готов ли я выполнить тестовое задание. Я ответил, что подумаю, если мне его пришлют. Письма с заданием не было недели две.

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

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

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

Форма загрузки:
Текстовое поле для ввода названия фото
Выбор файла

Таблица:
Превью фото (необходимо сделать уменьшенную копию фото (миниатюру); также данное превью должно являться ссылкой на оригинальное/полное изображение, которое открывается по клику на превью)
Название фото (которое пользователь указывает при загрузке)
Производитель и модель камеры (из EXIF, если присутствует)
Размер файла
Дата создания фото (из EXIF)
Дата загрузки фото
Кнопка удаления

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

Итак, в качестве веб-фреймворка для Python был выбран Tornado, я с ним давно знаком. Мы будем поднимать несколько backend серверов, поэтому нам понадобится балансер и Supervisor. Изначально я думал о HAProxy в качестве балансера, но тут меня осенило, что картинки может хорошо раздавать NGINX. В итоге в начале архитектура мне показалась такой: NGINX балансирует соединения и раздает статику с диска, 4 сервера Tornado обрабатывают запросы, Redis синхронизирует backend.

На Tornado упала ноша анализа поступающих картинок и создание миниатюр. В задании не сказано, какие форматы необходимо поддерживать, поэтому я поискал описание EXIF в википедии, где упоминаются форматы TIFF и JPEG. Если это все, то дела не так уж плохи, библиотека Pillow для Python поддерживает оба формата, а также EXIF метаданные. Но есть нюанс — TIFF изображения не открываются браузером. Это делает невозможным открытие оригинального файла в браузере, поэтому я решил перекодировать эти изображения в JPEG и дополнительно сохранить вместе с полученным файлом EXIF данные, из которых можно было бы восстановить всю необходимую информацию для отображения в таблице.

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

Решение с метаданными в JPEG мне показалось красивым, и, хотя Pillow вполне умеет сохранять EXIF в JPEG, сами метаданные при этом должны быть уже в бинарном формате. То есть, Pillow выдает метаданные в виде словаря, но вот из словаря в метаданные никак не умеет. Была найдена библиотека Gexiv2, так же работающая с метаданными, но ее установка потребовала сноровки.

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

Gexiv2 успешно редактирует EXIF в файлах, но с данными в памяти у него туго. А я принципиально не хотел дважды обращаться к файлу: один раз на запись JPEG, второй раз на запись в этот же файл метаданных. Я еще не знал, что меня ждет. А ждало меня следующее — в документации Gexiv2 были перечислены поддерживаемые форматы, такие как EXV, CR2, CRW и многие другие. Таким образом Pillow уже не справлялось с задачей чтения загружаемых изображений. Так я нашел ImageMagick, и соответствующий адаптер под Python — Wand.

Wand выглядел многообещающе — поддержка множество форматов, чтение EXIF, относительно простая установка. Но чтобы сохранять JPEG со своими метаданными мне все равно нужен Pillow. Потратив некоторое время мне повезло найти библиотеку piexif, которая помогала редактировать метаданные в Pillow, и одной проблемой стало меньше. Потратив несколько часов можно сесть и программировать.

Алгоритм был простой, Wand загружает картинку из памяти, выдает EXIF данные, потом Wand отдает буфер RGB, считаем его md5 хеш чтобы проверить на дубликаты, конвертируем буфер в JPEG и сохраняем со своими метаданными, плюс сохраняем миниатюру. Конечно же соответствующе обновляем данные в Redis. Осталось проверить. Однако найти в интернете картинки с метаданными, да еще и свежими — проблема. И я потратил еще немало времени на поиск программы, которая бы хорошо редактировала EXIF данные.

И вот, первый JPEG семпл готов, загружаем — работает! А вот второй семпл, CR2 файл размером 7MB выдал несколько сюрпризов. Первый — Wand не смог его прочитать из буфера, ему потребовалась подсказка формата в виде расширения исходного файла. Но и тут проблема, библиотека стала писать, что не находит какой-то временный файл. Опять поиски, оказалось нужно установить утилиту ufraw, и файл прочитался. За 11 секунд. А потом в JPEG вывалилось нечто больше похожее на шум чем на исходную картинку.

Изначально я грешил на Wand, мне казалось, что он криво конвертирует картинку в RGB буфер, однако, запустив калькулятор, я обнаружил что буфер ровно в 2 раза больше чем необходимо — то есть на канал приходится не 8, а 16 бит. Ура, одна строчка и все работает. Но что делать с долгой загрузкой файла? Даже если серверов будет четыре, такое же количество больших CR2 файлов просто сделают сервис недоступным.

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

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

Начал я со сборки Upload модуля NGINX, и, конечно же, безуспешно. Провозившись некоторое время, я понял, что автор его забросил, и на последней версии этот модуль не заработает. Ну и ладно — пусть Tornado сервер сохраняет входящие файлы на диск. Далее фактически копипаста в новые скрипты обработки изображения. Конфигурируем Supervisor и NGINX, допиливаем скрипт установки. И оно работает.

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

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

Исходники можно посмотреть на github. А я дальше болеть.

Комментарии (35)


  1. aml
    13.05.2015 12:12
    +13

    Ни хрена себе тестовое задание. А зарплату за него заплатят?


    1. RPG18
      13.05.2015 15:32
      +4

      А что тут собственно такого? В задание не сказано использовать NGINX, а простой рабочий прототип можно сварганить за пару часов. Окценты были сразу расставлены:

      Верстка не важна, уделяйте основное внимание бэкенду, оформлению кода, мелочам.


  1. frol
    13.05.2015 12:30
    -1

    Честно говоря, как мне кажется, тестовое задание как явление себя изжило в некотором роде. Лучше всего навыки демонстрирует активность на github/bitbucket/etc, при этом не обязательно иметь свои проекты — форки и даже оформленные отчёты об ошибках очень красочно иллюстрируют способности человека.

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


    1. aavezel
      13.05.2015 15:13

      Желательно после двухсотой )))


    1. semmaxim
      13.05.2015 15:56
      +3

      А если у человека нет аккаунта и/или совсем нет активности на github/bitbucket? Он вообще программировать не умеет?
      Ну нет у меня времени влезать в opensource-разработки, нету. А проблемы или баги, которые встречаются, обычно уже описаны и зафиксированы.


      1. frol
        13.05.2015 16:18
        +1

        Тогда, скорее всего, вы не сможете показать никакой готовый код из-за SLA, так что придётся оценивать как «без видимого результата работы», что для работодателя аналогично случаю «без опыта работы». Любой человек может научиться писать код не работая нигде, не у каждого будет проект, который стоит показать, так что остаётся для этого случая и остаются тестовые задания, но, как я и сказал раньше — тестовое задание не должно быть тестовым проектом, так как это слишком.


  1. frol
    13.05.2015 12:32
    +5

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


  1. m0sk1t
    13.05.2015 12:46
    +3

    Чем-то напомнило пост Как два программиста хлеб пекли =)


  1. coocheenin
    13.05.2015 15:43

    Скорее вакансия кодера/технолога, а не программиста. Решили не конкретизировать и написать «разработчик»…


  1. un1t
    13.05.2015 16:01
    +11

    Мы будем поднимать несколько backend серверов, поэтому нам понадобится балансер и Supervisor. Изначально я думал о HAProxy в качестве балансера, но тут меня осенило, что картинки может хорошо раздавать NGINX. В итоге в начале архитектура мне показалась такой: NGINX балансирует соединения и раздает статику с диска, 4 сервера Tornado обрабатывают запросы, Redis синхронизирует backend.


    Зачем все это нужно было делать, в задании про это ни слова.


    1. ptQa
      14.05.2015 00:32
      +1

      Именно, лучше бы тесты написал вместо этого.


  1. dzigoro
    13.05.2015 16:06
    +1

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

    Тестовое задание — это не только проверка ваших навыков кодера, но и навыков разработчика: умение обдумать, сформулировать и задать вопросы по заданию. Отсутствие вопросов обычно сильно портит впечатление.

    А так, Вы молодец, конечно.


    1. nxsofsys Автор
      13.05.2015 16:26

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


      1. flint
        13.05.2015 16:46
        +4

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

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

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


        1. nxsofsys Автор
          13.05.2015 16:58

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


  1. KeFA
    13.05.2015 16:06
    +3

    Что хотел выяснить работодатель этим заданием мне не очень понятно. Может требовалось написать свою библиотеку для получения EXIF данных?

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

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


    1. nxsofsys Автор
      13.05.2015 16:32

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


      1. JC_Piligrim
        14.05.2015 16:19
        +3

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


  1. iroln
    13.05.2015 16:11

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


    1. frol
      13.05.2015 16:24
      +4

      ИМХО:
      Я, конечно, не совсем в теме распознаваний и технологий с этим связанными, но неделю на тестовое задание — это по-моему перебор. Кроме того, задание такого объёма, что мне кажется оно займёт неделю фултайм работы (если не в режиме наколеночного скрипта это делать). Авторы тестового задания должно быть предлагают очень хорошие условия после устройства к ним, иначе я бы не стал тратить время на такое тестовое задание.


      1. RPG18
        13.05.2015 16:44

        Там да же подсказка есть

        поощряется использование OpenCV
        Распознавание дорожных знаков (OpenCV)

        Как понимаю все сводится к копипасту примеров по OpenCV.


        1. iroln
          13.05.2015 16:57

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

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


          1. iroln
            13.05.2015 17:07

            Там в конце ещё написано про нейросеть для классификации контуров, чтобы определить форму знаков, как я понимаю (так называемый Structural Analysis и Shape Descriptors). Так вот, в OpenCV нет никаких нейронных сетей, а есть только примитивы (кирпичики) и классификатор придётся придумывать, тестировать и писать самостоятельно, используя только лишь примитивы из OpenCV, и никакая копипаста тут не поможет.


        1. BelBES
          13.05.2015 17:14
          +1

          Для первого задания этот пример уже не подходит, т.к. знаки могут иметь различную форму, цвет, не иметь белой рамки по контуру и т.д. и т.п. Лучшего качества наверно можно было бы добиться на SVM+HoG, какойнибудь каскадный детктор или Deep Learning, но сомневаюсь, что к заданию приложили достаточное для обучения количество позитивов/негативов. Как простое решение можно было бы попробовать простой feature matching или контурный анализ попробовать, надеясь что знаки достаточно фичастые. Вообщем уже в первом задании можно потратить немало времени на проверку различных гипотез.
          Во втором задании да, можно наверно было-бы применить наработки из предложенной статьи, но вопрос качества остается открытым.
          Третье задание целиком копипастится из OpenCV.
          А с учетом того, что ко всем заданиям необходимо еще написать сопроводительные документы с анализом качества реализации и т.п., то как правильно сказал frol, тут действительно для качественного выполнения задания стоит тратить неделю фулл-тайма.

          iroln а откуда такое задание на собеседование и что за условия там предлагают, если хоть кто-то берется за его выполнение?:)


          1. iroln
            13.05.2015 17:35

            … каскадный детктор или Deep Learning, но сомневаюсь, что к заданию приложили достаточное для обучения количество позитивов/негативов
            Тестовые данные, которые были приложены к заданию, не позволяют использовать никакие обучающиеся алгоритмы (никаких позитивов/негативов). На счёт сроков и объёма задания, если делать всё по уму, при этом начиная с нуля (не имея работающего прототипа/модели), и недели фулл-тайма не хватит. Как вы и сказали:
            уже в первом задании можно потратить немало времени на проверку различных гипотез

            На счёт «откуда» и условий ответил в личку, думаю, так будет правильно.


            1. BelBES
              13.05.2015 18:35

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

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


  1. kronos
    13.05.2015 17:34
    -2

    Так а что в итоге-то? Взяли?


  1. kmike
    13.05.2015 17:35

    А зачем было EXIF записывать к превьюшке? Я так понял, на это ушло 90% времени, и про это ничего в ТЗ не было.


    1. nxsofsys Автор
      13.05.2015 17:46

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


  1. VBart
    13.05.2015 18:36

    Начал я со сборки Upload модуля NGINX, и, конечно же, безуспешно. Провозившись некоторое время, я понял, что автор его забросил, и на последней версии этот модуль не заработает. Ну и ладно — пусть Tornado сервер сохраняет входящие файлы на диск.
    Открою страшную тайну: NGINX умеет это делать без модуля из коробки.


    1. nxsofsys Автор
      13.05.2015 18:44

      Спасибо, очень полезное замечание.


  1. SelenIT2
    13.05.2015 18:53

    … (Не использовать проверку наличия EXIF данных в качестве валидации)
    Не позволять сохранять фото, созданные более года назад (проверять дату создания фото из EXIF).
    Если отсутствует дата создания фото в EXIF, тогда следует выдать ошибку и не добавлять файл.

    Так всё-таки?


    1. Pe4enie
      13.05.2015 20:42

      Вы из контекста вырвали, вчитайтесь:

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

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


      1. nxsofsys Автор
        13.05.2015 20:47

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


      1. SelenIT2
        13.05.2015 21:49

        Это логично. Но формулировка расплывчатая, и это открывает веселые перспективы. Вот так сдашь задание с выполненным последним пунктом, а его зарубят: «Сказано же — EXIF для валидации не использовать!» :)