Skip to content

Latest commit

 

History

History
1215 lines (1036 loc) · 99.6 KB

resume.org

File metadata and controls

1215 lines (1036 loc) · 99.6 KB

Модуль HeadHunter (Resume Operations)

START Жизненный цикл резюме

Создание резюме на hh.ru начинается с перехода на страницу “http://spb.hh.ru/applicant/resumes/view?resume=” где расположена форма, которая предлагает добавить следующие данные:

  • Фото (photo)
  • Имя, возраст, город (personal)
  • Контакты (contacts)
  • Желаемая должность и зарплата (job-position)
  • Образование (education)
  • Опыт работы (experience)

Каждый из вариантов ведет на свою страницу с шаблоном “http://spb.hh.ru/applicant/resumes/edit/{SECTION}?resume=”, где в {SECTON} подставляется название раздела. На этих страницах размещены формы, которые отправляют POST-запросы, формируя секции резюме. Рассмотрим эти POST-запросы подробнее в следующих подразделах.

После отправки POST-запроса сервер запоминает данные формы в сессии и возвращает заголовок LOCATION на основную страницу резюме, но теперь присваивает резюме идентификатор. Таким образом адрес становится таким: http://spb.hh.ru/applicant/resumes/view?resume=341309a0ff02d634530039ed1f543763556562

Drakma автоматически переходит по location, так что реальное значение resume нужно извлекать из возвращаемого значения uri.

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

Резюме также можно удалить по идентификатору.

Резюме сопровождается артефактами (фотографиями), которые привязываются к нему. Артефакты можно загружать, выбирать и удалять.

Видимость резюме можно настраивать. Существуют следующие настройки:

  • Всему интернету ()
  • Не видно никому Ваше резюме будет недоступно для просмотра всем работодателям и кадровым агентствам, а также не будет выводиться в результатах поиска по базе данных. Вы сможете откликаться таким резюме на заинтересовавшие вас вакансии сайта HeadHunter. При отклике на конкретную вакансию компании «Z», настройки видимости вашего резюме автоматически изменятся на «Не видно никому, кроме: компания «Z».
  • Компаниям, являющимся клиентами HeadHunter
  • Только перечисленным компаниям
  • Компаниям, зарегистрированным на HeadHunter, кроме… Ваше резюме будет доступно для просмотра всем компаниям и кадровым агентствам, которые зарегистрированы на HeadHunter, за исключением тех, которые вы отметите в специальном окне. Таким резюме вы сможете откликаться на все вакансии сайта HeadHunter, однако те компании, которым вы запретили просматривать свое резюме, не будут иметь к нему доступ через поиск по базе данных и по прямой ссылке. При отклике на конкретную вакансию компании «Z», внесенной вами в stop-список, настройки видимости вашего резюме автоматически изменятся, и компания «Z» удалится из stop-списка.
  • По прямой ссылке

Настройка видимости осуществляется на странице: http://spb.hh.ru/applicant/resumes/edit/visibility?resume=9555a7ecff02588d3c0039ed1f454162305732 и производится посылкой POST-запроса вида:

accessType.string={VISIBILITY} _xsrf=b2dccfd0ce2ff68b2c4f795ac6d549fb

Где вместо {VISIBILITY} посылается тип видимости:

-everyone -no_one -clients -invisibleResumeToVisible=true& accessType.string=clients -accessType.string=blacklist&_xsrf=b2dccfd0ce2ff68b2c4f795ac6d549fb -direct

[TODO] - Выполнить весь жизненный цикл резюме [TODO] - Осущестлять редактирование резюме и изменять его видимость

Этот блок необходим для генерации POST-запросов

Этот макрос формирует тело POST-запроса:

