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