JavaScript 逆向爬取实战

news2024/11/28 4:32:29

准备介绍: 当我们学习完整个 JS 逆向技巧后,这里是一次完整的分析爬取实战

案例介绍

本节案例网站不仅在 API 参数有加密, 而且前端 JS 也带有压缩混淆,其前端压缩打包工具使用 webpack , 混淆工具使用 javascript-obfuscator 。 分析该网站需要熟练掌握浏览器的开发者使用工具和一定的调试技巧,另外还需要用到一些 Hook 技术等辅助分析手段

案例网址: https://spa6.scrape.center  

看着没什么不同,点进去看一下每部电影的 URL 的变化

可以看到详情页的 URL 包含了一个长字符串,看上去像是 Base64 编码

接下来看 Ajax 请求, 我们从第一页到第十页依次点击一下,看看 Ajax 请求的变化

可以看到, Ajax 接口的 URL  里多了一个 token ,而且不同的页码, token 都是不一样的,它们看上去同样是 Base64 编码的字符串

另外,更困难的是,这个接口还有时效性。 如果我们把 Ajax 接口的 URL 直接复制下来,短期内可以访问,但是过段时间就无法访问了,会直接返回 401 状态码

我们这里把第一部电影的返回结果全部展开了, 但是刚才我们观察到第一部电影的 URL 是 

https://spa6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx

看起来是 Base64 编码,我们对其进行解码, 结果为 

ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1

看起来毫无规律,这个解码后的结果怎么来的? 返回的结果里也并不包含这个字符串,这又是怎么构造的

还有这仅仅是详情页的 URL , 其真实数据是通过 Ajax 加载的, 那么 Ajax 请求又是怎样的呢?

这里我们发现其 Ajax 接口除了包含刚才所说的 URL 中携带的字符串,又多了一个 token ,同样也是类似 Base64 编码的内容,总结下来这个网站就有如下特点

列表页的 Ajax 接口带有加密的 token

详情页的 URL 带有加密 id

详情页的 Ajax 接口参数带有加密 id 和加密的 token 

如果我们想要通过接口的形式爬取, 必须把这些加密 id 和 token 构造出来才行,而且必须一步步来。首先我们要构造出列表页 Ajax 接口的 token 参数,然后获取每部电影的数据信息,接着根据数据信息构造出加密 id 和加密 token 

到此为止,我们知道了这个网站接口的加密情况,下一步就是去找这个加密实现逻辑

由于是网页,所以其加密逻辑一定藏在前端代码里,但是,前端为了保护其接口加密逻辑不被轻易的分析出来,会采取压缩,混淆等方式来加大分析难度。下面我们来看看这个网站的源代码和 JS 文件是怎样的

首先看网站源代码,我们在网站上点击右键, 此时会弹出快捷菜单,然后点击查看源代码

<!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"><link rel=icon href=/favicon.ico><title>Scrape | Movie</title><link href=/css/chunk-19c920f8.2a6496e0.css rel=prefetch><link href=/css/chunk-2f73b8f3.5b462e16.css rel=prefetch><link href=/js/chunk-19c920f8.c3a1129d.js rel=prefetch><link href=/js/chunk-2f73b8f3.8f2fc3cd.js rel=prefetch><link href=/js/chunk-4dec7ef0.e4c2b130.js rel=prefetch><link href=/css/app.ea9d802a.css rel=preload as=style><link href=/js/app.5ef0d454.js rel=preload as=script><link href=/js/chunk-vendors.77daf991.js rel=preload as=script><link href=/css/app.ea9d802a.css rel=stylesheet></head><body><noscript><strong>We're sorry but portal doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.77daf991.js></script><script src=/js/app.5ef0d454.js></script></body></html>

这是一个典型的 SPA(单页 Web 应用)页面,其 JS 文件名带有编码字符, chunk, vendors 等关键字,这是经过 webpack 打包压缩后的源代码,目前主流起那段开发框架 Vue.js , React.js 等的输出结果都是类似这样的