(defmacro assembly-post (&body body)
  `(format nil "~{~A~^&~}"
           (mapcar #'(lambda (x)
                       (format nil "~A=~A" (car x) (cdr x)))
                   ,@body)))

Этот макрос отсылает POST-запрос, формируя его с помощью assembly-post:

(defmacro send-post ((url cookie-jar cookie-alist) &body body)
  `(drakma:http-request
    ,url
    :user-agent "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0"
    :method :post
    :content (assembly-post ,@body)
    :content-type "application/x-www-form-urlencoded; charset=UTF-8"
    :redirect 10
    :additional-headers
    `(("Accept" . "*/*")
      ("Accept-Language" . "en-US,en;q=0.5")
      ("X-Xsrftoken" . ,(cdr (assoc "_xsrf" ,cookie-alist :test #'equal)))
      ("X-Requested-With" . "XMLHttpRequest")
      ("Referer" . ,,url)
      ("Connection" . "keep-alive")
      ("Pragma" . "no-cache")
      ("Cache-Control" . "no-cache"))
    :cookie-jar ,cookie-jar
    :force-binary t))

Этот макрос оборачивает отправку POST-запроса в multiple-value-bind чтобы получить ответ:

(defmacro send-post-multiple-values ((personal-url cookie-jar cookie-alist &body alist) &body body)
  `(multiple-value-bind (body-or-stream status-code headers uri stream must-close reason-phrase)
       (send-post (,personal-url ,cookie-jar ,cookie-alist) ,@alist)
     ,@body))

Этот макрос получает cookie-alist - ассоциативный список ключей и значений cookie из cookie-jar:

(defmacro with-cookie-alist ((cookie-jar) &body body)
  `(let ((cookie-alist (mapcar #'(lambda (cookie)
                                   (cons (drakma:cookie-name cookie) (drakma:cookie-value cookie)))
                               (drakma:cookie-jar-cookies ,cookie-jar))))
     ,@body))

Теперь мы можем реализовать макросом основной сценарий заполения полей резюме: сначала запрашивем страницу, где размещены все резюме, потом запрашиваем страницу для заполнения одной из секций резюме, потом отправляем POST-запрос с заполенными полями, и наконец возвращаем полученный ответ:

(defmacro with-set-resume-section ((section-url &body post-data) &body body)
  ;; Сначала запросим основную страницу резюме
  `(let ((main-url (format nil "http://spb.hh.ru/applicant/resumes/view?resume=~A" resume-id)))
     (multiple-value-bind (response cookie-jar url)
         (hh-get-page main-url cookie-jar *hh_account* "http://spb.hh.ru")
       ;; Теперь запрашиваем section-url
       (multiple-value-bind (response cookie-jar url)
           (hh-get-page ,section-url cookie-jar *hh_account* "http://spb.hh.ru")
         (with-cookie-alist (cookie-jar)
           (send-post-multiple-values (,section-url cookie-jar cookie-alist ,@post-data)
             ,@body))))))

Фото (photo)

При выборе уже загруженных фото

photo.string=94187420 type=RESUME_PHOTO file= title=&_xsrf=b2dccfd0ce2ff68b2c4f795ac6d549fb

При загрузке новой фотографии

POST http://spb.hh.ru/applicant/resumes/artifacts/upload

Content-Type: multipart/form-data; boundary=---------------------------41026768278304188928476747 Content-Length: 1364120

-----------------------------41026768278304188928476747 Content-Disposition: form-data; name=”_xsrf”

b2dccfd0ce2ff68b2c4f795ac6d549fb -----------------------------41026768278304188928476747 Content-Disposition: form-data; name=”user”

3681852 -----------------------------41026768278304188928476747 Content-Disposition: form-data; name=”type”

RESUME_PHOTO -----------------------------41026768278304188928476747 Content-Disposition: form-data; name=”file”; filename=”20150726_212228.jpg” Content-Type: image/jpeg

ÿØÿá0OExif

Удаление фото

POST http://spb.hh.ru/applicant/resumes/artifacts/remove

id=98616186 user=3681852

Имя, возраст, город (personal)

Сопоставим каждому полю в POST-запросе соответствующий accessor:

lastName.stringlast-name
firstName.stringfirst-name
middleName.stringmiddle-name
birthday.datebirthday
gender.stringgender
area.stringarea
metro.stringmetro
relocation.stringrelocation
relocationArea.stringrelocation-area
businessTripReadiness.stringbusiness-trip-readiness
citizenshipcitizen-ship
citizenship.stringcitizen-ship
workTicketwork-ticket
workTicket.stringwork-ticket
travelTime.stringtravel-time

Сгенерируем из этой таблицы код, который формирует POST-запрос и напишем процедуру которая его отсылает:

(in-package #:moto)

(defun set-resume-personal (cookie-jar resume &optional (resume-id ""))
  (with-set-resume-section ((format nil "http://spb.hh.ru/applicant/resumes/edit/personal?resume=~A" resume-id)
                            <<gen_post(personal_eq, "resume")>>
                            )
    (values
     uri
     headers
     (flexi-streams:octets-to-string body-or-stream :external-format :utf-8))))

;; (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
;;   (print (set-resume-personal cookie-jar (car (all-resume)))))

Контакты (contacts)

Страница hh.ru, которая принимает POST-запрос, изменяющий контакты использует позиционные маркеры, вроде phone.type, которые отделяют друг от друга блоки одинаковых ключей. Поэтому мне пришлось немного модифицировать gen_post, чтобы позиционные маркеры отправлялись “как есть”, а не оборачивалось в вызов accessor-a. В остальном все работает таким же образом как и в предыдущем разделе

phone.type:cell
phone.countrycell-phone-country
phone.citycell-phone-city
phone.numbercell-phone-number
phone.commentcell-phone-comment
phone.type:home
phone.countryhome-phone-country
phone.cityhome-phone-city
phone.numberhome-phone-number
phone.commenthome-phone-comment
phone.type:work
phone.countryhome-phone-country
phone.cityhome-phone-city
phone.numberhome-phone-number
phone.commenthome-phone-comment
email.stringemail-string
preferredContact.stringpreferred-contact
personalSite.type:icq
personalSite.urlicq
personalSite.type:skype
personalSite.urlskype
personalSite.type:freelance
personalSite.urlfreelance
personalSite.type:moi_krug
personalSite.urlmoi_krug
personalSite.type:linkedin
personalSite.urllinkedin
personalSite.type:facebook
personalSite.urlfacebook
personalSite.type:livejournal
personalSite.urllivejournal
personalSite.type:personal
personalSite.urlpersonal-site
(in-package #:moto)

(defun set-resume-contacts (cookie-jar resume &optional (resume-id ""))
  (with-set-resume-section ((format nil "http://spb.hh.ru/applicant/resumes/edit/contacts?resume=~A" resume-id)
                            <<gen_post(contacts_eq, "resume")>>
                            )
    (values
     uri
     headers
     (flexi-streams:octets-to-string body-or-stream :external-format :utf-8))))

;; (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
;;   (print
;;    (set-resume-contacts cookie-jar (car (all-resume))
;;                         ;; "8eb43271ff030a44e00039ed1f735871443047"
;;                         )))

Желаемая должность и зарплата (resume-position)

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

profarea:
profarea:1
profarea:2
profarea:3
profarea:4
profarea:5
profarea:6
profarea:7
profarea:8
profarea:9
profarea:10
profarea:11
profarea:12
profarea:13
profarea:14
profarea:16
profarea:17
profarea:18
profarea:19
profarea:20
profarea:21
profarea:22
profarea:23
profarea:24
profarea:25
profarea:26
profarea:15
profarea:27
profarea:29
salary.amountsalary-amount
salary.currencysalary-currency
employment.stringemployment
workSchedule.stringwork-schedule

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

(in-package #:moto)

(defun set-resume-position (cookie-jar resume &optional (resume-id ""))
  (with-set-resume-section ((format nil "http://spb.hh.ru/applicant/resumes/edit/position?resume=~A" resume-id)
                            (append
                             `(("title.string" . ,(drakma:url-encode "Программист" :utf-8))
                               ("profArea"     . ,(drakma:url-encode (prof-area resume) :utf-8)))
                             (mapcar #'(lambda (x)
                                         `("specialization.string" . ,(drakma:url-encode x :utf-8)))
                                     (split-sequence:split-sequence #\Space (specializations resume)))
                             <<gen_post(position_eq, "resume")>>
                             ))
    (values
     uri
     headers
     (flexi-streams:octets-to-string body-or-stream :external-format :utf-8))))

(print
 (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
   (set-resume-position cookie-jar (car (all-resume))
                        ;; "8eb43271ff030a44e00039ed1f735871443047"
                        )))

Образование (education)

В этой секции требуется провести небольшой рефакторинг и убедиться что в POST-запросе не наблюдается дублирования полей primaryEducation.* и xsrf. Хотя может быть так и должно быть, т.к. в оригинальном запросе это дублирование есть.

Тем не менее пока все работает и так.

primaryEducation.id:
primaryEducation.name:
primaryEducation.universityId:
primaryEducation.facultyId:
primaryEducation.organization:
primaryEducation.result:
primaryEducation.specialtyId:
primaryEducation.year:
additionalEducation.idadditional-education-id
additionalEducation.nameadditional-education-name
additionalEducation.organizationadditional-education-organization
additionalEducation.resultadditional-education-result
additionalEducation.yearadditional-education-year
certificate.idcertificate-id
certificate.typecertificate-type
certificate.selectedcertificate-selected
certificate.ownerNamecertificate-ownerName
certificate.transcriptionIdcertificate-transcription-id
certificate.passwordcertificate-password
certificate.titlecertificate-title
certificate.achievementDatecertificate-achievementDate
certificate.urlcertificate-url
attestationEducation.idattestation-education-id
attestationEducation.nameattestation-education-name
attestationEducation.organizationattestation-education-organization
attestationEducation.resultattestation-education-result
attestationEducation.yearattestation-education-year
(in-package #:moto)

(defmacro if-zero-then-empty (&body body)
  (let ((it (gensym "IT-")))
    `(let ((,it ,@body))
       (if (equal 0 ,it)
           ""
           (drakma:url-encode (format nil "~A" ,it) :utf-8)))))

;; (macroexpand-1 '(if-zero-then-empty (education-id education)))

(defun set-resume-education (cookie-jar resume &optional (resume-id ""))
  (with-set-resume-section ((format nil "http://spb.hh.ru/applicant/resumes/edit/education?resume=~A" resume-id)
                            (append
                             `(("educationLevel.string" . ,(drakma:url-encode (education-level-string resume) :utf-8)))
                             (let ((primary-education-id (car (split-sequence:split-sequence #\Space (educations resume)))))
                               (if (null primary-education-id)
                                   (err "error education-id")
                                   (let ((education (get-education (parse-integer primary-education-id))))
                                     `(("primaryEducation.id"            . ,(if-zero-then-empty (education-id education)))
                                       ("primaryEducation.name"          . ,(drakma:url-encode (name education) :utf-8))
                                       ("primaryEducation.universityId"  . ,(drakma:url-encode (format nil "~A" (university-id education)) :utf-8))
                                       ("primaryEducation.facultyId"     . ,(if-zero-then-empty (faculty-id education)))
                                       ("primaryEducation.organization"  . ,(drakma:url-encode (organization education) :utf-8))
                                       ("primaryEducation.result"        . ,(drakma:url-encode (result education) :utf-8))
                                       ("primaryEducation.specialtyId"   . ,(drakma:url-encode (format nil "~A" (specialty-id education)) :utf-8))
                                       ("primaryEducation.year"          . ,(drakma:url-encode (format nil "~A" (year education)) :utf-8))))))
                             <<gen_post(education_eq, "resume")>>
                             (let ((langs))
                               (mapcar #'(lambda (x)
                                           (let ((lang (get-lang (parse-integer x))))
                                             (push `("language.id"     . ,(drakma:url-encode (format nil "~A"(lang-id lang))     :utf-8)) langs)
                                             (push `("language.degree" . ,(drakma:url-encode (format nil "~A" (lang-degree lang)) :utf-8)) langs)
                                             ))
                                       (split-sequence:split-sequence #\Space (languages resume)))
                               (reverse langs))
                             `(
                               ("_xsrf"                          . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal))))
                             )
                            )
    (values
     uri
     headers
     (flexi-streams:octets-to-string body-or-stream :external-format :utf-8))))

;; (print
;;  (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
;;    (set-resume-education cookie-jar (car (all-resume))
;;                      ;; "8eb43271ff030a44e00039ed1f735871443047"
;;                      )))

START Опыт работы (experience)

type:PORTFOLIO
portfolio.string:
file:
title:
(in-package #:moto)

;; дубль if-sero-then-empty
(defmacro url-enc (&body body)
  `(let ((it ,@body))
     (if (equal 0 it)
         ""
         (drakma:url-encode (format nil "~A" it) :utf-8))))

(defun set-resume-expirience (cookie-jar resume &optional (resume-id ""))
  (with-set-resume-section ((format nil "http://spb.hh.ru/applicant/resumes/edit/experience?resume=~A" resume-id)
                            (let ((resume (get-resume 1)))
                              (append
                               (apply 'append
                                      (loop :for item :in (split-sequence:split-sequence #\Space (expiriences resume)) :collect
                                         (let ((item (get-expirience (parse-integer item))))
                                           `(("experience.companyName"      . ,(url-enc (name item)))
                                             ("experience.companyId"        . ,(url-enc (company-id item)))
                                             ("experience.companyAreaId"    . ,(url-enc (company-area-id item)))
                                             ("experience.companyUrl"       . ,(url-enc (url item)))
                                             ("experience.companyIndustryId". ,(url-enc (industry-id item)))
                                             ("experience.companyIndustries". ,(url-enc (industries item)))
                                             ("experience.companyIndustries". "") ;; compatibility: предположительно направления деятельности
                                             ("experience.id"               . ,(url-enc (exp-id item)))
                                             ("experience.position"         . ,(url-enc (job-position item)))
                                             ("experience.startDate"        . ,(url-enc (start-date item)))
                                             ("experience.endDate"          . ,(url-enc (end-date item)))
                                             ("experience.description"      . ,(url-enc (description item)))
                                             ))))
                               (apply 'append
                                      (loop :for item :in (split-sequence:split-sequence #\Space (skills resume)) :collect
                                         (let ((item (get-skill (parse-integer item))))
                                           `(("keySkills.string"      . ,(url-enc (name item)))))))
                               `(("skills.string" . "%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%B5+%D0%B3%D0%BE%D0%B4%D1%8B+%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D1%83%D1%81%D1%8C+%D0%BD%D0%B0+%D0%BF%D0%B5%D0%BD%D1%81%D0%B8%D0%B8.%0D%0A%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B5%D0%B5+%D0%B2%D1%80%D0%B5%D0%BC%D1%8F+%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D1%83%D1%81%D1%8C+%D0%B2+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0%D1%85+%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%8B.%0D%0A%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%B5+%D0%B3%D0%BE%D0%B4%D1%8B+%D0%BF%D1%80%D0%BE%D1%85%D0%BE%D0%B4%D0%B8%D0%BB+%D1%81%D0%BB%D1%83%D0%B6%D0%B1%D1%83+%D0%B2+%D0%B0%D1%80%D0%BC%D0%B8%D0%B8.%0D%0A%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%B5+%D0%B3%D0%BE%D0%B4%D1%8B+%D0%BF%D1%80%D0%BE%D1%85%D0%BE%D0%B4%D0%B8%D0%BB+%D0%BE%D0%B1%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5+%D0%B1%D0%B5%D0%B7+%D0%B2%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8+%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%82%D1%8C.%0D%0A%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%B5+%D0%B3%D0%BE%D0%B4%D1%8B+%D0%BD%D0%B0%D1%85%D0%BE%D0%B4%D0%B8%D0%BB%D0%B0%D1%81%D1%8C+%D0%B2+%D0%B4%D0%B5%D0%BA%D1%80%D0%B5%D1%82%D0%BD%D0%BE%D0%BC+%D0%BE%D1%82%D0%BF%D1%83%D1%81%D0%BA%D0%B5.%0D%0A"))
                               (apply 'append
                                      (loop :for item :in (split-sequence:split-sequence #\Space (recommendations resume)) :collect
                                         (let ((item (get-recommendation (parse-integer item))))
                                           `(("recommendation.id"            . ,(url-enc (recommendation-id item)))
                                             ("recommendation.name"          . ,(url-enc (name item)))
                                             ("recommendation.position"      . ,(url-enc (job-position item)))
                                             ("recommendation.organization"  . ,(url-enc (organization item)))
                                             ("recommendation.contactInfo"   . ,(url-enc (contact-info item)))))))
                               <<gen_post(expirience_eq, "resume")>>
                               )))
    (values
     uri
     headers
     (flexi-streams:octets-to-string body-or-stream :external-format :utf-8))))

;; (print
;;  (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
;;    (set-resume-expirience cookie-jar (car (all-resume))
;;                      ;; "8eb43271ff030a44e00039ed1f735871443047"
;;                      )))

Публикация (touch)

(in-package #:moto)

(defun touch (cookie-jar &optional (resume-id ""))
  ;; Сначала запросим основную страницу резюме
  (let ((main-url "http://spb.hh.ru/applicant/resumes/view?resume="))
    (multiple-value-bind (response cookie-jar url)
        (hh-get-page main-url cookie-jar *hh_account* "http://spb.hh.ru")
      ;; Теперь запрашиваем touch
      (let ((touch-url "http://spb.hh.ru/applicant/resumes/edit/touch?resume="))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page touch-url cookie-jar *hh_account* "http://spb.hh.ru")
          ;; Получаем ключ-значения cookies
          (let ((cookie-alist (mapcar #'(lambda (cookie)
                                          (cons (drakma:cookie-name cookie) (drakma:cookie-value cookie)))
                                      (drakma:cookie-jar-cookies cookie-jar))))
            ;; Отправляем POST
             (multiple-value-bind (body-or-stream status-code headers uri stream must-close reason-phrase)
                 (drakma:http-request
                  touch-url
                  :user-agent "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0"
                  :method :post
                  :content (format nil "~{~A~^&~}"
                                   (mapcar #'(lambda (x)
                                               (format nil "~A=~A" (car x) (cdr x)))
                                           `(("resume" . "341309a0ff02d634530039ed1f543763556562")
                                             ("publish" . "next")
                                             ("createVisibleResume" . "true&_xsrf=b2dccfd0ce2ff68b2c4f795ac6d549fb"))
                                           ))
                  :content-type "application/x-www-form-urlencoded; charset=UTF-8"
                  :additional-headers
                  `(("Accept" . "*/*")
                    ("Accept-Language" . "en-US,en;q=0.5")
                    ;; ("Accept-Encoding" . "gzip, deflate")
                    ("X-Xsrftoken" . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal)))
                    ("X-Requested-With" . "XMLHttpRequest")
                    ("Referer" . ,touch-url)
                    ("Connection" . "keep-alive")
                    ("Pragma" . "no-cache")
                    ("Cache-Control" . "no-cache")
                    )
                  :cookie-jar cookie-jar
                  :redirect 10
                  :force-binary t)
               (return-from touch
                 (values
                  headers
                  (flexi-streams:octets-to-string body-or-stream :external-format :utf-8))))))))))

;; (print
;;  (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
;;    (touch cookie-jar)))

Удаление резюме

http://spb.hh.ru/applicant/deleteresume/

resumeId=47592531 _xsrf=b2dccfd0ce2ff68b2c4f795ac6d549fb

Синхронизация резюме на hh и в хранилище

Парсер резюме в личном кабинете

Создание резюме под вакасию по шаблону

Сущности шаблона резюме

Сборка

(in-package #:moto)

<<assembly_post>>

<<send_post>>

<<send_post_multiple_values>>

<<with_cookie_alist>>

<<with_set_resume_section>>

<<set_resume_personal>>

<<set_resume_contacts>>

<<set_resume_position>>

<<set_resume_education>>

<<set_resume_expirience>>

Язык описания резюме

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

Как оказалось при исследовании hh - обработчик POST-запроса там по видимому единый, поэтому можно отправлять POST соответствующей странице /edit/experience в /edit/personal. Думаю можно было бы отправить все данные одним пакетом, но пока мы стараемся быть похожими на обычный клиентский броузер.

(in-package :moto)

(defun job (&key name (company-id "") (company-area-id "2") site (industry-id "") (industries nil) (id "") position start-date end-date description)
  (list :name name :company-id company-id :company-area-id company-area-id :site site :industry-id industry-id :industries industries :id id :position position :start-date start-date :end-date end-date :description description))

(defun hh-post-resume (cookie-jar &rest plist &key resume-id &allow-other-keys)
  ;; Если идентификатор резюме не указан - то используем пустую строку
  (unless resume-id
    (setf resume-id ""))
  ;; Сначала запросим основную страницу всех резюме
  (let ((main-url (format nil "http://spb.hh.ru/applicant/resumes/view?resume=~A" resume-id)))
    (multiple-value-bind (response cookie-jar url)
        (hh-get-page main-url cookie-jar *hh_account* "http://spb.hh.ru")
      ;; Теперь запросим страницу personal
      (let ((section-url (format nil "http://spb.hh.ru/applicant/resumes/edit/personal?resume=~A" resume-id)))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page section-url cookie-jar *hh_account* "http://spb.hh.ru")
          (with-cookie-alist (cookie-jar)
            (send-post-multiple-values
             (section-url
              cookie-jar
              cookie-alist
              `(("lastName.string" . ,(drakma:url-encode (getf plist :last-name) :utf-8))
                ("firstName.string" . ,(drakma:url-encode (getf plist :first-name) :utf-8))
                ("middleName.string" . ,(drakma:url-encode (getf plist :middle-name) :utf-8))
                ("birthday.date" . ,(drakma:url-encode (getf plist :birthday) :utf-8))
                ("gender.string" . ,(drakma:url-encode (getf plist :gender) :utf-8))
                ("area.string" . ,(drakma:url-encode (getf plist :area) :utf-8))
                ("metro.string" . ,(drakma:url-encode (getf plist :metro) :utf-8))
                ("relocation.string" . ,(drakma:url-encode (getf plist :relocation) :utf-8))
                ("relocationArea.string" . ,(drakma:url-encode (getf plist :relocation-area) :utf-8))
                ("businessTripReadiness.string" . ,(drakma:url-encode (getf plist :business-trip-readiness) :utf-8))
                ("citizenship" . ,(drakma:url-encode (getf plist :citizen-ship) :utf-8))
                ("citizenship.string" . ,(drakma:url-encode (getf plist :citizen-ship) :utf-8))
                ("workTicket" . ,(drakma:url-encode (getf plist :work-ticket) :utf-8))
                ("workTicket.string" . ,(drakma:url-encode (getf plist :work-ticket) :utf-8))
                ("travelTime.string" . ,(drakma:url-encode (getf plist :travel-time) :utf-8)))
              )
             ;; Получаем идентификатор резюме
             (setf resume-id (cadr (split-sequence:split-sequence #\= (puri:uri-query uri))))))))
      ;; EDUCATION
      (let ((section-url (format nil "http://spb.hh.ru/applicant/resumes/edit/education?resume=~A" resume-id)))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page section-url cookie-jar *hh_account* "http://spb.hh.ru")
          (with-cookie-alist (cookie-jar)
            (send-post-multiple-values
             (section-url
              cookie-jar
              cookie-alist
              (append
               `(("educationLevel.string" . ,(drakma:url-encode "higher" :utf-8)))
               (let ((edu (getf plist :primary-educations)))
                 `(("primaryEducation.id"            . "")
                   ("primaryEducation.name"          . ,(drakma:url-encode (getf edu :name) :utf-8))
                   ("primaryEducation.universityId"  . ,(drakma:url-encode (format nil "~A" 39864) :utf-8))
                   ("primaryEducation.facultyId"     . ,(if-zero-then-empty 0))
                   ("primaryEducation.organization"  . ,(drakma:url-encode (getf edu :organization) :utf-8))
                   ("primaryEducation.result"        . ,(drakma:url-encode (getf edu :result) :utf-8))
                   ("primaryEducation.specialtyId"   . ,(drakma:url-encode (format nil "~A" (getf edu :specialty-id)) :utf-8))
                   ("primaryEducation.year"          . ,(drakma:url-encode (format nil "~A" (getf edu :year)) :utf-8))))
               `(("additionalEducation.id" . ,(drakma:url-encode (getf plist :additional-education-id) :utf-8))
                 ("additionalEducation.name" . ,(drakma:url-encode (getf plist :additional-education-name) :utf-8))
                 ("additionalEducation.organization" . ,(drakma:url-encode (getf plist :additional-education-organization) :utf-8))
                 ("additionalEducation.result" . ,(drakma:url-encode (getf plist :additional-education-result) :utf-8))
                 ("additionalEducation.year" . ,(drakma:url-encode (getf plist :additional-education-year) :utf-8))
                 ("certificate.id" . ,(drakma:url-encode (getf plist :certificate-id) :utf-8))
                 ("certificate.type" . ,(drakma:url-encode (getf plist :certificate-type) :utf-8))
                 ("certificate.selected" . ,(drakma:url-encode (getf plist :certificate-selected) :utf-8))
                 ("certificate.ownerName" . ,(drakma:url-encode (getf plist :certificate-ownerName) :utf-8))
                 ("certificate.transcriptionId" . ,(drakma:url-encode (getf plist :certificate-transcription-id) :utf-8))
                 ("certificate.password" . ,(drakma:url-encode (getf plist :certificate-password) :utf-8))
                 ("certificate.title" . ,(drakma:url-encode (getf plist :certificate-title) :utf-8))
                 ("certificate.achievementDate" . ,(drakma:url-encode (getf plist :certificate-achievementDate) :utf-8))
                 ("certificate.url" . ,(drakma:url-encode (getf plist :certificate-url) :utf-8))
                 ("attestationEducation.id" . ,(drakma:url-encode (getf plist :attestation-education-id) :utf-8))
                 ("attestationEducation.name" . ,(drakma:url-encode (getf plist :attestation-education-name) :utf-8))
                 ("attestationEducation.organization" . ,(drakma:url-encode (getf plist :attestation-education-organization) :utf-8))
                 ("attestationEducation.result" . ,(drakma:url-encode (getf plist :attestation-education-result) :utf-8))
                 ("attestationEducation.year" . ,(drakma:url-encode (getf plist :attestation-education-year) :utf-8)))
               `(
                 ("_xsrf"                          . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal))))
               ))))))
      ;; POSITION
      (let ((section-url (format nil "http://spb.hh.ru/applicant/resumes/edit/position?resume=~A" resume-id)))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page section-url cookie-jar *hh_account* "http://spb.hh.ru")
          (with-cookie-alist (cookie-jar)
            (send-post-multiple-values
             (section-url
              cookie-jar
              cookie-alist
              (append
               `(("title.string" . ,(drakma:url-encode (getf plist :title) :utf-8))
                 ("profArea"     . ,(drakma:url-encode "1" :utf-8)))
               (mapcar #'(lambda (x)
                           `("specialization.string" . ,(drakma:url-encode x :utf-8)))
                       (split-sequence:split-sequence #\Space (getf plist :specalizations)))
               `(("profarea" . "")
                 ("profarea" . "1")
                 ("profarea" . "2")
                 ("profarea" . "3")
                 ("profarea" . "4")
                 ("profarea" . "5")
                 ("profarea" . "6")
                 ("profarea" . "7")
                 ("profarea" . "8")
                 ("profarea" . "9")
                 ("profarea" . "10")
                 ("profarea" . "11")
                 ("profarea" . "12")
                 ("profarea" . "13")
                 ("profarea" . "14")
                 ("profarea" . "16")
                 ("profarea" . "17")
                 ("profarea" . "18")
                 ("profarea" . "19")
                 ("profarea" . "20")
                 ("profarea" . "21")
                 ("profarea" . "22")
                 ("profarea" . "23")
                 ("profarea" . "24")
                 ("profarea" . "25")
                 ("profarea" . "26")
                 ("profarea" . "15")
                 ("profarea" . "27")
                 ("profarea" . "29")
                 ("salary.amount" . ,(drakma:url-encode (getf plist :salary-amount) :utf-8))
                 ("salary.currency" . ,(drakma:url-encode "RUR" :utf-8))
                 ("employment.string" . ,(drakma:url-encode "full" :utf-8))
                 ("workSchedule.string" . ,(drakma:url-encode "full_day" :utf-8)))
               `(
                 ("_xsrf"                          . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal))))))))))
      ;; EXPIRIENCE
      (let ((section-url (format nil "https://spb.hh.ru/applicant/resumes/edit/experience?resume=~A&field=experience" resume-id)))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page section-url cookie-jar *hh_account* "http://spb.hh.ru")
          (with-cookie-alist (cookie-jar)
            (send-post-multiple-values
             (section-url
              cookie-jar
              cookie-alist
              (append
               (loop :for exp :in (getf plist :expiriences) :append
                  (append
                   `(("experience.companyName" . ,(url-enc (getf exp :name)))
                     ("experience.companyId" . ,(url-enc (getf exp :company-id)))
                     ("experience.companyAreaId" . ,(url-enc (getf exp :company-area-id)))
                     ("experience.companyUrl" . ,(url-enc (getf exp :site)))
                     ("experience.companyIndustryId" . ,(url-enc (getf exp :industry-id))))
                   (loop :for indstr-id :in (getf exp :industries)
                      :append
                      `(("experience.companyIndustries" . ,(format nil "~A" indstr-id))))
                   `(("experience.companyIndustries" . "")
                     ("experience.id" . "")
                     ("experience.position" . ,(url-enc (getf exp :position)))
                     ("experience.startDate" . ,(url-enc (getf exp :start-date)))
                     ("experience.endDate" . ,(url-enc (getf exp :end-date)))
                     ("experience.description" . ,(url-enc (getf exp :description))))))
               `(("skills.string"  . ,(url-enc (getf plist :about))))
               `(("_xsrf"                          . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal))))))
             (split-sequence:split-sequence #\= (puri:uri-query uri))
             ))))
      ;; LANGUAGES
      (let ((section-url (format nil "https://spb.hh.ru/applicant/resumes/edit/education?resume=~A&field=language" resume-id)))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page section-url cookie-jar *hh_account* "http://spb.hh.ru")
          (with-cookie-alist (cookie-jar)
            (send-post-multiple-values
             (section-url
              cookie-jar
              cookie-alist
              (append
               (loop :for lang :in (getf plist :languages) :append
                  `(("language.id"     . ,(drakma:url-encode (format nil "~A" (getf lang :lang-id)) :utf-8))
                    ("language.degree" . ,(drakma:url-encode (format nil "~A" (getf lang :lang-degree)) :utf-8))))
               `(("_xsrf" . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal))))))
             (split-sequence:split-sequence #\= (puri:uri-query uri))
             ))))
      ;; CONTACTS
      (let ((section-url (format nil "https://spb.hh.ru/applicant/resumes/edit/contacts?resume=~A" resume-id)))
        (multiple-value-bind (response cookie-jar url)
            (hh-get-page section-url cookie-jar *hh_account* "http://spb.hh.ru")
          (with-cookie-alist (cookie-jar)
            (send-post-multiple-values
             (section-url
              cookie-jar
              cookie-alist
              (append
               (let ((contacts (getf plist :contacts)))
                 `(("phone.type" . "cell")
                   ("phone.formatted" . ,(drakma:url-encode (getf contacts :cell-phone) :utf-8))
                   ("phone.comment" . ,(drakma:url-encode (getf contacts :cell-phone-comment) :utf-8))
                   ("phone.type" . "home")
                   ("phone.formatted" . ,(drakma:url-encode (getf contacts :home-phone) :utf-8))
                   ("phone.comment" . ,(drakma:url-encode (getf contacts :home-phone-comment) :utf-8))
                   ("phone.type" . "work")
                   ("phone.formatted" . ,(drakma:url-encode (getf contacts :work-phone) :utf-8))
                   ("phone.comment" . ,(drakma:url-encode (getf contacts :work-phone-comment) :utf-8))
                   ("email.string" . ,(drakma:url-encode (getf contacts :email-string) :utf-8))
                   ("preferredContact.string" . "email")
                   ("personalSite.type" . "icq")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :icq) :utf-8))
                   ("personalSite.type" . "skype")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :skype) :utf-8))
                   ("personalSite.type" . "freelance")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :freelance) :utf-8))
                   ("personalSite.type" . "moi_krug")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :moi_krug) :utf-8))
                   ("personalSite.type" . "linkedin")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :linkedin) :utf-8))
                   ("personalSite.type" . "facebook")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :facebook) :utf-8))
                   ("personalSite.type" . "personal")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :personal) :utf-8))
                   ("personalSite.type" . "livejournal")
                   ("personalSite.url" . ,(drakma:url-encode (getf contacts :livejournal) :utf-8))))
               `(("_xsrf" . ,(cdr (assoc "_xsrf" cookie-alist :test #'equal))))))
             (split-sequence:split-sequence #\= (puri:uri-query uri))
             ))))
      )))

;; title.string=IT-%D0%B4%D0%B8%D1%80%D0%B5%D0%BA%D1%82%D0%BE%D1%80+%2F+%D0%A0%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C+%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B8+%D0%9F%D0%9EG2450&
;; profArea=1&specialization.string=221&specialization.string=3&salary.amount=160000&salary.currency=RUR&employment.string=full&workSchedule.string=full_day&_xsrf=5e9fb9ec46e64214f25085132e141e7e

(progn
  (let ((cookie-jar (make-instance 'drakma:cookie-jar)))
    (print
     (hh-post-resume cookie-jar
                     :title (concatenate 'string "Руководитель разработки ПО " (symbol-name (gensym)))
                     :specalizations "3 221"
                     :salary-amount "250000"
                     :last-name "Глухов"
                     :first-name "Михаил"
                     :middle-name "Михайлович"
                     :birthday "1982-12-15"
                     :gender "male"
                     :area "2"
                     :metro ""
                     :relocation "relocation_possible"
                     :relocation-area "1"
                     :business-trip-readiness "ready"
                     :citizen-ship "113"
                     :work-ticket "113"
                     :travel-time "any"
                     :education-level-string "higher"
                     :primary-educations `(:education-id "0"
                                                         ;; :name "Санкт-Петербургский государственный университет культуры и искусств, Санкт-Петербург"
                                                         :name "Санкт-Петербургский национальный исследовательский университет информационных технологий, механики и оптики, Санкт-Петербург"
                                                         ;; :university-id "39864"
                                                         :university-id "39872"
                                                         :faculty-id "0"
                                                         ;; :organization "Режиссуры"
                                                         :organization "Прикладная математика и информатика"
                                                         ;; :result "Режиссура мультимедиа программ"
                                                         :result "Математические модели и алгоритмы в разработке программного обеспечения"
                                                         :specialty-id "" :year "2020")
                     :additional-education-id ""
                     :additional-education-name ""
                     :additional-education-organization ""
                     :additional-education-result ""
                     :additional-education-year ""
                     :certificate-id ""
                     :certificate-type ""
                     :certificate-selected ""
                     :certificate-ownerName ""
                     :certificate-transcription-id ""
                     :certificate-password ""
                     :certificate-title ""
                     :certificate-achievementDate ""
                     :certificate-url ""
                     :attestation-education-id ""
                     :attestation-education-name ""
                     :attestation-education-organization ""
                     :attestation-education-result ""
                     :attestation-education-year ""
                     :expiriences (list
                                   ;; (job :name "ООО КОМПО ГРУПП"
                                   ;;      :site "http://componentality.com"
                                   ;;      :position  "Системный архитектор"
                                   ;;      :start-date "2016-07-01"
                                   ;;      :end-date  ""
                                   ;;      :description "123")
                                   (job :name "ООО Автоматон"
                                        :site ""
                                        :position  "TeamLead, IT-Architect"
                                        :start-date "2015-12-01"
                                        :end-date  "" ;; "2016-07-01"
                                        :description "Компания занимается разработкой и эксплуатацией автоматизированных парковок.

Я возглавил исследовательский проект по разработке новой парковочной системы: аппаратной и программной части.

Технологии:
- Проектирование печатных плат - Kikad, Altium Designer
- Программирование: С/С++, Assembler, Erlang (телефония), PHP/JS: Symfony+React (веб-интерфейс), EmacsLisp - кодогенерация для \"исполняемых спецификаций\" и утилиты для совместной удаленной работы в команде
- Архитектурный стек - Linux on ARM Cortex A8 и Symphony+React в интерфейсе управления платной парковкой.

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

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

Мои достижения:

- Спроектировал программно-аппаратную архитектуру системы автоматизации платных парковок.
- Спланировал и организовал работы по разработке ПО и аппаратной части, включая подбор электронных компонентов и схемотехническое проектирование.
- Самостоятельно реализовал бизнес-логику и уровень представления (Рабочее Место Оператора) на Symfony и React
- Руководил работами по реализации транспортного уровня и уровня абстракции оборудования, выполненными удаленными разработчиками (C/C++, модули ядра, драйверы устройств)
- Организовал паралельную разработку по модульному принципу (чтобы ускорить создание продукта) и методологии kanban
- Внедрил Continuous Integration и процесс управления жизненным циклом (релизы, исправление ошибок, добавление возможностей, технический контроль качества, автоматизированное тестирование)
- Реализовал безопасное (цифровая подпись) и отказоустойчивое (откат на предыдующую версию при провале тестов) обновление прошивок через интернет.
- Автоматизировал создание и хранение документации, с использованием версионирования на базе GIT и \"executable specifications\".")
                                   (job :name "ООО БКН"
                                        :site "http://bkn.ru"
                                        :position  "ИТ-директор"
                                        :start-date "2015-04-01"
                                        :end-date  "2015-12-01"
                                        :description "Компания - второй после \"Бюллетеня недвижимости\" информационный источник в области недвижимости по С-Пб и ЛО.

Руководил разработкой и продвижением информационных решений автоматизации бизнеса агентств недвижимости (b2b и b2c).

Стек технологий: C# и ASP.NET, ExtJs

Достижения:

- Используя данные \"межагентской БД bkn-profi\" в короткие сроки создал раздел о жилых комплексах и новостройках, который по обьему вскоре достиг 60% сайта, что позволило резко увеличить доходы от рекламы на сайте.
- Реализовал на сайте bkn.ru раздел поиска и подбора квартир, комнат и жилых домов первичного и вторичного рынка, интегрировал его с межагентской БД объектов недвижимости.")
                                   (job :name "Тренд"
                                        :site "http://trend-spb.ru"
                                        :position  "Ведущий инженер-программист"
                                        :start-date "2014-08-01"
                                        :end-date  "2015-03-01"
                                        :description "Компания - молодое быстрорастущее агенство недвижимости, специализирующееся на первичном рынке (новостройки)

Автоматизировал бизнес-процесс агенства по продажам недвижимости (новостройки).

Стек технологий: Php, Nginx, Mysql, PostgreSql

Достижения:

До моего прихода агенты и риэлторы использовали skype и google docs для выполнения задач, но после увеличения численности в 4 раза эти инструменты стали неэффективны. Я внедрил CRM собственной разработки, модули которой (экспертная система выставления цен, интерактивный подбор объектов) освободили работников от рутины.

Также был реорганизован сайт компании с использованием современных технологий.")
                                   (job :name "Частная компания (алготрейдинг)"
                                        :site "http://aintsys.com"
                                        :position  "Lisp/Erlang-разработчик"
                                        :start-date "2012-04-01"
                                        :end-date  "2014-08-01"
                                        :description "Разрабатывал решения в сфере электронных валют на базе технологии BlockChain.

Стек технологий: Erlang, Common Lisp, C++

К сожалению, по условиям NDA я не имею права распространять в сети информацию о деятельности компании и моих разработках :(")
                                   (job :name "ООО РАВТА"
                                        :site "http://ravta.ru"
                                        :position "Директор по IT"
                                        :start-date "2012-01-01"
                                        :end-date  "2012-04-01"
                                        :description "Компания - интернет-магазин запчастей, комплектующих и расходных материалов для автомобилей.

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

Достижения:

Внедрил на фирме 1-С Предприятие и 1С-Склад и обеспечил ее интеграцию с системой TechDoc.
")
                                   (job :name "WizardSoft"
                                        :site "http://wizardsoft.ru"
                                        :position "Ведущий разработчик, архитектор"
                                        :start-date "2011-05-01"
                                        :end-date "2012-01-01"
                                        :description "Компания занимается автоматизацией управления затратами в стоительстве.
Достижения:

Разработал высоконагрузочный портал для проведения строительных тендеров. Прототип реализовал на Common Lisp, Postmodern и PostgreSQL. После приемки прототип был существенно расширен и переписан на PHP")
                                   (job :name "ЦиFры"
                                        :site "http://www.320-8080.ru"
                                        :position "Архитектор-проектировщик, веб-программист"
                                        :start-date "2009-09-01"
                                        :end-date "2011-04-01"
                                        :description "Компания - интернет-магазин цифровой техники.

Стек технологий: PHP, MySql, Jquery, Common Lisp, Memcached

Достижения:

- На первом этапе в кратчайшие сроки подготовил legacy-код к новогодним нагрузкам путем внедрения кэширования.
- Затема полностью перепроектировал и реализовал на высоконагрузочный интернет-магазин.
")
                                   (job :name "ООО Вебдом"
                                        :site  "http://webdom.net"
                                        :position "Ведущий веб-разработчик"
                                        :start-date "2007-01-01"
                                        :end-date "2009-09-01"
                                        :description "Веб-студия
Стек технологий: Php, Nginx, MySql

Достижения:

Cпроектировал и разработал масштабируемый фреймворк, на котором теперь работает компания. CMS на его основе поставляются клиентам.")
                                   (job :name "Почин"
                                        :site "http://pochin.ru"
                                        :position "Программист"
                                        :start-date "2005-09-01"
                                        :end-date "2007-01-01"
                                        :description "Компания - интернет-магазин авточехлов, автозапчастей и автоинструмента.

Стек технологий: LAMP

Первоначально начинал как фриланс-программист, но скоро сотрудничество стало постоянным.

Достижения:

- Спроектировал и разработал интернет-магазин (три версии за полтора года)"))
                     :about "Я - человек, который может поднять крупный проект с нуля и довести его до продакшена, организовав процесс разработки, включая ревью кода, найм и обучение разработчиков, тестирование и Contunious Integration. Разумеется, это значит, что я умею не только руководить, но и хорошо программирую. Временами я еще бываю скромным (сегодня не тот день, да), поэтому я старался сделать резюме возможно более кратким, рассчитывая на ваши вопросы в отношении тех пунктов, которые вас заинтересуют (например, в отношении мобильной разработки)

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

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

Как тимлид, я весьма озабочен тем, чтобы быть возможно более сильным с точки зрения программистких навыков и компетенций. Я активный участник и регулярный докладчик в fprog-комьюнити и на ITGM. Это важно, т.к. никакой хороший программист не хотел бы работать с кем-то, кто слабее, чем он - исправлять чужие ошибки и терять время, которое мог бы потратить на обучение у более сильного. Еще мне проще нанимать. Да и в процессе работы тоже проще: профессиональное уважение значит больше чем денежная мотивация.

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

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

Когда вы нанимаете программиста, перед вами стоят три вопроса. Умный ли он? Способен ли выполнить то, что нужно? Сможете ли вы с ним работать? Тот, кто умён, но неспособен выполнить задание, может быть вашим другом, но не работником. Вы можете обсуждать с ним свои проблемы, тогда как он будет тянуть с выполнением важной работы. Тот, кто способен выполнять задания, но неумён — тот неэффективен: неумные люди выполняют работу трудоёмким способом, работа с ними продвигается медленно и полна разочарований. Ну а с тем, с кем вы не можете работать - вы просто не сможете работать.

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

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

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

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

Но если бы пришлось записать признаки того, почему некто кажется мне умным, я бы сделал акцент на трёх моментах. Во-первых, насколько глубоки его познания? Спросите, о чём он думал в последнее время, и \"прощупайте\" его на эту тему. Похоже ли на то, что у него есть детальное понимание предмета? Может ли он понятно объяснить его? (Понятные объяснения — признак подлинного понимания) Знает ли он о предмете то, чего не знаете вы?

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

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

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

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

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

Теперь, если вы действительно дочитали до этого места - резонно было бы спросить: \"А сам то ты, Миша, отвечаешь поставленным тобой критериям?\". Чтобы определить могу ли я делать нужные вещи взгляните на мое резюме. Последнее из того, что я делал - это система, управляющая сетью парковок. В парковках много всего интересного: датчики положения машины, шлагбаумы, навигация и тарифы в разное время суток - организовать это в коде совсем не тривиально. Я занимался созданием печатных плат, подбором компонентов, разработкой бизнес-логики и написанием низкоуровневых программ - и это показывает, что я могу делать сложные вещи. Показателем качества работы может быть тот факт, что за полгода нам ни разу не приходилось делать рефакторинг и выбрасывать большое количество кода - вы можете убедиться в этом посмотрев в мой репозиторий: https://github.com/rigidus/aspp (ASPP значит \"Автоматизированная Система Платной Парковки\"). По соглашению с фирмой я не могу выложить последнюю версию кода, но и прототипа достаточно, чтобы, к примеру, оценить вклад, просто посмотрев на долю и содержание коммитов.

До этого я несколько лет работал в сфере недвижимости, разрабатывая сайты и информационные системы, на PHP и C#, но так как для меня веб-программирование - это привычная среда - все эти достижения не ощущаются мной как выдающиеся. Впрочем, заказчики не жаловались :)

Несколько ранее я работал программистом-исследователем и имел дело с технологией BlockChain. Это была очень интересная работа, но я довольно мало могу о ней рассказать (NDA)

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

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

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

Ну и наконец, чтобы определить, сможете ли вы со мной работать, вам стоит пригласить меня на собеседование. Я был бы вам признателен, если бы это было skype-собеседование, по крайней мере в первый раз, т.к. таким образом, мы смогли бы сэкономить кучу времени на дороге. Впрочем, если вы хотите показать мне офис или тестовый стенд вашей технологии, я с удовольствием приеду к вам в удобное время. Почему бы не позвонить мне прямо сейчас? Мой телефон: 8(911)286-92-90

Ну а пока вы раздумываете, я оставлю тут список ключевых компетенций:
- Linux, FreeBSD
- PHP, JavaScript, Python, C/C++, Java, Common Lisp, Erlang
- Nginx, Apache, Memcache, Redis, RabbitMQ
- MySQL, PostgreSQL
- Git, Svn
- JavaSсript, JQuery, ExtJS
- JSON, OpenID, XML, XML+RPC, closure-template, Sphinx, PHPUnit

Также знаком с: Delphi/Pascal, Assembler80x86, Forth

Умею сниффать сниффером и профайлить профайлером.

Не боюсь регулярных выражений.

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

Умею управлять машиной Тьюринга и стрелять из конечного автомата.

Грамотно выражаю свои мысли на бумаге, устно, на пальцах; и с первого раза воспринимаю чужие с тех же носителей.

Целиком спроектировал и реализовал:
- http://320-8080.ru
- http://pochin.ru
- http://rigidus.ru
- http://izverg.ru

Мой код работает в:
- http://bkn.ru
- https://trend-spb.ru/
- http://toursfera.ru
- http://chembalt.ru
- http://parus-ltd.ru
- http://www.3-trans.ru
- http://spsstroy.ru
- http://gtmorstroy.com
- http://www.mva-group.ru
"
|#
#|
"Плотно сижу на Common Lisp с сентября 2009 года, неоднократно был замечен в употреблении Common Lisp-а в продакшене, в т.ч. на находясь на рабочем месте . Периодов ремисии не было, проходил комплексную терапию у доктора Д.Э. Кнута (EmacsLisp в качестве заместительной + literate programming). В анамнезе несколько обострений - попытка написать лисп на форте, а форт - на пхп, тяжелая наследственность: в юности (до 1998 года) изучал технологии самоходного программного обеспечения, но на стационарном лечении не находился ввиду либерального законодательства и по малолетству.

Разрабатываю коммерческие проекты на Common Lisp c 2009 года. Понимаю макросы, conditions-restarts, использую основные распространенные библиотеки - postmodern, parenscript, closer-mop, cl-fad и.т.п.

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

С 2010-го года все проекты, реализованные мной на других языках, сначала были спрототипированы на Common Lisp для быстрого анализа и разработки идей. Предпочитаю при любых затруднениях (например в использовании библиотеки) взглянуть на исходный код, иногда даже прежде обращения к документации. Как правило многое становится понятно сразу.

В собственных проектах стараюсь применять методы искусственного интеллекта (главным образом из Artifical Intelligence - A Modern Approach - S.J. Russel, P. Norvig)

Также обладаю хорошим пониманием языков, близких к лиспу (Scheme, Clojure, Racket), довольно хорошим - других функциональных, логических и объектных языков (Haskell, Prolog, Smalltalk) и основанных на них технологиях."
|#
                     :languages `((:lang-id "34" :lang-degree "native")
                                  (:lang-id "57" :lang-degree "can_read")
                                  (:lang-id "58" :lang-degree "basic")
                                  (:lang-id "59" :lang-degree "none"))
                     :contacts `(:cell-phone "+79112869290"
                                             :cell-phone-comment "В данный момент в роуминге, пожалуйста, используйте skype или email"
                                             :home-phone ""
                                             :home-phone-comment ""
                                             :work-phone ""
                                             :work-phone-comment ""
                                             :email-string "[email protected]"
                                             :preferred-contact "email"
                                             :icq ""
                                             :skype "i.am.rigidus"
                                             :freelance ""
                                             :moi_krug ""
                                             :linkedin ""
                                             :facebook ""
                                             :livejournal "" ;; "http://rigidus.livejournal.com"
                                             :personal-site "http://rigidus.ru")
                     ))))


;; КЛЮЧЕВЫЕ НАВЫКИ

;; Java JavaScript jQuery Lisp PHP5 PostgreSQL RabbitMQ Symphony2 Yii Zend Framework

;; https://spb.hh.ru/applicant/resumes/edit/experience?resume=1036680cff007465bc0039ed1f736563726574&field=keySkills
;; keySkills.string=Java
;; keySkills.string=JavaScript
;; keySkills.string=jQuery
;; keySkills.string=Lisp
;; keySkills.string=PHP5
;; keySkills.string=PostgreSQL
;; keySkills.string=RabbitMQ
;; keySkills.string=Symphony2
;; keySkills.string=Yii
;; keySkills.string=Zend+Framework
;; keySkills.string=MS+Visio
;; keySkills.string=Nginx