
前几天在掘金看到一篇讲利用 puppeteer 进行页面测试的文章,瞬间对这个无头浏览器来了兴趣。一通了解以后,爱不释手。
Puppeteer(中文翻译”操纵木偶的人”) 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个Node库,提供了一个高级的 API 来控制DevTools协议上的无头版 Chrome 。也能够配置为使用完整(非无头)的 Chrome。Chrome素来在浏览器界稳执牛耳,所以,Chrome Headless 必将成为 web 应用自动化测试的行业标杆。使用Puppeteer,至关于同时具备 Linux 和 Chrome 双端的操做能力,应用场景可谓很是之多。如:
puppeteer 虽然很强大,安装使用却很简单。可是由于要 down 一个浏览器到 package 里。因此安装 puppeteer推荐使用 cnpm,完整的安装指令以下:
npm i bufferutil utf-8-validate cnpm && npx cnpm puppeteer复制代码
安装完成后的使用也很简单,官方给的文档很详细,基本上有什么需求,看着文档就能捣鼓出来。上面的
小demo 就是一个简单的根据物流单号获取物流状态的小工具:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'] })
const page = await browser.newPage()
await page.goto('https://www.kuaidi100.com/', { timeout: 0, waitUntil: 'networkidle2' })
const input = await page.$('#postid')
await input.type(process.argv.slice(2)[0] || '557006432812950')
const query = await page.$('#query')
page.on('response', async res => {
if (res._url.includes('/query')) {
console.log(JSON.parse(await res.text()))
await page.close()
await browser.close()
}
})
await query.click()
})();复制代码
node index.js '物流单号'复制代码
打开浏览器 => 打开Tab => 输入URL回车 => 找到输入框并输入 => 点击搜索 => 获取数据复制代码
可是仅仅这个是没有办法实现需求的,好比我如今须要一个接口,带着物流单号 Get 一下就能获得物流动态数据。仅仅这样的话,单单是速度就会让人抓狂:
await puppeteer.launch()
await browser.newPage()
await page.goto()复制代码
这三步走下来就至少须要 2 s。固然这个不一样的设备都有所不一样。可是显然咱们不能将这三步放在接口逻辑里,而是要提早打开 puppeteer,等接收到请求直接执行:
const input = await page.$('#postid')
await input.type('xxxxxxxxxx')
const query = await page.$('#query')
await query.click()复制代码
可是这样会有问题,问题就是当存在并发请求时候。全部的请求操做的都是同一个页面,同一个 input ,同一个 button。这种状况下是没有办法保证,当 click() 触发时,input 里的value 是否是当前接口请求时带来的参数。
这种状况下就要考虑加锁了,还须要一个执行队列在在并发量大时来放置等待中的物流单号,同时咱们也须要多个 tab 来加强处理能力,以及一个分发函数,将不一样的请求分发至不一样 tab。
const URL = require('url')
const events = require('events');
const puppeteer = require('puppeteer');
const company = require('./util/exoresscom')
const pageNum = 3
let nowPageNum = 0
const pageList = []
const requestList = []
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'] })
for(let i = 0; i < pageNum; i ++) {
const page = await browser.newPage()
page.goto('https://www.kuaidi100.com/', { timeout: 0, waitUntil: 'domcontentloaded' }).then(() => {
nowPageNum ++
})
page.on('response', async res => {
if (res._url.includes('/query?')) {
const result = JSON.parse(await res.text())
result.com ? result.comInfo = company.find(e => e.number == result.com) : ''
!result.nu ? result.nu = URL.parse(res.url(), true).query.postid : ''
event.emit('REQUEST_OK', result)
}
})
pageList.push({
page,
requesting: false,
async request(order_num) {
this.requesting = true
const input = await this.page.$('#postid')
await input.type(order_num)
const query = await this.page.$('#query')
await query.click()
this.requesting = false
}
})
}复制代码
router.get("/express", async (ctx) => {
if (ctx.request.query.num) {
if (nowPageNum) {
distribute(ctx.request.query.num)
try {
ctx.body = await new Promise(resolve => event.on('REQUEST_OK', data => data.nu == ctx.request.query.num && resolve(data)))
} catch (error) {
ctx.body = { msg: '爬取失败' }
}
} else {
ctx.body = { msg: '爬虫启动中' }
}
} else {
ctx.body = { msg: '订单号不合法' }
}
})复制代码
const distribute = order_num => {
if (!requestList.includes(order_num)) {
const free = pageList.find(e => !e.requesting)
free ? free.request(order_num) : requestList.push(order_num)
}
}复制代码
page.on('response', async res => {
if (res._url.includes('/query?')) {
const result = JSON.parse(await res.text())
result.com ? result.comInfo = company.find(e => e.number == result.com) : ''
!result.nu ? result.nu = URL.parse(res.url(), true).query.postid : ''
event.emit('REQUEST_OK', result)
}
})复制代码
此时,会有两处可以接收到响应完成的数据,分别是全局的:
event.on('REQUEST_OK', () => requestList.length && distribute(requestList.splice(0, 1)[0])) 复制代码
ctx.body = await new Promise(resolve => event.on('REQUEST_OK', data => data.nu == ctx.request.query.num && resolve(data))) 复制代码
固然,这只是最理想状况下的逻辑处理流程,实际中的项目要考虑的状况要比这多太多了。因此这仅仅是我学习 nodejs 过程当中对 events 和 爬虫 的一次实践。完整的接口代码在这里: