詳解Node使用Puppeteer完成一次復(fù)雜的爬蟲
本文介紹了詳解Node使用Puppeteer完成一次復(fù)雜的爬蟲,分享給大家,具體如下:
架構(gòu)圖
Puppeteer架構(gòu)圖
- Puppeteer 通過 devTools 與 browser 通信
- Browser 一個(gè)可以擁有多個(gè)頁面的瀏覽器(chroium)實(shí)例
- Page 至少含有一個(gè) Frame 的頁面
- Frame 至少還有一個(gè)用于執(zhí)行 javascript 的執(zhí)行環(huán)境,也可以拓展多個(gè)執(zhí)行環(huán)境
前言
最近想要入手一臺臺式機(jī),筆記本的i5在打開網(wǎng)頁和vsc的時(shí)候有明顯卡頓的情況,因此打算配1臺 i7 + GTX1070TI or GTX1080TI的電腦,直接在淘寶上搜需要翻頁太多,并且圖片太多,腦容量接受不了,因此想爬一些數(shù)據(jù),利用圖形化分析一下最近價(jià)格的走勢。因此寫了一個(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.
簡而言之,這貨是一個(gè)提供高級API的node庫,能夠通過devtool控制headless模式的chrome或者chromium,它可以在headless模式下模擬任何的人為操作。
和cheerio的區(qū)別
cherrico本質(zhì)上只是一個(gè)使用類似jquery的語法操作HTML文檔的庫,使用cherrico爬取數(shù)據(jù),只是請求到靜態(tài)的HTML文檔,如果網(wǎng)頁內(nèi)部的數(shù)據(jù)是通過ajax動(dòng)態(tài)獲取的,那么便爬去不到的相應(yīng)的數(shù)據(jù)。而Puppeteer能夠模擬一個(gè)瀏覽器的運(yùn)行環(huán)境,能夠請求網(wǎng)站信息,并運(yùn)行網(wǎng)站內(nèi)部的邏輯。然后再通過WS協(xié)議動(dòng)態(tài)的獲取頁面內(nèi)部的數(shù)據(jù),并能夠進(jìn)行任何模擬的操作(點(diǎn)擊、滑動(dòng)、hover等),并且支持跳轉(zhuǎn)頁面,多頁面管理。甚至能注入node上的腳本到瀏覽器內(nèi)部環(huán)境運(yùn)行,總之,你能對一個(gè)網(wǎng)頁做的操作它都能做,你不能做的它也能做。
開始
本文不是一個(gè)手把手教程,因此需要你有基本的Puppeteer API常識,如果不懂,請先看看官方介紹
Puppeteer官方站點(diǎn)
PuppeteerAPI
首先我們觀察要爬去的網(wǎng)站信息 GTX1080
這是我們要爬取的淘寶網(wǎng)頁,只有中間的商品項(xiàng)目是我們需要爬取的內(nèi)容,仔細(xì)分析它的結(jié)構(gòu),相信一個(gè)前端都有這樣的能力。
我使用的Typescript,能夠獲得完整的Puppetter及相關(guān)庫的API提示,如果你不會(huì)TS,只需要將相關(guān)的代碼換成ES的語法就好了
// 引入一些需要用到的庫以及一些聲明 import * as puppeteer from 'puppeteer' // 引入Puppeteer import mongo from '../lib/mongoDb' // 需要用到的 mongodb庫,用來存取爬取的數(shù)據(jù) import chalk from 'chalk' // 一個(gè)美化 console 輸出的庫 const log = console.log // 縮寫 console.log const TOTAL_PAGE = 50 // 定義需要爬取的網(wǎng)頁數(shù)量,對應(yīng)頁面下部的跳轉(zhuǎn)鏈接 // 定義要爬去的數(shù)據(jù)結(jié)構(gòu) interface IWriteData { link: string // 爬取到的商品詳情鏈接 picture: string // 爬取到的圖片鏈接 price: number // 價(jià)格,number類型,需要從爬取下來的數(shù)據(jù)進(jìn)行轉(zhuǎn)型 title: string // 爬取到的商品標(biāo)題 } // 格式化的進(jìn)度輸出 用來顯示當(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 }
接下來我們開始進(jìn)入到爬蟲的主要邏輯
// 因?yàn)槲覀冃枰玫酱罅康?await 語句,因此在外層包裹一個(gè) async function async function main() { // Do something } main()
// 進(jìn)入代碼的主邏輯 async function main() { // 首先通過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è)新的頁面 const page = await browser.newPage() // 監(jiān)聽頁面內(nèi)部的console消息 page.on('console', msg => { if (typeof msg === 'object') { console.dir(msg) } else { log(chalk.blue(msg)) } }) // 打開我們剛剛看見的淘寶頁面 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('頁面初次加載完畢')) // 使用一個(gè) for await 循環(huán),不能一個(gè)時(shí)間打開多個(gè)網(wǎng)絡(luò)請求,這樣容易因?yàn)閮?nèi)存過大而掛掉 for (let i = 1; i <= TOTAL_PAGE; i++) { // 找到分頁的輸入框以及跳轉(zhuǎn)按鈕 const pageInput = await page.$(`.J_Input[type='number']`) const submit = await page.$('.J_Submit') // 模擬輸入要跳轉(zhuǎn)的頁數(shù) await pageInput.type('' + i) // 模擬點(diǎn)擊跳轉(zhuǎn) await submit.click() // 等待頁面加載完畢,這里設(shè)置的是固定的時(shí)間間隔,之前使用過page.waitForNavigation(),但是因?yàn)榈却臅r(shí)間過久導(dǎo)致報(bào)錯(cuò)(Puppeteer默認(rèn)的請求超時(shí)是30s,可以修改),因?yàn)檫@個(gè)頁面總有一些不需要的資源要加載,而我的網(wǎng)絡(luò)最近日了狗,會(huì)導(dǎo)致超時(shí),因此我設(shè)定等待2.5s就夠了 await page.waitFor(2500) // 清除當(dāng)前的控制臺信息 console.clear() // 打印當(dāng)前的爬取進(jìn)度 log(chalk.yellow(formatProgress(i))) log(chalk.yellow('頁面數(shù)據(jù)加載完畢')) // 處理數(shù)據(jù),這個(gè)函數(shù)的實(shí)現(xiàn)在下面 await handleData() // 一個(gè)頁面爬取完畢以后稍微歇歇,不然太快淘寶會(huì)把你當(dāng)成機(jī)器人彈出驗(yàn)證碼(雖然我們本來就是機(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è)對象 async function handleData() { // 現(xiàn)在我們進(jìn)入瀏覽器內(nèi)部搞些事情,通過page.evaluate方法,該方法的參數(shù)是一個(gè)函數(shù),這個(gè)函數(shù)將會(huì)在頁面內(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類型 通過~~轉(zhuǎn)換為整數(shù)number類型 let price = item.querySelector('strong') writeData.price = ~~price.innerText // 找到商品的標(biāo)題,淘寶的商品標(biāo)題有高亮效果,里面有很多的span標(biāo)簽,不過一樣可以通過innerText獲取文本信息 let title: HTMLAnchorElement = item.querySelector('.title>a') writeData.title = title.innerText // 將這個(gè)標(biāo)簽頁的數(shù)據(jù)push進(jìn)剛才聲明的結(jié)果數(shù)組 writeDataList.push(writeData) } // 當(dāng)前頁面所有的返回給外部環(huán)境 return writeDataList }) // 得到數(shù)據(jù)以后寫入到mongodb const result = await mongo.insertMany('GTX1080', list) log(chalk.yellow('寫入數(shù)據(jù)庫完畢')) } } 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)致的低級錯(cuò)誤?;旧嫌昧薚S以后,敲代碼都能一遍過
puppeteer.png
2、爬蟲的性能問題?
因?yàn)镻uppeteer會(huì)啟動(dòng)一個(gè)瀏覽器,執(zhí)行內(nèi)部的邏輯,所以占用的內(nèi)存是蠻多的,看了看控制臺,這個(gè)node進(jìn)程大概占用300MB左右的內(nèi)存。
我的頁面是一個(gè)個(gè)爬的,如果想更快的爬取可以啟動(dòng)多個(gè)進(jìn)程,注意,V8是單線程的,所以在一個(gè)進(jìn)程內(nèi)部打開多個(gè)頁面是沒有意義的,需要配置不同的參數(shù)打開不同的node進(jìn)程,當(dāng)然也可以通過node的cluster(集群)實(shí)現(xiàn),本質(zhì)都是一樣的
我在爬取的過程中也設(shè)置了不同的等待時(shí)間,一方面是為了等待網(wǎng)頁的加載,一方面避免淘寶識別到我是爬蟲彈驗(yàn)證碼
3、Puppeteer的其它功能
這里僅僅利用了Puppeteer的一些基本特性,實(shí)際上Puppeteer還有更多的功能。比如引入node上的處理函數(shù)在瀏覽器內(nèi)部執(zhí)行,將當(dāng)前頁面保存為pdf或者png圖片。并且還可以通過const browser = await puppeteer.launch({ headless: false })啟動(dòng)一個(gè)帶界面效果的瀏覽器,你可以看見你的爬蟲是如何運(yùn)作的。此外一些需要登錄的網(wǎng)站,如果你不想識別驗(yàn)證碼委托第三方進(jìn)行處理,你也可以關(guān)閉headless,然后在程序中設(shè)置等待時(shí)間,手動(dòng)完成一些驗(yàn)證從而達(dá)到登錄的目的。
當(dāng)然google制作了一個(gè)這么牛逼的庫可不只是用來做爬蟲爬取數(shù)據(jù)的,這個(gè)庫也用作于一些自動(dòng)化的性能分析、界面測試、前端網(wǎng)站監(jiān)控等
4、一些其它方面的思考
總得來說制作爬蟲爬取數(shù)據(jù)是一項(xiàng)較為復(fù)雜并考察多項(xiàng)基本功的練習(xí)項(xiàng)目,在這個(gè)爬蟲里多次使用到了async,這就需要對async、Promise等相關(guān)知識充分的了解。在分析DOM收集數(shù)據(jù)時(shí),也多次利用了原生的方法獲取DOM屬性(如果網(wǎng)站有jquery也可以直接用,沒有的話需要外部注入,在typescript下需要進(jìn)行一些配置,避免報(bào)錯(cuò)未識別的$變量,這樣就可以通過jquery語法操作DOM),考察了對DOM相關(guān)API的熟練程度。
另外這只是一個(gè)面向過程的編程,我們完全可以將它封裝為一個(gè)類進(jìn)行操作,這也考察了對ES的OOP理解
最后
本文的源代碼Github,喜歡的朋友給個(gè)star吧
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Node.js中child_process實(shí)現(xiàn)多進(jìn)程
這篇文章主要介紹了Node.js中child_process實(shí)現(xiàn)多進(jìn)程,需要的朋友可以參考下2015-02-02node爬取微博的數(shù)據(jù)的簡單封裝庫nodeweibo使用指南
這篇文章主要介紹了node爬取微博的數(shù)據(jù)的簡單封裝庫nodeweibo使用指南,需要的朋友可以參考下2015-01-01node.js與C語言 實(shí)現(xiàn)遍歷文件夾下最大的文件,并輸出路徑,大小
這篇文章主要介紹了node.js與C語言 實(shí)現(xiàn)遍歷文件夾下最大的文件,并輸出路徑,大小的相關(guān)資料,需要的朋友可以參考下2017-01-01node.js調(diào)用C++開發(fā)的模塊實(shí)例
這篇文章主要介紹了node.js調(diào)用C++開發(fā)的模塊實(shí)例,在node的程序中,如果有大數(shù)據(jù)量的計(jì)算,處理起來比較慢,可以用C++來處理,然后通過回調(diào)(callback的形式),返回給node,需要的朋友可以參考下2015-07-07nodejs獲取微信小程序帶參數(shù)二維碼實(shí)現(xiàn)代碼
這篇文章主要介紹了nodejs獲取微信小程序帶參數(shù)二維碼實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04nodejs使用readline逐行讀取和寫入文件的實(shí)現(xiàn)
這篇文章給大家介紹了nodejs使用readline逐行讀取和寫入文件的實(shí)現(xiàn)方法,文中通過代碼示例給大家講解的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01