详解Node使用Puppeteer完成一次复杂的爬虫


Posted in Javascript onApril 18, 2018

本文介绍了详解Node使用Puppeteer完成一次复杂的爬虫,分享给大家,具体如下:

架构图

详解Node使用Puppeteer完成一次复杂的爬虫

Puppeteer架构图

  1. Puppeteer 通过 devTools 与 browser 通信
  2. Browser 一个可以拥有多个页面的浏览器(chroium)实例
  3. Page 至少含有一个 Frame 的页面
  4. 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

首先我们观察要爬去的网站信息 GTX1080

这是我们要爬取的淘宝网页,只有中间的商品项目是我们需要爬取的内容,仔细分析它的结构,相信一个前端都有这样的能力。

我使用的Typescript,能够获得完整的Puppetter及相关库的API提示,如果你不会TS,只需要将相关的代码换成ES的语法就好了

// 引入一些需要用到的库以及一些声明
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以后,敲代码都能一遍过

详解Node使用Puppeteer完成一次复杂的爬虫

puppeteer.png

2、爬虫的性能问题?

因为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理解

最后

本文的源代码Github,喜欢的朋友给个star吧

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery多行滚动/向左或向上滚动/响应鼠标实现思路及代码
Jan 23 Javascript
javascript中直接引用Microsoft的COM生成Word
Jan 20 Javascript
分离与继承的思想实现图片上传后的预览功能:ImageUploadView
Apr 07 Javascript
Javascript对象字面量的理解
Jun 22 Javascript
微信小程序链接传参并跳转新页面
Nov 29 Javascript
jQuery焦点图轮播效果实现方法
Dec 19 Javascript
基于vue的fullpage.js单页滚动插件
Mar 20 Javascript
label+input实现按钮开关切换效果的实例
Aug 16 Javascript
Vuejs中使用markdown服务器端渲染的示例
Nov 22 Javascript
vue 2.x 中axios 封装的get 和post方法
Feb 28 Javascript
基于vue写一个全局Message组件的实现
Aug 15 Javascript
JS实现移动端可折叠导航菜单(现代都市风)
Jul 07 Javascript
jQuery滚动条美化插件nicescroll简单用法示例
Apr 18 #jQuery
Angular 如何使用第三方库的方法
Apr 18 #Javascript
jQuery实现的淡入淡出与滑入滑出效果示例
Apr 18 #jQuery
浅谈mvvm-simple双向绑定简单实现
Apr 18 #Javascript
JS点击动态添加标签、删除指定标签的代码
Apr 18 #Javascript
jQuery实现的手动拖动控制进度条效果示例【测试可用】
Apr 18 #jQuery
浅谈vuepress 踩坑记
Apr 18 #Javascript
You might like
如何在PHP中进行身份认证
2006/10/09 PHP
在Windows系统上安装PHP运行环境文字教程
2010/07/19 PHP
php获取百度收录、百度热词及百度快照的方法
2015/04/02 PHP
使用XHProf查找PHP性能瓶颈的实例
2017/12/13 PHP
Thinkphp 3.2框架使用Redis的方法详解
2019/10/24 PHP
jquery 插件学习(四)
2012/08/06 Javascript
IE及IE6浏览器中判断JS文件加载成功失败的方法
2015/02/18 Javascript
jquery带翻页动画的电子杂志代码分享
2015/08/21 Javascript
javascript RegExp 使用说明
2016/05/21 Javascript
AngularJS入门教程之控制器详解
2016/07/27 Javascript
jQuery事件用法详解
2016/10/06 Javascript
JS请求servlet功能示例
2017/06/01 Javascript
jQuery+vue.js实现的九宫格拼图游戏完整实例【附源码下载】
2017/09/12 jQuery
使用InstantClick.js让页面提前加载200ms
2017/09/12 Javascript
使用ionic(选项卡栏tab) icon(图标) ionic上拉菜单(ActionSheet) 实现通讯录界面切换实例代码
2017/10/20 Javascript
webpack4.x打包过程详解
2018/07/18 Javascript
跨域请求两种方法 jsonp和cors的实现
2018/11/11 Javascript
vue实现分页栏效果
2019/06/28 Javascript
Python实现去除代码前行号的方法
2015/03/10 Python
Python检测一个对象是否为字符串类的方法
2015/05/21 Python
Python SQLite3数据库日期与时间常见函数用法分析
2017/08/14 Python
python互斥锁、加锁、同步机制、异步通信知识总结
2018/02/11 Python
python实现旋转和水平翻转的方法
2018/10/25 Python
小白教你PyCharm从下载到安装再到科学使用PyCharm2020最新激活码
2020/09/25 Python
Canvas 文字碰撞检测并抽稀的方法
2019/05/27 HTML / CSS
html5 viewport使用方法示例详解
2013/12/02 HTML / CSS
localStorage、sessionStorage使用总结
2017/11/17 HTML / CSS
西班牙家用电器和电子产品购物网站:Mi Electro
2019/02/25 全球购物
监理资料员岗位职责
2014/01/03 职场文书
《姥姥的剪纸》教学反思
2014/02/25 职场文书
工伤事故证明
2014/10/20 职场文书
2014年体检中心工作总结
2014/12/23 职场文书
党支部意见范文
2015/06/02 职场文书
tensorflow+k-means聚类简单实现猫狗图像分类的方法
2021/04/28 Python
Redis IP地址的绑定的实现
2021/05/08 Redis
vue中控制mock在开发环境使用,在生产环境禁用方式
2022/04/06 Vue.js