Веб-скрейпинг страницы видео YouTube с помощью Nodejs


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

Полный код

Если вам не нужны объяснения, посмотрите пример полного кода в онлайн IDE

const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");

puppeteer.use(StealthPlugin());

const videoLink = "https://www.youtube.com/watch?v=fou37kNbsqE"; // link to video page

async function scrollPage(page, scrollContainer) {
  let lastHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
  while (true) {
    await page.evaluate(`window.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 fillDataFromPage(page, newDesign) {
  const dataFromPage = await page.evaluate((newDesign) => {
    const date = document
      .querySelector(newDesign ? "#description-inline-expander > yt-formatted-string span:nth-child(3)" : "#info-strings yt-formatted-string")
      ?.textContent.trim();
    const views = document
      .querySelector(newDesign ? "#description-inline-expander > yt-formatted-string span:nth-child(1)" : "#info-text #count")
      ?.textContent.trim();
    return {
      title: document.querySelector(`${newDesign ? "#title >" : "#info-contents"} h1`)?.textContent.trim(),
      likes: parseInt(
        document
          .querySelector(`${newDesign ? "#top-row" : "#menu"} #top-level-buttons-computed > ytd-toggle-button-renderer:first-child #text`)
          ?.getAttribute("aria-label")
          .replace(",", "")
      ),
      channel: {
        name: document.querySelector(`${newDesign ? "#owner" : "ytd-video-owner-renderer"} #channel-name #text > a`)?.textContent.trim(),
        link: `https://www.youtube.com${document.querySelector(`${newDesign ? "#owner" : ""} ytd-video-owner-renderer > a`)?.getAttribute("href")}`,
        thumbnail: document.querySelector(`${newDesign ? "#owner" : "ytd-video-owner-renderer"} #avatar #img`)?.getAttribute("src"),
      },
      date,
      views: views && parseInt(views.replace(",", "")),
      description: newDesign
        ? document.querySelector("#description-inline-expander > yt-formatted-string")?.textContent.replace(date, "").replace(views, "").trim()
        : document.querySelector("#meta #description")?.textContent.trim(),
      duration: document.querySelector(".ytp-time-duration")?.textContent.trim(),
      hashtags: Array.from(document.querySelectorAll(`${newDesign ? "#super-title" : "#info-contents .super-title"} a`)).map((el) =>
        el.textContent.trim()
      ),
      suggestedVideos: Array.from(document.querySelectorAll("ytd-compact-video-renderer")).map((el) => ({
        title: el.querySelector("#video-title")?.textContent.trim(),
        link: `https://www.youtube.com${el.querySelector("#thumbnail")?.getAttribute("href")}`,
        channelName: el.querySelector("#channel-name #text")?.textContent.trim(),
        date: el.querySelector("#metadata-line span:nth-child(2)")?.textContent.trim(),
        views: el.querySelector("#metadata-line span:nth-child(1)")?.textContent.trim(),
        duration: el.querySelector("#overlays #text")?.textContent.trim(),
        thumbnail: el.querySelector("#img")?.getAttribute("src"),
      })),
      comments: Array.from(document.querySelectorAll("#contents > ytd-comment-thread-renderer")).map((el) => ({
        author: el.querySelector("#author-text")?.textContent.trim(),
        link: `https://www.youtube.com${el.querySelector("#author-text")?.getAttribute("href")}`,
        date: el.querySelector(".published-time-text")?.textContent.trim(),
        likes: el.querySelector("#vote-count-middle")?.textContent.trim(),
        comment: el.querySelector("#content-text")?.textContent.trim(),
        avatar: el.querySelector("#author-thumbnail #img")?.getAttribute("src"),
      })),
    };
  }, newDesign);
  return dataFromPage;
}

