js逆向学习- 爬虫下载「极简壁纸」图片

声明:本文章仅供学习参考,请勿滥用爬虫下载

目录

声明:本文章仅供学习参考,请勿滥用爬虫下载

一、确定抓取思路

二、顺藤摸瓜

总结


前言 

分享下爬取「极简壁纸」网站图片的爬虫流程,主要是分享个人处理的思路。

网站地址(bs64):aHR0cHM6Ly9iei56enptaC5jbi9pbmRleA== 

一、确定抓取思路

  1. 打开网站,先确定下图片下载保存操作的基本逻辑
    1. )可以看到网站首页列表加载了多张图片,点击其中一张,会弹出一个巨大的预览窗口,点击下载按钮后就可以下载原图片。不需要多余的东西,非常友好
    2. )我们可以暂且认为一张图片可能有三种状态,可以称之为列表页的略缩图、点击展开后的预览图、提供下载的原图。我们的目标就是获取原图的下载链接
    3. )那我们就从点击下载这一步开始往前推
  2. 打开控制台-网络面板,清除cookie,刷新页面。然后点击预览一张图片。这时候再清除网络面板的浏览记录,因为接下来我们要点击下载,要看下载请求的内容
  3. 可以看到下面几个请求包
  4. 很显然其中第三个才是下载图片的请求,复制请求链接在新窗口打开,可以看到确实下载了图片。当然这时候我们还不能确定这个链接需不需要我们自己构造,能不能直接拿到。简单看下这个链接的请求参数:/wallpaper/origin/5c23d52f880511ebb6edd017c2d2eca2.jpg?response-content-disposition=attachment&auth_key=1711900800-340294865c06427f78a2d41ce8e2f291dccd045c-0-eb48d71e26b14ebcc2ac2fead8f390d3
    其中显然可以拆成三部分。一部分是文件路径5c23d52f880511ebb6edd017c2d2eca2;一部分是参数response-content-disposition,显然是固定的attachment;还有个长长的auth_key。

       那我们开始找图片链接哪里来的


二、顺藤摸瓜

  1. 全局搜索图片地址 5c23d52f880511ebb6edd017c2d2eca2 ,可以看到源码中没有找到任何内容,只有请求地址中包含。尝试搜索bz/v3/getUrl也一样。说明图片的地址大概率是被加密了
  2. 点击检查页面元素,看下网页加载的图片地址
  3. 可以看到网页上图片的地址也是包含一串特殊字符,对比下就会发现,网页略缩图地址是5c23d52f880511ebb6edd017c2d2eca220, 相比5c23d52f880511ebb6edd017c2d2eca2,结尾多了个20,20是什么还不确定,可以先不管。可以确定的是这串字符就是这张图片的关键,必然在某个请求中返回来加密的。
  4. 重新刷新页面,筛选请求Fetch/XHR, 可以很明显看到/getData,应该就是获取数据的接口。
  5. 返回结果中的result显然是加密内容,应该就是我们想要的包含图片地址的内容
  6. 观察该请求,或者直接复制cURl然后转成python代码请求,会发现不需要cookie或有加密的参数,直接请求即可。那就开始破解响应解密
    1. 添加XHR断点
    2. 刷新页面,断住。但是可以看到并没有跟预期一样,断在send()的地方,而是个很奇怪的地方。假设我们并不了解axios是什么,很陌生,这里尝试往下继续执行看看。会发现代码已经被OB混淆了,执行下来也没办法看出什么有用的信息
    3. 这时候先换个方式试试。先删掉这个XHR断点。然后直接全局搜索接口名称/getData
    4. 只有一处,点击跳转对应位置,在对应位置打上断点。重新刷新页面,断住了
    5. 此时我们已经可以观察到,对应位置前面,实际上就是请求参数定义的地方。那么这一块应该就是请求定义的地方。
    6. 那我们的目标是定位到请求完成返回响应后,进行解密的地方,那就继续向下执行。虽然这里的变量名什么的被混淆过,但我们先不需要关心它到底是什么。实际上我们能大概读懂代码:这里用了await, 并且能看到post和目标地址、应该就是个异步请求,然后对状态码什么的做判断
    7. 不好读懂也没关系,先找到我们想要的变量值即可。正常向下走(一般就是先用”跳过下一个函数调用“这个),可以看到_0x18b0af这个变量已经有值了,看到status 200就知道是响应的结果,看一下是不是我们想要的结果
    8. 可以鼠标选中查看,也可以在控制台输出查看
    9. 展开后很显然能看到,data就是响应的内容,但是这里已经是被解密后的结果。这说明在上一行代码的执行中,已经完成了请求和响应解密的过程。
    10. 点击展开观察下result中的内容,就能看到,确实包含了我们想要的5c23d52f880511ebb6edd017c2d2eca2。再看下另外几个值,一个t, 是2或者1;w和h比较明显能看出来,就是图片的像素。这里看到了注意了就行。
    11. 那就重新刷新,回到前面断点断住(这里如果选中`_0x1eed3d['a']['post']('bz/v3/getData', _0x4d066f)`会发现这就是个Promise对象,这里实际上就是执行了Promise实例,不熟悉可以了解下。实际上不知道也没关系,完全可以不用管)
    12. 点击执行进入“下一个函数调用“。为什么现在是执行这个?因为刚刚用左边的”跳过下一个函数调用“,直接就已经拿到结果了,那就慢点走看看,所以点这个执行,钻进其他方法内部看看
    13. 可以看到进入到构建Promise实例函数的方法内了,按常规来看那么then后面接的应该就是获取到响应后执行的回调函数了。也可以直接看出来,后面有对响应内容执行的某些操作
    14. 打个断点执行过去,打印下result的内容。以及后面用json.parse解析,大概率就是解密后做序列化了,打印下看看,确实是的,基本已经可以断定,_0x1683d3['a']['decipher']就是解密的方法
    15. 选中_0x1683d3['a']['decipher'],在弹出的函数信息中点击跳转到对应js文件位置中,可以看到是嵌套了好几层函数,先不管,本地新建js文件复制进去。然后补一下这三个函数,同样选中点击跳转,发现就都在前面,大致是一些运算逻辑。不用管具体内容,整体复制下来
    16. 稍微改下关键的函数名,看起来更舒服,直接执行
    17. 报错ReferenceError: window is not defined,那就还是补上window = global。重新执行,完美打印出结果
    18. 至此响应的解密过程就完成了。简单验证下(代码demo如下)
      1. js部分代码
        window = global
        function _0x476691(_0x4eff8a) {}
        function _0x300484(_0x17a9d3) {}
        function _0x4176cc(_0x529333) {}
        function decryptData(text) {
            return _0x300484(_0x476691(_0x4176cc(text)));
        }
        
        text = ''
        
        console.log(decryptData(text))

      2. python部分。执行下,没有问题
        import execjs
        import requests
        import json
        from proxy.get_proxy import get_proxy
        
        with open('test.js', 'r') as f:
            js_ = f.read()
        js_c = execjs.compile(js_)
        
        def get_page(page_num):
            list_headers = {
                "authority": "api.zzzmh.cn",
                "accept": "application/json, text/plain, */*",
                "content-type": "application/json;charset=UTF-8",
                "origin": "https://bz.zzzmh.cn",
                "referer": "https://bz.zzzmh.cn/",
                "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
            }
            url = "https://api.zzzmh.cn/bz/v3/getData"
            data = {
                "size": 24,
                "current": page_num,
                "sort": 0,
                "category": 0,
                "resolution": 0,
                "color": 0,
                "categoryId": 0,
                "ratio": 0
            }
            data = json.dumps(data, separators=(',', ':'))
            response = requests.post(url, headers=list_headers, data=data, proxies=get_proxy())
            page_data = json.loads(js_c.call('decryptData', json.loads(response.text)['result']))
            return page_data
        
        print(get_page(1))

  7. 我们需要的列表请求解密,获取到图片下载路径的过程已经实现了,那么接下来就是找url中的auth_key是如何生成的
    1. 还是先把浏览器cookie、以及我们打过的断点什么的取消/清除掉(每次开始新的数据定位之前,为了避免一些无效的干扰,一般最好都恢复原始状态)。然后刷新页面
    2. 这时候保留网络请求记录,再点击略缩图->打开预览图后点击载,然后看最新的下载请求的url中的auth_key,直接取一部分在请求中搜索一下
    3. 比如我这里取最后的eb48d71e26b14ebcc2ac2fead8f390d3进行搜索,会发现只有两个结果。一个是下载图片时请求的地址,另一个是某个请求的响应头中的Location。这时我们可以知道,当我们点击下载图片时,是先请求了某个地址,然后发生重定向转到了我们真正下载图片的地址。那么这里我们只需要用requests简单模拟重定向过程就行了
      1. 实际上我们发现重定向不一定需要这么繁琐的过程,当我们检查预览图上的下载按钮时也能看到重定向前的地址。复制下来新窗口打开,就会发现重定向到了新地址。或者我们第一次下载图片看网络请求记录时如果注意到了,也可以发现。这里各人处理方式、步骤不同。只要最终找到就行
  8. 我们可以看到,原请求地址是/bz/v3/getUrl/5c23d52f880511ebb6edd017c2d2eca229,其中很明显也包含了5c23d52f880511ebb6edd017c2d2eca2,只是结尾多了个29。前面我们已经注意到略缩图地址结尾多了20。20是什么不确定我们可以先不管 因为我们不需要略缩图,但是这里包含29的地址是我们需要构造的请求地址,那就没办法避开了,必须先确定它是什么。
    1. 一般这种情况,第一步先观察,是不是固定值。如果不是,再看看是不是有什么规律,比如跟位置有关、或者顺序有关?这里我们就多点开几张图片下载试试。
    2. 首先在多下载几张不同的图片之后就会发现,要么是29,要么是19,暂时看不到其他情况(这里最后有一种最极端的情况就是我们实在无法判断到底是29还是19,那就只能29和19都试试,万一就是随机的都能用呢?或者就是在代码上兼容,29、19都请求一次,哪个成功了就用哪个。这些都可以是解决问题的一种方式)
    3. 这里可以说有两种判断方式:
      1. 按常规方式,我们需要知道29是什么,那就要找到请求地址构造逻辑。
        1. 既然是构造url,那先拿url一部分去搜,比如就拿/getUrl, 搜出来发现刚好就一个地方,并且正好就是一个相加的逻辑。打上断点,刷新页面
        2. 可以很清楚看到其中_0x107034传参就是我们想要的值。这时候就可以开始通过右侧堆栈往前找,看是哪一步传入的
        3. 比较容易能找到对应的最开始传参调用的地方,查看其中_0x4b8fb3其实已经很明显了,就是我们获取到的响应,包含t, 还有图片像素的东西。后面的0x0就是0,因为这还是在生成略缩图地址,细心点能看到代码后面,有个0x9,就是生成预览图时执行的部分
        4. 那这个逻辑就很清晰了,这个29就是两部分,2是取列表页响应result.list中包含的那个t值,9是固定值
      2. 另一种判断29如何生成的方式,其实就是凭感觉。
        1. 因为开始我们破解出列表页响应内容的时候,已经注意到里面包含一个未知的t值,也是1或者2。很容易就能想到29中的2可能就是t,;另外9一直没变,大概率是个固定值。
        2. 那就只需要逐一对比了,拿到响应result,然后在网页上逐个点击图片,逐个对比,会发现都能对得上,就可以确定了。
      3. 这时我们需要的原图的原地址的url构造方式也确定了。先把代码完成看看
        1. 简单写下python代码
          def download_picture(page_data):
              for i in page_data['list']:
                  first_headers = {
                      "authority": "api.zzzmh.cn",
                      "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
                      "accept-language": "zh-CN,zh;q=0.9",
                      "cache-control": "no-cache",
                      "pragma": "no-cache",
                      "referer": "https://bz.zzzmh.cn/",
                      "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
                  }
                  first_url = f'https://api.zzzmh.cn/bz/v3/getUrl/{i["i"]}{i["t"]}9'
                  first_res = requests.get(url=first_url, headers=first_headers, proxies=get_proxy())
                  with open(f"./img/{i['i']}.jpg", 'wb') as f:
                      f.write(first_res.content)
          
          download_picture(get_page(1))
        2. 执行后正常下载,查看下图片大小什么的,跟网页下载没有什么区别。
  9. 通过上述过程实际上已经完成了全部内容。这里额外说明一点,因为我之前在抓取该站点时发现,在最后使用原url请求下载原图时,通过request无法正常进行302重定向,返回都是403。通过抓包发现,该请求只能通过h2协议请求。所以改成通过httpx的h2请求后手动获取Location地址触发二次请求,才完成下载。但现在好像没有碰到这个问题了。

总结

仅供学习参考,请勿恶意采集

  • 32
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值