接下来,我们再看一下其 JS 代码是什么样子的。 在开发者工具中打开 Sources 选项卡下的 Page 选项卡, 然后打开 js 文件夹, 在这里我们能看到 JS 的源代码

我们随便复制一些,看看什么样子的

(window['webpackJsonp'] = window['webpackJsonp'] || [])['push']([['chunk-2f73b8f3'], {
    '02f4': function(_0x22a2a8, _0xab6f51, _0x4245f8) {
        var _0x249496 = _0x4245f8('4588')
          , _0x123c24 = _0x4245f8('be13');
        _0x22a2a8['exports'] = function(_0x486f5a) {
            return function(_0x411fbe, _0x52029e) {
                var _0x51e1ce, _0xe9c0f4, _0x4b2912 = String(_0x123c24(_0x411fbe)), _0x4d2326 = _0x249496(_0x52029e), _0x110e8e = _0x4b2912['length'];
                return _0x4d2326 < 0x0 || _0x4d2326 >= _0x110e8e ? _0x486f5a ? '' : void 0x0 : (_0x51e1ce = _0x4b2912['charCodeAt'](_0x4d2326),
                _0x51e1ce < 0xd800 || _0x51e1ce > 0xdbff || _0x4d2326 + 0x1 === _0x110e8e || (_0xe9c0f4 = _0x4b2912['charCodeAt'](_0x4d2326 + 0x1)) < 0xdc00 || _0xe9c0f4 > 0xdfff ? _0x486f5a ? _0x4b2912['charAt'](_0x4d2326) : _0x51e1ce : _0x486f5a ? _0x4b2912['slice'](_0x4d2326, _0x4d2326 + 0x2) : _0xe9c0f4 - 0xdc00 + (_0x51e1ce - 0xd800 << 0xa) + 0x10000);
            }
            ;
        }

可以看到一些变量是十六进制字符串,而且代码被压缩了

没错,我们就是要从这里找出 token 和 id 的构造逻辑

寻找列表页 Ajax 入口

这里简单介绍两种寻找入口的方法

全局搜索标志字符串

设置 Ajax 断点

全局搜索标志字符串

一些关键的字符串通常会被作为寻找 JS 混淆入口的依据,我们可以通过全局搜索的方式来查找,然后根据搜索到的结果答题观察入口是否为我们想找的入口

重新打开 Ajax 接口, 看一下请求的 Ajax 接口

这里 Ajax 接口的 URL 为 

https://spa6.scrape.center/api/movie/?limit=10&offset=0&token=ZjI3YjI1NDRlYjM2NjFkNWNjM2M0MGIzYjZkY2UwMmJhNTgxYWM3ZSwxNzIzNDU2NDE1

,可以看到带有 limit . offset , token 三个参数,关键就是找 token , 我们就全局搜索是否存在 token  点击开发者右上角的“三个小竖点” 然后点击 Search 

这样我们就进入了全局搜索模式,搜索 token,可以看到搜索到了几个结果

观察一下,下面两个结果可能是我们想要的,点击第一个进入看看,此时定位到一个 JS 文件

如果是一行,就点击左下角的 { } 进行格式化,然后我们定位到 token ,可以看到这里有 limit , offset , token 。然后观察其他逻辑,基本上能够确定这就是构造 Ajax 请求的地方,如果不是的话,可以继续搜索其他文件观察

设置 Ajax 断点

由于这里的字符串 token 并没有被混淆, 所以上面的方法是奏效的。因为这种字符串非常容易称为寻找入口的依据,所以这样的字符串也会被混淆成类似 Unicode , Base64 , RC4 等的编码形式,这样我们就不能轻松的搜索到了

另外,我们也可以通过 XHR 断点,方便的找到发起 Ajax 请求的一些入口位置

我们可以在 Sources 选项卡右侧 XHR/fetch Breakpoints 处添加一个断点,首先点击 + 号, 此时就会让我们输入匹配的 URL 内容,由于 Ajax 接口的形式是 /api/movie/?limit=10..... 这样的格式,所以截取一段填进去就好了,这里填的就是 /api/movie 

然后重新刷新页面,就进入了断点模式

如果代码又变成一行,我们还是点击 { } 格式化代码,找到断点位置,这里可以看到有一个 send 字符,我们可以初步猜测它相当于发送 Ajax 请求的一瞬间

这里我们来回溯查找相关逻辑,点击 Call Stack , 这里记录了 JS 方法的逐层调用过程

当前指向的是一个名为 anonymous (也就是匿名)的调用,在他下面显示了调用 anonymous 的方法名,叫作: _0x29474e 的方法,然后  _0x29474e 下面的方法又是调用  _0x29474e 的,一次类推。我们可以继续找下去,观察类似 token 这样的信息,就能找到对应的位置了。

最后我们找到了 onFetchData , 这个方法实现了 token的构造逻辑,这样就成功找到了 token 的参数构造位置了

到此为止,我们就通过两个方法找到入口了,其实还有其他寻找入口的方法,比如 Hook 关键函数等

寻找列表页加密逻辑

我们已经找到 token 的位置了, 可以观察这个 token 对应的变量,它叫作 _0x263439  所以关键就要看这个变量哪里来的

怎么找呢? 加断点就好了

看一下变量在哪里生成的,然后我们在对应的行添加断点, 我们先取消刚才打的 XHR 断点

这时我们就设置了一个断点,由于只有一个断点,刷新网页之后,我们会发现网页停在新断点上

这时我们就能观察到正在运行的一些变量了,比如把鼠标放在各个变量上,可以看到变量的值和类型,把鼠标放在 _0x2fa7bd 上,会有一个浮窗提示

另外,还可以在右侧的 Watch 面板中添加想要查看的变量,这行代码的内容如下

 , _0x263439 = Object(_0x2fa7bd['a'])(this['$store']['state']['url']['index']);

我们比较感兴趣的可能是  _0x2fa7bd 和 this 里面的 $store 属性。 展开 Watch 面板, 然后点击 + 号, 把想看到的变量添加到 Watch 面板中

可以发现  _0x2fa7bd  是一个对象, 它具有属性 a , 其值是一个方法。 

this['$store']['state']['url']['index'] 的值其实就是 /api/movie , 即 Ajax 请求 URL 的 Path 。  _0x263439 就是调用前者的方法传入 /api/movie 得到的

下一步就是去寻找这个方法。我们可以把 Watch 面板的 _0x2fa7bd  展开,这里会显示  FunctionLocation , 就是这个函数代码的位置

点击进入,这时我们就进入一个新的名字为 _0x456254 的方法里,在这个方法里,应该就有 token 的生成逻辑了。添加断点,然后点击右上方的 Resume scrpt execution 按钮

这时我们会发现单步执行到 

 for (var _0x5da681 = Math['round'](new Date()['getTime']  

位置,接下来我们不断进行单步调试,观察一下里面的执行逻辑和每一步调试的结果有什么变化,在每步的执行过程中,我们可以发现一些运行值被打到代码右侧并高亮表示,这里教程说的是在 Watch 下面会有每步的结果,不过我的并没有任何结果,但是在 Scope 下面倒是有一些数据的结果

Watch 下面并没有任何数据

最后,我们总结出这个 token 的构造逻辑

传入 /api/movie 会构造一个初始化列表,将变量命名为 _0x31a891

获取当前时间戳,命名为 _0x5da681,调用 push 方法将其添加到 _0x31a891 变量代表的列表中

将 _0x31a891 变量用 , 拼接,然后进行 SHAI 编码,命名为 _0xf7c3c7 

将 _0xf7c3c7 (SHAI 编码结果) 和 _0x5da681(时间戳)用逗号拼接,命名为 _0x3c8435

将 _0x3c8435 进行 Base64 编码,命名为 _0x104b5b 得到最后的 token 

经过反复观察,可以得出以上逻辑,其中变量可以实时查看,同时也可以自己输入到控制台验证

现在加密逻辑分析出来了,基本思路是

将 /api/movie 放到一个列表中

在列表中加入当前时间戳

将列表内容用逗号拼接

将拼接结果进行 SHAI 编码

将编码结果和时间戳再次拼接

将拼接后的结果进行 Base64 编码

使用 Python 实现列表页爬取

要用 Python 实现这个逻辑,我们需要借助两个库, 一个是 hashlib ,它提供了 sha1 方法,另一个是 base64 它提供了 b64encode 方法对结果进行 Base64 编码

import hashlib
import time
import base64
from typing import List, Any
import requests

INDEX_URL = 'https://spa6.scrape.center/api/movie/?limit={limit}&offset={offset}&token={token}'
LIMIT = 10
OFFSET = 0


def get_token(args: List[Any]):
    timestamp = str(int(time.time()))
    args.append(timestamp)
    sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest()
    return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8')

args = ['/api/movie']
token = get_token(args=args)
index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token)
response = requests.get(index_url)
print('response', response.json())

部分输出结果 

response {'count': 103, 'results': [{'id': 1, 'name': '霸王别姬', 'alias': 'Farewell My Concubine', 'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'categories': ['剧情', '爱情'], 'published_at': '1993-07-26', 'minute': 171, 'score': 9.5, 'regions': ['中国内地', '中国香港']}, {'id': 2, 'name': '这个杀手不太冷', 'alias': 'Léon', 'cover':  'https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg@464w_644h_1e_1c', 'categories': ['动画', '歌舞', '冒险'], 'published_at': '1995-07-15', 'minute': 89, 'score': 9.0, 'regions': ['美国']}]}
 

寻找详情页 id 入口

我们观察前面的输出结果

这里我们看到,有个 id 是 1 , 另外还有一些其他字段, 如电影名称,封面,类别等,这里面一定有某个信息是用来唯一区分电影的

但是当我们点击第一部电影的 信息时,可以看到它跳转了 URL 为 

https://spa6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx

的页面,可以看到这里的 URL 里面有一个加密 id 为  ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx

它和电影信息有什么关系呢?

如果仔细观察,其实可以找出规律来,但是这总归是观察出来的,如果遇到一些观察不出来规律的,那就很麻烦了。因此还是要靠技巧去找到它真正的加密位置。这时候该怎么办?

分析一下,这个加密 id 怎么生成的。

点击详情页的时候,我们就可以看到它访问的 URL 里面就带上了  ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx          这个加密 id 了,。而且不同详情页的加密 id 是不同的,这说明这个加密 id 的构造依赖于列表页 Ajax 的返回结果。因此,可以确定这个加密 id 的生成发生在 Ajax 请求完成后或点击详情页的一瞬间, 为了进一步确定发生在何时,我们查看页面源码,可以看到在没点击前,详情页的 href 里面就已经带有加密 id 了

由此可以肯定,这个加密在 Ajax 请求完成之后生成的,而且肯定也是由 JS 生成的。

怎么去找 Ajax 完成之后的事件呢? 是否应该去找 Ajax 完成之后的事件呢?

可以试试。 在 Sources 面板的右侧, 有一个 Event Listener Breakpoints , 这里有一个 XHR 的监听,包括发起时,成功后,发生错误时的一些监听, 这里我们勾选上 readystatechange 事件,代表 Ajax 得到响应时的事件,其他断点都可以删除了,然后刷新一下页面

可以看到此时就停在 Ajax 得到响应时的位置了。我们怎么知道这个 id 是怎么加密的呢?可以通过断点一步步调试下去,但这个过程非常繁琐,因为这里可能会逐渐用到页面 UI 渲染的一些底层实现,甚至可能找着找着都不知道找到哪里去了

怎么办呢?这里我们就可以使用 Hook ,这个加密 id 是一个 base64 编码的字符串,那么生成过程中想必调用了 JS 的 Base64 编码的方法。这个方法叫做 btoa 。当然, Base64 也有其他实现方法,比如利用 crypto-js  库实现,可能底层调用的就不是 btoa 方法了

现在我们其实并不确定是不是通过调用 btoa 方法实现的 Base64 编码, 那就先试试

要实现 Hook , 关键在于将原来的方法改写, 这里我们其实就是  Hook btoa 这个方法了, btoa 这个方法属于 window 对选哪个,这里直接改写 window 对象的 btoa 方法即可

(function (){
    'use strict'
    function hook(object, attr){
        var func = object[attr]
        object[attr] = function (){
            console.log('hooked', object, attr, arguments)
            var ret = func.apply(object, arguments)
            debugger
            console.log('result', ret)
            return ret
        }
    }
    hook(window, 'btoa')
})()

这里我们定义了一个 hook 方法,给其传入 object 和 attr 参数, 意思就是 Hook object 对象的 attr 参数,例如,如果我们想 Hook alert 方法,那就把 object 设置为 window , 把 attr 设置为 alert 。这里我们想要 Hook Base64 的编码方法, 所以只需要 Hook window 对象的 btoa 方法就可以了

hook 方法的第一句 var func  = object[attr] , 相当于把它赋值为一个变量,我们调用 func 方法就可以实现和原来相同的功能。然后我们改写这个方法的定义,将其改成一个新方法。在新的方法中,通过 func.apply 方法有重新调用了原来的方法。这样我们可以保证前后方法的执行效果不受影响的前提下,在 func 方法执行的前后加入自己的代码,如使用 console.log 将信息输出到控制台,通过 debugger 进入断点等。在这个过程中,我们先临时保存 func 方法, 然后定义一个新方法来接管程序控制权,在其中自定义我们想要的实现,同时新方法重新调回 func 方法,保证前后结果不受影响。因此,我们达到了在不影响原有方法效果的前提下,可以实现在方法的前后实现自定义的功能,就是 Hook 的完整实现过程

最后,我们调用 hook 方法,传入 window 对象和 btoa 字符串即可

怎么注入这个代码呢?

控制台注入

重写 JS

Tampermonkey 注入

控制台注入

对于我们这个场景,控制台注入其实就够了,我们先来介绍这个方法,就是直接在控制台输入这行代码并运行即可

首先我们将页面恢复到最初的状态

然后打开控制台输入前面的那段代码 回车

执行完这段代码之后,就相当于我们已经把 window 的 btoa 方法改写了,然后在控制台输入

btoa('germey')

回车

可以看到它进入了我们自定义的 debugger 的位置停下来了,我们把断点向下执行,然后点击         Resume script execution 按钮, 就可以看到控制台输出了一些对应的结果

我们通过 Hook 的方式改写了 btoa 方法, 使其每次在调用的时候都能停到一个断点,同时还能输出对应的结果

接下来,怎么用 hook 找到对应的加密 id 入口?

由于此时我们是控制台直接输入的 Hook 代码,所以页面刷新就无效了。但我们这个网站是 SPA 页面,点击详情页的时候是不会刷新整个页面的,因此这段代码依然生效。如果不是 SPA 页面,即每次访问都需要刷新页面网站,那么这种注入方式就不生效了

我们想要 Hook 列表页 Ajax 加载完成后的逻辑, 对应的就是加密 id 的 Base64 编码过程, 怎样在不刷新页面的情况下,复现这个操作呢? 很简单,点击下一页就好了

这时候,点击第二页的按钮,可以看到它确实再次停到了 Hook 方法的 debugger 处。 由于列表页的 Ajax 和 加密 id 都带有 Base64 编码操作,所以都能 Hook 到。接着,官产对应的 Arguments 或当前网站的行为, 或者观察栈信息,我们就能大体知道现在走到哪个位置了, 从而进一步通过栈的调用信息找到调用 Base64 编码的位置

根据调用栈的信息,我们可以观察这些变量是在哪一层发生变化。比如对于最后一层,我们可以很明显看到它执行了 Base64 编码,编码前的结果是:

ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1

编码后的结果

ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx

控制台输出的结果 

那么核心问题来了,编码前的 

ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1

是怎么来的? 我们展开栈信息,一层层看这个字符串的变化情况,如果不变,就看下一层,如果改变了,就停下来细看。最后在第五层找到了它的变化过程

var _0x11a046 = 'ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb'

是一个写死的字符串,然后和 _0x177944 拼接形成了最后的字符串,那么 _0x177944 是怎么来的,继续向下看,

可以看到 Ajax 返回结果的单个电影信息的 id

因此,这个逻辑就清楚了,就是 固定字符串 ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb 加上电影 id ,然后进行 Base64 编码即可

在控制台注入的不好之处在于,页面刷新就失效了,而代码必须在页面加载完才能注入,所以并不能在一开始生效。

重写 JS 

借助 Chrome 浏览器的 Overrides 功能,我们可以实现某些 JS 文件的重写和保存。 Overrides 会在本地生成一个 JS 文件副本, 以后每次刷新,都会使用副本内容

这里我们需要切换到 Sources 面板中的 Overrides 选项卡,然后选择一个文件夹,比如这里我自定义了一个 ChromeOverrides 文件夹

然后随便选择一个 JS 脚本,在后面贴上这段注入的脚本,保存文件,如果页面崩溃,刷新一下页面就好了

同时我们还注意到,目前直接进入到了断点模式,并且成功 Hook 到了 btoa 方法。

其实 Overrides 的功能很有用,有了它,我们可以持久化保存任意修改的 JS 代码,想在哪里修改都可以,甚至可以直接修改 JS 的执行逻辑

Tampermonkey 注入

如果不想使用 Overrides 的方式注入,我们也可以使用 Tampeermonkey 插件来注入

开始之前,先关闭所有断点和 刚才的 Overrides 功能,以防干扰

Tampermonkey 的安装和简单使用

写文章-CSDN创作中心

我们可以将脚本内容改写成下面这样

// ==UserScript==
// @name         HookBase64
// @namespace    https://scrape.center/
// @version      0.1
// @description  Hook Base64 encode function
// @author       Germey
// @match        https://spa6.scrape.center/
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    function hook(object, attr){
        var func = object[attr]
        console.log('func', func)
        object[attr] = function(){
            console.log('hooked', object, attr)
            var ret = func.apply(object, arguments)
            debugger
            return ret
        }
    }
    hook(window, 'btoa')
})()

这时候启动脚本,重新刷新页面,可以发现成功 Hook btoa 方法

寻找详情页 Ajax 的 token

现在我们已经找到详情页的加密 id 了,但是还差一步,其 Ajax 请求也有一个 token 

因为也是 Ajax 请求,我们可以通过前面提到的同样的方法对该 token 的生成逻辑进行分析,最终可以发现其实这个 token 和详情页 token 的构造逻辑是一样的

使用 Python 实现详情页的爬取

现在,我们已经成功把详情页的加密 id 和 Ajax 请求的 token 找出来了,下一步就是使用 Python 完成爬取,这里我们只实现第一页的爬取

import hashlib
import time
import base64
from typing import List, Any
import requests

INDEX_URL = 'https://spa6.scrape.center/api/movie/?limit={limit}&offset={offset}&token={token}'
DETAIL_URL = 'https://spa6.scrape.center/api/movie/{id}?token={token}'
LIMIT = 10
OFFSET = 0
SECRET = 'ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb'


def get_token(args: List[Any]):
    timestamp = str(int(time.time()))
    args.append(timestamp)
    sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest()
    return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8')

args = ['/api/movie']
token = get_token(args=args)
index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token)
response = requests.get(index_url)
print('response', response.json())

result = response.json()
for item in result['results']:
    id = item['id']
    encrypt_id = base64.b64encode((SECRET + str(id)).encode('utf-8')).decode('utf-8')
    args = [f'/api/movie/{encrypt_id}']
    token = get_token(args=args)
    detail_url = DETAIL_URL.format(id=encrypt_id, token=token)
    response = requests.get(detail_url)
    print('response', response.json())

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2034892.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring @Transactional事务传播行为详解

目录 一、无事务情况 二、有事务情况 REQUIRED SUPPORTS MANDATORY REQUIRES_NEW NOT_SUPPORTED NEVER NESTED Spring的事务传播机制用于控制在多个事务方法相互调用时事务的行为。 在复杂的业务场景中&#xff0c;多个事务方法之间的调用可能会导致事务的一致性&…

谷粒商城【renren-fast-vue】:npm install 报错

谷粒商城【renren-fast-vue】&#xff1a;npm install 报错 报错信息报错原因解决办法 报错信息 谷粒商城【renren-fast-vue】&#xff1a;npm install 报错 npm install 下载依赖的时候报错sass 版本与 node 版本不对应 报错原因 直接使用 npm 下载依赖&#xff0c;可能会…

RCE技巧

RCE技巧 Linux命令长度限制突破方法8个字符限制绕过过滤英文字母和数字php版本7php版本5 Linux命令长度限制突破方法 8个字符限制绕过 <?php <?php $param $_REQUEST[param]; if (strlen($param) < 8) {echo shell_exec($param); }shell_exec — 通过 shell 执行…

【大模型从入门到精通14】openAI API 构建和评估大型语言模型(LLM)应用2

这里写目录标题 评估大型语言模型&#xff08;LLM&#xff09;输出的方法构建评估标准实施评估协议利用专家比较案例研究评估客户服务聊天机器人学术文本摘要高级评估技术 评估大型语言模型&#xff08;LLM&#xff09;输出的方法 评估大型语言模型&#xff08;LLM&#xff09…

甄选范文“论软件设计方法及其应”软考高级论文系统架构设计师论文

论文真题 软件设计(Software Design,SD)根据软件需求规格说明书设计软件系统的整体结构、划分功能模块、确定每个模块的实现算法以及程序流程等,形成软件的具体设计方案。软件设计把许多事物和问题按不同的层次和角度进行抽象,将问题或事物进行模块化分解,以便更容易解决…

无人机之电机篇

一、无人机使用什么类型的电动机 无人机主要使用直流无刷电机和伺服电机。 直流无刷电机通常用于无人机的推进系统&#xff0c;因为它具有强大的驱动力和高功率输出&#xff0c;能够为无人机提供足够的推力。 此外&#xff0c;直流无刷电机具有电动机启动转矩大、无刷向触点…

MTF-SFR总结/探讨

空间频率响应&#xff08;SFR&#xff09;定义 在iso12233:2000中&#xff0c;空间频率响应&#xff08;SFR&#xff09;测量被定义为通过分析倾斜黑白边缘附近的相机数据而测量的值。 图像清晰度测试方法 通过ISO12233测试图像清晰度的方法&#xff0c;一般有 TVline测试和S…

去中心化技术的崛起:探索Web3的新时代

引言&#xff1a; Web3是互联网发展的新阶段&#xff0c;它通过去中心化技术重新定义了数字世界的运作方式。这一新时代不仅带来了技术上的突破&#xff0c;也为社会互动和数据管理开辟了新的前景。本文将深入探讨Web3的核心技术、应用领域、全球影响以及面临的挑战&#xff0…

nvm的下载和使用(Windows)

NVM&#xff08;Node Version Manager&#xff09;是一个用于管理多个Node.js版本的工具&#xff0c;它允许用户在同一台机器上安装和使用多个Node.js版本。 一、NVM的基本功能 多版本支持&#xff1a;NVM允许用户在同一台机器上安装多个Node.js版本&#xff0c;方便处理不同…

极光流星大爆发

卑微仔广东持续200%含云量&#xff0c;线上观望大家分享的极光与流星共舞的神奇场景。 极光与流星相伴的瞬间&#xff0c;永远震撼于绝美的星空 开始放毒&#xff08;放图放图&#xff09;&#xff08;以下均拍摄于12日晚至13日晨这一时间段&#xff09;&#xff1a; 先驱猎光…

Qt之2048项目的介绍

文章目录 前言项目介绍项目截图技术介绍1. Qt 框架2. 界面绘制3. 用户输入4. 游戏逻辑5. 音效处理总结前言 2048 是一款流行的益智游戏,通过滑动屏幕上的数字方块,使相同的数字合并并生成更大的数字,最终目标是生成2048这个数字。本文介绍了基于 Qt 框架开发的一个 2048 游…

超声波清洗机哪个品牌好用?品质上等的超声波眼镜清洗机评选

随着科技的发展&#xff0c;超声波清洗机已经成为了人们生活中的清洁神器&#xff0c;它只需要清水便可以清洗假牙、刮胡刀、牙刷、眼镜、化妆工具等小物件&#xff0c;而且能够清洗到物件中的角落缝隙&#xff0c;在专业设备上还同时具备消毒除菌的功能&#xff0c;既能保证清…

软件检测报告的客观性与权威性如何确定

确保软件检测报告的客观性与权威性乃是软件测试进程中的关键要素&#xff0c;以下乃是若干确保报告质量与信誉的举措&#xff1a; 其一&#xff0c;拣选获认证的测试机构&#xff1a;选取具备 CMA&#xff08;中国计量认证&#xff09;以及 CNAS&#xff08;中国合格评定国家认…

AxMath保姆级安装教程(word联用)及使用TIPS

一、软件介绍 AxMath是一款数学公式编辑器软件。它提供了一个直观的界面&#xff0c;使用户可以轻松创建和编辑数学公式。AxMath支持多种数学符号、方程式、函数、矩阵等的输入和编辑&#xff0c;并提供了丰富的数学符号库和模板&#xff0c;方便用户快速创建复杂的数学公式。…

33_对bluecms v1.6进行代码审计、用代码审计三种方法分别进行实施、bluecms v1.6下载与安装、定向功能分析法

部署bluecms v1.6 靶场下载地址&#xff1a; https://wwtt.lanzn.com/b00uyckd9a 密码:2x71 访问 http://127.0.0.1/bluecms/install/ 数据库名称建议跟网站名一样 进入mysql-front查看&#xff0c;出现bluecms数据库&#xff0c;并且库中有很多表 然后访问前台&#xff1a;h…

DW_ahb_databook学习及部分AHB知识回顾

一、DW_ahb框图 Arbiter: 一次只允许一个master发起数据传输&#xff0c;同时可以选择slave Optional Internal Decoder: 通过解码系统地址总线为AHB上的从机生成外设选择。每个slave都可以指定一个起始和结束地址&#xff0c;该地址必须与1kb边界对齐。 Optional External D…

【云原生】高可用集群KEEPALIVED(理论篇)

一、高可用集群 1.1 集群类型 LB:Load Balance 负载均衡 LVS/HAProxy/nginx(http/upstream, stream/upstream)HA:High Availability 高可用集群数据库、RedisSPoF: Single Point of Failure&#xff0c;解决单点故障HPC: High Performance computing 高性能集群 1.2 系统可用…

第二十五天培训笔记

2 、在 python 中连接数据库并结合游标对数据库进行操作 前提&#xff1a;要有 python3 环境 pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple // 设置 pip3 的全 局配置&#xff0c;将默认的 Python 包索引源&#xff08; index-url &am…

Raspberry Pi Pico 2 上实现:实时机器学习(ML)音频噪音抑制功能

Arm 公司的首席软件工程师 Sandeep Mistry 为我们展示了一种全新的巧妙方法&#xff1a; 在 Raspberry Pi Pico 2 上如何将音频噪音抑制应用于麦克风输入。 机器学习&#xff08;ML&#xff09;技术彻底改变了许多软件应用程序的开发方式。应用程序开发人员现在可以为所需系统整…

【C++ 项目】负载均衡在线 OJ

文章目录 &#x1f308; 一、项目介绍&#x1f308; 二、项目源码&#x1f308; 三、项目演示⭐ 1. 前端界面展示⭐ 2. 后端界面展示 &#x1f308; 四、项目准备⭐ 1. 项目所用技术⭐ 2. 项目开发环境⭐ 3. 项目宏观结构 &#x1f308; 五、comm 公共模块⭐ 1. util.hpp 工具⭐…