Я пытаюсь научиться использовать Puppeteer для очистки страницы Reddit. Новый Reddit имеет динамически добавляемый контент и бесконечную прокрутку. Я получаю довольно противоречивые результаты от кода и с трудом отлаживаю и выясняю, как заставить это работать.
Основной файл server.js, здесь мало что происходит.
'use strict';
const express = require('express');
const cors = require('cors');
const app = express();
const cheerio = require('./redditScraper');
app.use(express.json());
app.use(
cors({
origin: ['http://localhost:3000']
})
);
app.get('/', (req, res) => {
let { dynamicScraper } = cheerio;
dynamicScraper()
.then(html => {
console.info('data was sent');
res.json(html);
})
.catch(err => {
console.info(err);
});
});
app.listen(process.env.PORT || 8080, () => {
console.info(`Your app is listening on port ${process.env.PORT || 8080}`);
});
Файл со скребком
'use strict';
const rp = require('request-promise');
const $ = require('cheerio');
const puppeteer = require('puppeteer');
const url2 = 'https://www.reddit.com/r/GameDeals/';
const cheerio = {
dynamicScraper: function() {
return puppeteer
.launch()
.then(browser => {
return browser.newPage();
})
.then(page => {
return page.goto(url2)
.then(() => {
//attempting to scroll through page to cause loading in more elements, doesn't seem to work
page.evaluate(() => {
window.scrollBy(0, window.innerHeight);
})
return page.content()
});
})
.then(html => {
//should log the the first post's a tag's href value
console.info($('.scrollerItem div:nth-of-type(2) article div div:nth-of-type(3) a', html).attr('href'));
const urls = [];
//should be the total number of a tag's across all posts
const numLinks = $('.scrollerItem div:nth-of-type(2) article div div:nth-of-type(3) a', html).attr('href').length;
const links = $('.scrollerItem div:nth-of-type(2) article div div:nth-of-type(3) a', html);
//was using numLinks as increment counter but was getting undefined, as the pages only seems to load inconsistent between 10-20 elements
for (let i=0; i<10; i++) {
urls.push(links[i].attribs.href);
}
console.info('start of urls:', urls);
console.info('scraped urls:', urls.length);
console.info('intended number of urls to be scraped:', numLinks);
// console.info($('.scrollerItem div:nth-of-type(2) article div div:nth-of-type(3) a', html).length);
})
.catch(err => console.info(err));
}
}
module.exports = cheerio;
Приведенный выше код в настоящее время работает, однако, как вы можете видеть из комментариев, у меня счетчик жестко запрограммирован на 10, что, очевидно, не является общим количеством <a href=#> на странице.
Вот вывод для вышеизложенного:
[nodemon] starting `node server.js`
Your app is listening on port 8080
https://www.gamebillet.com/garfield-kart
start of urls: [ 'https://www.gamebillet.com/garfield-kart',
'https://www.humblebundle.com/store/deep-rock-galactic?hmb_source=humble_home&hmb_medium=product_tile&hmb_campaign=mosaic_section_1_layout_index_9_layout_type_twos_tile_index_1', 'https://www.greenmangaming.com/games/batman-arkham-asylum-game-of-the-year/',
'https://www.humblebundle.com/store/ftl-faster-than-light',
'https://www.greenmangaming.com/vip/vip-deals/',
'https://store.steampowered.com/app/320300/',
'https://store.steampowered.com/app/356650/Deaths_Gambit/',
'https://www.chrono.gg/?=Turmoil',
'https://www.fanatical.com/en/game/slain',
'https://freebies.indiegala.com/super-destronaut/?dev_id=freebies' ]
scraped urls: 10
numLinks: 40
data was sent
Вот результат, когда цикл for изменен на numlinks
for (let i=0; i<numLinks; i++) {
urls.push(links[i].attribs.href);
}
[nodemon] starting `node server.js`
Your app is listening on port 8080
https://www.gamebillet.com/garfield-kart
TypeError: Cannot read property 'attribs' of undefined
at puppeteer.launch.then.then.then.html (/file.js:49:40)
at process._tickCallback (internal/process/next_tick.js:68:7)
data was sent
Я надеюсь, что это не слишком большой беспорядок, чтобы читать. Буду признателен за любую помощь. Спасибо.
Я пытаюсь реализовать асинхронный способ, но не знаю, как вернуть значение, которое будет использоваться в обработчике маршрута?
dynamicScraper: function() {
async function f() {
const browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.goto(url2, { waitUntil: 'networkidle0' });
const links = await page.evaluate(async () => {
const scrollfar = document.body.clientHeight;
console.info(scrollfar); //trying to find the height
window.scrollBy(0, scrollfar);
await new Promise(resolve => setTimeout(resolve, 5000));
return [...document.querySelectorAll('.scrollerItem div:nth-of-type(2) article div div:nth-of-type(3) a')]
.map((el) => el.href);
});
console.info(links, links.length);
await browser.close();
//how do I return the value to pass to the route handler?
return (links);
};
return(f());
}
Я получаю от console.info.
Your app is listening on port 8080
[ 'https://www.nintendo.com/games/detail/celeste-switch',
'https://store.steampowered.com/app/460790/Bayonetta/',
'https://www.greenmangaming.com/de/vip/vip-deals/']
Но ответ от сервера клиенту - это пустой объект в браузере.
{}
Неважно, понял, что это необходимо для обработки обещания из асинхронной функции.
dynamicScraper().then((output) => {
res.json(output);
});
Вы должны полагаться на сканирование их нового дизайна? Нельзя ли использовать старый сайт (old.reddit.com) или использовать их API? Это должно быть намного проще.
Я только что попробовал networkidle0 вариант goto(), и кажется, что он работает, так как я постоянно могу получить первые 24 URL-адреса. Что, вероятно, предполагает, что это начальная нагрузка. Но установив 25+ в счетчике цикла for, я все еще получаю неопределенное значение. Что может быть потому, что он не прокручивает страницу вниз, чтобы загрузить больше?
Спасибо, Томас Дондорф, я наткнулся на это во время поиска, но в конечном итоге я думаю, что использование API Reddit решит эту проблему, но на самом деле не поможет мне, если я столкнусь с другим веб-сайтом с динамическим контентом.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


