当前位置: 首页 > news >正文

JavaScript 逆向爬取实战

准备介绍: 当我们学习完整个 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 requestsINDEX_URL = 'https://spa6.scrape.center/api/movie/?limit={limit}&offset={offset}&token={token}'
LIMIT = 10
OFFSET = 0def 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)debuggerconsole.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 requestsINDEX_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())

相关文章:

JavaScript 逆向爬取实战

准备介绍&#xff1a; 当我们学习完整个 JS 逆向技巧后&#xff0c;这里是一次完整的分析爬取实战 案例介绍 本节案例网站不仅在 API 参数有加密&#xff0c; 而且前端 JS 也带有压缩混淆&#xff0c;其前端压缩打包工具使用 webpack , 混淆工具使用 javascript-obfuscator 。…...

Vue 项目中导入文件时如何默认找寻该文件夹下的 index.vue 文件

文章目录 需求分析 需求 如下图&#xff0c;在Vue 项目中导入 frequencyChange 文件夹时如何默认找寻该文件夹下的 index.vue 文件 分析 确保项目结构和命名约定 首先&#xff0c;确保你的 Vue 单文件组件按照约定命名&#xff0c;例如&#xff1a; components/Example/inde…...

Idea2023.3.3 —— SourceTree与gitee关联

SourceTree SourceTree链接: https://pan.baidu.com/s/1oqPxhpHeNOOiuRRQydes6g?pwdngru 提取码: ngru 点击Generate 分别保存私钥和公钥 gitee官网注册 这是gitee的公钥&#xff0c;与上面SourceTree的公钥私钥不一样 gitee生成公钥&#xff0c;确保本地安装好git git链接: h…...

一文HDMI (High-Definition Multimedia Interface)

HDMI&#xff08;High-Definition Multimedia Interface&#xff0c;高清多媒体接口&#xff09;是一种紧凑的音视频接口&#xff0c;它能够将未压缩的视频数据以及压缩或未压缩的数字音频数据&#xff0c;从符合HDMI标准的源设备无缝传输到兼容的计算机显示器、视频投影仪、数…...

【HBZ分享】高并发下如何设计缓存来提升系统性能?

普通模式 普通模式即前段调用后端接口&#xff0c;然后后端先查缓存&#xff0c; 查不到的情况下再查数据库&#xff0c;然后把数据库中的内容放到缓存中。瓶颈&#xff1a;瓶颈在于tomcat的性能&#xff0c;一般并发可以&#xff0c;面临海量并发冲击&#xff0c;tomcat就显得…...

【AI 绘画】 文生图图生图(基于diffusers)

AI 绘画- 文生图&图生图&#xff08;基于diffusers&#xff09; 1. 效果展示 本次测试主要结果展示如下&#xff1a; SDXL文生图 可爱Lora 2. 基本原理 模型基本原理介绍如下 stable diffusion首先训练一个自编码器&#xff0c;学习将图像数据压缩为低维表示。通过使…...

已解决HarmonyOS模拟器卡顿问题

以下是一些可以尝试用来解决 HarmonyOS 模拟器卡顿问题的方法&#xff1a; 一、检查系统资源占用 关闭不必要的后台程序 在电脑上&#xff0c;通过任务管理器&#xff08;Windows 系统中按 Ctrl Shift Esc&#xff0c;Mac 系统通过活动监视器&#xff09;查看并关闭占用大量 …...

C++ | 深入理解C++中的特殊类设计和单例模式(懒汉模式、饿汉模式)

目录 特殊类设计和单例模式 1、不可拷贝类 2、只能在堆上创建对象的类 3、只能在栈上创建对象的类 4、不可继承的类 5、单例模式(懒汉模式、饿汉模式) 特殊类设计和单例模式 在C编程中&#xff0c;类的设计往往需要满足特定的需求和约束。特殊类设计模式提供了一种方法来…...

Java设计模式之中介者模式

Java设计模式之中介者模式 在软件开发中&#xff0c;设计模式是解决常见问题的最佳实践。通过运用设计模式&#xff0c;我们可以提高代码的可维护性、可扩展性以及可读性。今天&#xff0c;我们将探讨一种非常重要的行为型设计模式——中介者模式&#xff08;Mediator Pattern…...

实现父组件调用子组件方法时报错:[Vue warn]: Invalid vnode type when creating vnode: null.

使用uniapp实现父组件调用子组件方法时报错&#xff1a;[Vue warn]: Invalid vnode type when creating vnode: null. 实现代码如下&#xff1a; 子组件&#xff1a; <template><view><view class"toolsHeader"><view class"toolsTitl…...

Java面试八股之什么是消息队列

什么是消息队列 消息队列&#xff08;Message Queue&#xff09;是一种应用程序间通信&#xff08;IPC&#xff09;的形式&#xff0c;它允许进程将消息发送到另一个消息队列&#xff0c;接收端则可以在任何时刻从队列中取出这些消息进行处理。消息队列提供了一种异步处理、解…...