async function getYoutubeVideoPageResults() {
  const browser = await puppeteer.launch({
    headless: false,
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  const page = await browser.newPage();

  await page.setDefaultNavigationTimeout(60000);
  await page.goto(videoLink);

  await page.waitForSelector("#contents");

  const isDesign1 = await page.$("#title > h1");

  if (isDesign1) {
    await page.click("#description-inline-expander #expand");
  } else {
    await page.click("#meta #more");
  }
  const scrollContainer = "ytd-app";

  await scrollPage(page, scrollContainer);

  await page.waitForTimeout(10000);

  const infoFromVideoPage = await fillDataFromPage(page, isDesign1);

  await browser.close();

  return infoFromVideoPage;
}

getYoutubeVideoPageResults().then((result) => console.dir(result, { depth: null }));
Вход в полноэкранный режим Выйти из полноэкранного режима

Подготовка

Сначала нам нужно создать проект Node.js* и добавить в него пакеты npm puppeteer, puppeteer-extra и puppeteer-extra-plugin-stealth для управления Chromium (или Chrome, или Firefox, но сейчас мы работаем только с Chromium, который используется по умолчанию) через протокол DevTools в режиме headless или non-headless.

Для этого в директории с нашим проектом откройте командную строку и введите 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. На скриншоте ниже показана разница.

Процесс

Расширение SelectorGadget Chrome использовалось для захвата селекторов CSS при нажатии на нужный элемент в браузере. Если у вас возникли трудности с пониманием этого, у нас есть специальная статья в блоге SerpApi, посвященная веб-скрапингу с CSS-селекторами.

Приведенный ниже рисунок иллюстрирует подход к выбору различных частей результатов.

Объяснение кода

Объявите константы из необходимых библиотек:

const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
Войти в полноэкранный режим Выйти из полноэкранного режима
Код Пояснение
puppeteer Библиотека управления Chromium
StealthPlugin библиотека для предотвращения обнаружения веб-сайтом того, что вы используете веб-драйвер

Далее мы «скажем» puppeteer использовать StealthPlugin и напишем ссылку на страницу с видео:

puppeteer.use(StealthPlugin());

const videoLink = "https://www.youtube.com/watch?v=fou37kNbsqE"; // link to video page
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее мы напишем функцию для прокрутки страницы. Первым шагом будет получение текущей высоты прокрутки контейнера:

async function scrollPage(page, scrollContainer) {
  let lastHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
Войти в полноэкранный режим Выйти из полноэкранного режима

После этого нам нужно прокрутить страницу до тех пор, пока не останется комментариев, используя цикл while, который позволяет нам итерировать бесконечно, пока мы не решим выйти из цикла. Он используется в сочетании с evaluate() для выполнения кода в консоли браузера:

while (true) {
    await page.evaluate(`window.scrollTo(0, document.querySelector("${scrollContainer}").scrollHeight)`);
    await page.waitForTimeout(2000);  // waiting 2000 ms before continue
Вход в полноэкранный режим Выход из полноэкранного режима

Последним шагом будет проверка, совпадает ли текущая высота с предыдущей, и если да, то выход из цикла прокрутки. В противном случае обновите текущую высоту на предыдущую и продолжите прокрутку:

    let newHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
    if (newHeight === lastHeight) {
      break;
    }
    lastHeight = newHeight;
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Далее мы напишем функцию для получения данных со страницы видео:

async function fillDataFromPage(page, newDesign) {
  ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Далее в fillDataFromPage пишем функцию evaluate и передаем в нее переменную newDesign, чтобы использовать ее в контексте страницы:

  const dataFromPage = await page.evaluate((newDesign) => {
    ...
  }, newDesign);
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем в функции evaluate мы напишем код для получения date и views отдельно, потому что эти данные понадобятся нам в будущем:

    const date = document
      .querySelector(newDesign ? "#description-inline-expander > yt-formatted-string span:nth-child(3)" : "#info-strings yt-formatted-string")
      ?.textContent.trim();
    const views = document
      .querySelector(newDesign ? "#description-inline-expander > yt-formatted-string span:nth-child(1)" : "#info-text #count")
      ?.textContent.trim();
Войти в полноэкранный режим Выход из полноэкранного режима
Код Пояснение
document.querySelector("someSelector") возвращает первый html элемент с селектором someSelector, который является любым дочерним элементом html элемента document
.textContent получает необработанный текст html-элемента
.trim() удаляет пробельные символы с обоих концов строки.

Далее мы получаем title и likes, используя метод .querySelector() документа и метод .getAttribute("aria-label") селектора, который был найден:

    return {
      title: document.querySelector(`${newDesign ? "#title >" : "#info-contents"} h1`)?.textContent.trim(),
      likes: parseInt(
        document
          .querySelector(`${newDesign ? "#top-row" : "#menu"} #top-level-buttons-computed > ytd-toggle-button-renderer:first-child #text`)
          ?.getAttribute("aria-label")
          .replace(",", "")
      ),
Вход в полноэкранный режим Выйти из полноэкранного режима

После этого мы получаем информацию о канале, а именно name, link и thumbnail:

      channel: {
        name: document.querySelector(`${newDesign ? "#owner" : "ytd-video-owner-renderer"} #channel-name #text > a`)?.textContent.trim(),
        link: `https://www.youtube.com${document.querySelector(`${newDesign ? "#owner" : ""} ytd-video-owner-renderer > a`)?.getAttribute("href")}`,
        thumbnail: document.querySelector(`${newDesign ? "#owner" : "ytd-video-owner-renderer"} #avatar #img`)?.getAttribute("src"),
      },
Вход в полноэкранный режим Выход из полноэкранного режима

Далее мы запишем date и views, которые были получены ранее, и вернем их из функции evaluate. Также необходимо удалить date и views из строки description, полученной из нового дизайна страницы с этими полями:

      date,
      views: views && parseInt(views.replace(",", "")),
      description: newDesign
        ? document.querySelector("#description-inline-expander > yt-formatted-string")?.textContent.replace(date, "").replace(views, "").trim()
        : document.querySelector("#meta #description")?.textContent.trim(),
Войти в полноэкранный режим Выход из полноэкранного режима

Далее мы получаем duration и hashtags. Для получения hashtags необходимо использовать метод .querySelectorAll(), который возвращает статический NodeList, представляющий список элементов документа, соответствующих css-селекторам в скобках, и преобразовать результат в массив с помощью метода Array.from():

      duration: document.querySelector(".ytp-time-duration")?.textContent.trim(),
      hashtags: Array.from(document.querySelectorAll(`${newDesign ? "#super-title" : "#info-contents .super-title"} a`)).map((el) =>
        el.textContent.trim()
      ),
Вход в полноэкранный режим Выход из полноэкранного режима

Затем нужно получить информацию suggestedVideos, которая состоит из title, link, channelName, date, views, duration и thumbnail:

      suggestedVideos: Array.from(document.querySelectorAll("ytd-compact-video-renderer")).map((el) => ({
        title: el.querySelector("#video-title")?.textContent.trim(),
        link: `https://www.youtube.com${el.querySelector("#thumbnail")?.getAttribute("href")}`,
        channelName: el.querySelector("#channel-name #text")?.textContent.trim(),
        date: el.querySelector("#metadata-line span:nth-child(2)")?.textContent.trim(),
        views: el.querySelector("#metadata-line span:nth-child(1)")?.textContent.trim(),
        duration: el.querySelector("#overlays #text")?.textContent.trim(),
        thumbnail: el.querySelector("#img")?.getAttribute("src"),
      })),
Войти в полноэкранный режим Выйти из полноэкранного режима

И последнее, мы получаем все комментарии с полной информацией (author, link, date, likes, comment и avatar):

      comments: Array.from(document.querySelectorAll("#contents > ytd-comment-thread-renderer")).map((el) => ({
        author: el.querySelector("#author-text")?.textContent.trim(),
        link: `https://www.youtube.com${el.querySelector("#author-text")?.getAttribute("href")}`,
        date: el.querySelector(".published-time-text")?.textContent.trim(),
        likes: el.querySelector("#vote-count-middle")?.textContent.trim(),
        comment: el.querySelector("#content-text")?.textContent.trim(),
        avatar: el.querySelector("#author-thumbnail #img")?.getAttribute("src"),
      })),
    };
Войдите в полноэкранный режим Выход из полноэкранного режима

Далее напишите функцию для управления браузером и получения информации:

async function getYoutubeVideoPageResults() {
  ...
}
Вход в полноэкранный режим Выйти из полноэкранного режима

В этой функции сначала нужно определить 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();
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее мы изменяем время ожидания селекторов по умолчанию (30 сек) на 60000 мс (1 мин) для медленного интернет-соединения с помощью метода .setDefaultNavigationTimeout() и переходим по URL videoLink с помощью метода .goto():

  await page.setDefaultNavigationTimeout(60000);
  await page.goto(videoLink);
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы используем метод .waitForSelector(), чтобы дождаться создания селектора #contents на странице. Далее мы пытаемся найти селектор #title > h1 и сохранить его в константе isDesign1 с помощью метода .$(), чтобы нажать (метод .click()) на нужный селектор кнопки show more:

  await page.waitForSelector("#contents");

  const isDesign1 = await page.$("#title > h1");

  if (isDesign1) {
    await page.click("#description-inline-expander #expand");
  } else {
    await page.click("#meta #more");
  }
Вход в полноэкранный режим Выход из полноэкранного режима
  const scrollContainer = "ytd-app";

  await scrollPage(page, scrollContainer);

  await page.waitForTimeout(10000);
Войти в полноэкранный режим Выход из полноэкранного режима

И наконец, мы получаем и возвращаем данные со страницы и закрываем браузер:

  const infoFromVideoPage = await fillDataFromPage(page, isDesign1);

  await browser.close();

  return infoFromVideoPage;
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем запустить наш парсер. Для этого введите node YOUR_FILE_NAME в командную строку. Где YOUR_FILE_NAME — это имя вашего файла .js.

Вывод

{
   "title":"The Life of Luke Skywalker • Entire Timeline Explained (Star Wars)",
   "likes":14699,
   "channel":{
      "name":"MovieFlame",
      "link":"https://www.youtube.com/c/MovieFlame",
      "thumbnail":"https://yt3.ggpht.com/ytc/AMLnZu86EFuWtLin_e9RrleT2PJVyFBMA6u9-QcI7calxQ=s48-c-k-c0x00ffffff-no-rj"
   },
   "date":"Jan 8, 2020",
   "views":708814,
   "description":"Patreon: https://www.patreon.com/MovieFlamePro...n""+""Twitter: https://twitter.com/MovieFlameProdn""+""Personal Instagram: https://www.instagram.com/morgan_ross18/n""+""Facebook: https://www.facebook.com/MovieFlame/n""+""n""+""Music- By Ross Bugden https://www.youtube.com/watch?v=9qk-v...",
   "duration":"28:02",
   "hashtags":[

   ],
   "suggestedVideos":[
      {
         "title":"The Life of Obi-Wan Kenobi Explained (Padawan, Clone Wars & Tatooine Years)",
         "link":"https://www.youtube.com/watch?v=2uKLSAyNNQY",
         "channelName":"MovieFlame",
         "date":"4 years ago",
         "views":"2.3M views",
         "duration":"18:23",
         "thumbnail":"https://i.ytimg.com/vi/2uKLSAyNNQY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCAa04Nks-1bkpApP2bnvPUI48sjg"
      },
        ... and other suggested videos
   ],
   "comments":[
      {
         "author":"MovieFlame",
         "link":"https://www.youtube.com/channel/UCOajpsI8t3Eg-u-s2j_c-cQ",
         "date":"2 years ago (edited)",
         "likes":"765",
         "comment":"Boy did this video take a lot of hard work and a ton of research PLEASE LIKE AND SHARE so my hard work pays off! You guys are the best! :)",
         "avatar":"https://yt3.ggpht.com/ytc/AMLnZu86EFuWtLin_e9RrleT2PJVyFBMA6u9-QcI7calxQ=s48-c-k-c0x00ffffff-no-rj"
      },
        ... and other comments
   ]
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Если вы хотите увидеть некоторые проекты, сделанные с помощью SerpApi, пожалуйста, напишите мне сообщение.


Присоединяйтесь к нам на Twitter | YouTube

Add a Feature Request💫 or a Bug🐞

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