
前幾天在掘金看到一篇講利用 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 和 爬蟲 的一次實踐。完整的接口代碼在這裏: