chenxiaochun / blog

🖋️ChenXiaoChun's blog
181 stars 15 forks source link

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

Open chenxiaochun opened 6 years ago

chenxiaochun commented 6 years ago

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

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

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

官方文档

akeyboardlife commented 6 years ago

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

感谢你的代码。

chenxiaochun commented 6 years ago

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

Asher-Tan commented 6 years ago

赞!

dev-zlcode commented 5 years ago

终端请求少了一行代码否则不生效 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 commented 5 years ago

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

perklet commented 5 years ago

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

chenxiaochun commented 5 years ago

@yifeikong ,可以的。

yangweijie commented 5 years ago

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

chenxiaochun commented 5 years ago

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

yangweijie commented 5 years ago

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

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

chenxiaochun commented 5 years ago

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

MBearo commented 5 years ago

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

MBearo commented 5 years ago

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

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

v2x2 commented 4 years ago

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

chenxiaochun commented 3 years ago

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

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

yangyang5214 commented 2 years ago

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