Upwork, одна из крупнейших фриланс-площадок в открытом вебе, а её публичные страницы поиска вакансий служат постоянным сигналом о том, кого нанимают клиенты. Каждое объявление содержит заголовок, бюджет или почасовую ставку, список требуемых навыков и время публикации, и именно эти данные используют рекрутеры, исследователи рынка и сами фрилансеры для отслеживания спроса: каких навыков не хватает, какие ставки устанавливает категория и где появляются новые проектные работы.
В этом руководстве показано, как скрейпить вакансии Upwork с помощью JavaScript и Node.js, используя cheerio. Вы создадите небольшой, готовый к запуску скрейпер, который загружает публичную страницу поиска вакансий Upwork через Crawling API, парсит заголовок каждого объявления, бюджет или ставку, требуемые навыки, время публикации и ссылку, а затем экспортирует результат в JSON и CSV. Всё руководство ограничено публичными объявлениями о работе. Оно не касается личных профилей фрилансеров, контактных данных или чего-либо за логином, а раздел о легальности ближе к концу не является формальностью, поэтому прочитайте его перед тем, как направить скрейпер на реальные объёмы.
Что вы создадите
Скрипт на Node.js, который принимает публичный URL поиска вакансий Upwork, получает готовый HTML через Crawling API и извлекает структурированную запись для каждого объявления на странице результатов. В качестве примера используется поиск «SEO expert», и для каждого объявления извлекаются следующие поля:
- Заголовок, название объявления о работе, например «SEO Expert for Technical Site Audit».
- Бюджет или ставка, фиксированный бюджет или диапазон почасовой ставки, указанный на карточке, например «$1,000» или «$25.00-$50.00».
- Навыки, список тегов требуемых навыков, прикреплённых к объявлению.
- Время публикации, относительное время публикации, например «Posted 3 hours ago».
- Ссылка, URL страницы конкретного объявления о работе.
Почему обычный запрос не работает на Upwork
Если запросить URL поиска вакансий Upwork с помощью обычного HTTP-клиента, карточки вакансий возвращаются редко. Две вещи работают против вас. Во-первых, Upwork рендерит результаты поиска в браузере с помощью JavaScript, поэтому исходный HTML является почти пустой оболочкой до тех пор, пока не выполнятся скрипты страницы. Во-вторых, Upwork агрессивно помечает автоматизированный трафик: датацентровые IP и паттерны запросов, не похожие на настоящий браузер, блокируются или подвергаются rate-limit ещё до того, как дойдут до отрендеренных объявлений.
Таким образом, работающий скрейпер Upwork требует двух вещей в одном запросе: браузера, который действительно рендерит страницу, и IP, который платформа воспринимает как реального посетителя. Можно собрать это самостоятельно с помощью headless-браузера плюс пула ротирующих резидентных прокси, но соединение этих компонентов и поддержание их работоспособности, это большая часть всей работы. Crawling API объединяет оба в одном вызове: вы отправляете ему URL, он рендерит страницу за доверенным IP и возвращает готовый HTML для парсинга с помощью cheerio. Подробнее о том, почему клиентский рендеринг ломает наивные скрейперы, читайте в руководстве о том, как краулить JavaScript-сайты.
Crawling API даёт вам два токена: обычный и JavaScript-токен. Страницы поиска Upwork рендерятся на стороне клиента, поэтому для того, чтобы страница вернулась заполненной, нужен JavaScript-токен (рендеринг в настоящем браузере). Запрос с обычным токеном обычно вернёт пустую оболочку.
Предварительные требования
Перед написанием кода необходимо подготовить несколько вещей. Ни одна из них не займёт много времени.
Базовые знания JavaScript и Node.js. Вы должны уметь писать и запускать Node-скрипты и устанавливать пакеты с помощью npm. Если вы новичок в Node, руководство по созданию веб-скрейпера с Node.js охватывает основы, которые предполагает этот туториал.
Node.js 16 или новее. Проверьте версию командой node --version. Если Node.js не установлен, установите его с сайта Node.js или через менеджер версий, например nvm.
Аккаунт Crawlbase и токен. Зарегистрируйтесь, откройте панель управления и скопируйте JavaScript-токен со страницы документации аккаунта. Бесплатный уровень даёт до 20 000 запросов без привязки карты. Относитесь к токену как к паролю: он аутентифицирует ваши запросы, поэтому не добавляйте его в систему контроля версий.
Настройка проекта
Создайте папку проекта, инициализируйте её и установите две библиотеки, которые нужны скрейперу.
node --version mkdir upwork-jobs-scraper && cd upwork-jobs-scraper npm init -y npm install crawlbase cheerio
Две зависимости выполняют всю работу: crawlbase, официальный Node-клиент для Crawling API, а cheerio парсит возвращённый HTML с jQuery-подобным API, позволяя извлекать отдельные поля по CSS-селектору. Создайте в этой папке файл upwork-scraper.js и добавьте код из шагов ниже.
Шаг 1: Получение отрендеренной страницы поиска вакансий
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с JavaScript-токеном и запросите URL поиска. Поскольку Upwork рендерится на стороне клиента, передайте API небольшое ожидание, чтобы скрипты завершились до захвата HTML. Проверка кода статуса перед парсингом делает ошибки заметными, а не скрытыми.
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const upworkSearchURL = 'https://www.upwork.com/nx/search/jobs/?q=seo%20expert'; // Render the page in a real browser, then wait for scripts to settle const options = { ajax_wait: 'true', page_wait: 3000 }; api .get(upworkSearchURL, options) .then((response) => { if (response.statusCode === 200) { console.log(response.body.slice(0, 500)); } }) .catch((error) => console.error('API request error:', error));
Запустите скрипт командой node upwork-scraper.js, и в начале тела должна появиться настоящая разметка вакансий Upwork, а не урезанная оболочка. Это подтверждает, что рендеринг работает, прежде чем вы напишете хоть один селектор. Параметры ajax_wait и page_wait указывают API запустить страницу в настоящем браузере и дать скриптам время заполнить карточки вакансий. Если вы предпочитаете, чтобы API сам парсил стандартные поля вместо написания селекторов вручную, передайте параметр autoparse и получите структурированный JSON напрямую.
// Ask the API to render and parse, returning JSON const options = { ajax_wait: 'true', page_wait: 3000, autoparse: 'true' }; api .get(upworkSearchURL, options) .then((response) => { if (response.statusCode === 200) { console.log(JSON.parse(response.body)); } }) .catch((error) => console.error('API request error:', error));
Этот первый запрос только что вернул полностью отрендеренную страницу поиска Upwork без headless-браузера или прокси на вашей стороне. Crawling API запускает страницу в настоящем браузере, ждёт, пока JavaScript заполнит карточки вакансий, ротирует резидентные IP на серверной стороне и обрабатывает вызовы, которые Upwork бросает скрейперам, возвращая готовый HTML (или autoparsed JSON) из одного вызова. Начните с публичного поиска вакансий на бесплатном уровне.
Шаг 2: Парсинг каждого объявления с помощью cheerio
Имея готовый HTML, загрузите его в cheerio и пройдитесь по карточкам вакансий. Upwork размещает каждое объявление в повторяющемся контейнере-статье на странице результатов поиска, поэтому вы выбираете каждую карточку, а затем читаете заголовок, бюджет или ставку, навыки, время публикации и ссылку внутри неё. Защитное считывание каждого поля не даёт одному отсутствующему значению сломать весь запуск.
const cheerio = require('cheerio'); function parseJobs(html) { const $ = cheerio.load(html); const jobs = []; const cards = $('article[data-test="JobTile"]'); cards.each((index, element) => { const card = $(element); const job = {}; // Title and link share one anchor const titleLink = card.find('[data-test="job-tile-title-link"]'); job.title = titleLink.text().trim(); const href = titleLink.attr('href'); job.link = href ? new URL(href, 'https://www.upwork.com').href : ''; // Posted time, e.g. "Posted 3 hours ago" job.posted = card .find('[data-test="job-pubilshed-date"]') .text() .replace(/\s+/g, ' ') .trim(); // Budget (fixed price) or hourly rate range const budget = card .find('[data-test="is-fixed-price"] strong') .last() .text() .trim(); const hourly = card .find('[data-test="job-type-label"]') .text() .trim(); job.budget = budget || hourly || 'Not specified'; // Required skill tags job.skills = card .find('[data-test="token"] span') .map((i, el) => $(el).text().trim()) .get() .filter(Boolean); if (job.title) jobs.push(job); }); return jobs; }
Несколько деталей обеспечивают точность. Заголовок и ссылка берутся из одного якоря job-tile-title-link, поэтому один поиск даёт оба, а относительный href разворачивается в абсолютный URL, чтобы он работал вне страницы. Время публикации читается из атрибута job-pubilshed-date (собственное написание Upwork, сохранено как есть, чтобы селектор совпадал), с удалением лишних пробелов. Обработка бюджета покрывает оба типа объявлений: объявление с фиксированной ценой показывает сумму внутри блока is-fixed-price, а почасовое объявление несёт метку ставки в job-type-label, поэтому парсер берёт то, что присутствует. Навыки собираются из тегов-токенов в чистый массив.
Атрибуты data-test Upwork стабильнее сгенерированных имён классов, но всё равно меняются без предупреждения. Воспринимайте селекторы выше как начальный шаблон, а не контракт. Когда поле возвращается пустым, повторно просмотрите живую страницу в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, норма для любого боевого скрейпера, а не признак поломки.
Шаг 3: Сборка полного скрипта с экспортом в JSON и CSV
Теперь соедините запрос и парсинг в один запускаемый скрипт, а затем запишите записи на диск в форматах JSON и CSV. Запрос без параметра autoparse возвращает сырой отрендеренный HTML, который и ожидает парсер cheerio.
const fs = require('fs'); const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); async function crawl(pageUrl) { const options = { ajax_wait: 'true', page_wait: 3000 }; const response = await api.get(pageUrl, options); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function toCsv(rows) { const headers = ['title', 'budget', 'skills', 'posted', 'link']; const escape = (value) => `"${String(value).replace(/"/g, '""')}"`; const lines = [headers.join(',')]; for (const row of rows) { const flat = { ...row, skills: row.skills.join('; ') }; lines.push(headers.map((h) => escape(flat[h])).join(',')); } return lines.join('\n'); } async function main() { const url = 'https://www.upwork.com/nx/search/jobs/?q=seo%20expert'; const html = await crawl(url); if (!html) return; const jobs = parseJobs(html); fs.writeFileSync('upwork-jobs.json', JSON.stringify(jobs, null, 2)); fs.writeFileSync('upwork-jobs.csv', toCsv(jobs)); console.log(`Saved ${jobs.length} job postings to JSON and CSV`); } main();
Вставьте функцию parseJobs из Шага 2 в тот же файл, чтобы main мог её вызвать. Запустите командой node upwork-scraper.js и получите два файла: upwork-jobs.json с полными структурированными записями и upwork-jobs.csv, готовый к открытию в таблице. Вспомогательная функция toCsv сводит массив навыков в одну ячейку с разделением точкой с запятой, цитирует каждое поле и удваивает встроенные кавычки, что важно, так как заголовки вакансий часто содержат запятые.
Как выглядит вывод
Файл JSON содержит по одному объекту на объявление в порядке результатов поиска, каждый с заголовком, бюджетом или ставкой, навыками, временем публикации и ссылкой.
[ { "title": "SEO Expert for Technical Site Audit", "budget": "$1,000", "skills": ["SEO", "Technical SEO", "On-Page SEO"], "posted": "Posted 3 hours ago", "link": "https://www.upwork.com/jobs/SEO-Expert-Technical-Site-Audit_~012abc" }, { "title": "Ongoing SEO Content Strategy", "budget": "Hourly: $25.00-$50.00", "skills": ["SEO", "Content Writing", "Keyword Research"], "posted": "Posted yesterday", "link": "https://www.upwork.com/jobs/Ongoing-SEO-Content-Strategy_~034def" } ]
CSV отражает те же строки с заголовочной строкой, поэтому он напрямую открывается в Excel, Google Sheets или любом пайплайне данных, читающем файлы с разделителями.
title,budget,skills,posted,link "SEO Expert for Technical Site Audit","$1,000","SEO; Technical SEO; On-Page SEO","Posted 3 hours ago","https://www.upwork.com/jobs/SEO-Expert-Technical-Site-Audit_~012abc" "Ongoing SEO Content Strategy","Hourly: $25.00-$50.00","SEO; Content Writing; Keyword Research","Posted yesterday","https://www.upwork.com/jobs/Ongoing-SEO-Content-Strategy_~034def"
Масштабирование на несколько поисков и страниц
Один поисковый запрос, это демо; в реальной задаче обходится несколько запросов и несколько страниц результатов. URL поиска Upwork принимает параметр запроса q и параметр page, поэтому можно составить список поисков, пройтись по страницам каждого, распарсить каждую страницу той же функцией и пометить каждую строку своим запросом перед экспортом. Поскольку все страницы результатов поиска имеют одинаковую структуру карточек, уже написанный парсер работает для всех них без изменений.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function scrapeSearch(query, pages) { const all = []; for (let page = 1; page <= pages; page++) { const url = `https://www.upwork.com/nx/search/jobs/?q=${encodeURIComponent( query )}&page=${page}`; const html = await crawl(url); if (!html) continue; const rows = parseJobs(html).map((j) => ({ query, ...j })); all.push(...rows); await sleep(2000); } return all; } scrapeSearch('seo expert', 3).then((rows) => { console.log(`Collected ${rows.length} postings across pages`); });
Этот паттерн напрямую применим для исследования рынка труда и рекрутинговой работы. Тот же подход для других площадок найма, смотрите руководства о том, как скрейпить вакансии Indeed и как скрейпить вакансии Monster с помощью Python, охватывающие сторону страниц результатов этих сайтов.
Как оставаться незаблокированным
Даже при решённом рендеринге Upwork следит за трафиком, похожим на скрейпер. Несколько привычек сохраняют работоспособность запуска, и они применимы к любой сложной коммерческой цели.
-
Контролируйте темп запросов. Вводите задержку между запросами страниц, а не долбите страницы в плотном цикле, как это делает вызов
sleepвыше. Распределение запросов, единственный наиболее важный фактор соблюдения rate-limit Upwork. - Опирайтесь на ротацию. Пул резидентных IP распределяет запросы по множеству адресов реальных пользователей, чтобы ни один из них не превысил лимит или не получил вызов. Crawling API делает это за вас; если вы создаёте собственный стек, именно эту часть нужно сделать правильно.
- Читайте коды статуса. Запуск, начавший возвращать вызовы или не-200 ответы, сигнализирует о том, что текущий темп или IP-уровень больше недостаточен. Воспринимайте это как сигнал к отступлению, а не как шум для игнорирования.
Для более широкого плана действий смотрите руководство о том, как скрейпить сайты без блокировок.
Законен ли скрейпинг Upwork?
Допустимость скрейпинга Upwork зависит от условий использования Upwork, вашей юрисдикции и того, что вы делаете с данными. Условия использования Upwork ограничивают автоматизированный доступ и скрейпинг, поэтому сбор данных может противоречить этим условиям независимо от того, насколько тщательно выполнены технические требования. Ни один из кодов здесь это не меняет; он просто делает работоспособной техническую часть. Прочитайте Условия использования Upwork и его robots.txt и относитесь к обоим как к границе того, что вы собираете. Соблюдайте заявленные ожидания Upwork по частоте запросов и держите объём запросов достаточно низким, чтобы не перегружать серверы.
Данное руководство намеренно ограничено только публичными объявлениями о работе: заголовок, бюджет или ставка, требуемые навыки, время публикации и ссылка на объявление, всё это видно на публичной странице поиска без логина. Оно не касается профилей фрилансеров, имён, фотографий, контактных данных, истории работы, заработков и любых других персональных данных, и не охватывает ничего за логином. Эта граница важна юридически. Объявления о работе, это публичные деловые объявления, но профили фрилансеров являются персональными данными, и как только вы начинаете собирать данные об идентифицируемых людях, применяется закон о конфиденциальности. По GDPR в ЕС и CCPA в Калифорнии скрейпинг и хранение персональных данных требует правового основания, а у людей есть права на доступ и удаление. Безопасная и защищаемая позиция, оставаться на публичных объявлениях, никогда не составлять профили людей и не собирать контактные данные для нежелательных рассылок.
Если ваш проект требует большего, чем публичные объявления, или вы хотите гарантированную структуру и коммерческие права, ищите официальный канал, а не более изощрённый скрейпер. Upwork предлагает программу enterprise и API для санкционированного доступа к данным с чёткими условиями использования, что является правильным инструментом, когда вам нужен объём или договорная основа. Для агрегированных, неперсональных маркетинговых исследований (какие навыки востребованы, какие ставки устанавливает категория, как меняется объём объявлений) обычно достаточно публичных объявлений без какой-либо индивидуальной идентификации, и именно это и собирает данный скрейпер.
Ключевые выводы
- Upwork рендерит результаты поиска на стороне клиента и жёстко блокирует. Обычный запрос возвращает пустую оболочку или вызов, поэтому перед парсингом страница должна быть отрендерена за доверенным IP.
-
Crawling API делает оба в одном вызове. Используйте JavaScript-токен с
ajax_waitиpage_wait, чтобы карточки вакансий заполнились; берите сырой HTML для самостоятельного парсинга или передайтеautoparse: 'true'для получения JSON. - cheerio извлекает поля. Выбирайте каждый тайл вакансии, затем читайте заголовок, бюджет или ставку, навыки, время публикации и ссылку, и ожидайте, что селекторы будут меняться со временем.
- Экспортируйте в JSON и CSV. Записывайте структурированные записи в оба формата, сводя массив навыков и цитируя поля CSV, чтобы заголовки с запятыми оставались целыми.
- Оставайтесь только на публичных объявлениях. Собирайте публичные данные о вакансиях, никогда не трогайте профили фрилансеров или персональные данные, соблюдайте ToS и robots.txt Upwork, и учитывайте GDPR и CCPA при работе с персональными данными.
Часто задаваемые вопросы
Какие данные можно легально скрейпить с Upwork?
Придерживайтесь публичных объявлений о работе: заголовок, бюджет или почасовая ставка, требуемые навыки, время публикации и ссылка на каждое объявление, всё это отображается на публичной странице поиска без логина. Избегайте профилей фрилансеров, имён, фотографий, контактных данных, истории работы и заработков, поскольку это персональные данные, подпадающие под закон о конфиденциальности. Данное руководство ограничено публичными объявлениями именно по этой причине.
Почему обычный запрос возвращает неполные данные с Upwork?
Потому что Upwork рендерит результаты поиска на стороне клиента с помощью JavaScript и блокирует автоматизированный трафик. Сырой HTTP-запрос с датацентрового IP обычно возвращает пустую оболочку или страницу блокировки вместо карточек вакансий. Чтобы получить полную страницу, нужно отрендерить её в настоящем браузере за доверенным IP, чем и занимается Crawling API при использовании JavaScript-токена.
Нужен ли обычный токен или JavaScript-токен?
Используйте JavaScript-токен. Страницы поиска Upwork рендерятся в браузере, поэтому запрос с обычным токеном возвращает незаполненную оболочку. JavaScript-токен запускает страницу в настоящем браузере, а передача ajax_wait с коротким page_wait даёт скриптам страницы время заполнить карточки вакансий до захвата HTML.
Мои селекторы возвращают пустые значения. Что изменилось?
Почти наверняка разметка Upwork. Даже атрибуты data-test, используемые здесь, могут меняться без предупреждения, поэтому селекторы, работавшие в прошлом месяце, могут сломаться. Повторно просмотрите живую страницу в инструментах разработчика браузера и обновите селекторы. Периодическое обслуживание селекторов, норма для любого боевого скрейпера.
Можно ли скрейпить профили фрилансеров или контактные данные с Upwork?
Нет, и данное руководство этого не охватывает. Профили фрилансеров, имена, контактные данные, история работы и заработки, это персональные данные, а не публичные деловые объявления, поэтому их скрейпинг влечёт обязательства по GDPR и CCPA и противоречит условиям Upwork. Ограничивайте сбор публичными объявлениями о работе, никогда не составляйте профили людей и не собирайте контактные данные для нежелательных рассылок. Для санкционированного доступа к большему, чем публичные объявления, используйте официальный API или enterprise-программу Upwork.
Как избежать блокировки при скрейпинге Upwork?
Держите темп запросов с одного IP низким, добавляйте задержки между запросами страниц и маршрутизируйте через ротирующие резидентные IP, чтобы ни один адрес не превысил rate-limit или не получил вызов. Crawling API управляет ротацией, доверенным пулом IP и обработкой вызовов; если вы создаёте собственный стек, именно в эту часть стоит вложиться. Следите за кодами статуса и отступайте, когда начинаете получать не-200 ответы.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
