詳解Node使用Puppeteer完成一次復(fù)雜的爬蟲
本文介紹了詳解Node使用Puppeteer完成一次復(fù)雜的爬蟲,分享給大家,具體如下:
架構(gòu)圖

Puppeteer架構(gòu)圖
- Puppeteer 通過(guò) devTools 與 browser 通信
- Browser 一個(gè)可以擁有多個(gè)頁(yè)面的瀏覽器(chroium)實(shí)例
- Page 至少含有一個(gè) Frame 的頁(yè)面
- Frame 至少還有一個(gè)用于執(zhí)行 javascript 的執(zhí)行環(huán)境,也可以拓展多個(gè)執(zhí)行環(huán)境
前言
最近想要入手一臺(tái)臺(tái)式機(jī),筆記本的i5在打開網(wǎng)頁(yè)和vsc的時(shí)候有明顯卡頓的情況,因此打算配1臺(tái) i7 + GTX1070TI or GTX1080TI的電腦,直接在淘寶上搜需要翻頁(yè)太多,并且圖片太多,腦容量接受不了,因此想爬一些數(shù)據(jù),利用圖形化分析一下最近價(jià)格的走勢(shì)。因此寫了一個(gè)用Puppeteer寫了一個(gè)爬蟲爬去相關(guān)數(shù)據(jù)。
什么是Puppeteer?
Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome or Chromium.
簡(jiǎn)而言之,這貨是一個(gè)提供高級(jí)API的node庫(kù),能夠通過(guò)devtool控制headless模式的chrome或者chromium,它可以在headless模式下模擬任何的人為操作。
和cheerio的區(qū)別
cherrico本質(zhì)上只是一個(gè)使用類似jquery的語(yǔ)法操作HTML文檔的庫(kù),使用cherrico爬取數(shù)據(jù),只是請(qǐng)求到靜態(tài)的HTML文檔,如果網(wǎng)頁(yè)內(nèi)部的數(shù)據(jù)是通過(guò)ajax動(dòng)態(tài)獲取的,那么便爬去不到的相應(yīng)的數(shù)據(jù)。而Puppeteer能夠模擬一個(gè)瀏覽器的運(yùn)行環(huán)境,能夠請(qǐng)求網(wǎng)站信息,并運(yùn)行網(wǎng)站內(nèi)部的邏輯。然后再通過(guò)WS協(xié)議動(dòng)態(tài)的獲取頁(yè)面內(nèi)部的數(shù)據(jù),并能夠進(jìn)行任何模擬的操作(點(diǎn)擊、滑動(dòng)、hover等),并且支持跳轉(zhuǎn)頁(yè)面,多頁(yè)面管理。甚至能注入node上的腳本到瀏覽器內(nèi)部環(huán)境運(yùn)行,總之,你能對(duì)一個(gè)網(wǎng)頁(yè)做的操作它都能做,你不能做的它也能做。
開始
本文不是一個(gè)手把手教程,因此需要你有基本的Puppeteer API常識(shí),如果不懂,請(qǐng)先看看官方介紹
Puppeteer官方站點(diǎn)
PuppeteerAPI
首先我們觀察要爬去的網(wǎng)站信息 GTX1080
這是我們要爬取的淘寶網(wǎng)頁(yè),只有中間的商品項(xiàng)目是我們需要爬取的內(nèi)容,仔細(xì)分析它的結(jié)構(gòu),相信一個(gè)前端都有這樣的能力。
我使用的Typescript,能夠獲得完整的Puppetter及相關(guān)庫(kù)的API提示,如果你不會(huì)TS,只需要將相關(guān)的代碼換成ES的語(yǔ)法就好了
// 引入一些需要用到的庫(kù)以及一些聲明
import * as puppeteer from 'puppeteer' // 引入Puppeteer
import mongo from '../lib/mongoDb' // 需要用到的 mongodb庫(kù),用來(lái)存取爬取的數(shù)據(jù)
import chalk from 'chalk' // 一個(gè)美化 console 輸出的庫(kù)
const log = console.log // 縮寫 console.log
const TOTAL_PAGE = 50 // 定義需要爬取的網(wǎng)頁(yè)數(shù)量,對(duì)應(yīng)頁(yè)面下部的跳轉(zhuǎn)鏈接
// 定義要爬去的數(shù)據(jù)結(jié)構(gòu)
interface IWriteData {
link: string // 爬取到的商品詳情鏈接
picture: string // 爬取到的圖片鏈接
price: number // 價(jià)格,number類型,需要從爬取下來(lái)的數(shù)據(jù)進(jìn)行轉(zhuǎn)型
title: string // 爬取到的商品標(biāo)題
}
// 格式化的進(jìn)度輸出 用來(lái)顯示當(dāng)前爬取的進(jìn)度
function formatProgress (current: number): string {
let percent = (current / TOTAL_PAGE) * 100
let done = ~~(current / TOTAL_PAGE * 40)
let left = 40 - done
let str = `當(dāng)前進(jìn)度:[${''.padStart(done, '=')}${''.padStart(left, '-')}] ${percent}%`
return str
}
接下來(lái)我們開始進(jìn)入到爬蟲的主要邏輯
// 因?yàn)槲覀冃枰玫酱罅康?await 語(yǔ)句,因此在外層包裹一個(gè) async function
async function main() {
// Do something
}
main()
// 進(jìn)入代碼的主邏輯
async function main() {
// 首先通過(guò)Puppeteer啟動(dòng)一個(gè)瀏覽器環(huán)境
const browser = await puppeteer.launch()
log(chalk.green('服務(wù)正常啟動(dòng)'))
// 使用 try catch 捕獲異步中的錯(cuò)誤進(jìn)行統(tǒng)一的錯(cuò)誤處理
try {
// 打開一個(gè)新的頁(yè)面
const page = await browser.newPage()
// 監(jiān)聽頁(yè)面內(nèi)部的console消息
page.on('console', msg => {
if (typeof msg === 'object') {
console.dir(msg)
} else {
log(chalk.blue(msg))
}
})
// 打開我們剛剛看見的淘寶頁(yè)面
await page.goto('https://s.taobao.com/search?q=gtx1080&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20180416&ie=utf8')
log(chalk.yellow('頁(yè)面初次加載完畢'))
// 使用一個(gè) for await 循環(huán),不能一個(gè)時(shí)間打開多個(gè)網(wǎng)絡(luò)請(qǐng)求,這樣容易因?yàn)閮?nèi)存過(guò)大而掛掉
for (let i = 1; i <= TOTAL_PAGE; i++) {
// 找到分頁(yè)的輸入框以及跳轉(zhuǎn)按鈕
const pageInput = await page.$(`.J_Input[type='number']`)
const submit = await page.$('.J_Submit')
// 模擬輸入要跳轉(zhuǎn)的頁(yè)數(shù)
await pageInput.type('' + i)
// 模擬點(diǎn)擊跳轉(zhuǎn)
await submit.click()
// 等待頁(yè)面加載完畢,這里設(shè)置的是固定的時(shí)間間隔,之前使用過(guò)page.waitForNavigation(),但是因?yàn)榈却臅r(shí)間過(guò)久導(dǎo)致報(bào)錯(cuò)(Puppeteer默認(rèn)的請(qǐng)求超時(shí)是30s,可以修改),因?yàn)檫@個(gè)頁(yè)面總有一些不需要的資源要加載,而我的網(wǎng)絡(luò)最近日了狗,會(huì)導(dǎo)致超時(shí),因此我設(shè)定等待2.5s就夠了
await page.waitFor(2500)
// 清除當(dāng)前的控制臺(tái)信息
console.clear()
// 打印當(dāng)前的爬取進(jìn)度
log(chalk.yellow(formatProgress(i)))
log(chalk.yellow('頁(yè)面數(shù)據(jù)加載完畢'))
// 處理數(shù)據(jù),這個(gè)函數(shù)的實(shí)現(xiàn)在下面
await handleData()
// 一個(gè)頁(yè)面爬取完畢以后稍微歇歇,不然太快淘寶會(huì)把你當(dāng)成機(jī)器人彈出驗(yàn)證碼(雖然我們本來(lái)就是機(jī)器人)
await page.waitFor(2500)
}
// 所有的數(shù)據(jù)爬取完畢后關(guān)閉瀏覽器
await browser.close()
log(chalk.green('服務(wù)正常結(jié)束'))
// 這是一個(gè)在內(nèi)部聲明的函數(shù),之所以在內(nèi)部聲明而不是外部,是因?yàn)樵趦?nèi)部可以獲取相關(guān)的上下文信息,如果在外部聲明我還要傳入 page 這個(gè)對(duì)象
async function handleData() {
// 現(xiàn)在我們進(jìn)入瀏覽器內(nèi)部搞些事情,通過(guò)page.evaluate方法,該方法的參數(shù)是一個(gè)函數(shù),這個(gè)函數(shù)將會(huì)在頁(yè)面內(nèi)部運(yùn)行,這個(gè)函數(shù)的返回的數(shù)據(jù)將會(huì)以Promise的形式返回到外部
const list = await page.evaluate(() => {
// 先聲明一個(gè)用于存儲(chǔ)爬取數(shù)據(jù)的數(shù)組
const writeDataList: IWriteData[] = []
// 獲取到所有的商品元素
let itemList = document.querySelectorAll('.item.J_MouserOnverReq')
// 遍歷每一個(gè)元素,整理需要爬取的數(shù)據(jù)
for (let item of itemList) {
// 首先聲明一個(gè)爬取的數(shù)據(jù)結(jié)構(gòu)
let writeData: IWriteData = {
picture: undefined,
link: undefined,
title: undefined,
price: undefined
}
// 找到商品圖片的地址
let img = item.querySelector('img')
writeData.picture = img.src
// 找到商品的鏈接
let link: HTMLAnchorElement = item.querySelector('.pic-link.J_ClickStat.J_ItemPicA')
writeData.link = link.href
// 找到商品的價(jià)格,默認(rèn)是string類型 通過(guò)~~轉(zhuǎn)換為整數(shù)number類型
let price = item.querySelector('strong')
writeData.price = ~~price.innerText
// 找到商品的標(biāo)題,淘寶的商品標(biāo)題有高亮效果,里面有很多的span標(biāo)簽,不過(guò)一樣可以通過(guò)innerText獲取文本信息
let title: HTMLAnchorElement = item.querySelector('.title>a')
writeData.title = title.innerText
// 將這個(gè)標(biāo)簽頁(yè)的數(shù)據(jù)push進(jìn)剛才聲明的結(jié)果數(shù)組
writeDataList.push(writeData)
}
// 當(dāng)前頁(yè)面所有的返回給外部環(huán)境
return writeDataList
})
// 得到數(shù)據(jù)以后寫入到mongodb
const result = await mongo.insertMany('GTX1080', list)
log(chalk.yellow('寫入數(shù)據(jù)庫(kù)完畢'))
}
} catch (error) {
// 出現(xiàn)任何錯(cuò)誤,打印錯(cuò)誤消息并且關(guān)閉瀏覽器
console.log(error)
log(chalk.red('服務(wù)意外終止'))
await browser.close()
} finally {
// 最后要退出進(jìn)程
process.exit(0)
}
}
思考
1、為什么使用Typescript?
因?yàn)門ypescript就是好用啊,我也背不住Puppeteer的全部API,也不想每一個(gè)都查,所以使用TS就能智能提醒了,也能避免因?yàn)槠磳憣?dǎo)致的低級(jí)錯(cuò)誤?;旧嫌昧薚S以后,敲代碼都能一遍過(guò)

