Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
chenxiaochun opened this issue Oct 24, 2017 · 16 comments
Open

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

chenxiaochun opened this issue Oct 24, 2017 · 16 comments
Labels

Comments

@chenxiaochun
Copy link
Owner

chenxiaochun commented Oct 24, 2017

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();
})();

官方文档

@chenxiaochun chenxiaochun changed the title 无头浏览器Puppeteer Puppeteer:模拟浏览器操作行为的利器 Oct 31, 2017
@akeyboardlife
Copy link

每次滚动100有点慢,我直接让每次滚动document.body.scrollHeight,同时调高了时间间隔。

感谢你的代码。

@chenxiaochun
Copy link
Owner Author

@akeyboardlife ,有帮助就好,客气了!

@Asher-Tan
Copy link

赞!

@dev-zlcode
Copy link

终端请求少了一行代码否则不生效
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
let url = interceptedRequest.url();
if (url.indexOf('mmbiz.qpic.cn') > -1) {
interceptedRequest.abort();
// console.log(url);
} else {
interceptedRequest.continue();
}

});

@leafney
Copy link

leafney commented Apr 5, 2019

@yiqiesuiyuan Yes, it works well after adding await page.setRequestInterception(true); ,Thanks!

@yifeikong
Copy link

请问可以转载么,会注明出处和链接

@chenxiaochun
Copy link
Owner Author

@yifeikong ,可以的。

@yangweijie
Copy link

如何模拟设置下拉列表的值,甚至下拉列表是联动ajax关联请求的

@chenxiaochun
Copy link
Owner Author

@yangweijie ,这个得具体情况具体分析。如果是类似于地址关联的下拉框。可以等前面的下拉框变动之后,等待2秒钟,再去设置后面的下拉框值。这些在 puppeteer中都是有相关api的,可以查看一下。

@yangweijie
Copy link

@yangweijie ,这个得具体情况具体分析。如果是类似于地址关联的下拉框。可以等前面的下拉框变动之后,等待2秒钟,再去设置后面的下拉框值。这些在 puppeteer中都是有相关api的,可以查看一下。

问题就不知道怎么让前面的下拉框变动,keyboard.type无效,要写额外js设置值吗? 有的下拉可能 是其他js框架渲染的, 比如 这一个注册界面的 省市 下拉 http://interactive.cponline.cnipa.gov.cn/app/03_jh/login/register-qiye.jsp

@chenxiaochun
Copy link
Owner Author

@yangweijie ,我其实也好久不看它的文档了。刚才翻了一下文档,看到这个方法:https://pptr.dev/#?product=Puppeteer&version=v1.18.1&show=api-pageselectselector-values,估计可能对你有用。

@MBearo
Copy link

MBearo commented Aug 22, 2019

大佬有遇到截出来的图是空白么?用了你提供的方法,查了半天也不知道从何下手

@MBearo
Copy link

MBearo commented Aug 22, 2019

大佬有遇到截出来的图是空白么?用了你提供的方法,查了半天也不知道从何下手

把 headless 设置为 true 就好了,感谢感谢

@v2x2
Copy link

v2x2 commented Nov 27, 2019

我尝试了设置了headless为true,还有滚动截图和fullPage: true,但是我截出来的图还是有白色的一段,我截的图片是偏大的有10000*20000,请问有什么方法可以解决吗?

@chenxiaochun
Copy link
Owner Author

我尝试了设置了headless为true,还有滚动截图和fullPage: true,但是我截出来的图还是有白色的一段,我截的图片是偏大的有10000*20000,请问有什么方法可以解决吗?

估计是图片太大,在截屏的时候还没有完全加载出来

@yangyang5214
Copy link

@v2x2 是不是在截图的时候页面还没加载完成

下面是我的例子 🌰:

导出 PDF, 页面的长度取决于数据多少,数据是由 Api 加载的, 所以导致页面大小不确定,等待 api 加载完,页面重新渲染完成,再指定 导出的高度。

const puppeteer = require('puppeteer');
 
(async () => {
  const browser = await puppeteer.launch({
    // headless: false, 进行 pdf 导出 要为 false
    ignoreHTTPSErrors: true
  });
  const page = await browser.newPage();
  await page.goto('xxxxxx);
 
 
  //等待 api 渲染完毕
  await sleep(3 * 1000)
 
  // 获取 API 加载完,实际的页面长度
  let height = await page.evaluate('document.body.scrollHeight')
  let width = await page.evaluate('document.body.scrollWidth')
 
  //可以不设置,page.pdf 传入参数即可
  // await page.setViewport({
  //   width: width,
  //   height: height
  // });
 
  let params = {
    printBackground: true,
    scale: 1,
    height: height,
    width: width,
    path: 'index.pdf'
  }
 
  await page.pdf(params);
 
  await browser.close();
})();
 
 
function sleep(ms) {
  ms = (ms) ? ms : 0;
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

10 participants