Puppeteer - 滚动直到无法滚动

102

我处于这样一种情况:当我向下滚动时会创建新的内容,新的内容有特定的类名。

如何才能持续地向下滚动直到所有元素都加载完成?

换句话说,我希望达到这样的阶段:如果我继续向下滚动,就不会再加载新内容了。

我曾使用代码来滚动页面,并配合一个

await page.waitForSelector('.class_name');
这种方法的问题在于,所有元素加载完成后,代码会继续向下滚动,不会创建新元素,最终会出现超时错误。
这是代码:
await page.evaluate( () => {
  window.scrollBy(0, window.innerHeight);
});
await page.waitForSelector('.class_name');

1
听起来你使用的滚动代码可能有问题。你能否把它加入到你的问题中? - Grant Miller
如果我继续向下滚动,就不会加载新内容。在您的代码中定义“没有新内容将被加载”,并进行检查。同时,超时时间可以重新定义。但是,Grant Miller是正确的,请提供您的代码和最好的目标站点URL。 - Vaviloff
非常感谢!我已经更新了代码。由于这是一个本地网站,我无法发布URL...“没有新内容会加载”意味着网站已经加载了所有可用的元素,因此,当我不断向下滚动并使用page.waitForSelector()时,不会出现新的元素,我的代码会无限期地等待,直到它抛出超时错误。 - user1584421
5
您可以尝试使用以下代码:await page.evaluate('window.scrollTo(0, document.body.scrollHeight)') - Ondrej Kvasnovsky
13个回答

167
试试这个:
const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();
    await page.goto('https://www.yoursite.com');
    await page.setViewport({
        width: 1200,
        height: 800
    });

    await autoScroll(page);

    await page.screenshot({
        path: 'yoursite.png',
        fullPage: true
    });

    await browser.close();
})();

async function autoScroll(page){
    await page.evaluate(async () => {
        await new Promise((resolve) => {
            var totalHeight = 0;
            var distance = 100;
            var timer = setInterval(() => {
                var scrollHeight = document.body.scrollHeight;
                window.scrollBy(0, distance);
                totalHeight += distance;

                if(totalHeight >= scrollHeight - window.innerHeight){
                    clearInterval(timer);
                    resolve();
                }
            }, 100);
        });
    });
}

来源:https://github.com/chenxiaochun/blog/issues/38

编辑

window.innerHeight添加到计算中,因为可滚动距离是页面高度减去视口高度,而不是整个页面高度。

编辑2

当然,丹(来自评论的)要想在滚动过程中添加计数器以停止滚动,您需要引入一个变量,每次迭代时它都会增加。当它达到某个值(例如50次滚动)时,您清除间隔并解析承诺。

以下是将滚动限制设置为50的修改后的代码:

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();
    await page.goto('https://www.yoursite.com');
    await page.setViewport({
        width: 1200,
        height: 800
    });

    await autoScroll(page, 50);  // set limit to 50 scrolls

    await page.screenshot({
        path: 'yoursite.png',
        fullPage: true
    });

    await browser.close();
})();

async function autoScroll(page, maxScrolls){
    await page.evaluate(async (maxScrolls) => {
        await new Promise((resolve) => {
            var totalHeight = 0;
            var distance = 100;
            var scrolls = 0;  // scrolls counter
            var timer = setInterval(() => {
                var scrollHeight = document.body.scrollHeight;
                window.scrollBy(0, distance);
                totalHeight += distance;
                scrolls++;  // increment counter

                // stop scrolling if reached the end or the maximum number of scrolls
                if(totalHeight >= scrollHeight - window.innerHeight || scrolls >= maxScrolls){
                    clearInterval(timer);
                    resolve();
                }
            }, 100);
        });
    }, maxScrolls);  // pass maxScrolls to the function
}


7
速度设置为100太快了,会直接跳过整个自动滚动,我不得不使用400...有没有办法在停止自动滚动前检测出一个类、元素的出现? 速度设为100太快,会导致整个自动滚动被跳过,我不得不将速度设为400... 有没有办法在停止自动滚动前检测某个类或元素的出现? - CodeGuru
1
当您进行“评估”时,您会使用文档上下文的引用。因此,您可以使用标准选择器,并使用“ getBoundingClientRect”检查其位置。 - Cory
1
lqbal:这可能与您的xvfb有关。尝试将headless: false更改为headless: true - Cory
1
@JannisIoannou,看一下这个MDNwindow是一个全局浏览器对象,代表脚本正在运行的窗口。如果你在Node中引用window,会出现错误。 - Cory
2
@JannisIoannou:要在您的puppeteer实例上执行JavaScript代码,您可以使用evaluate方法。将在evaluate内运行的代码视为在浏览器控制台中运行它一样。在这种情况下,当调用evaluate时,window会自动创建。请查看evaluate方法以获取更多上下文信息。 - Cory
显示剩余10条评论

