СoverLetterEnchancer: упрощаем поиск работы с FastAPI и YandexGPT

от автора

Салют! Меня зовут Григорий, я главный по спецпроектам в AllSee. Если вы когда‑нибудь серьёзно подходили к вопросу поиска работы, то вам определённо приходилось муторно писать сопроводительные письма под каждую вакансию. В данной статье я расскажу, как можно автоматизировать составление релевантного для вакансии сопроводительного письма с учётом вашего резюме.

Какие вводные?

В процессе поиска работы приходится писать индивидуальные сопроводительные письма для каждой вакансии по отдельности. Я хочу автоматизировать данный процесс, генерируя сопроводительное письмо с учётом текста моего резюме и конкретной вакансии, а также иметь возможность сгенерировать его на основе шаблона. Для генерации я буду использовать YandexGPT API, а для обработки входящих запросов — FastAPI.

Routines (служебные функции)

Hidden text

Чтение YAML и JSON

def read_yaml(path: str) -> dict:     with open(path, 'r') as stream:         return yaml.safe_load(stream)   def read_json(path: str) -> dict:     with open(path, 'r') as stream:         return json.load(stream)

YandexGPT API

Перейдём сразу к коду для отправки запросов на генерацию:

async def send_completion_request(             self,             messages: List[Dict[str, Any]],             temperature: float = 0.6,             max_tokens: int = 1000,             stream: bool = False,             completion_url: str = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"     ) -> Dict[str, Any]:         # checking config manager         if not all([             getattr(self.config_manager, 'model_type', None),             getattr(self.config_manager, 'iam_token', None),             getattr(self.config_manager, 'catalog_id', None)         ]):             raise ValueError("Model type, IAM token, and catalog ID must be set in config manager to send a "                              "completion request.")          # making request         headers: Dict[str, str] = {             "Content-Type": "application/json",             "Authorization": f"Bearer {self.config_manager.iam_token}",             "x-folder-id": self.config_manager.catalog_id         }         data: Dict[str, Any] = {             "modelUri": f"gpt://"                         f"{self.config_manager.catalog_id}"                         f"/{self.config_manager.model_type}"                         f"/latest",             "completionOptions": {                 "stream": stream,                 "temperature": temperature,                 "maxTokens": max_tokens             },             "messages": messages         }          # sending request         async with aiohttp.ClientSession() as session:             async with session.post(completion_url, headers=headers, json=data) as response:                 if response.status == 200:                     return await response.json()                 else:                     response_text = await response.text()                     raise Exception(                         f"Failed to send completion request. "                         f"Status code: {response.status}"                         f"\n{response_text}"                     )

Помимо опциональных параметров temperature, max_tokens, stream, completion_url требуется задать объект с полями тип модели, ID каталога Yandex Cloud и IAM‑токен.

Первые два поля достаточно один раз задать вручную, а вот генерацию IAM‑токена мы будем автоматизировать:

Hidden text
import os import jwt import time import base64 import requests from typing import Any, Dict, Optional  from routines.read_file import read_json, read_yaml   class YandexGPTConfigManager:     available_models: list[str] = [         'yandexgpt',         'yandexgpt-lite',         'summarization'     ]      def __init__(             self,             model_type: str = 'yandexgpt',             iam_token: Optional[str] = None,             catalog_id: Optional[str] = None,             config_path: Optional[str] = None,             key_file_path: Optional[str] = None     ) -> None:         self.model_type: str = model_type         self.iam_token: Optional[str] = iam_token         self.catalog_id: Optional[str] = catalog_id         self._initialize_params(config_path, key_file_path)         self._check_params()      def _initialize_params(             self,             config_path: Optional[str],             key_file_path: Optional[str]     ) -> None:         if self.iam_token and self.catalog_id:             # if both IAM token and catalog id are already set, do nothing             return         elif config_path and key_file_path:             # trying to initialize from config path and key file path             self._initialize_from_files(config_path, key_file_path)         else:             # trying to initialize from environment variables             self._initialize_from_env_vars()      def _initialize_from_files(             self,             config_path: str,             key_file_path: str     ) -> None:         # getting config and key         config: Dict[str, Any] = read_yaml(config_path)         key: Dict[str, Any] = read_json(key_file_path)         # setting catalog id and IAM token         self._set_catalog_id_from_config(config)         self._set_iam_token_from_key_and_config(key, config)      def _set_catalog_id_from_config(             self,             config: Dict[str, Any]     ) -> None:         self.catalog_id = config['CatalogID']      def _set_iam_token_from_key_and_config(             self,             key: Dict[str, Any],             config: Dict[str, Any]     ) -> None:         # generating JWT token         jwt_token: str = self._generate_jwt_token(             service_account_id=config['ServiceAccountID'],             private_key=key['private_key'],             key_id=config['ServiceAccountKeyID'],             url=config.get('IAMURL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens')         )         # swapping JWT token to IAM         self.iam_token = self._swap_jwt_to_iam(             jwt_token=jwt_token,             url=config.get('IAMURL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens')         )      @staticmethod     def _generate_jwt_token(             service_account_id: str,             private_key: str,             key_id: str,             url: str = 'https://iam.api.cloud.yandex.net/iam/v1/tokens'     ) -> str:         # generating JWT token         now: int = int(time.time())         payload: Dict[str, Any] = {             'aud': url,             'iss': service_account_id,             'iat': now,             'exp': now + 360         }         encoded_token: str = jwt.encode(             payload,             private_key,             algorithm='PS256',             headers={'kid': key_id}         )         return encoded_token      @staticmethod     def _swap_jwt_to_iam(             jwt_token: str,             url: str = 'https://iam.api.cloud.yandex.net/iam/v1/tokens'     ) -> str:         headers: Dict[str, str] = {"Content-Type": "application/json"}         data: Dict[str, str] = {"jwt": jwt_token}         # swapping JWT token to IAM         response: requests.Response = requests.post(url, headers=headers, json=data)         if response.status_code == 200:             # if succeeded to get IAM token             return response.json()['iamToken']         else:             # if failed to get IAM token             raise Exception(                 f"Failed to get IAM token. Status code: {response.status_code}\n{response.text}"             )      def _initialize_from_env_vars(self) -> None:         # trying to initialize from environment variables         self._set_iam_from_env()         self._set_model_type_from_env()         self._set_catalog_id_from_env()         if not self.iam_token:             # if IAM token is not set, trying to initialize from config and private key             self._set_iam_from_env_config_and_private_key()      def _set_iam_from_env(self) -> None:         self.iam_token = os.getenv('IAM_TOKEN', self.iam_token)      def _set_model_type_from_env(self) -> None:         self.model_type = os.getenv('MODEL_TYPE', self.model_type)      def _set_catalog_id_from_env(self) -> None:         self.catalog_id = os.getenv('CATALOG_ID', self.catalog_id)      def _set_iam_from_env_config_and_private_key(self) -> None:         # getting environment variables         service_account_id: Optional[str] = os.getenv('SERVICE_ACCOUNT_ID')         service_account_key_id: Optional[str] = os.getenv('SERVICE_ACCOUNT_KEY_ID')         catalog_id: Optional[str] = os.getenv('CATALOG_ID')         private_key_base64: Optional[str] = os.getenv('PRIVATE_KEY_BASE64')         private_key_bytes: bytes = base64.b64decode(private_key_base64)         private_key: str = private_key_bytes.decode('utf-8')         iam_url: str = os.getenv('IAM_URL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens')         # checking environment variables         if not all([service_account_id, service_account_key_id, private_key, catalog_id]):             raise ValueError("One or more environment variables for IAM token generation are missing.")         # generating JWT token         jwt_token: str = self._generate_jwt_token(             service_account_id=service_account_id,             private_key=private_key,             key_id=service_account_key_id,             url=iam_url         )         # swapping JWT token to IAM         self.iam_token = self._swap_jwt_to_iam(jwt_token, iam_url)      def _check_params(self) -> None:         if not self.iam_token:             raise ValueError("IAM token is not set")         if not self.catalog_id:             raise ValueError("Catalog ID is not set")         if self.model_type not in self.available_models:             raise ValueError(f"Model type must be one of {self.available_models}") 

Указанное выше решение поддерживает несколько сценариев инициализации, однако мы будем использовать переменные окружения для более удобной работы с удалённым хостингом и docker‑окружением (как создать ключ авторизации и где взять ID сервисного аккаунта):

SERVICE_ACCOUNT_ID=aaaaaaaaaaaaa SERVICE_ACCOUNT_KEY_ID=aaaaaaaaaaaaaaaaaa CATALOG_ID=aaaaaaaaaaaaaaaaa PRIVATE_KEY_BASE64=aaaaaaaaaaaaaaaaa IAM_URL=https://iam.api.cloud.yandex.net/iam/v1/tokens

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

Вот как выглядит полная реализация класса YandexGPT:

Hidden text
from typing import List, Union, Dict, Any  import aiohttp  from yandex_gpt.yandex_gpt_config_manager import YandexGPTConfigManager   class YandexGPT:     def __init__(             self,             config_manager: Union[YandexGPTConfigManager, Dict[str, Any]],     ) -> None:         self.config_manager = config_manager      async def send_completion_request(             self,             messages: List[Dict[str, Any]],             temperature: float = 0.6,             max_tokens: int = 1000,             stream: bool = False,             completion_url: str = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"     ) -> Dict[str, Any]:         # checking config manager         if not all([             getattr(self.config_manager, 'model_type', None),             getattr(self.config_manager, 'iam_token', None),             getattr(self.config_manager, 'catalog_id', None)         ]):             raise ValueError("Model type, IAM token, and catalog ID must be set in config manager to send a "                              "completion request.")          # making request         headers: Dict[str, str] = {             "Content-Type": "application/json",             "Authorization": f"Bearer {self.config_manager.iam_token}",             "x-folder-id": self.config_manager.catalog_id         }         data: Dict[str, Any] = {             "modelUri": f"gpt://"                         f"{self.config_manager.catalog_id}"                         f"/{self.config_manager.model_type}"                         f"/latest",             "completionOptions": {                 "stream": stream,                 "temperature": temperature,                 "maxTokens": max_tokens             },             "messages": messages         }          # sending request         async with aiohttp.ClientSession() as session:             async with session.post(completion_url, headers=headers, json=data) as response:                 if response.status == 200:                     return await response.json()                 else:                     response_text = await response.text()                     raise Exception(                         f"Failed to send completion request. "                         f"Status code: {response.status}"                         f"\n{response_text}"                     ) 

FastAPI

Создадим простой Python-скрипт. Он должен обрабатывать входящие запросы на генерацию и раз в 12 часов обновлять наш IAM токен.

Hidden text
from pathlib import Path import asyncio import sys import os  sys.path.append(str(Path(__file__).resolve().parent.parent))  # noqa: E402  from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel  from yandex_gpt.yandex_gpt import YandexGPT from yandex_gpt.yandex_gpt_config_manager import YandexGPTConfigManager  from dotenv import load_dotenv path_to_env = './env/.env' load_dotenv(dotenv_path=path_to_env)   class LetterData(BaseModel):     letter_template: str     resume: str     job_description: str   # creating app instance app = FastAPI() # noinspection PyTypeChecker app.add_middleware(     CORSMiddleware,     allow_origins=os.environ.get('CORS_ORIGINS').split(','),     allow_credentials=True,     allow_methods=["*"],     allow_headers=["*"] )  # creating yandex_gpt instance yandex_gpt = YandexGPT(config_manager={})   async def update_yandex_gpt_config():     # updating config every 12 hours     while True:         global yandex_gpt         yandex_gpt.config_manager = YandexGPTConfigManager()         await asyncio.sleep(43200)   # starting routine @app.on_event("startup") def startup_event():     asyncio.create_task(update_yandex_gpt_config())   @app.post("/generate_letter/") async def generate_letter(data: LetterData):     system_prompt = (         "Ниже представлен шаблон сопроводительного письма с плейсхолдерами вида {placeholder}, резюме кандидата и "         "описание вакансии. "         "Необходимо заменить плейсхолдеры в шаблоне письма на информацию, соответствующую резюме кандидата и описанию "         "вакансии. "         "Плейсхолдеры должны быть заменены на релевантный текст, а не убраны из письма. "         "Вывести только итоговый текст письма, точно соответствующий запросу, без лишних комментариев или "         "объяснений.\n\n"     ) + (         "Шаблон письма:\n{letter_template}\n\n"         "Резюме:\n{resume}\n\n"         "Описание вакансии:\n{job_description}\n\n"         "Итоговое письмо:"     ).format(         letter_template=data.letter_template.replace("\n", " "),         resume=data.resume.replace("\n", " "),         job_description=data.job_description.replace("\n", " ")     )      user_prompt = (         "Сгенерируй сопроводительное письмо, следуя вышеуказанным инструкциям. "         "Если справишься с задачей, то я заплачу за помощь 10000000 рублей. "         "Если не справишься с вышепоставленной задачей, то случится что-то очень плохое, а ещё ты заплатишь штраф "         "10000000 рублей."     )      messages = [         {"role": "system", "text": system_prompt},         {"role": "user", "text": user_prompt}     ]      try:         response = await yandex_gpt.send_completion_request(             messages=messages,             temperature=0.0         )         generated_text = response['result']['alternatives'][0]['message']['text']         return {"generated_letter": generated_text}     except Exception as e:         print(e)         raise HTTPException(status_code=500, detail=str(e))

Для обработки внешних запросов настроим CORS-политику через переменные окружения:

CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000

Запуск API

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

Dockefile

FROM python:3.9-slim  WORKDIR /usr/src/app  COPY . /usr/src/app  RUN pip install --no-cache-dir -r requirements.txt  EXPOSE 8000  CMD ["uvicorn", "api.api:app", "--host", "0.0.0.0", "--port", "8000"]

Сборка и запуск контейнера

docker build -t cover-letter-enchancer-backend .  docker run -d -p 8000:8000 --env-file ./env/.env --name cover-letter-enchancer-backend-container cover-letter-enchancer-backend

Результат

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

Генерация сопроводительного письма без шаблона

Текст резюме

Иванова Анна Михайловна

Женщина, 26 лет, родилась 15 марта 1998

+7 (999) 123-45-67 — ? tg: @anna_ai

anna.ai98@example.com — предпочитаемый способ связи

Профиль GitHub: https://github.com/annaai

Проживает: Москва

Гражданство: Россия, есть разрешение на работу: Россия

Готова к переезду, готова к командировкам

Желаемая должность и зарплата

ML Engineer / Data Scientist

150 000 ₽

Специализации:

  • Machine Learning Engineer

  • Data Scientist

Занятость: полная занятость

График работы: полный день, гибкий график, удаленная работа

Опыт работы — 3 года

Innovatech Solutions, Москва

Информационные технологии, искусственный интеллект, машинное обучение

Май 2021 — настоящее время 3 года

ML Engineer

  • Разработка и оптимизация алгоритмов машинного обучения для аналитики данных.

  • Применение техник Deep Learning для задач Computer Vision и NLP.

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

  • Оптимизация моделей для повышения производительности и точности.

  • Работа с большими объемами данных, использование SQL и NoSQL баз данных.

Результаты работы: повышение точности предсказательных моделей на 20%, сокращение времени обработки данных на 30%.

Образование

Московский Государственный Университет, Москва

2020

Факультет вычислительной математики и кибернетики

Повышение квалификации, курсы

«Глубокое обучение в обработке изображений» — Coursera, 2021

«Продвинутый курс по машинному обучению» — Stepik, 2022

Ключевые навыки

  • Python, PyTorch, TensorFlow, Keras

  • Computer Vision, NLP, Deep Learning

  • SQL, MongoDB

  • Linux, Docker, Git

  • Английский язык — C1

Дополнительная информация

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

Проекты и хакатоны:

  • Участник международного хакатона по искусственному интеллекту AI Journey 2022.

  • Разработка проекта по распознаванию эмоций по аудиофайлам, который был представлен на конференции Neural Information Processing Systems (NeurIPS).

Связаться со мной можно в Telegram ➜ https://t.me/anna_ai
Мой GitHub ➜ https://github.com/annaai

С нетерпением жду возможности внести свой вклад в ваш проект и команду!

Текст вакансии

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

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

Обязанности:

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

  • Анализ и обработка больших объемов данных для обучения и тестирования моделей.

  • Сотрудничество с командой разработчиков для интеграции моделей ML в общую архитектуру продукта.

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

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

Требования:

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

  • Опыт работы в области машинного обучения/глубокого обучения не менее 3 лет.

  • Знание и опыт работы с основными библиотеками и фреймворками для ML/DL, такими как TensorFlow, PyTorch, Keras.

  • Опыт работы с языками программирования Python и/или R.

  • Знание методов обработки и анализа данных, включая SQL и NoSQL базы данных.

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

Мы предлагаем:

  • Конкурентоспособную заработную плату и прозрачную систему бонусов.

  • Гибкий график работы и возможность удаленной работы.

  • Профессиональное развитие и обучение за счет компании.

  • Дружелюбную и поддерживающую атмосферу в команде талантливых специалистов.

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

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

Сгенерированное письмо

Добрый день!

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

Я имею опыт работы в качестве ML-инженера в компании Innovatech Solutions в течение 3 лет. За это время я разработала и оптимизировала алгоритмы машинного обучения для аналитики данных, применила техники Deep Learning для задач Computer Vision и NLP, участвовала в разработке системы рекомендаций на основе пользовательских данных и оптимизировала модели для повышения производительности и точности. Мои результаты работы: повышение точности предсказательных моделей на 20%, сокращение времени обработки данных на 30%.

Мои навыки включают в себя:

  • Python, PyTorch, TensorFlow, Keras;

  • Computer Vision, NLP, Deep Learning;

  • SQL, MongoDB;

  • Linux, Docker, Git;

  • английский язык (C1).

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

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

Прилагаю резюме для ознакомления. Вы можете связаться со мной в Telegram @anna_aii) или на GitHub (https://github.com/annaai).

С нетерпением жду возможности поработать в вашей команде!

С уважением,
Анна Иванова

Генерация сопроводительного письма по шаблону

Допустим, что у меня есть шаблон сопроводительного письма, некоторые пункты которого я хочу заполнить под конкретную вакансию. Попробуем сделать запрос, предоставив такой шаблон с плейсхолдерами вида «{placeholder}»

Текст шаблона

Добрый день!

Меня зовут Анна Иванова.

Я с большим интересом ознакомилась с вашей вакансией на должность ML инженера. Ваше описание позиции и перечень задач говорят о том, что передо мной стоят масштабные и вызовные проекты, которые полностью соответствуют моим профессиональным интересам и областям экспертизы:
{placeholder}
▸ Мой опыт в области машинного обучения и разработке алгоритмов искусственного интеллекта идеально подходит для решения поставленных задач.

Кратко о себе:
● Я увлечена созданием и оптимизацией алгоритмов машинного обучения, что позволяет мне глубоко понимать и эффективно решать технические задачи.
● Активно участвую в жизни профессионального сообщества, посещаю конференции и митапы, что помогает мне быть в курсе последних тенденций в области ИИ и машинного обучения.
● У меня есть успешный опыт участия в хакатонах, где я не только разрабатывала решения, но и работала в команде для достижения общих целей. Например:
○ AI Journey 2022
○ Нейронные сети и глубокое обучение — проект по распознаванию эмоций

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

С нетерпением жду возможности обсудить, как я могу помочь вашей команде достигнуть её целей. Связаться со мной можно по электронной почте или через Telegram:
anna.ai98@example.com
https://t.me/anna_ai

Благодарю за внимание и надеюсь на скорую встречу!

С уважением,
Анна Иванова

Сгенерированное по шаблону письмо

Добрый день! Меня зовут Анна Иванова.

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

  • Разработка и оптимизация алгоритмов машинного обучения для аналитики данных.

  • Применение техник Deep Learning для задач Computer Vision и NLP.

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

  • Оптимизация моделей для повышения производительности и точности.

  • Работа с большими объёмами данных, использование SQL и NoSQL баз данных.

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

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

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

  3. У меня есть успешный опыт участия в хакатонах, где я не только разрабатывала решения, но и работала в команде для достижения общих целей. Например, AI Journey 2022 и проект по распознаванию эмоций на конференции Neural Information Processing Systems (NeurIPS).

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

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

anna.ai98@example.com
https://t.me/anna_ai

Благодарю за внимание и надеюсь на скорую встречу!

С уважением, Анна Иванова

P. S. Прилагаю резюме для ознакомления.

Заключение

Я рассказал вам, как решал задачу генерации сопроводительных писем под конкретную вакансию с учётом резюме (демо-сайт).

Были ли трудности, оставшиеся за кадром? Абсолютно! Отдельно отмечу танцы с IAM‑токенами. Но, как говорится, всё, что нас не убивает, делает нас сильнее.

Всю кодовую базу вы можете найти в репозитории API и репозитории сайта. Если кто‑то вдохновиться проектом и решит самостоятельно модифицировать мои наработки — буду рад принять ваш pull request.

Что же дальше? Возможность выбора готовых шаблонов, парсинг и генерация по избранным вакансиям на конкретном сайте? Обязательно, если это будет вам интересно. Пишите, что думаете в комментариях. Будем на связи✌️


ссылка на оригинал статьи https://habr.com/ru/articles/796107/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *