WebSocket 爬虫资源文件下载案例分析

这是「进击的Coder」的第 705 篇技术分享

作者:TheWeiJun

来源:逆向与爬虫的故事

“ 

阅读本文大概需要 9 分钟。

  ”

 目录


一、websocket 简介

二、websocket 机制

三、实战案例分析

四、完整代码实现

五、实战心得分享


趣味模块

       小明是一名爬虫工程师,有一天小明去参加面试。面试官问了小明很多专业性的爬虫知识点,小明都一一对答如流;作为一个工作多年的老司机来说,这点难度不算什么!正当小明洋洋得意的时候,面试官对小明说:你有接触过 websocket 爬虫吗?小明一下子傻眼了,什么是 websocket 爬虫?那么接下来,让我们一起去看看 TheWeiJun 的 websocket 实战案例分享吧,小明阅读后,立马找到了心仪的工作。


一、什么是 websocket?

前言:WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双通讯的协议。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。被广泛应用于对数据实时性要求较高的场景,如体育赛事播报、股票走势分析、在线聊天等。HTML5 定义了 WebSocket 协议,能更好地节省服务器资源和带宽,并且能够更实时地进行通讯。


二、websocket 机制

websocket 通信过程:

  • 客户端发起握手请求

  • 服务器端收到请求后验证并返回握手结果

  • 连接建立成功,服务器端开始推送消息

b10664182e8e647ce4372d568795c875.png

总结说明:浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。


三、实战案例分析

1、打开指定目标网站,访问指定 url 页面,截图如下所示:

ff6cecd0cc7683569264e206d7245129.png

2、点击 Download 按钮,触发下载操作,在开发者模式中观察请求包,截图如下所示:

46a5d343ca6839fb42d5baccb95a1f82.png

触发下载机制后,我们对浏览器页面截图如下:

c026af76c431225ad7577c585de67493.png

初判断:初步怀疑是使用的 AJAX 请求来下载数据,但是通过观察开发者模式数据包;并没有发现我们要的 zip 包内容。正当我疑惑时,在开发者模式【WS】栏目中看到了 websocket 请求包。

3、我们点开【WS】,会发现服务器一直在传输新的信息,这些信息应该就是我们要的数据。

5aca7e92b2639214a9a360a943ff5c64.png

总结:观察该请求包,我们会看见三条客户端请求服务器端的绿色请求信息,也就是验证信息 message。并且经过多次刷新观察,我们发现随着下载资源的 url 路径变动,里面的 url 参数也在变动;而且我还发现,里面的 url 参数就是我刚刚下载的资源的 url 详情页的 id。可以更加确信下载的 zip 资源地址走的就是 websocket 协议。

4、查看该请求的 url、message 参数,然后我们用 python 去实现发包过程,截图如下所示:

03591e7606cdf5d78dcdda8c5a373c7f.png

代码运行后,居然报错了,截图如下所示:

3b64999d3381b86d990846aa8bb73b53.png

定位到问题后,在 websocket 添加 ssl 证书认证后,截图如下:

import ssl
import certifi
ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(certifi.where())
async def call_wss_api(url, message):
    async with websockets.connect(url, ssl=ssl_context) as websocket:
        download_status = False
        while websocket.open:


            response = await websocket.recv()
            if response == 'o':
                await websocket.send(message)
                continue
            print(response)

b4574fef25ad4fb4bcc0a0fead9abb1d.png

总结:再次运行代码后,观察上图,我们发现代码运行后,websocket 服务器端除了返回两条信息后不会再持续推送信息给我们,这和刚刚我们在浏览器开发者工具中看到的不一样啊。这个时候我怀疑是不是会有一些上报的操作呢,然后我再次对比,发现了规律。

5、清空 Network 路径下的所有请求包,再次触发下载机制,请求包截图如下:

0bbd10ecf279735c40fb914665e74e63.png

总结:通过求证可以看到,的确有上报机制。当我们触发下载机制时,浏览器会发送一个 ajax 请求给 web 服务器去请求下载资源,服务器收到我们上报的信息后,会根据我们提交的参数及任务 id 找到对应的资源,通过 websocket 协议推送数据给我们,这种策略确实很少遇到。

6、流程透明化后,我们梳理一下整个过程,我画了一个简单的数据流程图,如下所示:

beb0ca2aa2c78277fde508d61b897cee.png

7、代码开发完毕后,下载截图如下所示:

7e54686f40a0eddaa342024a05c0f669.png

