Как использовать Puppeteer для очистки страницы Reddit?

Я пытаюсь научиться использовать 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/']

Но ответ от сервера клиенту - это пустой объект в браузере. {}

Обновить/изменить 2:

Неважно, понял, что это необходимо для обработки обещания из асинхронной функции.

dynamicScraper().then((output) => {
        res.json(output);
    });

Возможно, это скорее подсказка, чем решение: посмотрите на networkidle0 вариант goto() или waitFor( [selector] ) методы.

Sirko 31.03.2019 09:43

Вы должны полагаться на сканирование их нового дизайна? Нельзя ли использовать старый сайт (old.reddit.com) или использовать их API? Это должно быть намного проще.

Thomas Dondorf 31.03.2019 09:53

Я только что попробовал networkidle0 вариант goto(), и кажется, что он работает, так как я постоянно могу получить первые 24 URL-адреса. Что, вероятно, предполагает, что это начальная нагрузка. Но установив 25+ в счетчике цикла for, я все еще получаю неопределенное значение. Что может быть потому, что он не прокручивает страницу вниз, чтобы загрузить больше?

Kaleidics 31.03.2019 10:00

Спасибо, Томас Дондорф, я наткнулся на это во время поиска, но в конечном итоге я думаю, что использование API Reddit решит эту проблему, но на самом деле не поможет мне, если я столкнусь с другим веб-сайтом с динамическим контентом.

Kaleidics 31.03.2019 10:02
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
4
903
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

У вас есть несколько проблем в вашем коде:

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. Я использовал только промисы.

Kaleidics 31.03.2019 23:38

Другие вопросы по теме