Что будет соскоблено
Полный код
Если вам не нужны объяснения, посмотрите пример полного кода в онлайн IDE
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
puppeteer.use(StealthPlugin());
const searchString = "javascript developer"; // what we want to search
const encodedString = encodeURI(searchString); // what we want to search for in URI encoding
const requestParams = {
q: encodedString, // our encoded search string
hl: "en", // parameter defines the language to use for the Google search
uule: "w+CAIQICIKY2FsaWZvcm5pYQ", // encoded location
};
const domain = `https://www.google.com`;
async function scrollPage(page, scrollContainer) {
let lastHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
while (true) {
await page.evaluate(`document.querySelector("${scrollContainer}").scrollTo(0, document.querySelector("${scrollContainer}").scrollHeight)`);
await page.waitForTimeout(2000);
let newHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
if (newHeight === lastHeight) {
break;
}
lastHeight = newHeight;
}
}
async function fillInfoFromPage(page) {
return await page.evaluate(async () => {
return Array.from(document.querySelectorAll(".iFjolb")).map((el) => ({
title: el.querySelector(".BjJfJf").textContent.trim(),
companyName: el.querySelector(".vNEEBe").textContent.trim(),
location: el.querySelectorAll(".Qk80Jf")[0].textContent.trim(),
via: el.querySelectorAll(".Qk80Jf")[1].textContent.trim(),
thumbnail: el.querySelector(".pJ3Uqf img")?.getAttribute("src"),
extensions: Array.from(el.querySelectorAll(".oNwCmf .I2Cbhb .LL4CDc")).map((el) => el.textContent.trim()),
}));
});
}
async function getJobsInfo() {
const browser = await puppeteer.launch({
headless: false,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const URL = `${domain}/search?ibp=htl;jobs&hl=${requestParams.hl}&q=${requestParams.q}&uule=${requestParams.uule}`;
await page.setDefaultNavigationTimeout(60000);
await page.goto(URL);
await page.waitForSelector(".iFjolb");
await page.waitForTimeout(1000);
await scrollPage(page, ".zxU94d");
const jobs = await fillInfoFromPage(page);
await browser.close();
return jobs;
}
getJobsInfo().then((result) => console.dir(result, { depth: null }));
Подготовка
Сначала нам нужно создать проект Node.js* и добавить пакеты npm
puppeteer
, puppeteer-extra
и puppeteer-extra-plugin-stealth
для управления Chromium (или Chrome, или Firefox, но сейчас мы работаем только с Chromium, который используется по умолчанию) через протокол DevTools в безголовом или неголовом режиме.
Для этого в директории с нашим проектом откройте командную строку и введите npm init -y
, а затем npm i puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
.
*Если у вас не установлен Node.js, вы можете загрузить его с сайта nodejs.org и следовать документации по установке.
📌Примечание: также вы можете использовать puppeteer
без каких-либо расширений, но я настоятельно рекомендую использовать его с puppeteer-extra
с puppeteer-extra-plugin-stealth
для предотвращения обнаружения веб-сайтом того, что вы используете безголовый Chromium или что вы используете веб-драйвер. Вы можете проверить это на сайте тестов безголового Chrome. На скриншоте ниже показана разница.
Процесс
Прежде всего, нам нужно прокрутить все объявления о работе, пока не останется ни одного загруженного объявления, что является сложной частью, описанной ниже.
Следующий шаг — извлечение данных из HTML-элементов после завершения прокрутки. Процесс получения нужных CSS-селекторов довольно прост с помощью расширения SelectorGadget Chrome, которое позволяет нам получать CSS-селекторы, щелкая по нужному элементу в браузере. Однако это не всегда работает идеально, особенно когда на сайте активно используется JavaScript.
У нас есть отдельная статья в блоге SerpApi, посвященная веб-скраппингу с CSS-селекторами, если вы хотите узнать о них немного больше.
Приведенный ниже рисунок иллюстрирует подход к выбору различных частей результатов.
Объяснение кода
Объявите puppeteer
для управления браузером Chromium из библиотеки puppeteer-extra
и StealthPlugin
для предотвращения обнаружения веб-сайтом того, что вы используете веб-драйвер из библиотеки puppeteer-extra-plugin-stealth
:
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
Далее мы «говорим» puppeteer
использовать StealthPlugin
, пишем, что мы хотим искать и кодируем это в строку URI:
puppeteer.use(StealthPlugin());
const searchString = "javascript developer"; // what we want to search
const encodedString = encodeURI(searchString); // what we want to search for in URI encoding
Далее пишем необходимые параметры запроса и URL домена Google:
📌Примечание: параметр uule
— это закодированный параметр местоположения. Вы можете сделать его с помощью UULE Generator for Google.
const requestParams = {
q: encodedString, // our encoded search string
hl: "en", // parameter defines the language to use for the Google search
uule: "w+CAIQICIKY2FsaWZvcm5pYQ", // encoded location
};
const domain = `https://www.google.com`;
Далее мы напишем функцию для прокрутки страницы, чтобы загрузить все статьи:
async function scrollPage(page, scrollContainer) {
...
}
В этой функции сначала нужно получить высоту scrollContainer
(используя метод evaluate()
). Затем мы используем цикл while
, в котором мы прокручиваем вниз scrollContainer
, ждем 2 секунды (используя метод waitForTimeout
) и получаем новую высоту scrollContainer
.
Далее мы проверяем, если newHeight
равна lastHeight
, то останавливаем цикл. В противном случае мы определяем значение newHeight
в переменную lastHeight
и повторяем снова, пока страница не будет прокручена вниз до конца:
let lastHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
while (true) {
await page.evaluate(`document.querySelector("${scrollContainer}").scrollTo(0, document.querySelector("${scrollContainer}").scrollHeight)`);
await page.waitForTimeout(2000);
let newHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
if (newHeight === lastHeight) {
break;
}
lastHeight = newHeight;
}
Далее мы напишем функцию для получения данных о заданиях со страницы:
async function fillInfoFromPage(page) {
...
}
В этой функции мы получаем информацию из контекста страницы и сохраняем ее в возвращаемом массиве. Сначала нам нужно получить все результаты заданий, доступные на странице (метод querySelectorAll()
), и создать новый массив из полученного NodeList (Array.from()
):
return await page.evaluate(async () => {
return Array.from(document.querySelectorAll(".iFjolb")).map((el) => ({
Далее мы присваиваем необходимые данные ключу каждого объекта. Мы можем сделать это с помощью методов textContent
и trim()
, которые получают необработанный текст и удаляют белое пространство с обеих сторон строки. Если нам нужно получить ссылки, мы используем метод getAttribute()
для получения атрибутов элемента HTML "src"
:
title: el.querySelector(".BjJfJf").textContent.trim(),
companyName: el.querySelector(".vNEEBe").textContent.trim(),
location: el.querySelectorAll(".Qk80Jf")[0].textContent.trim(),
via: el.querySelectorAll(".Qk80Jf")[1].textContent.trim(),
thumbnail: el.querySelector(".pJ3Uqf img")?.getAttribute("src"),
extensions: Array.from(el.querySelectorAll(".oNwCmf .I2Cbhb .LL4CDc")).map((el) => el.textContent.trim()),
Далее напишем функцию для управления браузером и получения информации:
async function getJobsInfo() {
...
}
В этой функции сначала нужно определить browser
, используя метод puppeteer.launch({options})
с текущими options
, такими как headless: false
и args: ["--no-sandbox", "--disable-setuid-sandbox"]
.
Эти опции означают, что мы используем режим headless и массив с аргументами, которые мы используем для разрешения запуска процесса браузера в онлайн IDE. Затем мы открываем новую страницу
:
const browser = await puppeteer.launch({
headless: false,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
Далее мы определяем URL полного запроса, изменяем время ожидания селекторов по умолчанию (30 сек) на 60000 мс (1 мин) для медленного интернет-соединения с помощью метода .setDefaultNavigationTimeout()
, переходим на URL
с помощью метода .goto()
и используем метод .waitForSelector()
для ожидания загрузки селектора:
const URL = `${domain}/search?ibp=htl;jobs&hl=${requestParams.hl}&q=${requestParams.q}&uule=${requestParams.uule}`;
await page.setDefaultNavigationTimeout(60000);
await page.goto(URL);
await page.waitForSelector(".iFjolb");
И, наконец, мы ждем, пока страница будет прокручена, сохраняем данные о заданиях со страницы в константе jobs
, закрываем браузер и возвращаем полученные данные:
await scrollPage(page, ".zxU94d");
const jobs = await fillInfoFromPage(page);
await browser.close();
return jobs;
Теперь мы можем запустить наш парсер:
$ node YOUR_FILE_NAME # YOUR_FILE_NAME is the name of your .js file
Выход
[
{
"title":"Python Developer Python-JavaScript and vue.js",
"companyName":"Dice",
"location":"San Francisco, CA",
"via":"via LinkedIn",
"thumbnail":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQKlgydP7sElaJC9qPrtNHwBhyTMHYgii1RPWsy&s=0",
"extensions":[
"5 days ago",
"Contractor"
]
},
{
"title":"Remote Senior JavaScript Developer",
"companyName":"Jobot",
"location":"Las Vegas, NV",
"via":"via Central Illinois Proud Jobs",
"extensions":[
"4 days ago",
"Full-time",
"No degree mentioned"
]
},
... and other results
]
Использование API Google Jobs из SerpApi
Этот раздел предназначен для того, чтобы показать сравнение между DIY решением и нашим решением.
Самое большое отличие заключается в том, что вам не нужно использовать автоматизацию браузера для соскабливания результатов, создавать парсер с нуля и поддерживать его.
Существует также вероятность того, что запрос может быть заблокирован в какой-то момент Google, мы обрабатываем это на нашем бэкенде, поэтому нет необходимости выяснять, как сделать это самостоятельно или выяснять, какой CAPTCHA, прокси-провайдер использовать.
Сначала нам нужно установить google-search-results-nodejs
:
npm i google-search-results-nodejs
Вот полный пример кода, если вам не нужно объяснение:
const SerpApi = require("google-search-results-nodejs");
const search = new SerpApi.GoogleSearch(process.env.API_KEY);
const searchString = "javascript developer"; // what we want to search
const params = {
engine: "google_jobs", // search engine
q: searchString, // search query
hl: "en", // Parameter defines the language to use for the Google search
uule: "w+CAIQICIKY2FsaWZvcm5pYQ", // encoded location
};
const getJson = () => {
return new Promise((resolve) => {
search.json(params, resolve);
});
};
const getResults = async () => {
const organicResults = [];
while (true) {
const json = await getJson();
if (json.search_information?.jobs_results_state === "Fully empty") break;
organicResults.push(...json.jobs_results);
params.start ? (params.start += 10) : (params.start = 10);
}
return organicResults;
};
getResults().then((result) => console.dir(result, { depth: null }));
Объяснение кода
Во-первых, нам нужно объявить SerpApi
из библиотеки google-search-results-nodejs
и определить новый экземпляр search
с вашим ключом API из SerpApi:
const SerpApi = require("google-search-results-nodejs");
const search = new SerpApi.GoogleSearch(API_KEY);
Далее пишем поисковый запрос и необходимые параметры для выполнения запроса:
📌Примечание: параметр uule
— это закодированный параметр местоположения. Вы можете сделать его с помощью UULE Generator for Google.
const searchString = "javascript developer"; // what we want to search
const params = {
engine: "google_jobs", // search engine
q: searchString, // search query
hl: "en", // Parameter defines the language to use for the Google search
uule: "w+CAIQICIKY2FsaWZvcm5pYQ", // encoded location
};
Далее мы обернем метод поиска из библиотеки SerpApi в обещание для дальнейшей работы с результатами поиска:
const getJson = () => {
return new Promise((resolve) => {
search.json(params, resolve);
});
};
И, наконец, мы объявляем функцию getResult
, которая получает данные со страницы и возвращает их:
const getResults = async () => {
...
};
В этой функции сначала объявляется массив organicResults
с данными результатов:
const organicResults = [];
Далее нам нужно использовать цикл while
. В этом цикле мы получаем json
с результатами, проверяем, присутствуют ли результаты на странице (jobs_results_state
не "Fully empty"
), заносим результаты в массив organicResults
, определяем начальный номер на странице результатов и повторяем цикл до тех пор, пока результаты не будут отсутствовать на странице:
while (true) {
const json = await getJson();
if (json.search_information?.jobs_results_state === "Fully empty") break;
organicResults.push(...json.jobs_results);
params.start ? (params.start += 10) : (params.start = 10);
}
return organicResults;
После этого запускаем функцию getResults
и выводим всю полученную информацию в консоль с помощью метода console.dir
, который позволяет использовать объект с необходимыми параметрами для изменения параметров вывода по умолчанию:
getResults().then((result) => console.dir(result, { depth: null }));
Вывод
[
{
"title": "Python Developer Python-JavaScript and vue.js",
"company_name": "Dice",
"location": "San Francisco, CA",
"via": "via LinkedIn",
"description": "Dice is the leading career destination for tech experts at every stage of their careers. Our client, Mitchell Martin, Inc., is seeking the following. Apply via Dice today!\n\nPython Developer Python-JavaScript and vue.js...\n\nPosition Type: Contract\n\nJob responsibilities:\n\nAs a member of the Company Bioinformatics team, you will work closely with other Bioinformatics developers and laboratory staff to provide technical leadership, and develop & deploy workflows for our laboratory LIMS that enable automated high throughput workflows for our DNA sequencing laboratories.\n• Develop and deploy software that manages the operational activities in our specialty genetics laboratories\n• Ensure availability, performance, and scalability of workflows\n• Work closely with product owners, software engineers and R&D scientists to gather and implement requirements\n• Build and maintain code that interacts with a 3rd party vendor application\n• Guide and mentors other engineers and project team members\n\nRequired Skills and Qualifications 5+ years of experience Python, JavaScript and vue.js\n• Proficient in Python, JavaScript and Vue.js Experience in using version control tools, e.g., Gitlab\n• B.S. in Bioengineering, Computer Science, MS/PhD preferred\n• 3+ years of experience working in a regulated industrial life sciences environment or equivalent\n• 5+ years of experience Python, JavaScript and vue.js\n• Experienced in using version control tools, e.g., Gitlab\n• Familiar with working in a Linux environment\n• Familiar with writing unit tests\n• Familiarity with typical laboratory workflows and robotic automation used by DNA sequencing laboratories is a plus\n• Knowledge of L7 ESP LIMS is a plus\n• Demonstrated ability to work with vendor APIs (or file-based communication) for integration and development\n• Experience to develop APIs in MuleSoft is a plus\n• Experience supporting and maintaining applications that interact with 3rd party Vendor software\n• Demonstrated ability to work in a team and communicate effectively with laboratory personal and R&D scientists\n• Proficient in Python, JavaScript and Vue.js Experience in using version control tools, e.g., Gitlab\n• provided by Dice",
"thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTchgwk0qIvqPnMlAcqO5451PRYsMDccWFDcD5pGeE&s",
"extensions": ["5 days ago", "Contractor"],
"detected_extensions": {
"posted_at": "5 days ago",
"schedule_type": "Contractor"
},
"job_id": "eyJqb2JfdGl0bGUiOiJQeXRob24gRGV2ZWxvcGVyIFB5dGhvbi1KYXZhU2NyaXB0IGFuZCB2dWUuanMiLCJodGlkb2NpZCI6InVvWXpSMGhPWjZvQUFBQUFBQUFBQUE9PSIsInV1bGUiOiJ3K0NBSVFJQ0lLWTJGc2FXWnZjbTVwWVEiLCJobCI6ImVuIiwiZmMiOiJFcUlDQ3VJQlFVRjBWbXhpUVV0V1kyZFRiWGszZWxwcmNHWnBjVmswTVhCUk9EQkVUems1VkVocWJFWmtWRXBFWHpNNVIzSjFVMkZCZVZoU1FVNTFSVmhDTUhZd2NGZFpRVTVvTTFGWWVtUk5WbnBmZDFOTWJUazBVblJqV21OcVlXb3RVMUpFU0VSck5GWnNWV0l6TjA1NE5XMWhiMnQyUmpWd1UxODViR042YVV0QmJsUTVTalJ2YzFWaFMwSlVNM2xHUWpFdGNGcEllVkpzUWpWeVRGQlRSbDl2Y1VsMlh6TlNkaTFIZFZCWU9WVm1SaTFNV0hkMlpTMDJjVGRqWWxaaU16Rk9jakl0YVZvMVJISnhla2hXWkZkT1dGOVdjRkpGZVRCNlkzUlNSMVF6VHpadVFSSVhNelYzVlZrMVlsWkVjbGN4Y1hSelVHdFBkVTF0UVVrYUlrRkVWWGxGUjJSdE5FUlVNaTFxUkdWbmRHbHBObWhZY1VOcmNYQXdOSGhhVmxFIiwiZmN2IjoiMyIsImZjX2lkIjoiZmNfMSIsImFwcGx5X2xpbmsiOnsidGl0bGUiOiIubkZnMmVie2ZvbnQtd2VpZ2h0OjUwMH0uQmk2RGRje2ZvbnQtd2VpZ2h0OjUwMH1BcHBseSBvbiBMaW5rZWRJbiIsImxpbmsiOiJodHRwczovL3d3dy5saW5rZWRpbi5jb20vam9icy92aWV3L3B5dGhvbi1kZXZlbG9wZXItcHl0aG9uLWphdmFzY3JpcHQtYW5kLXZ1ZS1qcy1hdC1kaWNlLTMyNDU2NzQxMTU/dXRtX2NhbXBhaWduPWdvb2dsZV9qb2JzX2FwcGx5XHUwMDI2dXRtX3NvdXJjZT1nb29nbGVfam9ic19hcHBseVx1MDAyNnV0bV9tZWRpdW09b3JnYW5pYyJ9fQ=="
},
{
"title": "Staff JavaScript Developer - 50% REMOTE",
"company_name": "Jobot",
"location": "Los Angeles, CA",
"via": "via KTLA Jobs",
"description": "Growing technology company in Cambridge, MA looking for a sharp Senior JavaScript Developer to join their growing team!\n\nThis Jobot Job is hosted by Roxy Kupfert...\n\nAre you a fit? Easy Apply now by clicking the Apply button and sending us your resume.\n\nSalary $120,000 - $220,000 per year\n\nA Bit About Us\n\nLocated in Cambridge, MA we are a rapidly growing company in the internet technology space. We are looking for a sharp Senior JavaScript Developer to join our team and hit the ground running!\n\nWhy join us?\n\nWe offer a comprehensive compensation package including but not limited to\n• A highly competitive base salary ranging from $120K-$220K + EQUITY + BONUSES!\n• Full benefits (Medical, Dental, Vision)\n• 401K with match\n• Great work/life balance - ability to work partially remote / partially in the office\n• Opportunity to work alongside other brilliant engineers\n• Flexible work schedule\n• Catered lunches\n• Paid gym membership\n• Foosball and Ping Pong tables\nJob Details\n• Integrating user components on server-side JavaScript\n• Building performant applications with high availability and low latency\n• Ensuring security, accessibility, and privacy concerns are handled\n• Writing maintainable code with extensive test coverage, including load tests\nMUST HAVE, experience with\n• Modern JavaScript\n• React and/or Redux\n• Developing well-structured, performant web applications with component-based architectures\nNICE TO HAVE, experience with\n• Security and data concerns such as privacy, data integrity, etc.\n• Node.js\n• Containerization / cloud environments\n• REST, JSON, API design and micro-services\n• Common UX patterns, accessibility, and cross-browser, cross-device implementations.\n• Understanding of algorithms, data structures and design patterns\n• CI/CD pipelines\nIf this sounds like you, please apply through the link or email your resume directly to roxy.kupfert@!\n\nInterested in hearing more? Easy Apply now by clicking the Apply button",
"extensions": ["4 days ago", "Full-time", "No degree mentioned", "Health insurance", "Dental insurance"],
"detected_extensions": {
"posted_at": "4 days ago",
"schedule_type": "Full-time"
},
"job_id": "eyJqb2JfdGl0bGUiOiJTdGFmZiBKYXZhU2NyaXB0IERldmVsb3BlciAtIDUwJSBSRU1PVEUiLCJodGlkb2NpZCI6InZsVmN0d2s5RklFQUFBQUFBQUFBQUE9PSIsInV1bGUiOiJ3K0NBSVFJQ0lLWTJGc2FXWnZjbTVwWVEiLCJobCI6ImVuIiwiZmMiOiJFb3dDQ3N3QlFVRjBWbXhpUTJoYVYwMUhiWHB6Y1hwYVNrZDRTRzVUZVdaUkxVNUpRWFZvUTJGV01XZFNRMVZ1V0cxcVJETjZjVGMwZURsMVMyaEhZM2x3ZFVSaVRXZDVjREJGVmt4TU9GQklhR050ZFVzNFFtODFTWEJJVDNwcVJGRndkSE5aVGkxVmRuZzVaRU5UU0RaWVJsaEpZVXB4Tm5WTWJURllUbTF2Wm1WMmFGQkxURjlZVTFCeVNISkRUVFZ1TjA1UE9FeHliWFZ2Ym1acmR6TlplblpzVWpJd2NXZExaVzVhY2xrMFVrSlVheTAzZFY5T1ZFcGhSMDh4WjFkSmFWWmtUMkZGYlVaNFVVVkpZblZCRWhjek5YZFZXVFZpVmtSeVZ6RnhkSE5RYTA5MVRXMUJTUm9pUVVSVmVVVkhabDlNZFZSb2RrZ3dRek56Y0ZaSFkxQTFiek5sZW13eE9IUldVUSIsImZjdiI6IjMiLCJmY19pZCI6ImZjXzMiLCJhcHBseV9saW5rIjp7InRpdGxlIjoiQXBwbHkgb24gS1RMQSBKb2JzIiwibGluayI6Imh0dHBzOi8vam9icy5rdGxhLmNvbS9qb2JzL3N0YWZmLWphdmFzY3JpcHQtZGV2ZWxvcGVyLTUwLXJlbW90ZS1sb3MtYW5nZWxlcy1jYWxpZm9ybmlhLzY5OTIyMTQ2My0yLz91dG1fY2FtcGFpZ249Z29vZ2xlX2pvYnNfYXBwbHlcdTAwMjZ1dG1fc291cmNlPWdvb2dsZV9qb2JzX2FwcGx5XHUwMDI2dXRtX21lZGl1bT1vcmdhbmljIn19"
}
]
Ссылки
- Код в онлайн IDE
- API Google Jobs
Если вы хотите увидеть некоторые проекты, сделанные с помощью SerpApi, пожалуйста, напишите мне сообщение.
Присоединяйтесь к нам на Twitter | YouTube
Add a Feature Request💫 or a Bug🐞