- BrainTools - https://www.braintools.ru -

Это мучительный цикл многократного копипастинга одной и той же информации, внесения сотен мелких правок в резюме и написания сопроводительных писем, которые должны выглядеть, как мольба, но не слишком очевидная.
Обратим внимание [1] на следующее: повторяющиеся задачи + структурированный процесс = идеальный кандидат для автоматизации.
Поэтому я поступил так, как поступил бы любой разработчик в здравом уме — создал систему автоматизации всей этой фигни. В конечном итоге я смог разослать 250 откликов на вакансии за 20 минут. (Ирония заключается в том, что я получил оффер ещё до того, как закончил создавать эту систему. Подробнее об этом ниже.)
В статье я расскажу, как я это сделал.
Задумайтесь — каждый отклик на вакансию представляет собой один и тот же простой паттерн:
Это похоже на очень скучную видеоигру, в которой ты снова и снова выполняешь один и тот же квест, надеясь на иные результаты.
Я начал с того, что набросал небольшие скрипты на Python, чтобы проверить, сработает ли эта безумная идея. Разбил я её на части следующим образом:
Первая сложность: необходимость нахождения вакансий в больших количествах. Я попробовал веб-скрейпинг, но быстро понял, что доски объявлений подобны снежинкам — скрейпинг каждой из них бесит уникальным образом.
Я попробовал передавать веб-страницы целиком модели LLM, чтобы она очистила данные, но:
Поэтому я выбрал олдскульный подход — ручное копирование HTML. Да, это примитивно. И да, это работает. Иногда лучшим решением оказывается самое простое.
В сыром HTML творился бардак, поэтому мне нужно было структурировать данные следующим образом:
{
"job_link": "https://example.com/job/12345",
"job_id": "12345",
"job_role": "software developer",
"employer": "Tech Corp Inc",
"location": "San Francisco, CA",
"work_arrangement": "Remote",
"salary": "$150,000"
}
Полезный совет: можно просто показать ChatGPT пример HTML и нужный вам формат вывода, тогда модель сама напишет скрипт парсинга.
Эта часть была простой, но потребовала небольших хитростей. Для каждой вакансии я выполнял GET-запрос для получения полного описания. Каждый запрос возвращал сырой HTML, содержавший все элементы веб-сайта — панели навигации, всплывающие окна, мусор внизу страницы и прочее.
Я написал простой парсер HTML для вырезания всего, кроме самого описания вакансии. Иногда возникали дополнительные трудности, например, приходилось нажать на кнопку, чтобы открыть почтовый адрес рекрутёра или информацию о компании. Хорошо то, что за раз мы работаем только с одной доской объявлений, поэтому разбираться со всеми этими паттернами придётся лишь однократно.
Полезный совет: всегда добавляйте паузы между запросами. У себя я установил задержку в 2-3 секунды. Да, это замедляет процесс, но гораздо хуже будет, если ваш IP забанят. Не будьте тем, кто DDOS-ит веб-сайты с вакансиями — я добавил паузы между запросами, потому что я не злодей.
Здесь всё становится интереснее. Объявления о вакансиях похожи на людей — у всех них есть одинаковые основные части, но упорядочены они хаотически. В некоторых требуемые навыки указаны в начале, в других они сокрыты среди абзацев корпоративного новояза.
На помощь моей психике пришёл промт LLM:
const prompt = `Проанализируй этот HTML-контент из объявления о вакансии и извлеки информацию в структурированном формате JSON.
[... HTML-контент ...]
Отформатируй ответ в виде валидного объекта JSON именно с такими ключами:
- contact_email (почта для связи)
- application_instructions (инструкции по подаче отклика на вакансию)
- job_posting_text (текст объявления о вакансии, в markdown)
- job_posting_link (ссылка на объявление о вакансии)
- additional_info (дополнительная информация) (зарплата, местоположение и так далее)
- job_title (название должности)
- job_company (компания)
- job_department (отдел)
- job_location (местоположение офиса)
- job_skills (требуемые для работы навыки)
- job_instructions (инструкции по подаче заявления о приёме)
опциональные ключи
- hiring_manager_name (ФИО менеджера по найму)
-
- job_portal (портал объявлений)
`
const prompt = `Please analyze these HTML contents from a job posting and extract information into a structured JSON format.
[... HTML content ...]
Format the response as valid JSON object with these exact keys:
- contact_email
- application_instructions
- job_posting_text (in markdown)
- job_posting_link
- additional_info (salary, location, etc.)
- job_title
- job_company
- job_department
- job_location
- job_skills
- job_instructions (how to apply)
optional keys
- hiring_manager_name
-
- job_portal
`
Что самое важное при написании хороших сопроводительных писем? Контекст. Я передал LLM своё резюме и подробности о вакансии. Благодаря этому ИИ сможет сопоставить мой рабочий опыт [2] с требованиями компании. Внезапно все эти шаблонные письма «С радостью воспользуюсь этой возможностью» обретут какую-то конкретику.
Вот промт, который позволил это реализовать
const prompt = `Помоги мне написать профессиональное письмо с откликом на вакансию на основании следующей информации:
=== МОЁ РЕЗЮМЕ ===
${resumeMarkdown}
=== ПОДРОБНОСТИ О ВАКАНСИИ ===
Название должности: ${job_title}
Компания: ${job_company}
Отдел: ${job_department || ''}
Местоположение офиса: ${job_location || ''}
Описание должности: ${job_posting_text }
Требуемые навыки: ${job_skills?.join(', ') || ''}
Инструкции по подаче заявления: ${job_instructions || ''}
Дополнительный контекст:
- ФИО менеджера по найму: ${hiring_manager_name || ''}
- Источник ссылки: ${referral_source || 'Job board'}
- Портал объявлений: ${job_portal || ''}
Инструкции:
1. Создай сразу готовое к отправке электронное письмо, не содержащее никаких текстовых заглушек и не требующее правок
2. В случае отсутствия какой-то критически важной информации (например, названия компании или должности) отвечай сообщением об ошибке, а не генерируй незавершённый контент
3. Пропускай все опциональные поля, если они пусты, не добавляя вместо них текстовых заглушек
4. Используй естественную структуру предложений, а не очевидный язык шаблонов
5. Включай конкретные подробности из резюме и описания вакансии, чтобы продемонстрировать искренний интерес и пригодность кандидата к должности
6. Все ссылки и контактная информация должны быть правильно отформатированы и готовы к использованию
Отформатируй ответ в виде объекта JSON со следующими ключами:
{
"status": "success" или "error",
"error_message": "Присутствует, только если status равен error, с объяснением об отсутствии критически важной информации",
"email": {
"subject": "Строка темы письма",
"body_html": "Тело письма в формате HTML с правильным форматированием",
"body_text": "Версия письма в текстовом виде без форматирования",
"metadata": {
"key_points_addressed": ["список основных учтённых пунктов"],
"skills_highlighted": ["список упомянутых навыков"],
"resume_matches": ["конкретные навыки/опыт из резюме, соответствующие требованиям к кандидату"],
"missing_recommended_info": ["опциональные поля, которые отсутствуют, но могли бы в случае своего наличия повысить убедительность поданного заявления"],
"tone_analysis": "краткий анализ тональности письма"
}
}
}
Критичные обязательные поля (возвращай ошибку в случае их отсутствия):
- Название должности
- Название компании
- Описание вакансии
- Содержимое резюме
Рекомендованные, но опциональные поля:
- ФИО менеджера по найму
- Отдел
- Местоположение офиса
- Инструкции по подаче заявления
- Источник ссылки
- Список требуемых навыков
Проверь, что весь HTML в body_html имеет подходящие завершающие символы для JSON и что в нём использованы только основные теги форматирования (p, br, b, i, ul, li), чтобы обеспечить максимальную совместимость с клиентами электронной почты.
`
const prompt = `Please help me write a professional job application email based on the following information:
=== MY RESUME ===
${resumeMarkdown}
=== JOB DETAILS ===
Job Title: ${job_title}
Company: ${job_company}
Department: ${job_department || ''}
Location: ${job_location || ''}
Job Description: ${job_posting_text }
Required Skills: ${job_skills?.join(', ') || ''}
Application Instructions: ${job_instructions || ''}
Additional Context:
- Hiring Manager Name: ${hiring_manager_name || ''}
- Referral Source: ${referral_source || 'Job board'}
- Application Portal: ${job_portal || ''}
Instructions:
1. Create an email that is ready to send without any placeholders or edits needed
2. If any critical information is missing (like company name or job title), respond with an error message instead of generating incomplete content
3. Skip any optional fields if they're empty rather than including placeholder text
4. Use natural sentence structure instead of obvious template language
5. Include specific details from both the resume and job description to show genuine interest and fit
6. Any links or contact information should be properly formatted and ready to use
Format the response as a JSON object with these keys:
{
"status": "success" or "error",
"error_message": "Only present if status is error, explaining what critical information is missing",
"email": {
"subject": "The email subject line",
"body_html": "The email body in HTML format with proper formatting",
"body_text": "The plain text version of the email",
"metadata": {
"key_points_addressed": ["list of main points addressed"],
"skills_highlighted": ["list of skills mentioned"],
"resume_matches": ["specific experiences/skills from resume that match job requirements"],
"missing_recommended_info": ["optional fields that were missing but would strengthen the application if available"],
"tone_analysis": "brief analysis of the email's tone"
}
}
}
Critical required fields (will return error if missing):
- Job title
- Company name
- Job description
- Resume content
Recommended but optional fields:
- Hiring manager name
- Department
- Location
- Application instructions
- Referral source
- Required skills list
Please ensure all HTML in body_html is properly escaped for JSON and uses only basic formatting tags (p, br, b, i, ul, li) to ensure maximum email client compatibility.
`
Промт выполняет следующие задачи:
Очень важно то, что он сразу выдаёт ошибку [3] при отсутствии критически важной информации. Больше никаких писем «Я увидел вашу вакансию». Или в сопроводительном письме есть суть, или мы его не отправляем, точка.
(Я начинаю все свои промты с «please», чтобы после неизбежного захвата мира искусственным интеллектом [4] он не считал меня врагом.)
Последний этап — рассылка наших прекрасно структурированных писем. Вам кажется, это просто? Достаточно подключить сервис электронной почты и начать бомбардировку?
Не стоит торопиться. Мне нужно:
Для проверки я сначала отправил все письма на тестовый аккаунт. Полезный совет: при рассылке писем рекрутёрам добавляйте себя в BCC. Нет ничего хуже, кроме как гадать «а получили ли вообще письмо?»
На этом этапе POC я просто воспользовался простым сервисом электронной почты Mailgun. Быстро, грязно, но эффективно. Не волнуйтесь, ниже я подробно расскажу о том, на что мне пришлось пойти для создания полнофункциональной системы управления электронной почтой.
Proof of concept сработал лучше, чем я ожидал. Я мог извлекать вакансии с конкретных досок объявлений, парсить их и генерировать персонализированные письма. И для этого оказалось достаточно всего нескольких скриптов на Python.
Но это было лишь началом. В чём же заключалась истинная трудность? В превращении этих скриптов в приложение, которое могло бы:
Вот, что я узнал: мечты умирают в пропасти между «это работающий POC» и «это работающее приложение». Но мы всё равно преодолеем эту пропасть.
Скрипты на Python эволюционировали в разные типы скриптов в приложении, каждый из которых обрабатывал отдельную часть процесса. Каждая операция поиска работы превратилась в «кампанию» со своим собственным конвейером. Это работает следующим образом:
1. Хранилище сырого HTML: сюда мы сбрасываем сырой HTML с досок объявлений.
// Пример html из вакансии
<article id="article-42478761" class="action-buttons"><a href="/jobsearch/jobposting/42478761?source=searchresults"
id="ajaxupdateform:j_id_31_3_3p:1:j_id_31_3_3r" class="resultJobItem">
<h3 class="title">
<span class="flag">
<span class="new">
New
</span><span class="telework">On site</span><span class="postedonJB">
Posted on Job Bank
<span class="description"><span class="fa fa-info-circle" aria-hidden="true"></span>This job was
posted directly by the employer on Job Bank.</span>
</span>
</span>
<span class="job-source job-source-icon-16"><span class="wb-inv">Job Bank</span></span>
<span class="noctitle"> software developer
</span>
</h3>
<ul class="list-unstyled">
<li class="date">November 08, 2024
</li>
<li class="business">OMEGA SOFTWARE SERVICES LTD.</li>
<li class="location"><span class="fas fa-map-marker-alt" aria-hidden="true"></span> <span
class="wb-inv">Location</span>
Scarborough (ON)
</li>
<li class="salary"><span class="fa fa-dollar" aria-hidden="true"></span>
Salary:
$50.00 hourly</li>
<li class="source"><span class="job-source job-source-icon-16"><span class="wb-inv">Job
Bank</span></span>
<span class="wb-inv">Job number:</span>
<span class="fa fa-hashtag" aria-hidden="true"></span>
3146897
</li>
</ul>
</a><span id="ajaxupdateform:j_id_31_3_3p:1:favouritegroup" class="float job-action">
<a href="/login" data-jobid="42478761" class="favourite saveLoginRedirectURI"
onclick="saveLoginRedirectURIListener(this);">
<span class="wb-inv">software developer - Save to favourites</span>
</a></span>
</article>
2. Первоначальная очистка: скрипт превращает этот хаос в структурированный JSON:
{
"job_link": "https://www.jobbank.gc.ca/jobsearch/jobposting/42478761?source=searchresults",
"job_id": "42478761",
"job_role": "software developer",
"employer": "OMEGA SOFTWARE SERVICES LTD.",
"location": "Scarborough (ON)",
"work_arrangement": "On site",
"salary": "$50.00 hourly"
}
3. Получение вакансии: ещё один скрипт переходит по каждому из URL вакансии и получает полную публикацию (с уважительными паузами между запросами, мы ведь не дикари).
4. Очистка данных вакансий: этот скрипт использует ИИ для превращения постов с вакансиями в чистые структурированные данные, содержащие:
{
"job_id": "42313964",
"processed_timestamp": "2024-12-25T19:45:39.829Z",
"original_fetch_timestamp": "2024-12-25T19:40:46.187Z",
"job_json": {
"contact_email": "careers@wiasystems.com",
"application_instructions": "To apply, please send your resume and cover letter to careers@wiasystems.com.",
"job_posting_text": "# Job Postingnn## Job Title: Software Engineernn**Job Description:**nn- Education: Bachelor's degree in Computer Science or related fieldn- Experience: 2 years to less than 3 yearsn- Location: Vancouver, BCn- Work Arrangement: Hybrid (in-person and remote)nn## Job Responsibilities:nn- Collect and document user's requirementsn- Coordinate the development, installation, integration and operation of computer-based systemsn- Define system functionalityn- Develop flowcharts, layouts, and documentation to identify solutionsn- Develop process and network models to optimize architecturen- Develop software solutions by studying systems flow, data usage, and work processesn- Evaluate the performance and reliability of system designsn- Evaluate user feedbackn- Execute full lifecycle software developmentn- Prepare plan to maintain softwaren- Research technical information to design, develop, and test computer-based systemsn- Synthesize technical information for every phase of the cycle of a computer-based systemn- Upgrade and maintain softwaren- Lead and coordinate teams of information systems professionals in the development of software and integrated information systems, process control software, and other embedded software control systemsnn## Required Skills and Qualifications:nn- Agilen- Cloudn- Development and operations (DevOps)n- Eclipsen- Jiran- Microsoft Visual Studion- HTMLn- Intranetn- Internetn- XML Technology (XSL, XSD, DTD)n- Serversn- Desktop applicationsn- Enterprise Applications Integration (EAI)n- Javan- File management softwaren- Word processing softwaren- X Windowsn- Servletn- Object-Oriented programming languagesn- Presentation softwaren- Mail server softwaren- Project management softwaren- Programming softwaren- SQLn- Database softwaren- Programming languagesn- Software developmentn- XMLn- MS Officen- Spreadsheetn- Oraclen- TCP/IPn- Amazon Web Services (AWS)n- Gitn- Atlassian Confluencen- GitHubn- Performance testingn- Postmann- Software quality assurancen- MS Exceln- MS Outlookn- MS SQL Servernn### Benefits:nn- Health benefits: Dental plan, Health care plan, Vision care benefitsn- Other benefits: Learning/training paid by employer, Other benefits, Paid time off (volunteering or personal days)nnFor more information about the position and to apply, please send your resume and cover letter to careers@wiasystems.com.",
"job_posting_link": "https://www.jobbank.gc.ca/jobsearch/jobposting/42313964?source=searchresults",
"additional_info": {
"salary": "CAD 60.50 per hour",
"location": "Vancouver, BC",
"job_role": "Software Engineer",
"company_name": "WIA Software Systems Inc.",
"job_type": "Permanent, Full-time",
"required_experience": "2 years to less than 3 years",
"required_education": "Bachelor's degree in Computer Science or related field",
"language_requirements": "English",
"work_arrangement": "Hybrid (in-person and remote)"
}
},
"raw_gpt_responce": ""
},
5. Генерация писем: скрипт берёт резюме + данные вакансии и создаёт персонализированные заявки, которые не выглядят так, как будто составлены роботом.
6. Отправка писем: последний этап, позволяющий вашим письмам достичь адресатов.

