详解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 相关文章推荐
总结一些js自定义的函数
Aug 05 Javascript
javascript 字符 Escape,encodeURI,encodeURIComponent
Jul 09 Javascript
javascript 用原型继承来实现对象系统
Mar 22 Javascript
基于JQuery的cookie插件
Apr 07 Javascript
jQuery 动画弹出窗体支持多种展现方式
Apr 29 Javascript
jquery用data方法获取某个元素上的事件
Jun 23 Javascript
Javascript 是你的高阶函数(高级应用)
Jun 15 Javascript
Sea.JS知识总结
May 05 Javascript
AngularJS 依赖注入详解和简单实例
Jul 28 Javascript
es6新特性之 class 基本用法解析
May 05 Javascript
vue给组件传递不同的值方法
Sep 29 Javascript
微信小程序实现列表左右滑动
Nov 19 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
支持oicq头像的留言簿(二)
2006/10/09 PHP
Laravel执行migrate命令提示:No such file or directory的解决方法
2016/03/16 PHP
Ubuntu中支持PHP5与PHP7双版本的简单实现
2018/08/19 PHP
JavaScript入门教程(12) js对象化编程
2009/01/31 Javascript
JQuery实现用户名无刷新验证的小例子
2013/03/22 Javascript
jQuery列表拖动排列具体实现
2013/11/04 Javascript
Jjcarousellite 实现图片列表滚动的简单实例
2013/11/29 Javascript
如何编写高质量JS代码
2014/12/28 Javascript
JS实现当前页居中分页效果的方法
2015/06/18 Javascript
JavaScript实现添加及删除事件的方法小结
2015/08/04 Javascript
Bootstrap入门书籍之(四)菜单、按钮及导航
2016/02/17 Javascript
实例剖析AngularJS框架中数据的双向绑定运用
2016/03/04 Javascript
JavaScript从0开始构思表情插件
2016/07/26 Javascript
jQuery实现文章图片弹出放大效果
2017/04/06 jQuery
JS设计模式之策略模式概念与用法分析
2018/02/05 Javascript
JavaScript学习笔记之DOM基础操作实例小结
2019/01/09 Javascript
JavaScript运行机制实例分析
2020/04/11 Javascript
手动实现vue2.0的双向数据绑定原理详解
2021/02/06 Vue.js
Python判断操作系统类型代码分享
2014/11/22 Python
剖析Python的Twisted框架的核心特性
2016/05/25 Python
Python 中pandas.read_excel详细介绍
2017/06/23 Python
Python编程实现从字典中提取子集的方法分析
2018/02/09 Python
python实现电脑自动关机
2018/06/20 Python
Python中栈、队列与优先级队列的实现方法
2019/06/30 Python
Python3+Appium实现多台移动设备操作的方法
2019/07/05 Python
python识别验证码图片实例详解
2020/02/17 Python
python如何提取英语pdf内容并翻译
2020/03/03 Python
python GUI库图形界面开发之PyQt5信号与槽事件处理机制详细介绍与实例解析
2020/03/08 Python
解决json中ensure_ascii=False的问题
2020/04/03 Python
五分钟学会HTML5的WebSocket协议
2019/11/22 HTML / CSS
英国领先的在线高尔夫设备零售商:Golfgeardirect
2020/12/11 全球购物
绿色城市实施方案
2014/03/19 职场文书
大学生村官考核材料
2014/05/23 职场文书
2014党员批评和自我批评思想汇报
2014/09/21 职场文书
2015年安全工作总结范文
2015/04/02 职场文书
NGINX 权限控制文件预览和下载的实现原理
2022/01/18 Servers