這次帶給大家Node使用Puppeteer做爬蟲,Node使用Puppeteer做爬蟲的注意事項有哪些,下面就是實戰案例,一起來看一下。
架構圖
Puppeteer架構圖
Puppeteer 透過devTools 與browser 通訊
#Browser 一個可以擁有多個頁面的瀏覽器(chroium)實例
#Page 至少含有一個Frame 的頁面
Frame 至少還有一個用於執行javascript 的執行環境,也可以拓展多個執行環境
#前言
##最近想要入手一台桌上型電腦,筆記本的i5在開啟網頁和vsc的時候有明顯卡頓的情況,因此打算配1台i7 GTX1070TI or GTX1080TI的電腦,直接在淘寶上搜需要翻頁太多,並且圖片太多,腦容量接受不了,因此想爬一些數據,利用圖形化分析一下最近價格的走勢。因此寫了一個用Puppeteer寫了一個爬蟲爬去相關資料。什麼是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.簡而言之,這貨是一個提供高級API的node庫,能夠透過devtool控制headless模式的chrome或者chromium,它可以在headless模式下模擬任何的人為操作。和cheerio的區別
cherrico本質上只是一個使用類似jquery的語法操作HTML文檔的庫,使用cherrico爬取數據,只是請求到靜態的HTML文檔,如果網頁內部的資料是透過ajax動態取得的,那麼便爬去不到的相應的資料。而Puppeteer能夠模擬一個瀏覽器的運作環境,能夠請求網站訊息,並運行網站內部的邏輯。然後再透過WS協議動態的獲取頁面內部的數據,並能夠進行任何模擬的操作(點擊、滑動、hover等),並且支援跳轉頁面,多頁面管理。甚至能注入node上的腳本到瀏覽器內部環境運行,總之,你能對一個網頁做的操作它都能做,你不能做的它也能做。開始
本文不是手把手教程,因此需要你有基本的Puppeteer API常識,如果不懂,請先看看官方介紹Puppeteer官方網站
PuppeteerAPI
// 引入一些需要用到的库以及一些声明 import * as puppeteer from 'puppeteer' // 引入Puppeteer import mongo from '../lib/mongoDb' // 需要用到的 mongodb库,用来存取爬取的数据 import chalk from 'chalk' // 一个美化 console 输出的库 const log = console.log // 缩写 console.log const TOTAL_PAGE = 50 // 定义需要爬取的网页数量,对应页面下部的跳转链接 // 定义要爬去的数据结构 interface IWriteData { link: string // 爬取到的商品详情链接 picture: string // 爬取到的图片链接 price: number // 价格,number类型,需要从爬取下来的数据进行转型 title: string // 爬取到的商品标题 } // 格式化的进度输出 用来显示当前爬取的进度 function formatProgress (current: number): string { let percent = (current / TOTAL_PAGE) * 100 let done = ~~(current / TOTAL_PAGE * 40) let left = 40 - done let str = `当前进度:[${''.padStart(done, '=')}${''.padStart(left, '-')}] ${percent}%` return str }
// 因为我们需要用到大量的 await 语句,因此在外层包裹一个 async function async function main() { // Do something } main()
// 进入代码的主逻辑 async function main() { // 首先通过Puppeteer启动一个浏览器环境 const browser = await puppeteer.launch() log(chalk.green('服务正常启动')) // 使用 try catch 捕获异步中的错误进行统一的错误处理 try { // 打开一个新的页面 const page = await browser.newPage() // 监听页面内部的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('页面初次加载完毕')) // 使用一个 for await 循环,不能一个时间打开多个网络请求,这样容易因为内存过大而挂掉 for (let i = 1; i <= TOTAL_PAGE; i++) { // 找到分页的输入框以及跳转按钮 const pageInput = await page.$(`.J_Input[type='number']`) const submit = await page.$('.J_Submit') // 模拟输入要跳转的页数 await pageInput.type('' + i) // 模拟点击跳转 await submit.click() // 等待页面加载完毕,这里设置的是固定的时间间隔,之前使用过page.waitForNavigation(),但是因为等待的时间过久导致报错(Puppeteer默认的请求超时是30s,可以修改),因为这个页面总有一些不需要的资源要加载,而我的网络最近日了狗,会导致超时,因此我设定等待2.5s就够了 await page.waitFor(2500) // 清除当前的控制台信息 console.clear() // 打印当前的爬取进度 log(chalk.yellow(formatProgress(i))) log(chalk.yellow('页面数据加载完毕')) // 处理数据,这个函数的实现在下面 await handleData() // 一个页面爬取完毕以后稍微歇歇,不然太快淘宝会把你当成机器人弹出验证码(虽然我们本来就是机器人) await page.waitFor(2500) } // 所有的数据爬取完毕后关闭浏览器 await browser.close() log(chalk.green('服务正常结束')) // 这是一个在内部声明的函数,之所以在内部声明而不是外部,是因为在内部可以获取相关的上下文信息,如果在外部声明我还要传入 page 这个对象 async function handleData() { // 现在我们进入浏览器内部搞些事情,通过page.evaluate方法,该方法的参数是一个函数,这个函数将会在页面内部运行,这个函数的返回的数据将会以Promise的形式返回到外部 const list = await page.evaluate(() => { // 先声明一个用于存储爬取数据的数组 const writeDataList: IWriteData[] = [] // 获取到所有的商品元素 let itemList = document.querySelectorAll('.item.J_MouserOnverReq') // 遍历每一个元素,整理需要爬取的数据 for (let item of itemList) { // 首先声明一个爬取的数据结构 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 // 找到商品的价格,默认是string类型 通过~~转换为整数number类型 let price = item.querySelector('strong') writeData.price = ~~price.innerText // 找到商品的标题,淘宝的商品标题有高亮效果,里面有很多的span标签,不过一样可以通过innerText获取文本信息 let title: HTMLAnchorElement = item.querySelector('.title>a') writeData.title = title.innerText // 将这个标签页的数据push进刚才声明的结果数组 writeDataList.push(writeData) } // 当前页面所有的返回给外部环境 return writeDataList }) // 得到数据以后写入到mongodb const result = await mongo.insertMany('GTX1080', list) log(chalk.yellow('写入数据库完毕')) } } catch (error) { // 出现任何错误,打印错误消息并且关闭浏览器 console.log(error) log(chalk.red('服务意外终止')) await browser.close() } finally { // 最后要退出进程 process.exit(0) } }
思考
#1、為什麼要用Typescript?
因為Typescript就是好用啊,我也背不住Puppeteer的全部API,也不想每一個都查,所以使用TS就能智能提醒了,也能避免因為拼寫導致的低級錯誤。基本上用了TS以後,敲程式碼都能一遍過 puppeteer.png2、爬蟲的效能問題?
因為Puppeteer會啟動一個瀏覽器,執行內部的邏輯,所以佔用的記憶體是蠻多的,看了看控制台,這個node進程大概佔用300MB左右的記憶體。 我的頁面是一個個爬的,如果想更快的爬取可以啟動多個進程,注意,V8是單線程的,所以在一個進程內部打開多個頁面是沒有意義的,需要配置不同的參數來開啟不同的node進程,當然也可以透過node的cluster(集群)實現,本質都是一樣的我在爬取的過程中也設定了不同的等待時間,一方面是為了等待網頁的加載,一方面避免淘寶識別到我是爬蟲彈驗證碼
3、Puppeteer的其它功能
這裡僅僅利用了Puppeteer的一些基本特性,實際上Puppeteer還有更多的功能。例如引入node上的處理函數在瀏覽器內部執行,將目前頁面儲存為pdf或png圖片。並且還可以透過const browser = await puppeteer.launch({ headless: false })啟動一個帶有介面效果的瀏覽器,你可以看見你的爬蟲是如何運作的。另外一些需要登入的網站,如果你不想識別驗證碼委託第三方進行處理,你也可以關閉headless,然後在程式中設定等待時間,手動完成一些驗證從而達到登入的目的。
當然google製作了一個這麼牛逼的函式庫可不只是用來做爬蟲爬取資料的,這個函式庫也用來當作一些自動化的效能分析、介面測試、前端網站監控等
4、一些其它方面的思考
總得來說製作爬蟲爬取資料是一項較為複雜並考察多項基本功的練習項目,在這個爬蟲裡多次使用到了async,這就需要對async、Promise等相關知識充分的了解。在分析DOM收集資料時,也多次利用了原生的方法取得DOM屬性(如果網站有jquery也可以直接用,沒有的話需要外部注入,在typescript下需要進行一些配置,避免報錯未識別的$變量,這樣就可以透過jquery語法操作DOM),檢視了對DOM相關API的熟練程度。
另外這只是一個面向過程的編程,我們完全可以將它封裝為一個類別進行操作,這也考察了對ES的OOP理解
相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!
推薦閱讀:
以上是Node使用Puppeteer做爬蟲的詳細內容。更多資訊請關注PHP中文網其他相關文章!