【kubernetes】k8s配置资源管理

一、ConfigMap资源配置 ConfigMap保存的是不需要加密配置的信息 ConfigMap 功能在 Kubernetes1.2 版本中引入&#xff0c;许多应用程序会从配置文件、命令行参数或环境变量中读取配置信息。ConfigMap API 给我们提供了向容器中注入配置信息的机制&#xff0c;ConfigMap 可以被…...

高分六号卫星助力农业监测_卫星介绍_论文分享_数据获取

卫星遥感已经成为农业发展的重要支持工具。《“数据要素X”三年行动计划(2024-2026年)》指出,在现代农业交通运输、应急管理等领域鼓励探索利用遥感数据。为什么高分六号会经常应用于农业检测呢&#xff1f;本文将介绍高分六号卫星的农业检测特性、在农业应用中的优势、具体农业…...

【Linux网络】TCP协议

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 文章目录 引言1、TCP协议的特点2、确认应答机制3、超时重传机制4、TCP报头结构5、三次握手建立连接6、四次挥手断开连接总结 引言 TCP协议是传输层中非常重要的协议。本篇博客我们将从TCP头部信息、TCP状态转…...

Linux 基本指令讲解 上

linux 基本指令 clear 清屏 Alt Enter 全屏/退出全屏 pwd 显示当前用户所处路径 cd 改变目录 cd /root/mikecd … 返回上级目录cd - 返回最近所处的路径cd ~ 直接返回当前用户自己的家目 roor 中&#xff1a;/root普通用户中&#xff1a;/home/mike mkdir 创建一个文件夹(d) …...

深入了解指针(6)