Каждая кампания изолирована от других. Кампания одновременно может выполнять только один скрипт (например, переходить от очистки к извлечению вакансии, а затем к генерации письма), разные кампании проводятся независимо. Это можно сравнить со множеством сборочных линий — даже если одна линия останавливается, остальные продолжают сборку. Скрипт, поломавшийся в одной кампании, не помешает задачам, выполняемым в другой.
Я мог бы сказать, что каждый технологический элемент был выбран после тщательного изучения всех возможных вариантов. Но так ли это? На самом деле, для достижения цели я просто-напросто использовал то, что знал:
Приложение живёт на jaas.fun [5] (Job Application Automation System — да, я здорово умею придумывать названия).
Каждая кампания в системе полностью изолирована от остальных. Это было крайне важно, потому что:
Схема кампании отслеживает всё:
Каждый тип скрипта получает конкретные функции на основании своей роли:
Ни один скрипт не может получать доступ к функциям вне своего типа — скрипт очистки не может отправлять письма, а почтовый скрипт не может получать новые данные вакансий. Мы как будто даём каждому работнику только нужные ему инструменты, и ничего сверх того.
Вот это по-настоящему интересно. Помните, что мы должны безопасно выполнять потенциально ненадёжный код (скрипты очистки и обработки)? В этом нам поможет система выполнения скриптов.
Работает она следующим образом:
vm2 для создания среды-песочницы для каждого скрипта. Почему? Потому что выполнение произвольного JavaScript опасно, а я хочу спокойно спать по ночам.console.log, передаваемым в Redis.Система логгинга довольно удобна. Каждое сообщение лога вместо того, чтобы записываться в файл или в консоль:
Эта система очень отказоустойчива. В случае сбоя скрипта кампания помечается как неудачная, но ничто другое не ломается. При сбое воркера он перезапускается и продолжает работу с того места, где закончил. Можно в буквальном смысле закрыть браузер, сходить выпить кофе, а может, подготовиться к тем собеседованиям, о которых мы договоримся благодаря этой системе.
Когда скрипт завершает выполнение, воркер:
А поскольку вся система основана на очередях, вы можете запустить множество воркеров, если нужно обрабатывать большее количество кампаний.
Давайте поговорим о том, как происходит поток данных через систему:
Система генерации писем не просто отправляет формальные письма, но и создаёт полностью персонализированные заявки:
Система даже добавляет метаданные о том, насколько опыт пользователя соответствует требованиям вакансии. Как будто у нас есть очень привередливый живой редактор, который работает очень быстро.
В следующей части статьи я расскажу о следующем:
Кроме того, я поведаю, как получил оффер, ещё не завершив разработку этого проекта. (Спойлер: при этом я случайно загнал себя в угол автоматизацией.)
А пока можете зайти на jaas.fun [5], там есть:
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻 [6]
Автор: ru_vds
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/11092
URLs in this post:
[1] внимание: http://www.braintools.ru/article/7595
[2] опыт: http://www.braintools.ru/article/6952
[3] ошибку: http://www.braintools.ru/article/4192
[4] интеллектом: http://www.braintools.ru/article/7605
[5] jaas.fun: https://jaas.fun
[6] Telegram-канал со скидками, розыгрышами призов и новостями IT 💻: https://t.me/ruvds_community
[7] Image: https://ruvds.com/drive?utm_source=habr&utm_medium=article&utm_campaign=perevod&utm_content=250_otklikov_za_20_minut_kak_ya_avtomatiziroval_process_otvetov_na_vakansii
[8] Источник: https://habr.com/ru/companies/ruvds/articles/872114/?utm_source=habrahabr&utm_medium=rss&utm_campaign=872114
Нажмите здесь для печати.