• Node 进阶学习


    一、网络通信

    前言

    • 传统的Web平台大多数都需要专门的Web服务器作为容器,如 ASP、ASP.NET 需要 IIS 作为服务器,PHP 需要搭载 Apache 或 Nginx 环境等
    • Node 提供了 net、dgram、http、https 4个模块,分别用于处理TCP、UDP、HTTP、HTTPS,适用于服务器端和客户端

    1.1 构建 TCP 服务

    1.1.1 创建 TCP 服务端和客户端

    是一种面向连接的、可靠的、基于字节流的传输层通信协议

    // server.js
    const net = require('net')
    
    const server = net.createServer()
    
    server.on('connection', clientSocket => {
      console.log('客户端连接成功')
    
      // 监听客户端发送的数据
      clientSocket.on('data', data => {
        console.log(data.toString())
      })
    
      // 给当前连接的客户端发送数据
      clientSocket.write('hello')
    })
    
    server.listen(3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    // client.js
    const net = require('net')
    
    // 连接服务器
    const client = net.createConnection({
      host: '127.0.0.1',
      port: 3000
    })
    
    client.on('connect', () => {
      console.log('成功连接服务器')
    
      // 给服务端发消息
      client.write('world !')
    })
    
    // 监听服务器发送的数据
    client.on('data', data => {
      console.log(data.toString())
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    1.2 创建聊天室

    // server.js
    const net = require('net')
    
    const server = net.createServer()
    
    // 客户端数组
    const clients = []
    
    server.on('connection', clientSocket => {
      clients.push(clientSocket)
    
      // 监听客户端发送的数据
      clientSocket.on('data', data => {
        clients.forEach(socket => {
          // 排除自己,给所有连上的客户端发送消息
          if (socket !== clientSocket) {
            clientSocket.write('有人说:' + data)
          }
        })
      })
    
      // 监听客户端断开连接
      clientSocket.on('end', () => {
        const index = clients.findIndex(client => client === clientSocket)
        clients.splice(index, 1)
      })
    })
    
    server.listen(3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    // client.js
    const net = require('net')
    
    const client = net.createConnection({
      host: '127.0.0.1',
      port: 3000
    })
    
    client.on('connect', () => {
      // 监听终端的输入
      process.stdin.on('data', data => {
        client.write(data.toString().trim())
      })
    })
    
    // 监听服务器发送的数据
    client.on('data', data => {
      console.log(data.toString())
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.2 构建 UDP 服务

    UDPTCP
    连接无连接面向连接
    速度无需建立连接,速度较快需要建立连接,速度较慢
    目的主机一对一,一对多仅能一对一
    带宽UDP 报头较短,消耗带宽更少消耗更多的带宽
    消息边界
    可靠性
    顺序无序有序

    注意:UDP协议的这种乱序性基本上很少出现,通常只会在网络非常拥挤的情况下才有可能发生

    什么时候用 TCP,什么时候用 UDP?

    • 对速度要求比较高的时候使用UDP,例如视频聊天, QQ聊天
    • 对数据安全要求比较高的时候使用TCP,例如数据传输,文件下载
    • 假如对于视频聊天来说,如果画质优先那就选用TCP, 如果流畅度优先那就选用UDP

    1.2.1 UDP 单播

    单播是目的地址为单一目标的一种传播方式,地址范围:0.0.0.0 ~ 223.255.255.255

    // server.js
    const dgram = require('dgram')
    
    const server = dgram.createSocket('udp4')
    
    // 客户端连接成功触发
    server.on('listening', () => {
      const address = server.address()
      console.log(`server running ${address.address}:${address.port}`)
    })
    
    // 客户端发送消息时触发
    server.on('message', (msg, remoteInfo) => {
      console.log(`${remoteInfo.address}:${remoteInfo.port} 发送消息: ${msg}`)
      server.send('world', remoteInfo.port, remoteInfo.address)
    })
    
    // 服务器异常时触发
    server.on('error', err => {
      console.log('server error', err)
    })
    
    // 绑定端口
    server.bind(3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    // client.js
    const dgram = require('dgram')
    
    const client = dgram.createSocket('udp4')
    
    // 服务器连接成功触发
    client.on('listening', () => {
      const address = client.address()
      console.log(`client running ${address.address}:${address.port}`)
      client.send('hello', 3000, 'localhost')
    })
    
    // 服务器发送消息时触发
    client.on('message', (msg, remoteInfo) => {
      console.log(`${remoteInfo.address}:${remoteInfo.port} 发送消息: ${msg}`)
    })
    
    // 客户端异常时触发
    client.on('error', err => {
      console.log('client error', err)
    })
    
    // 绑定端口
    client.bind(8000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    1.2.2 UDP 广播

    • 目的地址为网络中的所有设备
    • 地址范围分为两种
      • 受限广播:它不被路由转发,IP 地址的网络字段和主机字段全为1就是地址 255.255.255.255
      • 直接广播:会被路由转发,IP 地址的网络字段定义这个网络,主机字段通常全为1,如 192.168.10.255
    // server.js
    const dgram = require('dgram')
    
    const server = dgram.createSocket('udp4')
    
    // 客户端连接成功触发
    server.on('listening', () => {
      const address = server.address()
      console.log(`server running ${address.address}:${address.port}`)
    
      server.setBroadcast(true) // 开启广播模式
      // server.setBroadcast(false) // 关闭广播模式
    
      server.send('hello', 8000, '255.255.255.255')
    
      // 每隔2s发送一条广播消息
      setInterval(() => {
        // 直接地址:192.168.10.255
        // 受限地址:255.255.255.255
        server.send('hello', 8000, '255.255.255.255')
      }, 2000)
    })
    
    // 客户端发送消息时触发
    server.on('message', (msg, remoteInfo) => {
      console.log(`${remoteInfo.address}:${remoteInfo.port} 发送消息: ${msg}`)
      server.send('world', remoteInfo.port, remoteInfo.address)
    })
    
    // 服务器异常时触发
    server.on('error', err => {
      console.log('server error', err)
    })
    
    // 绑定端口
    server.bind(3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    1.3 构建 HTTP 服务

    TCP 和 UDP 都属于网络传输层协议,如果要构建高效的网络应用,就应该从传输层进行着手。但是对于经典的浏览器网页和服务端通信场景,如果单纯的使用更底层的传输层协议则会变得麻烦。

    所以对于经典的B(Browser)S(Server)通信,基于传输层之上专门制定了更上一层的通信协议:HTTP,用于浏览器和服务端进行通信。由于 HTTP 协议本身并不考虑数据如何传输及其他细节问题,所以属于应用层协议。

    Server 实例

    API说明
    Event:‘close’服务关闭时触发
    Event:‘request’收到请求消息时触发
    server.close()关闭服务
    server.listening获取服务状态

    请求对象

    API说明
    request.method请求方法
    request.url请求路径
    request.headers请求头
    request.httpVersion请求HTTP协议版本

    响应对象

    API说明
    response.end()结束响应
    response.setHeader(name, value)设置响应头
    response.removeHeader(name, value)删除响应头
    response.statusCode设置响应状态码
    response.statusMessage设置响应状态短语
    response.write()写入响应数据
    response.writeHead()写入响应头

    1.3.1 创建基本 HTTP 服务

    const http = require('http')
    
    const server = http.createServer()
    
    server.on('request', (req, res) => {
      res.statusCode = 200
      res.setHeader('Content-Type', 'text/plain')
      res.end('Hello World\n')
    })
    
    server.listen(3000, '127.0.0.1', () => {
      console.log('server running')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.3.2 统一处理静态资源

    npm i mime

    const http = require('http')
    const fs = require('fs')
    const path = require('path')
    const mime = require('mime')
    
    const server = http.createServer()
    
    server.on('request', (req, res) => {
      const url = req.url
      // 统一处理静态资源
      fs.readFile(url, (err, data) => {
        if (err) {
          throw err
        }
    
        const contentType = mime.getType(path.extname(url))
        res.setHeader('Content-Type', contentType)
        res.end(data)
      })
    })
    
    server.listen(3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    二、Node 事件循环与多进程

    2.1 Node 事件循环

    当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下六个循环阶段,nodejs事件循环和浏览器的事件循环完全不一样。

    阶段概览

    • timers(定时器) : 此阶段执行那些由 setTimeout()setInterval() 调度的回调函数.

    • I/O callbacks(I/O回调) : 此阶段会执行几乎所有的回调函数, 除了 close callbacks(关闭回调) 和 那些由 timerssetImmediate()调度的回调.

    • idle(空转), prepare : 此阶段只在内部使用

    • poll(轮询) : 检索新的I/O事件; 在恰当的时候Node会阻塞在这个阶段

    • check(检查) : setImmediate() 设置的回调会在此阶段被调用

    • close callbacks(关闭事件的回调): 诸如 socket.on('close', ...) 此类的回调在此阶段被调用

    在事件循环的每次运行之间, Node.js会检查它是否在等待异步I/O或定时器, 如果没有的话就会自动关闭.

    如果event loop进入了 poll 阶段,且代码未设定timer,将会发生下面情况:

    • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
    • 如果poll queue为空,将会发生下面情况:
      • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
      • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue,一旦到达就立即执行

    如果event loop进入了 poll阶段,且代码设定了timer:

    • event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.

    2.2 Node 多进程

    2.2.1 多进程和多线程介绍

    进程是资源分配的最小单位,线程是CPU调度的最小单位

    “进程——资源分配的最小单位,线程——程序执行的最小单位”

    一个进程下面的线程是可以去通信的,共享资源

    线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成,线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

    进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉。

    • 谷歌浏览器

      • 进程: 一个tab就是一个进程
      • 线程: 一个tab又由多个线程组成,渲染线程,js执行线程,垃圾回收,service worker 等等
    • node服务

      • 进程:监听某一个端口的http服务
      • 线程: http服务由多个线程组成,比如:
        • 主线程:获取代码、编译执行
        • 编译线程:主线程执行的时候,可以优化代码
        • Profiler线程:记录哪些方法耗时,为优化提供支持
        • 其他线程:用于垃圾回收清除工作,因为是多个线程,所以可以并行清除

    2.2.2 多进程和多线程选择

    在这里插入图片描述

    1)需要频繁创建销毁的优先用线程

    这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

    2)需要进行大量计算的优先使用线程

    所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。

    3)强相关的处理用线程,弱相关的处理用进程

    什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

    一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

    4)可能要扩展到多机分布的用进程,多核分布的用线程

    5)都满足需求的情况下,用你最熟悉、最拿手的方式

    总结: 线程快而进程可靠性高。

    2.2.3 cluster 创建多进程

    const cluster = require('cluster')
    const http = require('http')
    const cpus = require('os').cpus().length // 获取CPU的个数
    
    if (cluster.isMaster) { // 主线程
      for (let i = 0; i < cpus; i++) {
        cluster.fork()
      }
    } else { // 子线程
      http.createServer((req, res) => {
        res.end('Hello World!')
      }).listen(3000, () => {
        console.log('Server Run 3000')
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.2.4 cluster 属性方法介绍

    1. isMaster 属性,返回该进程是不是主进程
    2. isWorker 属性,返回该进程是不是工作进程
    3. fork() 方法,只能通过主进程调用,衍生出一个新的 worker 进程,返回一个 worker 对象。和process.child的区别,不用创建一个新的child.js

    2.2.5 process 进程

    process 对象是 Node 的一个全局对象,提供当前 Node 进程的信息,他可以在脚本的任意位置使用,不必通过 require 命令加载

    属性

    1. process.argv 属性,返回一个数组,包含了启动 node 进程时的命令行参数
    2. process.env 返回包含用户环境信息的对象,可以在 脚本中对这个对象进行增删改查的操作
    3. process.pid 返回当前进程的进程号
    4. process.platform 返回当前的操作系统
    5. process.version 返回当前 node 版本

    方法

    1. process.cwd() 返回 node.js 进程当前工作目录
    2. process.chdir() 变更 node.js 进程的工作目录
    3. process.nextTick(fn) 将任务放到当前事件循环的尾部,添加到 ‘next tick’ 队列,一旦当前事件轮询队列的任务全部完成,在 next tick 队列中的所有 callback 会被依次调用
    4. process.exit() 退出当前进程,很多时候是不需要的
    5. process.kill(pid[,signal]) 给指定进程发送信号,包括但不限于结束进程

    事件

    1. beforeExit 事件,在 Node 清空了 EventLoop 之后,再没有任何待处理任务时触发,可以在这里再部署一些任务,使得 Node 进程不退出,显示的终止程序时(process.exit()),不会触发

    2. exit 事件,当前进程退出时触发,回调函数中只允许同步操作,因为执行完回调后,进程退出

    3. uncaughtException 事件,当前进程抛出一个没有捕获的错误时触发,可以用它在进程结束前进行一些已分配资源的同步清理操作,尝试用它来恢复应用的正常运行的操作是不安全的

        process.on('uncaughtException', err => {
          console.log(err)
        })
      
      • 1
      • 2
      • 3
    4. warning 事件,任何 Node.js 发出的进程警告,都会触发此事件

    三、爬虫

    爬虫的基本工作流程如下:

    1. 向指定的URL发送http请求
    2. 获取响应(HTML、XML、JSON、二进制等数据)
    3. 处理数据(解析DOM、解析JSON等)
    4. 将处理好的数据进行存储

    注意:在爬取目标网站之前,建议浏览该网站的robots.txt,来确保自己爬取的数据在对方允许范围之内

    3.1 爬虫基础

    http://web.itheima.com/teacher.html网站目标为例,最终目的是下载网站中所有老师的照片

    下载所有老师的照片,需要通过如下步骤实现:

    1. 发送http请求,获取整个网页内容
    2. 通过cheerio库对网页内容进行分析
    3. 提取img标签的src属性
    4. 使用download库进行批量图片下载

    3.1.1 发送一个HTTP请求

    const http = require('http')
    
    const req = http.request('http://web.itheima.com/teacher.html', res => {
      let data = ''
      // 监听 data 事件,获取传递过来的数据片段
      res.on('data', chunk => {
        data += chunk
      })
    
      // 监听 end 事件,获取数据完毕时触发
      res.on('end', () => {
        console.log(data)
      })
    })
    
    req.end()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.1.2 使用 cheerio 解析

    可以通过jQuery的api来获取DOM元素中的属性和内容
    npm i cheerio

    // 官方 DEMO
    const cheerio = require('cheerio')
    const $ = cheerio.load('

    Hello world

    '
    ) $('h2.title').text('Hello there!') $('h2').addClass('welcome') $.html() //=>

    Hello there!

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    const http = require('http')
    const cheerio = require('cheerio')
    
    const HOST = 'http://web.itheima.com'
    
    const req = http.request(HOST + '/teacher.html', res => {
      let data = ''
      // 监听 data 事件,获取传递过来的数据片段
      res.on('data', chunk => {
        data += chunk
      })
    
      // 监听 end 事件,获取数据完毕时触发
      res.on('end', () => {
        const $ = cheerio.load(data)
        // 获取所有图片
        const imgs = Array.prototype.map.call($('.tea_main .tea_con li > img'), item => HOST + $(item).attr('src'))
        console.log(imgs)
      })
    })
    
    req.end()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3.1.3 使用 download 解析

    npm i download

    const http = require('http')
    const cheerio = require('cheerio')
    const download = require('download')
    
    const HOST = 'www.xxx.com/'
    
    const req = http.request(HOST + 'teacher.html', res => {
      let data = ''
      // 监听 data 事件,获取传递过来的数据片段
      res.on('data', chunk => {
        data += chunk
      })
    
      // 监听 end 事件,获取数据完毕时触发
      res.on('end', () => {
        const $ = cheerio.load(data)
        // 获取所有图片,图片路径需转码
        const imgs = Array.prototype.map.call($('.tea_main .tea_con li > img'), item => encodeURI(HOST + $(item).attr('src')))
        
        // 批量下载图片到当前目录下 dist 文件夹下
        Promise.all(imgs.map(x => download(x, 'dist'))).then(() => {
          console.log('files downloaded!')
        })
      })
    })
    
    req.end()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    3.2 封装爬虫基础库

    3.2.1 初始化

    执行tsc --init初始化项目,生成ts配置文件

    TS配置:

    {
      "compilerOptions": {
        /* Basic Options */
        "target": "es2015", 
        "module": "commonjs", 
        "outDir": "./bin", 
        "rootDir": "./src", 
        "strict": true,
        "esModuleInterop": true 
      },
      "include": [
        "src/**/*"
      ],
      "exclude": [
        "node_modules",
        "**/*.spec.ts"
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.2.2 定义类

    父类

    // 父类 Spider
    const http = require('http')
    
    interface SpiderOptions {
      url: string,
      method?: string,
      headers?: object
    }
    
    // 定义抽象类
    export default abstract class Spider {
      // 定义成员
      options: SpiderOptions
    
      // 初始化
      constructor(options: SpiderOptions) {
        this.options = options
        this.start()
      }
    
      start() {
        const { url, method = 'get', headers } = this.options
        const req = http.request(url, {
          method,
          headers
        }, (res: any) => {
          let result: string = ''
          // 监听 data 事件,获取传递过来的数据片段
          res.on('data', (chunk: any) => {
            result += chunk
          })
    
          // 监听 end 事件,获取数据完毕时触发
          res.on('end', () => {
            // 调用抽象方法,具体实现由子子孙孙继承实现
            this.onCatchHTML(result)  
          })
        })
    
        req.end()
      }
    
      abstract onCatchHTML(result: string): any
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    子类

    // 子类 Photos
    const cheerio = require('cheerio')
    const download = require('download')
    
    const HOST = ''
    
    // 继承 Spider,实现onCatchHTML方法
    import Spider from './Spider'
    
    export default class Photos extends Spider {
      onCatchHTML(result: string) {
        const $ = cheerio.load(result)
        // 获取所有图片,图片路径需转码
        const imgs = Array.prototype.map.call($('.tea_main .tea_con li > img'), item => encodeURI(HOST + $(item).attr('src')))
        
        // 批量下载图片到当前目录下 dist 文件夹下
        Promise.all(imgs.map(x => download(x, 'dist'))).then(() => {
          console.log('files downloaded!')
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    测试

    import Photos from './Photos'
    
    new Photos({
      url: 'http://api/teacher.html'
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.3 Selenium

    Selenium是一个Web应用的自动化测试框架,可以创建回归测试来检验软件功能和用户需求,通过框架可以编写代码来启动浏览器进行自动化测试,换言之,用于做爬虫就可以使用代码启动浏览器,让真正的浏览器去打开网页,然后去网页中获取想要的信息!从而实现真正意义上无惧反爬虫手段!

    3.3.1 安装

    1. 根据平台下载需要的webdriver
    2. 项目中安装selenium-webdriver包
    3. 根据官方文档写一个小demo
    根据平台选择webdriver
    浏览器webdriver
    Chromechromedriver(.exe)
    Internet ExplorerIEDriverServer.exe
    EdgeMicrosoftWebDriver.msi
    Firefoxgeckodriver(.exe)
    Safarisafaridriver

    根据浏览器选择版本和平台(选浏览器版本号前三位一致的):
    在这里插入图片描述
    下载后放入项目根目录
    在这里插入图片描述

    3.3.2 自动打开百度搜索“淘宝“

    npm i selenium-webdriver

    const { Builder, By, Key, until } = require('selenium-webdriver')
    
    ;(async function example() {
      // 打开chrome浏览器
      const driver = await new Builder().forBrowser('chrome').build()
      try {
        // 自动打百度
        await driver.get('https://www.baidu.com')
        // 找到百度的 id 为 kw 的元素(也就是搜索框元素), 自动输入'淘宝'并回车
        await driver.findElement(By.id('kw')).sendKeys('淘宝', Key.ENTER)
        // 等 1s 修改网站 title
        console.log(await driver.wait(until.titleIs('淘宝_百度搜索'), 1000))
      } finally {
        // 关闭浏览器
        // await driver.quit()
      }
    })()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.3.3 核心API

    核心对象:

    • Builder
    • WebDriver
    • WebElement

    辅助对象:

    • By
    • Key
    Builder

    用于构建WebDriver对象的构造器

    const driver = new webdriver.Builder()
      .forBrowser('chrome')
      .build()
    
    • 1
    • 2
    • 3

    其他API如下:
    可以获取或设置一些Options

    在这里插入图片描述
    如需设置Chrome的Options,需要先导入Options:

    const { Options } = require('selenium-webdriver/chrome')
    const options = new Options()
    options.addArguments('Cookie=user_trace_token=20191130095945-889e634a-a79b-4b61-9ced-996eca44b107; X_HTTP_TOKEN=7470c50044327b9a2af2946eaad67653;')
    
    • 1
    • 2
    • 3
    WebDriver

    通过构造器创建好WebDriver后就可以使用API查找网页元素和获取信息了:

    在这里插入图片描述

    WebElement
    • getText() 获取文本内容
    • sendKeys() 发送一些按键指令
    • click() 点击该元素

    在这里插入图片描述

    四、WebSocket

    建立连接:

    WebSocket 连接必须由浏览器发起,因为请求协议是一个标准的 HTTP 请求,格式如下:

    GET ws://localhost:3000/ws/chat HTTP/1.1
    Host: localhost
    Upgrade: websocket
    Connection: Upgrade
    Origin: http://localhost:3000
    Sec-webSocket-Key: client-random-string
    Sec-webSocket-Version: 13
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    该请求和普通的HTTP请求有几点不同:

    1. GET请求的地址不是类似/path/,而是以ws:// 开头的地址
    2. 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接
    3. Sec-webSocket-Key是用于表示这个连接,并非用于加密数据
    4. Sec-webSocket-Version指定了WebSocket的协议版本

    如果服务器接受该请求,就会返回如下响应:

    HTTP/1.1 101 Switching Protoclos
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: server-random-string
    
    • 1
    • 2
    • 3
    • 4

    响应码 101 表示本次连接的HTTP协议即将被更改,更改后的协议就是 Upgrade: websocket指定的WebSocket协议

    4.1 特点

    (1)建立在 TCP 协议之上,服务器端的实现比较容易。

    (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

    (3)数据格式比较轻量,性能开销小,通信高效。

    (4)可以发送文本,也可以发送二进制数据。

    (5)没有同源限制,客户端可以与任意服务器通信。

    (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

    最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

    4.2 应用场景

    • 聊天室
    • 消息系统:推送消息、实况、股票基金等实时变化的数据
    • 点赞
    • 直播评论(弹幕)
    • 游戏
    • 协同编辑/编辑
    • 基于位置的应用
    • 在线教育(多媒体聊天、文字消息)

    4.3 ws 模块

    npm i ws

    简易聊天室

    // client.js
    const WebSocket = require('ws')
    
    // 连接socket服务器
    const ws = new WebSocket('ws://localhost:8080?token=XXX')
    
    ws.onopen = function () {
      console.log('socket 连接成功')
      ws.send('客户端给服务器发送消息')
    }
    
    // 监听服务器发送消息
    ws.onmessage = function (messageInfo) {
      console.log(messageInfo.data)
    }
    
    ws.onerror = function () {
      console.log('socket 连接失败')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    // server.js
    // 创建 socket 服务器
    const WebSocket = require('ws')
    const WebSocketServer = WebSocket.WebSocketServer
    
    const wss = new WebSocketServer({ port: 8080 })
    
    wss.on('connection', function connection(ws) {
      // 监听客户端传过来的消息
      ws.on('message', function message(data, isBinary) {
        console.log('received: %s', data)
    
        // 一个客户端 WebSocket 广播到所有其他连接的 WebSocket 客户端,不包括它自己
        wss.clients.forEach(function each(client) {
          if (client !== ws && client.readyState === WebSocket.OPEN) {
            client.send(data, { binary: isBinary })
          }
        })
      })
    
      // 给客户端发送消息
      ws.send('给客户端发送消息')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4.4 socket.io

    1)客户端

    DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Documenttitle>
    head>
    <body>
      <input type="text" id="input" />
      <button id="btn">发送button>
    
      <script src="/socket.io/socket.io.js">script>
      <script>
        const socket = io()
    
        btn.onclick = function() {
          const input = document.getElementById('input')
          const value = input.value
          // 给服务端发送消息
          socket.emit('chat msg', value)
          input.value = ''
        }
    
        // 监听服务端发送的消息
        socket.on('message', function(msg) {
          console.log(msg)
        })
      script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    2)服务端

    const express = require('express')
    const app = express()
    const server = require('http').Server(app)
    const io = require('socket.io')(server)
    
    app.get('/', function (req, res) {
      res.sendFile(__dirname + '/public/index.html')
    })
    
    // 监听连接
    io.sockets.on('connection', function (socket) {
      // 获取客户端的消息
      socket.on('chat msg', function (msg) {
        console.log('msg from client: ' + msg)
        // 发送消息给客户端
        socket.send('server says: ' + msg)
      })
    })
    
    server.listen(3000, function () {
      console.log('server is running on: 3000')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
  • 相关阅读:
    8.1 SafeSEH对异常处理的保护原理
    [C++从入门到精通] 带你彻底了解String类型的相关用法
    蚂蚁金服开源的这份SpringBoot笔记,两周时间在GitHub星标43.5k
    Flutter气泡框实现
    如何便捷获取参考文献的引用格式?
    设计模式——观察者模式
    asp.net乡村旅游管理系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio
    Moonriver在Kusama的第1年
    一文搞懂drag&drop浏览器拖放功能的实现
    课程设计:班级通讯录管理系统(Java+MySQL)
  • 原文地址:https://blog.csdn.net/weixin_44257930/article/details/125554271