У вас есть несколько проблем в вашем коде:
page.goto(url2)
По умолчанию page.goto будет ждать только события load. Изменение его на page.goto(url2, { waitUntil: 'networkidle0' }) будет ждать, пока все запросы не будут выполнены.
page.evaluate(() => {
window.scrollBy(0, window.innerHeight);
})
Вам не хватает await перед этим утверждением (или вам нужно встроить его в поток промисов). Кроме того, вы прокручиваете не до конца страницы, а только до высоты вашего окна. Вы должны использовать document.body.clientHeight, чтобы прокрутить страницу до конца.
Кроме того, вам нужно подождать некоторое время (или ожидаемый селектор) после прокрутки. Вы можете использовать этот код, чтобы подождать одну секунду, чтобы у страницы было достаточно времени для загрузки большего количества контента:
new Promise(resolve => setTimeout(resolve, 5000))
Что касается вашей общей идеи, я бы рекомендовал использовать только puppeteer вместо использования puppeteer для навигации, а затем извлекать все данные и помещать их в cheerio. Если вы используете только puppeteer, ваш код может быть таким же простым (вам все равно придется обернуть его в функцию):
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.goto('https://www.reddit.com/r/GameDeals/', { waitUntil: 'networkidle0' });
const links = await page.evaluate(async () => {
window.scrollBy(0, document.body.clientHeight);
await new Promise(resolve => setTimeout(resolve, 5000)); // wait for some time, you might need to figure out a good value for this yourself
return [...document.querySelectorAll('.scrollerItem div:nth-of-type(2) article div div:nth-of-type(3) a')]
.map((el) => el.href);
});
console.info(links, links.length);
await browser.close();
})();
Спасибо за ответ Томас Дондорф. Мне придется изучить async/await. Я использовал только промисы.
Возможно, это скорее подсказка, чем решение: посмотрите на
networkidle0вариантgoto()илиwaitFor( [selector] )методы.