文章目录 1.函数指针数组2.转移表3.回调函数 1.函数指针数组 存放函数指针的数组 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int add(int x, int y) {return x y; } int sub(int x, int y) {return x - y; }int main() {int (*p1)(int x, int y) add;int (…...

【Linux系列】known_hosts详解

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

MySQL4 多表查询 内连接

内连接 多表查询内连接 多表查询 数据准备 CREATE DATABASE db4; USE db4; -- 创建部门表 create table if not exists dept(deptno varchar(20) primary key , -- 部门号name varchar(20) -- 部门名字 );-- 创建员工表 create table if not exists emp(eid varchar(20) pr…...

力扣45:跳跃游戏2题解

题目链接&#xff1a; https://leetcode.cn/problems/jump-game-ii/description/?envTypestudy-plan-v2&envIdtop-100-liked 参考的代码随想录里面的题解&#xff1a; 题目 难度 中等&#xff0c;标的是中等难度&#xff0c;而且我之前做过这道题&#xff0c;但是我没写…...

产品经理-产品被同行抄袭如何处理(32)

在面试当中,如果被问到,产品被同行抄袭如何处理 竞争分析、战略决策。 1&#xff09;对行业相关事件的关注度&#xff1a;如之前对腾讯抄袭事件的讨论&#xff0c;如果能结合“被抄袭”后的失败案例&#xff08;例如联众&#xff09;和反抄袭的成功案例&#xff08;例如豆瓣&am…...

LDR6020在Type-C手机同时充电与USB2.0数据传输方案

随着科技的飞速发展&#xff0c;Type-C接口已成为智能手机等移动设备的主流充电和数据传输接口。为了满足用户对于高效充电与稳定数据传输的双重需求&#xff0c;乐得瑞科技推出的LDR6020芯片凭借其卓越的性能和丰富的功能&#xff0c;为Type-C手机提供了同时充电与USB2.0数据传…...

Python学习笔记(七)

""" 演示数据容器字典的定义 """# # 定义字典{key:value,key2:value2} 注意key不能是字典&#xff0c;value可以任意 # my_dict1 {"王力鸿": 99, "周杰轮": 88, "林俊节": 77}# #定义空字典 # my_dict2 {} # my…...

mysql中log

目录 MySQL 日志系统概述 日志类型 日志的作用和重要性 Mermaid图示 1. Undo Log 和 Redo Log 的协同工作图 2. Redo Log 确保持久性的流程图 Undo Log&#xff08;回滚日志&#xff09; 事务的原子性&#xff08;Atomicity&#xff09;保障 事务回滚机制 MVCC&#…...

iOS Object-C 创建类别(Category) 与使用

有时候使用系统给出类或者第三方的类,但是呢它们自带的属性和方法又太少,不够我们的业务使用,这时候就需要给“系统的类或者第三方类”创建一个类别(Category),把自己的想添加的属性和方法写进来. Category模式用于向已经存在的类添加方法从而达到扩展已有类的目的 一:创建Ca…...

Rocky系统部署k8s1.28.2单节点集群(Containerd)+Kuboard

目录 Kubernetes介绍 Kubernetes具备的功能 Kubernetes集群角色 Master管理节点组件 Node工作节点组件 非必须的集群插件 Kubernetes集群类型 Kubernetes集群规划 集群前期环境准备 开启Bridge网桥过滤 关闭SWAP交换分区 安装Containerd软件包 K8s集群部署方式 集…...

Springboot整合Flowable入门-学习笔记

目录 1、定义流程&#xff08;画图&#xff09; 2、Springboot部署流程 3、Springboot删除所有流程 4、Springboot根据 流程部署ID 查询 流程定义ID 5、Springboot启动(发起)流程 6、Springboot查询任务 6.1全部任务 6.2我的任务&#xff08;代办任务&#xff09; 7、…...

C语言常见的题目

1、 从源码到可执行文件会经历怎样的过程&#xff1f; 预编译&#xff1a;去掉空格、注释&#xff0c;处理预定义的指令&#xff0c;生成处理后的源代码文件。 编译&#xff1a;翻译成汇编代码&#xff0c;生成汇编文件。 汇编&#xff1a;翻译成机器码&#xff0c;生成一个或…...

Android13适配记录

多语言支持&#xff0c;此功能在国内被阉割 配置后在设置内可以选择 <?xml version"1.0" encoding"utf-8"?> <locale-config xmlns:android"http://schemas.android.com/apk/res/android"><locale android:name"zh" …...

Android TV上OTT PWA应用开发的播放器选择:video.js vs exoplayer

跨平台 OTT PWA 应用开发&#xff0c;最方便的当然是选用 video.js 库。但是既然是安卓平台&#xff0c;exoplayer 看起来总是最稳妥的选择 介绍 Exoplayer 是 Android media3 的一个实现&#xff0c;以前是独立出来的&#xff0c;现在已经合并到 androidx.media3 中了。 Vid…...

24.8.14 《CLR via C#》 笔记12

第十五章 枚举类型和位标志 使用枚举类型而不是硬编码的理由&#xff1a;枚举类型更易编写&#xff0c;阅读和维护&#xff1b;枚举类型是强类型枚举类型是值类型&#xff0c;不能定义任何方法&#xff0c;属性或事件&#xff0c;可利用扩展方法向枚举类型添加方法枚举类型定义…...

专业的网站开发服务/知乎推广渠道

相信大家都玩过微信H5游戏&#xff0c;例如前几年的围住神经猫&#xff0c;也曾收到过好友分享来的H5游戏链接&#xff0c;因为好奇点进去一探究竟。现在&#xff0c;越来越多的商家开始将H5游戏运用的品牌营销上来&#xff0c;H5游戏营销也受到了重视&#xff0c;那么&#xf…...

动漫游戏制作专业是本科专业吗/seo优化或网站编辑

一、作业描述  编写Java程序&#xff0c;模拟简单的计算器。 定义名为Number的类&#xff0c;其中有两个整型数据成员n1和n2&#xff0c;应声明为私有。编写构造方法&#xff0c;赋予n1和n2初始值&#xff0c;再为该类定义加&#xff08;addition&#xff09;、减&#xff08;…...

wordpress4.9漏洞利用/网站综合排名信息查询

1. 单例模式的简单实现 2. 单例模式的特点 3. 多线程安全的单例模式 4. 模版类的单例模式的实现 5. 使用单例模式需要注意的问题 1. 简单的单例模式如下&#xff1a; 1 class Singleton {2 private:3 Singleton() {};4 ~Singleton() {};5 public:6 static Singleto…...

公司企业文化内容范本/淘宝seo什么意思

1.先umount组建好的raid:umount /dev/md02.停止raid设备&#xff1a;mdadm -S /dev/md03.此时如果忘了raid中的硬盘名称&#xff0c;要么重启系统&#xff0c;要么运行:mdadm -A -s /dev/md0 然后再用mdadm -D /dev/md0查看raid中包含哪几个硬盘。再次运行第二步停止命令:mdadm…...

seo网站优化论文/百度最新版本2022

1.我们14年坚持的理念才成就了今天的成功&#xff0c;别人才进来了一两年就想与天猫和淘宝竞争是不可能的。他们应该去补充天猫和淘宝没有的东西&#xff0c;而不是去跟淘宝天猫比。现在的电商企业应该利用BAT去发展自己。2.至于IPO,我们自己也不知道在哪里IPO,今天是双11&…...

郑州建设网站费用/棋牌软件制作开发多少钱

从最开始的&#xff0c;SSH到SpringMVC&#xff0c;随着Spring的发展&#xff0c;使得开发越来越容易了&#xff0c;SpringBoot已经成为Java程序员必会的一项&#xff0c;以下给小伙伴整理了30道相关面试题&#xff0c;也可以作为知识点&#xff0c;学习收藏起来。1.什么是Spri…...