Веб-скрейпинг органических результатов Google Jobs с помощью Nodejs


Что будет соскоблено

Полный код

Если вам не нужны объяснения, посмотрите пример полного кода в онлайн 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🐞

Оцените статью
devanswers.ru
Добавить комментарий