Skip to content

Puppeteer:模拟浏览器操作行为的利器 #38

@chenxiaochun

Description

@chenxiaochun

Puppeteer 出自于 GoogleChrome 团队,是一个可以用来模拟 Chrome 浏览器各种操作行为的 nodejs 库,基于谷歌的开发工具协议

它可以用来模拟你在浏览器中大多数常见操作,比如:

  • 生成页面的截图或者是PDF
  • 抓取单页应用和生成预渲染的内容
  • 抓取网站内容
  • 自动提交表单、UI测试、模拟键盘输入等
  • 创新一个最新的、自动化的测试环境,可以在最新版本 Chrome 浏览器上运行你的测试用例
  • 捕获你的站点的时间轴,帮助你找出需要优化的问题

Puppeteer 运行依赖的 nodejs 版本最低是6.4.0,但是由于示例中使用了async/await的特性,所以我建议你使用7.6.0以及更高的版本。

安装 Puppeteer

yarn add puppeteer

//或者

npm install puppeteer

截屏示例

第一个示例:自动跳转到 https://example.com 并生成一张截图:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

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

Puppeteer 设置的默认可视区域大小是800*600像素。上面示例中的网站页面小于这个尺寸,可以完整的截取出来。但是,你换成http://www.jd.com就不行了,所以,我们得使用page.setViewport()方法来重新定义可视区域的大小。

//设置截取页面的可视区域大小

const puppeteer = require('puppeteer');

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

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

运行查看截图,发现只是完整的截取了第一屏,后面几屏的怎么办?page.screenshot()方法提供了一个fullPage参数,用来设置截取整个页面。

//截取整个京东商城页面,但是因为有懒加载,所以不能截取到完整的内容

const puppeteer = require('puppeteer');

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

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

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

截取的确实是整个网站页面,但是有些楼层使用了懒加载机制,导致这些楼层就没有截取出来。解决办法就是能够让页面自动从顶部滚动到底部之后,再去进行截取,所以我们需要自己编写一个autoScroll()方法。

//自动滚动截取整个京东商城页面

const puppeteer = require('puppeteer');

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

    await autoScroll(page);

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

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

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

                if(totalHeight >= scrollHeight){
                    clearInterval(timer);
                    resolve();
                }
            }, 100);
        });
    });
}

重点解释一下autoScroll()方法的实现。totalHeight用来记录页面的当前高度,初始值为0。distance用来表示每次向下滚动的距离,这里为100像素。接着使用了一个定时器,每隔100毫秒向下滚动distance设定的距离,然后累加到totalHeight,直到它大于等于页面的实际高度document.body.scrollHeight之后,才会清除定时器,并将Promise对象的状态置为resolve()

页面滚动完成之后,后面的处理跟上面一样了,直接执行截屏操作就可以了。

模拟用户输入与鼠标事件

上面已经说过,puppeteer 还可以模拟键盘的输入操作和鼠标单击事件,基于这些我们可以自然想到可以用它模拟表单提交操作。

编写了一个简单的 html 页面来模拟表单:

<!DOCTYPE html>
<html>
<head>
<title>index</title>
<style type="text/css">
input, button{
    font-size: 20px;
}
</style>
<script type="text/javascript">
function submit(){
    alert('提交成功!');
}
</script>
</head>
<body>
文本框:<input type="text" name="" id="text"> <button id="button" onclick="submit()">提交</button>
</body>
</html>

image

在文本框中自动输入一串数字,然后自动点击提交按钮。我们用到了 puppeteer 的 page.typepage.click方法,前者用于模拟输入,后者用于模拟单击操作。

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();

    await page.goto("localhost:80/html/index.html", {
        waitUntil: "networkidle"
    });

    await page.type('#text', '123456789', {
        delay: 100
    });

    await page.waitFor(500);

    await page.click('#button', {
        delay: 500
    })

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

10 -31-2017 11-18-42

puppeteer.launch()方法在之前的版本中有一个devtool: true参数,可在页面中自动打开 Chrome 的开发者工具。可是后面的版本,不知道什么原因给去掉了。如果你现在还有此需求,可以这样写:

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false,
        args: ['--auto-open-devtools-for-tabs']
    });
    const page = await browser.newPage();

    await page.goto("http://www.jd.com", {
        waitUntil: "networkidle"
    });

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

可以使用page.emulate()方法来模拟各种移动设备。最重要的是userAgent参数,因为服务器一般都是根据这个参数值来决定显示的页面类型的。

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();

    await page.goto("http://www.jd.com", {
        waitUntil: "networkidle"
    });

    await page.emulate({
        viewport: {
            width: 375,
            height: 667,
            isMobile: true
        },
        userAgent: '"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"'
    });
})();

此外,还有page.hover()用来模拟 mouseover 的操作;page.reload()用来模拟刷新操作;page.title()用来获取网页标题。这些大家都可以自己去使用挖掘一下。

过滤页面中的元素

有时候我打开一个网页可能只是想分析它里面的超级链接,并不想让页面加载图片,这可以大大加快页面的访问速度。所以,你可以给页面绑定一个request的事件,可以通过它的回调函数参数获取到当前页面加载的每一个请求,并加以处理。

我们这里就可以根据它的url()来判断当前请求是图片的话,直接将其abort(),否则continue()即可。

const puppeteer = require('puppeteer');

puppeteer.launch({
  headless: false
}).then(async browser => {
  const page = await browser.newPage();
  await page.setRequestInterception(true);
  await page.setViewport({
    width: 1200,
    height: 800
  });

  page.on('request', interceptedRequest => {
    let url = interceptedRequest.url();
    if(url.indexOf('.png') > -1 || url.indexOf('.jpg') > -1)
      interceptedRequest.abort();
    else
      interceptedRequest.continue();
  });
  await page.goto('https://www.jd.com');
  await autoScroll(page);
  // await browser.close();
});

创建隐私模式

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    // Create a new incognito browser context
    const context = await browser.createIncognitoBrowserContext();
    // Create a new page inside context.
    const page = await context.newPage();
    // ... do stuff with page ...
    await page.goto('https://example.com');
    // Dispose context once it's no longer needed.
    await context.close();
})();

官方文档

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions