Monster это одна из крупнейших досок вакансий в интернете, с тысячами активных объявлений по разным отраслям и регионам. Каждое публичное объявление несёт тот вид структурированного сигнала, который питает исследования рынка труда, конкурентный анализ для рекрутеров и пользовательские инструменты поиска работы: название должности, нанимающая компания, местоположение, дата размещения и ссылка на полное объявление. Загвоздка в том, что Monster собирает результаты поиска в браузере с помощью JavaScript и лениво подгружает новые карточки по мере прокрутки, поэтому обычный HTTP запрос отдаёт вам почти пустую оболочку вместо объявлений, за которыми вы пришли.
Это руководство показывает, как надёжно парсить вакансии Monster с помощью Python. Вы создадите небольшой готовый к запуску парсер, который получает отрисованную страницу поиска через Crawling API, разбирает каждую карточку вакансии с помощью BeautifulSoup и печатает чистый структурированный вывод. Мы держим весь разбор ограниченным публичными данными объявлений о вакансиях, а раздел о юридических аспектах ближе к концу не является шаблонной формальностью, поэтому прочитайте его, прежде чем направлять это на любой реальный объём.
Что вы создадите
Скрипт на Python, который принимает публичный URL поиска Monster, получает отрисованный HTML через Crawling API и извлекает структурированную запись для каждого объявления на странице. В качестве сквозного примера мы используем поиск вакансий разработчиков и выберем следующие поля по каждому объявлению:
- Название должности рекламируемая роль, например "Java Developer".
- Компания работодатель за объявлением.
- Местоположение где базируется вакансия, например "New York, NY".
- Дата размещения насколько недавно появилось объявление, когда Monster это показывает.
- URL вакансии ссылка на полное объявление, чтобы вы могли продолжить по каждой роли.
Почему обычный запрос не работает на Monster
Если вы запросите URL поиска Monster через простой HTTP клиент, вы получите ответ со статусом 200 и почти без данных объявлений в теле. Против вас работают две вещи. Во-первых, Monster отрисовывает свои карточки вакансий в браузере с помощью JavaScript, поэтому исходный HTML это оболочка, которая заполняется только после выполнения скриптов страницы. Во-вторых, страница результатов использует пагинацию на основе прокрутки: новые карточки подгружаются по мере того, как настоящий пользователь прокручивает вниз, поэтому даже отрисованный снимок, сделанный слишком рано, захватывает лишь первую горстку вакансий.
Поэтому работающему парсеру Monster нужны три вещи в одном запросе: браузер, который действительно отрисовывает страницу, IP, который платформа воспринимает как реального посетителя, и способ управлять прокруткой, чтобы появились лениво подгружаемые карточки. Вы можете собрать это самостоятельно с помощью headless браузера, скрипта прокрутки и пула ротируемых резидентных прокси, но сшить их вместе и поддерживать в рабочем состоянии это и есть большая часть работы. Crawling API объединяет все три компонента в одном вызове: вы отправляете ему URL с токеном JavaScript, он отрисовывает и прокручивает страницу за доверенным IP и возвращает вам готовый HTML для разбора.
Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; токен JavaScript (JS) сначала отрисовывает страницу в настоящем браузере. Monster отрисовывается на стороне клиента, поэтому здесь вам нужен JS токен. Использование обычного токена вернёт ту же пустую оболочку, что и обычный запрос, и из неё нечего разбирать.
Предварительные требования
Прежде чем писать какой-либо код, вам нужно подготовить несколько вещей. Ни одна из них не займёт много времени.
Базовый Python. Вам стоит уверенно писать и запускать скрипт на Python и устанавливать пакеты через pip. Если вы новичок в разборе HTML, наш вводный материал о том, как использовать BeautifulSoup в Python охватывает основы селекторов, на которые опирается этот учебник.
Python 3.8 или новее. Проверьте свою версию командой python --version. Если у вас её нет, установите её с python.org или через дистрибутив вроде Anaconda.
Аккаунт Crawlbase и JS токен. Зарегистрируйтесь, откройте панель управления и скопируйте свой токен JavaScript (JS) со страницы документации аккаунта. Относитесь к токену как к паролю: он аутентифицирует ваши запросы, поэтому держите его вне системы контроля версий. Бесплатный тариф включает достаточно запросов, чтобы пройти это руководство от начала до конца.
Настройте проект
Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, затем установите две библиотеки, которые нужны парсеру.
python --version python -m venv monster_env source monster_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой monster_env\Scripts\activate вместо строки source. Работу делают две зависимости: crawlbase это официальный клиент для Crawling API, а beautifulsoup4 разбирает возвращённый HTML, чтобы вы могли извлекать отдельные поля по CSS селектору.
Шаг 1: Получите отрисованную страницу поиска
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его своим JS токеном и запросите URL поиска. Проверка кода статуса перед разбором делает сбои громкими, а не тихими. Обратите внимание на две опции ожидания: ajax_wait сообщает API, что нужно дождаться окончания загрузки асинхронного контента, а page_wait выдерживает фиксированное число миллисекунд, чтобы поздно отрисовываемые карточки появились до того, как страница будет захвачена.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": page_url = "https://www.monster.com/jobs/search?q=Java+Developer&where=New+York" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
Запустите скрипт командой python scraper.py, и вы должны увидеть реальную разметку карточек вакансий, а не пустую оболочку, которую возвращает обычный запрос. Пять секунд это разумное стартовое значение page_wait; увеличьте его, если карточки возвращаются пустыми. Это подтверждает, что отрисовка работает, прежде чем вы напишете хоть один селектор.
Monster требует отрисованной, прокрученной страницы за доверенным IP, в одном вызове. Crawling API принимает JS токен, запускает страницу в настоящем браузере, управляет прокруткой, чтобы появились лениво подгружаемые карточки, и ротирует резидентные IP на стороне сервера, так что вам не нужно самостоятельно держать парк headless браузеров и пул прокси. Сначала направьте его на публичную страницу поиска на бесплатном тарифе.
Шаг 2: Изучите структуру карточки вакансии
Прежде чем писать селекторы, откройте страницу поиска Monster в своём браузере и осмотрите карточку вакансии в инструментах разработчика. На момент написания каждое объявление находится в элементе <article>, несущем атрибут data-testid="svx_jobCard", сгруппированном внутри контейнера с id JobCardGrid. Внутри каждой карточки нужные вам поля висят на стабильных тестовых id:
-
Название должности и URL находятся на
<a data-testid="jobTitle">: текст ссылки это название, аhrefэто URL объявления. -
Компания находится в
<span data-testid="company">. -
Местоположение находится в
<span data-testid="jobDetailLocation">. -
Дата размещения появляется в
<span data-testid="jobDetailDateRecency">, когда Monster показывает её для этого объявления.
Нацеливание на атрибуты data-testid, а не на имена CSS классов, сделано намеренно. Monster поставляет хешированные, сгенерированные при сборке имена классов, которые меняются при каждом развёртывании, тогда как тестовые id обычно остаются на месте, потому что от них зависит собственный набор тестов сайта. Это самые долговечные крючки, которые даёт вам страница.
Шаг 3: Разберите карточки вакансий с помощью BeautifulSoup
Имея на руках отрисованный HTML, загрузите его в BeautifulSoup, выберите каждую карточку вакансии и извлеките каждое поле из карточки по его тестовому id. Оберните чтение каждого поля в небольшой помощник, который возвращает пустую строку, когда элемент отсутствует, чтобы одно отсутствующее поле никогда не обрушило запуск.
from bs4 import BeautifulSoup def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else "" def parse_jobs(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select('div#JobCardGrid article[data-testid="svx_jobCard"]') jobs = [] for card in cards: title_link = card.select_one('a[data-testid="jobTitle"]') jobs.append({ "title": title_link.get_text(strip=True) if title_link else "", "company": text_of(card, 'span[data-testid="company"]'), "location": text_of(card, 'span[data-testid="jobDetailLocation"]'), "posted": text_of(card, 'span[data-testid="jobDetailDateRecency"]'), "url": title_link["href"] if title_link else "", }) return jobs
Ссылка с названием читается один раз и переиспользуется, поскольку она несёт и текст названия, и URL объявления в своём href. Всё остальное проходит через помощник text_of, который запрашивает один элемент и возвращает пустую строку, когда тот отсутствует, вместо того чтобы выбросить ошибку при вызове .get_text() на ничём. Это сохраняет извлечение устойчивым, когда карточка опускает поле, что часто встречается, поскольку не каждое объявление показывает дату размещения.
Разметка Monster меняется без предупреждения, и значения data-testid выше могут быть переименованы в будущем редизайне. Относитесь к ним как к стартовому шаблону, а не как к контракту. Когда поле возвращается пустым по всем карточкам, заново осмотрите живую страницу поиска в инструментах разработчика своего браузера и обновите селектор. Периодическое обслуживание селекторов это норма для любого боевого парсера, а не признак того, что что-то сломалось.
Шаг 4: Захватите всю страницу результатов с помощью прокрутки
Получение из Шага 1 отрисовывает страницу, но Monster подгружает только первую партию карточек, пока пользователь не прокрутит. Чтобы вытянуть весь набор результатов в одном запросе, передайте работу по прокрутке Crawling API вместо того, чтобы самостоятельно запускать headless браузер. Управляют этим две опции: scroll включает пагинацию на основе прокрутки, а scroll_interval задаёт, как долго API продолжает прокручивать, в секундах, до максимума в 60. Когда прокрутка включена, нет нужды также задавать page_wait, поскольку окно прокрутки уже даёт контенту время загрузиться.
def crawl_with_scroll(page_url): options = { "ajax_wait": "true", "scroll": "true", "scroll_interval": "60", } response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None
Подставьте crawl_with_scroll вместо простого crawl, когда вам нужна вся страница, а не только первые карточки. Парсер из Шага 3 не меняется: он по-прежнему выбирает каждый svx_jobCard в возвращённом HTML, только теперь их больше.
Шаг 5: Соберите всё вместе и сохраните в JSON
Теперь свяжите прокрученное получение, парсер и небольшую запись в JSON в один готовый к запуску скрипт. Получите отрисованный и прокрученный HTML, передайте его парсеру и запишите структурированные записи на диск.
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl_with_scroll(page_url): options = {"ajax_wait": "true", "scroll": "true", "scroll_interval": "60"} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else "" def parse_jobs(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select('div#JobCardGrid article[data-testid="svx_jobCard"]') jobs = [] for card in cards: title_link = card.select_one('a[data-testid="jobTitle"]') jobs.append({ "title": title_link.get_text(strip=True) if title_link else "", "company": text_of(card, 'span[data-testid="company"]'), "location": text_of(card, 'span[data-testid="jobDetailLocation"]'), "posted": text_of(card, 'span[data-testid="jobDetailDateRecency"]'), "url": title_link["href"] if title_link else "", }) return jobs def main(): page_url = "https://www.monster.com/jobs/search?q=Java+Developer&where=New+York" html = crawl_with_scroll(page_url) if not html: return jobs = parse_jobs(html) with open("monster_jobs.json", "w") as f: json.dump(jobs, f, indent=2) print(f"Saved {len(jobs)} jobs to monster_jobs.json") if __name__ == "__main__": main()
Как выглядит вывод
Запустите полный скрипт командой python scraper.py, и вы получите список чистых структурированных записей, по одной на объявление, готовый к записи в CSV или базу данных.
[ { "title": "Java Developer (Core Java)", "company": "Georgia IT Inc.", "location": "New York, NY", "posted": "2 days ago", "url": "https://www.monster.com/job-openings/java-developer-core-java-new-york-ny" }, { "title": "Java Backend Developer", "company": "Diverse Lynx", "location": "Manhattan, NY", "posted": "5 days ago", "url": "https://www.monster.com/job-openings/java-backend-developer-manhattan-ny" } ]
Перебор страниц результатов
Одна страница поиска это демонстрация; реальная задача проходит по множеству страниц и запросов. Monster делает пагинацию через параметр запроса page, поэтому вы можете обходить страницы, увеличивая его и переиспользуя ту же пару получения-и-разбора. Поскольку каждая страница результатов разделяет одну и ту же структуру карточек, парсер, который вы уже написали, работает по всем из них без изменений. Регулируйте темп цикла, чтобы не отправлять запросы один за другим.
import time def scrape_pages(query, where, pages=3): all_jobs = [] for page in range(1, pages + 1): url = ( "https://www.monster.com/jobs/search" f"?q={query}&where={where}&page={page}" ) html = crawl_with_scroll(url) if html: all_jobs.extend(parse_jobs(html)) time.sleep(2) return all_jobs
Вызов time.sleep(2) между страницами сделан намеренно. Долбить поиск в плотном цикле это самый быстрый способ получить ограничение, даже когда отрисовка и ротация сделаны за вас. Распределяйте запросы во времени и останавливайтесь рано, как только страница возвращает отсутствие новых карточек.
Как оставаться незаблокированным
Даже когда отрисовка и прокрутка сделаны, Monster следит за трафиком, похожим на парсер. Несколько привычек помогают запуску оставаться здоровым, и они применимы к любой сложной коммерческой цели.
- Регулируйте темп запросов. Распределяйте запросы во времени и варьируйте свои запросы вместо того, чтобы обходить один путь поиска на полной скорости.
- Опирайтесь на ротацию. Пул резидентных IP распределяет запросы по множеству адресов реальных пользователей, так что ни один из них не срабатывает лимит частоты. Crawling API делает это за вас; если вы собираете свой собственный стек, именно эту часть нужно сделать правильно.
- Читайте коды статуса. Запуск, который начинает возвращать проверки или ошибки, говорит вам, что текущая частота или уровень IP уже недостаточны. Воспринимайте это как сигнал отступить, а не как шум, который можно игнорировать.
За более широким сценарием обратитесь к тому, как парсить сайты, не получая блокировок и к более глубокому разбору о том, как обходить капчи при веб-скрапинге. Если ваш проект также затрагивает профессиональные сети, наше руководство о том, как парсить LinkedIn охватывает сопоставимую цель с учётом входа в систему. А если вы предпочитаете направлять свой собственный трафик через ротируемый пул вместо использования управляемого API, Smart AI Proxy (также называемый AI Proxy) даёт вам ту же ротацию резидентных IP в виде drop-in прокси эндпоинта.
Законно ли парсить Monster?
Разрешено ли парсить Monster, зависит от условий обслуживания Monster, вашей юрисдикции и того, что вы делаете с данными. Условия Monster ограничивают автоматический доступ, поэтому парсинг может противоречить этим условиям независимо от того, насколько аккуратен ваш инструментарий. Ничего из кода здесь это не меняет; он лишь заставляет работать техническую часть. Прочитайте условия обслуживания Monster и его robots.txt и относитесь к обоим как к границе того, что вы собираете.
Несколько правил, которых стоит придерживаться. Собирайте только публичные данные объявлений о вакансиях: название должности, компанию, местоположение, дату размещения и URL объявления, которые любой может увидеть на публичной странице поиска без входа в систему. Уважайте заявленные Monster ожидания по частоте и держите объём своих запросов достаточно низким, чтобы не нагружать его серверы. Строго ограничьте свою работу публичными объявлениями о вакансиях.
Это руководство намеренно ограничено публичными объявлениями о вакансиях, потому что это та граница, которая делает работу защитимой. Оно не охватывает персональные данные соискателей или рекрутеров, резюме или профили кандидатов, что-либо за входом в систему или платным уровнем, либо любую попытку обойти аутентификацию. Личная информация соискателей и рекрутеров это именно тот вид данных, который стоит оставить в покое. Если вашему проекту нужно больше, чем публичные объявления, правильным путём является официальное соглашение о данных или собственный инструментарий Monster для работодателей, а не более хитрый парсер.
Ключевые выводы
- Monster отрисовывается на стороне клиента. Обычный запрос возвращает пустую оболочку, поэтому вы должны отрисовать страницу, прежде чем её разбирать.
-
Отрисовка, доверенный IP и прокрутка идут вместе. Crawling API с JS токеном делает все три в одном вызове;
scrollиscroll_intervalвыводят лениво подгружаемые карточки в видимость. -
BeautifulSoup выполняет извлечение. Сопоставьте название, компанию, местоположение, дату размещения и URL с крючками
data-testidкарточки и ожидайте, что эти крючки будут дрейфовать. -
Масштабируйтесь перебором страниц. Обходите параметр
pageMonster тем же парсером и регулируйте темп цикла, чтобы вас не ограничивали. - Оставайтесь на публичных объявлениях. Уважайте ToS и robots.txt Monster и никогда не трогайте персональные данные соискателей или рекрутеров, резюме или страницы за стеной входа.
Часто задаваемые вопросы
Могу ли я парсить вакансии Monster только с помощью requests и BeautifulSoup?
Не надёжно. Monster отрисовывает свои карточки вакансий в браузере с помощью JavaScript и лениво подгружает новые по мере прокрутки, поэтому сырой вызов requests возвращает статус 200 с пустыми объявлениями. Вам нужно что-то, что сначала отрисовывает страницу и управляет прокруткой, что и делают JS токен Crawling API плюс опции прокрутки до того, как BeautifulSoup вообще увидит HTML.
Мне нужен обычный токен или JS токен для Monster?
JS токен. Обычный токен получает статический HTML, который на Monster это та же пустая оболочка, что возвращает обычный запрос. JS токен отрисовывает страницу в настоящем браузере, прежде чем вернуть HTML, поэтому карточки вакансий присутствуют, когда BeautifulSoup их разбирает.
Как мне обработать пагинацию прокрутки Monster?
Передайте scroll: "true" и scroll_interval: "60" в опциях запроса. Тогда Crawling API прокручивает страницу до 60 секунд, максимума, чтобы лениво подгружаемые карточки отрисовались до захвата HTML. С включённой прокруткой вам также не нужен page_wait. Чтобы выйти за пределы одной страницы результатов, увеличивайте параметр запроса page Monster и переиспользуйте ту же пару получения-и-разбора.
Мои селекторы возвращают пустые строки. Что изменилось?
Почти наверняка разметка Monster. Крючки data-testid, на которые опирается парсер, могут быть переименованы в редизайне, а сгенерированные при сборке имена классов постоянно меняются. Заново осмотрите живую страницу поиска в инструментах разработчика своего браузера и обновите селекторы. Периодическое обслуживание селекторов это норма для любого боевого парсера.
Могу ли я парсить резюме или контактные данные рекрутеров из Monster?
Нет, и это руководство этого не охватывает. Резюме, профили кандидатов и персональные данные рекрутеров или соискателей находятся за входом в систему или являются личной информацией, а не публичными данными объявлений о вакансиях. Парсинг контента за стеной входа или обход аутентификации, чтобы добраться до него, выходит за рамки здесь и противоречит условиям Monster. Держите свою область действия на публичных объявлениях на страницах поиска и списков.
Как избежать блокировки при парсинге Monster?
Держите частоту запросов на один IP низкой, варьируйте свои запросы вместо зацикливания на одном пути поиска и направляйте трафик через ротируемые резидентные IP, чтобы ни один адрес не срабатывал лимит частоты. Crawling API управляет ротацией и пулом доверенных IP за вас; если вы строите свой собственный стек, именно в эту часть стоит вложиться. Следите за кодами статуса и отступайте, когда начинаете видеть проверки.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