环节总结:观察上图,我们可以看到zip资源文件已经在下载了,为了方便大家观看,我特意加了一个进度条,可以实时查看进度。所有流程跑通后,接下来我们一起进入完整代码实现环节。


四、完整代码实现

核心代码实现如下:

import asyncio
import ssl
import certifi
import websockets
import json
import requests


ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(certifi.where())
def start_requests(_id):
    cookies = {
        # '_ga': 'GA1.2.640850929.1656902624',
        # '_gid': 'GA1.2.129165265.1656902624',
        'connect.sess': 's%3Aj%3A%7B%22notifications%22%3A%5B%5D%2C%22passport%22%3A%7B%22user%22%3A%22%22%7D%7D.m5wHu9pCIyrCxd%2BA3jPb%2BGXe%2B6Rqhw2jz9xGqcELyw4',
        # '_gat': '1',
    }
    headers = {
        'authority': 'clara.io',
        'accept': '*/*',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cache-control': 'no-cache',
        'content-type': 'multipart/form-data; boundary=----WebKitFormBoundarylAfH6invLQtiGuL1',
        'pragma': 'no-cache',
        'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"macOS"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin',
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36',
    }


    data = '------WebKitFormBoundarylAfH6invLQtiGuL1\r\nContent-Disposition: form-data; name="attributes"\r\n\r\n{"data":{"extension":"fbx","format":"Autodesk FBX (.fbx)","zip":true,"type":"Scene","exportOptions":{"enableDecimalPrecision":false,"exportPrecision":10,"centerScene":false,"alignSceneGround":false,"imageFormat":"Original","fbxFormat":1,"fbxVersion":5,"fbxUnit":"Meter","fbxEmbedTextures":true,"fbxAnimations":true,"fbxSkins":true,"stlFormat":2,"stlUnit":1,"stlCustomUnit":1}},"name":"Exporting Autodesk FBX (.fbx)","type":"export","_id":"%s","progress":0,"status":"starting"}\r\n------WebKitFormBoundarylAfH6invLQtiGuL1--\r\n' % _id
    response = requests.post('https://xxxxxx/api/scenes/xxxxxx/jobs', cookies=cookies,
                             headers=headers, data=data)
    print(response.text)


def run_spider():
    url = "wss://xxxx/modelio/865/dv3ojrji/websocket"
    msg = [
        "{\"userCookie\":\"c5Nli6kT1nSlK1NuLLSsnaHmJ+rBnzjMvCLb3Vrq4wc\",\"event\":\"listen\",\"uri\":\"/scenes/5a3b1964-df03-4695-8464-787472eb023a/jobs\"}"]
    asyncio.get_event_loop().run_until_complete(call_wss_api(url, json.dumps(msg)))


def json_loads(response):
    data = response[1:]
    text = json.loads(data)[0]
    return json.loads(text)




async def call_wss_api(url, message):
    async with websockets.connect(url, ssl=ssl_context) as websocket:
        download_status = False
        while websocket.open:


            response = await websocket.recv()
            if response == 'o':
                await websocket.send(message)
                continue


            item = json_loads(response)
            if not download_status:
                download_status = True
                _id = item.get("clientId")
                start_requests(_id)


            progress = item.get('data', {}).get("progress") or 0
            print(f"资源文件的当前进度条:{progress * 100}%")
            _data = item.get("data", {}).get("files") or []
            if _data:
                filename = _data[0].get("name")
                _hash = _data[0].get("hash")
                resources = f"https://xxxx/resources/{_hash}?filename={filename}"
                return resources




if __name__ == '__main__':
    run_spider()

五、实战心得分享

回顾整个分析流程,本次难点主要概括为以下几点:

  • 了解并熟练掌握 websocket 协议

  • 需要能找到资源下载规律

  • 如何定位到上报机制 log

  • websocket 异步代码实现

今天分享到这里就结束了,欢迎大家关注下期文章,我们不见不散⛽️

77d0be406abf7f808acc96bcfbe19470.png

End

崔庆才的新书《Python3网络爬虫开发实战(第二版)》已经正式上市了!书中详细介绍了零基础用 Python 开发爬虫的各方面知识,同时相比第一版新增了 JavaScript 逆向、Android 逆向、异步爬虫、深度学习、Kubernetes 相关内容,‍同时本书已经获得 Python 之父 Guido 的推荐,目前本书正在七折促销中!

内容介绍:《Python3网络爬虫开发实战(第二版)》内容介绍

f00f52d3f6ba4637d2d83ef3cc162cf6.jpeg

扫码购买

e61bcc57031e219a3a666ed9a38cf329.png

好文和朋友一起看~

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值