From 9434eeebfe1657ba833279eb70124d515ac50f95 Mon Sep 17 00:00:00 2001 From: Oscar Date: Mon, 1 Jun 2026 10:57:07 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(file):=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20collectPageVacancies=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B8=D0=BD=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BE=20=D0=B2=D0=B0?= =?UTF-8?q?=D0=BA=D0=B0=D0=BD=D1=81=D0=B8=D1=8F=D1=85=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hh/scraper.ts | 53 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/hh/scraper.ts b/src/hh/scraper.ts index a0b359e..5cabf41 100644 --- a/src/hh/scraper.ts +++ b/src/hh/scraper.ts @@ -216,6 +216,24 @@ export async function saveResume(chatId: number, resumeItem: ResumeListItem): Pr }) } +async function collectPageVacancies(page: Page) { + return page.$$eval( + '[data-qa="vacancy-serp__vacancy"]', + (cards) => { + return cards + .filter(card => card.querySelector('[data-qa="vacancy-serp__vacancy_response"]') !== null) + .map((card) => { + const titleEl = card.querySelector('[data-qa="serp-item__title"]') as HTMLAnchorElement | null + return { + href: titleEl?.href ?? '', + title: titleEl?.textContent?.trim() ?? '', + } + }) + .filter(v => v.href) + }, + ) +} + export async function applyToJobs( { query, area = 1, maxApplies = 10 }: ApplyOptions, { chatId, reporter }: { chatId: number, reporter: StatusReporter }, @@ -228,23 +246,36 @@ export async function applyToJobs( await loadSession(page, chatId) - const url = `https://hh.ru/search/vacancy?text=${encodeURIComponent(query)}&area=${area}` + const url = `https://hh.ru/search/vacancy?text=${encodeURIComponent(query)}&items_on_page=50&page=0` // &area=${area} await page.goto(url, { waitUntil: 'domcontentloaded' }) await page.waitForSelector('[data-qa="serp-item__title"]', { timeout: 10000 }).catch(() => null) - + await page.pause() if (!await page.$('[data-qa="profileAndResumes-button"]')) { return { ...results, error: 'Не авторизован. Выполните login' } } await status('✅ Авторизация выполнена') - const vacancies = await page.$$eval( - '[data-qa="serp-item__title"]', - links => links.map(a => ({ - href: (a as HTMLAnchorElement).href, - title: a.textContent?.trim() ?? '', - })), - ) + const vacancies = await collectPageVacancies(page) + + const pagerBlock = await page.$('[data-qa="pager-block"]') + if (pagerBlock) { + const maxPage = await page.$$eval( + '[data-qa="pager-block"] [data-qa="pager-page"]', + links => Math.max(...links.map((a) => { + const pageParam = new URL((a as HTMLAnchorElement).href).searchParams.get('page') + return Number(pageParam ?? 0) + })), + ) + console.log('[applyToJobs] Max page:', maxPage) + for (let p = 1; p <= maxPage; p++) { + const pageUrl = `https://hh.ru/search/vacancy?text=${encodeURIComponent(query)}&items_on_page=50&page=${p}` + await page.goto(pageUrl, { waitUntil: 'domcontentloaded' }) + await page.waitForSelector('[data-qa="vacancy-serp__vacancy"]', { timeout: 10000 }).catch(() => null) + const more = await collectPageVacancies(page) + vacancies.push(...more) + } + } await keep(`✅ Вакансий найдено: ${vacancies.length}`) @@ -298,6 +329,10 @@ export async function applyToJobs( await applyBtn.click() + const relocationConfirm = await page.waitForSelector('[data-qa="relocation-warning-confirm"]', { timeout: 2000 }).catch(() => null) + if (relocationConfirm) + await relocationConfirm.click() + if (await skipIfQuestionnaire(page, vacancy, ref, chatId, status, results)) continue