一, 前言
作为一名爬虫工程师, 在工作中常常会遇到爬取实时数据的需求, 比如体育赛事实时数据, 股市实时数据或币圈实时变化的数据. 如下图:
web 领域中, 用于实现数据'实时'更新的手段有轮询和 WebSocket 这两种. 轮询指的是客户端按照一定时间间隔 (如 1 秒) 访问服务端接口, 从而达到 '实时' 的效果, 虽然看起来数据像是实时更新的, 但实际上它有一定的时间间隔, 并不是真正的实时更新. 轮询通常采用 拉 模式, 由客户端主动从服务端拉取数据.
WebSocket 采用的是 推 模式, 由服务端主动将数据推送给客户端, 这种方式是真正的实时更新.
二, 什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议. 它使得客户端和服务器之间的数据交换变得更加简单, 允许服务端主动向客户端推送数据. 在 WebSocket API 中, 浏览器和服务器只需要完成一次握手, 两者之间就直接可以创建持久性的连接, 并进行双向数据传输.
WebSocket 优点
较少的控制开销: 只需要进行一次握手, 携带一次请求头信息即可, 后续只传输数据即可, 相比 HTTP 每次请求都携带请求头, WebSocket 非常省资源.
更强的实时性: 由于服务器可以主动推送消息, 这使得延迟变得可以忽略不计, 相比 HTTP 轮询的时间间隔, WebSocket 可以在相同的时间内进行多次传输.
二进制支持: WebSocket 支持二进制帧, 这意味着传输更节省.
......
爬虫面对 HTTP 和 WebSocket
Python 中的网络请求库非常多, Requests 是最常用的请求库之一, 它可以模拟发送网络请求. 但是这些请求都是基于 HTTP 协议的. 在面对 WebSocket 的时候 Requests 就发挥不料作用了, 必须使用能够连接 WebSocket 的库.
三, 爬取思路
这里以莱特币官网 http://www.laiteb.com/ 实时数据为例. WebSocket 的握手只发生一次, 所以如果需要通过浏览器开发者工具观察网络请求, 则需要在打开页面的情况下, 打开浏览器开发者工具, 定位到 NewWork 选项卡, 并输入或刷新当前页面, 才能观察到 WebSocket 的握手请求和数据传输情况. 这里以 Chrome 浏览器为例:
在开发者工具中提供了筛选功能, 其中 WS 选项代表只显示 WebSocket 连接的网络请求.
这时候可以看到请求记录列表中有一条名为 realTime 的记录, 鼠标左键点击它后, 开发者工具会分为左右两栏, 右侧列出本条请求记录的详细信息:
与 HTTP 请求不同的是, WebSocket 连接地址以 ws 或 wss 开头. 连接成功的状态码不是 200, 而是 101.
Headers 标签页记录的是 Request 和 Response 信息, 而 Frames 标签页中记录的则是双方互传的数据, 也是我们需要爬取的数据内容:
Frames 图中绿色箭头向上的数据是客户端发送给服务端的数据, 橙色箭头向下的数据是服务端推送给客户端的数据.
从数据顺序中可以看到, 客户端先发送:
{"action":"subscribe","args":["QuoteBin5m:14"]}
然后服务端才会推送信息(一直推送):
{"group":"QuoteBin5m:14","data":[{"low":"55.42","high":"55.63","open":"55.42","close":"55.59","last_price":"55.59","avg_price":"55.5111587372932781077","volume":"40078","timestamp":1551941701,"rise_fall_rate":"0.0030674846625766871","rise_fall_value":"0.17","base_coin_volume":"400.78","quote_coin_volume":"22247.7621987324"}]}
所以, 从发起握手到获得数据的整个流程为:
那么, 现在问题来了:
握手怎么弄?
连接保持怎么弄?
消息发送和接收怎么弄?
有什么库可以轻松实现吗?
四, aiowebsocket
Python 库中用于连接 WebSocket 的有很多, 但是易用, 稳定的有 websocket-client(非异步),websockets(异步),aiowebsocket(异步).
可以根据项目需求选择三者之一, 今天介绍的是异步 WebSocket 连接客户端 aiowebsocket. 其 GitHub 地址为: https://github.com/asyncins/aiowebsocket.
ReadMe 中介绍到: AioWebSocket 是一个遵循 WebSocket 规范的 异步 WebSocket 客户端, 相对于其他库它更轻, 更快.
它的安装和其他库一样简单, 使用 pip install aiowebsocket 即可. 安装好后, 我们可以根据 ReadMe 中提供的示例代码来测试:
- import asyncio
- import logging
- from datetime import datetime
- from aiowebsocket.converses import AioWebSocket
- async def startup(uri):
- async with AioWebSocket(uri) as AWS:
- converse = AWS.manipulator
- message = b'AioWebSocket - Async WebSocket Client'
- while True:
- await converse.send(message)
- print('{time}-Client send: {message}'
- .format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), message=message))
- mes = await converse.receive()
- print('{time}-Client receive: {rec}'
- .format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), rec=mes))
- if __name__ == '__main__':
- remote = 'ws://echo.websocket.org'
- try:
- asyncio.get_event_loop().run_until_complete(startup(remote))
- except KeyboardInterrupt as exc:
- logging.info('Quit.')
运行后的结果输出为:
2019-03-07 15:43:55-Client send: b'AioWebSocket - Async WebSocket Client' 2019-03-07 15:43:55-Client receive: b'AioWebSocket - Async WebSocket Client' 2019-03-07 15:43:55-Client send: b'AioWebSocket - Async WebSocket Client' 2019-03-07 15:43:56-Client receive: b'AioWebSocket - Async WebSocket Client' 2019-03-07 15:43:56-Client send: b'AioWebSocket - Async WebSocket Client' ......
send 表示客户端向服务端发送的消息
recive 表示服务端向客户端推送的消息
五, 编码获取数据
回到这一次的爬取需求, 目标网站是莱特币官网:
从刚才的网络请求记录中, 我们得知目标网站的 WebSocket 地址为: wss://API.bbxapp.vip/v1/ifcontract/realTime, 从地址中可以看出目标网站使用的是 wss, 也就是 ws 的安全版, 它们的关系跟 HTTP/HTTPS 一样. aiowebsocket 会自动处理并识别 ssl, 所以我们并不需要作额外的操作, 只需要将目标地址赋值给连接 uri 即可:
import asyncio import logging from datetime import datetime from aiowebsocket.converses import AioWebSocket async def startup(uri): async with AioWebSocket(uri) as AWS: converse = AWS.manipulator while True: mes = await converse.receive() print('{time}-Client receive: {rec}' .format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), rec=mes)) if __name__ == '__main__': remote = 'wss://api.bbxapp.vip/v1/ifcontract/realTime' try: asyncio.get_event_loop().run_until_complete(startup(remote)) except KeyboardInterrupt as exc: logging.info('Quit.')
运行代码后观察输出, 你会发现什么都没有发生. 既没有内容输出, 也没有断开连接, 程序一直在运行, 但是什么都没有:
这是为什么呢?
是对方不接受我方的请求吗?
还是有什么反爬虫限制呢?
实际上, 刚才的流程图可以解释这个问题:
整个流程中有一步是需要客户端给服务端发送指定的消息, 服务端验证后才会不停推送数据. 所以, 应该在消息读取前, 握手连接后加上消息发送的代码:
import asyncio import logging from datetime import datetime from aiowebsocket.converses import AioWebSocket async def startup(uri): async with AioWebSocket(uri) as AWS: converse = AWS.manipulator # 客户端给服务端发送消息 await converse.send('{"action":"subscribe","args":["QuoteBin5m:14"]}') while True: mes = await converse.receive() print('{time}-Client receive: {rec}' .format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), rec=mes)) if __name__ == '__main__': remote = 'wss://api.bbxapp.vip/v1/ifcontract/realTime' try: asyncio.get_event_loop().run_until_complete(startup(remote)) except KeyboardInterrupt as exc: logging.info('Quit.')
保存后运行, 就会看到数据源源不断的推送过来:
到这里, 爬虫就能够获取到想要的数据了.
aiowebsocket 做了什么
代码不长, 使用的时候只需要将目标网站 WebSocket 地址填入, 然后按照流程发送数据即可, 那么 aiowebsocket 在这个过程中做了什么呢?
首先, aiowebsocket 根据 WebSocket 地址, 向指定的服务端发送握手请求, 并校验握手结果.
然后, 在确认握手成功后, 将数据发送给服务端.
整个过程中为了保持连接不断开, aiowebsocket 会自动与服务端响应 ping pong.
最后, aiowebsocket 读取服务端推送的消息
[奎因:] 如果你认为 aiowebsocket 帮助了你, 那么请你到 GitHub https://github.com/asyncins/aiowebsocket 上给一个 Star. 如果在使用当中发现问题或者希望给 aiowebsocket 提建议, 那么也可以到 GitHub 上提出. 只要你提出建议, 就一定能够帮助 aiowebsocket 变的更好, 而 aiowebsocket 也能够继续为你服务.
来源: https://juejin.im/post/5c80b768f265da2dae514d4f