42
滚动到页面底部有两种方法:
  1. 使用 scrollIntoView (滚动到页面底部可能会生成更多内容的部分) 和选择器(例如,document.querySelectorAll('.class_name').length 检查是否生成了更多内容)
  2. 使用 scrollBy (逐步向下滚动页面)和 setTimeout 或者 setInterval (逐步检查是否滚动到页面底部)
这里是一种使用纯JavaScript中的 scrollIntoView和选择器实现的方式(假设 .class_name 是用于滚动到更多内容的选择器),我们可以在浏览器中运行: 方法1: 使用 scrollIntoView 和选择器
const delay = 3000;
const wait = (ms) => new Promise(res => setTimeout(res, ms));
const count = async () => document.querySelectorAll('.class_name').length;
const scrollDown = async () => {
  document.querySelector('.class_name:last-child')
    .scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' });
}

let preCount = 0;
let postCount = 0;
do {
  preCount = await count();
  await scrollDown();
  await wait(delay);
  postCount = await count();
} while (postCount > preCount);
await wait(delay);
在这种方法中,我们通过比较滚动前(preCount)和滚动后(postCount.class_name选择器的数量来检查我们是否到达页面底部。
if (postCount > precount) {
  // NOT bottom of page
} else {
  // bottom of page
}

以下是两种使用JavaScript中的setTimeoutsetIntervalscrollBy实现滚动的示例,我们可以在浏览器控制台中运行:

方法2a:使用setTimeout与scrollBy

const distance = 100;
const delay = 100;
while (document.scrollingElement.scrollTop + window.innerHeight < document.scrollingElement.scrollHeight) {
  document.scrollingElement.scrollBy(0, distance);
  await new Promise(resolve => { setTimeout(resolve, delay); });
}

方法2b:使用setInterval和scrollBy

const distance = 100;
const delay = 100;
const timer = setInterval(() => {
  document.scrollingElement.scrollBy(0, distance);
  if (document.scrollingElement.scrollTop + window.innerHeight >= document.scrollingElement.scrollHeight) {
    clearInterval(timer);
  }
}, delay);

在这种方法中,我们将document.scrollingElement.scrollTop + window.innerHeightdocument.scrollingElement.scrollHeight进行比较,以检查是否已滚动到页面底部:

if (document.scrollingElement.scrollTop + window.innerHeight < document.scrollingElement.scrollHeight) {
  // NOT bottom of page
} else {
  // bottom of page
}
如果上述JavaScript代码之一将页面滚动到底部,那么我们就知道它正在工作,并且我们可以使用Puppeteer自动化此过程。以下是示例Puppeteer Node.js脚本,它将向下滚动到页面底部并等待几秒钟后关闭浏览器。 Puppeteer方法1:使用选择器(.class_name)的scrollIntoView
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null,
    args: ['--window-size=800,600']
  });
  const page = await browser.newPage();
  await page.goto('https://example.com');

  const delay = 3000;
  let preCount = 0;
  let postCount = 0;
  do {
    preCount = await getCount(page);
    await scrollDown(page);
    await page.waitFor(delay);
    postCount = await getCount(page);
  } while (postCount > preCount);
  await page.waitFor(delay);

  await browser.close();
})();

async function getCount(page) {
  return await page.$$eval('.class_name', a => a.length);
}

async function scrollDown(page) {
  await page.$eval('.class_name:last-child', e => {
    e.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' });
  });
}

Puppeteer方法2a:使用setTimeout与scrollBy

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null,
    args: ['--window-size=800,600']
  });
  const page = await browser.newPage();
  await page.goto('https://example.com');

  await scrollToBottom(page);
  await page.waitFor(3000);

  await browser.close();
})();

async function scrollToBottom(page) {
  const distance = 100; // should be less than or equal to window.innerHeight
  const delay = 100;
  while (await page.evaluate(() => document.scrollingElement.scrollTop + window.innerHeight < document.scrollingElement.scrollHeight)) {
    await page.evaluate((y) => { document.scrollingElement.scrollBy(0, y); }, distance);
    await page.waitFor(delay);
  }
}