puppeteer.png
2、爬蟲的性能問(wèn)題?
因?yàn)镻uppeteer會(huì)啟動(dòng)一個(gè)瀏覽器,執(zhí)行內(nèi)部的邏輯,所以占用的內(nèi)存是蠻多的,看了看控制臺(tái),這個(gè)node進(jìn)程大概占用300MB左右的內(nèi)存。
我的頁(yè)面是一個(gè)個(gè)爬的,如果想更快的爬取可以啟動(dòng)多個(gè)進(jìn)程,注意,V8是單線程的,所以在一個(gè)進(jìn)程內(nèi)部打開多個(gè)頁(yè)面是沒有意義的,需要配置不同的參數(shù)打開不同的node進(jìn)程,當(dāng)然也可以通過(guò)node的cluster(集群)實(shí)現(xiàn),本質(zhì)都是一樣的
我在爬取的過(guò)程中也設(shè)置了不同的等待時(shí)間,一方面是為了等待網(wǎng)頁(yè)的加載,一方面避免淘寶識(shí)別到我是爬蟲彈驗(yàn)證碼
3、Puppeteer的其它功能
這里僅僅利用了Puppeteer的一些基本特性,實(shí)際上Puppeteer還有更多的功能。比如引入node上的處理函數(shù)在瀏覽器內(nèi)部執(zhí)行,將當(dāng)前頁(yè)面保存為pdf或者png圖片。并且還可以通過(guò)const browser = await puppeteer.launch({ headless: false })啟動(dòng)一個(gè)帶界面效果的瀏覽器,你可以看見你的爬蟲是如何運(yùn)作的。此外一些需要登錄的網(wǎng)站,如果你不想識(shí)別驗(yàn)證碼委托第三方進(jìn)行處理,你也可以關(guān)閉headless,然后在程序中設(shè)置等待時(shí)間,手動(dòng)完成一些驗(yàn)證從而達(dá)到登錄的目的。
當(dāng)然google制作了一個(gè)這么牛逼的庫(kù)可不只是用來(lái)做爬蟲爬取數(shù)據(jù)的,這個(gè)庫(kù)也用作于一些自動(dòng)化的性能分析、界面測(cè)試、前端網(wǎng)站監(jiān)控等
4、一些其它方面的思考
總得來(lái)說(shuō)制作爬蟲爬取數(shù)據(jù)是一項(xiàng)較為復(fù)雜并考察多項(xiàng)基本功的練習(xí)項(xiàng)目,在這個(gè)爬蟲里多次使用到了async,這就需要對(duì)async、Promise等相關(guān)知識(shí)充分的了解。在分析DOM收集數(shù)據(jù)時(shí),也多次利用了原生的方法獲取DOM屬性(如果網(wǎng)站有jquery也可以直接用,沒有的話需要外部注入,在typescript下需要進(jìn)行一些配置,避免報(bào)錯(cuò)未識(shí)別的$變量,這樣就可以通過(guò)jquery語(yǔ)法操作DOM),考察了對(duì)DOM相關(guān)API的熟練程度。
另外這只是一個(gè)面向過(guò)程的編程,我們完全可以將它封裝為一個(gè)類進(jìn)行操作,這也考察了對(duì)ES的OOP理解
最后
本文的源代碼Github,喜歡的朋友給個(gè)star吧
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
基于nodejs res.end和res.send的區(qū)別
今天小編就為大家分享一篇基于nodejs res.end和res.send的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
手把手教你實(shí)現(xiàn) Promise的使用方法
這篇文章主要介紹了手把手教你實(shí)現(xiàn) Promise的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
使用NestJS開發(fā)Node.js應(yīng)用的方法
這篇文章主要介紹了使用NestJS開發(fā)Node.js應(yīng)用的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
jwt在express中token的加密解密實(shí)現(xiàn)過(guò)程
文章詳細(xì)介紹了JWT在Node.js中的生成和驗(yàn)證過(guò)程,包括設(shè)置密鑰、使用中間件進(jìn)行token驗(yàn)證等步驟,并提供了一個(gè)完整的示例代碼,感興趣的朋友跟隨小編一起看看吧2025-01-01
Node中使用http-proxy-middleware實(shí)現(xiàn)代理跨域的方法步驟
本文主要介紹了Node中使用http-proxy-middleware實(shí)現(xiàn)代理跨域的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11