Puppeteer方法2b:使用setInterval与scrollBy

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null,
    args: ['--window-size=800,600']
  });
  const page = await browser.newPage();
  await page.goto('https://example.com');

  await page.evaluate(scrollToBottom);
  await page.waitFor(3000);

  await browser.close();
})();

async function scrollToBottom() {
  await new Promise(resolve => {
    const distance = 100; // should be less than or equal to window.innerHeight
    const delay = 100;
    const timer = setInterval(() => {
      document.scrollingElement.scrollBy(0, distance);
      if (document.scrollingElement.scrollTop + window.innerHeight >= document.scrollingElement.scrollHeight) {
        clearInterval(timer);
        resolve();
      }
    }, delay);
  });
}

14

基于这个链接的答案

await page.evaluate(() => {
  window.scrollTo(0, window.document.body.scrollHeight);
});

10
window.innerHeight 不能滚动到最底部,但使用 window.scrollTo(0,window.document.body.scrollHeight) 就可以了。 - K. Frank

10

更容易:

    await page.evaluate(async () => {
      let scrollPosition = 0
      let documentHeight = document.body.scrollHeight

      while (documentHeight > scrollPosition) {
        window.scrollBy(0, documentHeight)
        await new Promise(resolve => {
          setTimeout(resolve, 1000)
        })
        scrollPosition = documentHeight
        documentHeight = document.body.scrollHeight
      }
    })

8

很多解决方案都假设页面高度是固定的。这个实现即使页面高度改变也可以使用(例如,当用户向下滚动加载新内容时)。

await page.evaluate(() => new Promise((resolve) => {
  var scrollTop = -1;
  const interval = setInterval(() => {
    window.scrollBy(0, 100);
    if(document.documentElement.scrollTop !== scrollTop) {
      scrollTop = document.documentElement.scrollTop;
      return;
    }
    clearInterval(interval);
    resolve();
  }, 10);
}));

对于高度发生变化的页面,此函数响应更快。 - Raunaqss

7
与 @EdvinTr 类似的解决方案,它给我带来了很好的结果。 滚动并与页面的 Y 偏移量进行比较,非常简单。
let originalOffset = 0;
while (true) {
    await page.evaluate('window.scrollBy(0, document.body.scrollHeight)');
    await page.waitForTimeout(200);
    let newOffset = await page.evaluate('window.pageYOffset');
    if (originalOffset === newOffset) {
        break;
    }
    originalOffset = newOffset;
}

7

非常简单的解决方案

let lastHeight = await page.evaluate('document.body.scrollHeight');

    while (true) {
        await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
        await page.waitForTimeout(2000); // sleep a bit
        let newHeight = await page.evaluate('document.body.scrollHeight');
        if (newHeight === lastHeight) {
            break;
        }
        lastHeight = newHeight;
    }

4
你可以使用以下代码,使用page.keyboard对象:
await page.keyboard.press('ArrowDown');
delay(2000) //wait for 2 seconds
await page.keyboard.press('ArrowUp');
function delay(milliseconds) { //function for waiting
        return new Promise(resolve => {
          setTimeout(() => {
            resolve();
          }, milliseconds);
        });
      }

1
只有当我们拥有这些上下按钮时。 - Kapil Raghuwanshi
它在手机上不起作用? - Lenin Zapata

3
为什么不直接
await page.keyboard.press("PageDown");

那对我有效!谢谢! - Klnh13
太好了,我一直往下滚动到这里!这个方法很有效,而且简单易懂。 - undefined
很高兴我一直滚动到这里!这个方法很有效且简单易懂。 - ericArbour
对我来说,这个方法不起作用,因为我在底部有一个“粘性”内容。这个粘性部分保持在同一位置而不像通常打开页面时会向下移动。对我有效的是使用“scrollTo”解决方案。只是提供信息。 - mariodev

2
await page.keyboard.down('End')

基本上,在执行时,剧作家将按住键盘上的End键,如果您想要,可以使用press并添加一个循环,这将具有相同的效果。


1
请解释一下你的代码。 - Lajos Arpad
1
基本上,在执行时,Playwright 将按住键盘上的 End 键,如果您想要,可以在循环中使用 press 和 add,这将产生相同的效果。 - Poker Player
它在手机上不起作用? - Lenin Zapata
如果是操纵者或编剧模拟,则应该可以工作,因为它只在移动模式下使用浏览器。 - Poker Player
这正是我所需要的。谢谢! - Klnh13

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接