请选择 进入手机版 | 继续访问电脑版

[python] 喜马拉雅加密算法分析

编程语言 编程语言 2616 人阅读 | 1 人回复

发表于 2020-10-6 02:30:13 | 显示全部楼层 |阅读模式

所有源码我放到了 蓝奏云,所有人可以免费下载,知识用来共享:https://www.lanzoux.com/ibiWDg4gc2f
download.py为主入口文件,其中有两个参数你可能需要调节
  1. self.run_t = 7
  2. self.save_dir = r"D:\喜马拉雅"
复制代码

一个是同时下载个数,一个是保存路径,剩下的需要调节最下面的整本书的albumId了,另外其中我加入了转换mp3格式的函数,不过我默认注释掉了,如果想使用可以配置ffmpeg到环境变量中去
前言
这几天一直听听评书,发现喜马拉雅上的资源很多,不过很可惜都是付费的,所以我冲了一个月会员,简单写个爬虫,爬下来几10部,够我一年听的了
开始分析
打开chrome控制台,点击播放,最先拿到的一个接口就是
  1. https://mpay.ximalaya.com/mobile/track/pay/244130607/?device=pc
复制代码
a8cedb4a59f9fbf564b03d0e3b5e859.png

  1. {
  2. "ret": 0,
  3. "msg": "0",
  4. "trackId": 244130607,
  5. "uid": 170217760,
  6. "albumId": 30816438,
  7. "title": "《三体》第一季 第十集 聚会与大撕裂",
  8. "domain": "http://audiopay.cos.xmcdn.com",
  9. "totalLength": 12780565,
  10. "sampleDuration": 0,
  11. "sampleLength": 0,
  12. "isAuthorized": true,
  13. "apiVersion": "1.0.0",
  14. "seed": 9583,
  15. "fileId": "27*31*44*62*1*8*6*48*52*4*6*17*16*6*35*35*6*43*25*27*48*63*58*4*50*47*60*64*15*39*59*49*2*36*48*48*16*58*18*44*2*32*12*7*52*64*51*26*29*4*22*",
  16. "buyKey": "617574686f72697a6564",
  17. "duration": 1578,
  18. "ep": "20NvOoh6T39X3qwKO4cY5g5bVhg+1nfPHIQafFTmCXihnrqF2PjczO8O0auK1KJhDrJ30XMYfKJo2uz+xgwd3rwRPi5f",
  19. "highestQualityLevel": 1,
  20. "downloadQualityLevel": 1,
  21. "authorizedType": 1
  22. }
复制代码
这里,我充会员了,所以可以直接用浏览器中打开这个url,其中有用的字段有了只有几个 seed和 fileId两个通过js加密算法计算出 m4a的路径,并拼接主域名,然后 ep 经过另一个加密算法得到url的访问参数buy_key sign token timestamp,最后将它们拼接到一起才是一个完整的 音频的url
两个js加密算法
经过我调试我分别找到了这两个加密的 js算法
  • 计算 m4a的路径js算法:
    1. function vt(t) {
    2.                 this._randomSeed = t,
    3.                 this.cg_hun()
    4.             }
    5.             vt.prototype = {
    6.                 cg_hun: function() {
    7.                     this._cgStr = "";
    8.                     var t = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\\:._-1234567890"
    9.                       , e = t.length
    10.                       , n = 0;
    11.                     for (n = 0; n < e; n++) {
    12.                         var r = this.ran() * t.length
    13.                           , o = parseInt(r);
    14.                         this._cgStr += t.charAt(o),
    15.                         t = t.split(t.charAt(o)).join("")
    16.                     }
    17.                 },
    18.                 cg_fun: function(t) {
    19.                     t = t.split("*");
    20.                     var e = ""
    21.                       , n = 0;
    22.                     for (n = 0; n < t.length - 1; n++)
    23.                         e += this._cgStr.charAt(t[n]);
    24.                     return e
    25.                 },
    26.                 ran: function() {
    27.                     this._randomSeed = (211 * this._randomSeed + 30031) % 65536;
    28.                     return this._randomSeed / 65536
    29.                 },

    30.             };

    31. c = function(t, e) {
    32.     var n = new vt(t).cg_fun(e);
    33.     return "/" === n[0] ? n : "/".concat(n)
    34. }

    35. console.log(c(9583,"27*31*44*62*1*8*6*48*52*4*6*17*16*6*35*35*6*43*25*27*48*63*58*4*50*47*60*64*15*39*59*49*2*36*48*48*16*58*18*44*2*32*12*7*52*64*51*26*29*4*22*"))
    复制代码
    用node跑一下可以得到 m4a的路径
    输出:
    1. /group3/M04/9E/88/wKgMbF4ejn2TfGPRAMMEFYoRHXs027.m4a
    复制代码

  • 通过ep来计算url参数的js算法:
    1. Z = function() {
    2.                 throw new TypeError("Invalid attempt to destructure non-iterable instance")
    3.             }

    4. J = function(t, e) {
    5. var n = []
    6.   , r = !0
    7.   , o = !1
    8.   , i = void 0;
    9. try {
    10.     for (var a, u = t[Symbol.iterator](); !(r = (a = u.next()).done) && (n.push(a.value),
    11.     !e || n.length !== e); r = !0)
    12.         ;
    13. } catch (t) {
    14.     o = !0,
    15.     i = t
    16. } finally {
    17.     try {
    18.         r || null == u.return || u.return()
    19.     } finally {
    20.         if (o)
    21.             throw i
    22.     }
    23. }
    24. return n
    25. }

    26. Q = function(t) {
    27. if (Array.isArray(t))
    28.     return t
    29. }

    30. tt = function(t, e) {
    31.     return Q(t) || J(t, e) || Z()
    32. }

    33. function yt(t, e) {
    34.     for (var n, r = [], o = 0, i = "", a = 0; 256 > a; a++)
    35.         r[a] = a;
    36.     for (a = 0; 256 > a; a++)
    37.         o = (o + r[a] + t.charCodeAt(a % t.length)) % 256,
    38.         n = r[a],
    39.         r[a] = r[o],
    40.         r[o] = n;
    41.     for (var u = o = a = 0; u < e.length; u++)
    42.         o = (o + r[a = (a + 1) % 256]) % 256,
    43.         n = r[a],
    44.         r[a] = r[o],
    45.         r[o] = n,
    46.         i += String.fromCharCode(e.charCodeAt(u) ^ r[(r[a] + r[o]) % 256]);
    47.     return i
    48. }

    49. var mt = yt("xm", "Ä[üJ=†Û3áf÷N")
    50. gt = [19, 1, 4, 7, 30, 14, 28, 8, 24, 17, 6, 35, 34, 16, 9, 10, 13, 22, 32, 29, 31, 21, 18, 3, 2, 23, 25, 27, 11, 20, 5, 15, 12, 0, 33, 26]

    51. bt = function(t) {

    52. var e1 = yt(
    53.     function(t, e) {
    54.     for (var n = [], r = 0; r < t.length; r++) {
    55.         for (var o = "a" <= t[r] && "z" >= t[r] ? t[r].charCodeAt() - 97 : t[r].charCodeAt() - "0".charCodeAt() + 26, i = 0; 36 > i; i++)
    56.             if (e[i] == o) {
    57.                 o = i;
    58.                 break
    59.             }
    60.         n[r] = 25 < o ? String.fromCharCode(o - 26 + "0".charCodeAt()) : String.fromCharCode(o + 97)
    61.     }
    62.     return n.join("")
    63.     }("d" + mt + "9",gt)
    64.     ,
    65.     e2 = function(t) {
    66.         if (!t)
    67.             return "";
    68.         var e, n, r, o, i, a = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1];
    69.         for (o = (t = t.toString()).length,
    70.         r = 0,
    71.         i = ""; r < o; ) {
    72.             do {
    73.                 e = a[255 & t.charCodeAt(r++)]
    74.             } while (r < o && -1 == e);if (-1 == e)
    75.                 break;
    76.             do {
    77.                 n = a[255 & t.charCodeAt(r++)]
    78.             } while (r < o && -1 == n);if (-1 == n)
    79.                 break;
    80.             i += String.fromCharCode(e << 2 | (48 & n) >> 4);
    81.             do {
    82.                 if (61 == (e = 255 & t.charCodeAt(r++)))
    83.                     return i;
    84.                 e = a[e]
    85.             } while (r < o && -1 == e);if (-1 == e)
    86.                 break;
    87.             i += String.fromCharCode((15 & n) << 4 | (60 & e) >> 2);
    88.             do {
    89.                 if (61 == (n = 255 & t.charCodeAt(r++)))
    90.                     return i;
    91.                 n = a[n]
    92.             } while (r < o && -1 == n);if (-1 == n)
    93.                 break;
    94.             i += String.fromCharCode((3 & e) << 6 | n)
    95.         }
    96.         return i
    97.     }(t)
    98.     ).split("-")

    99. console.log(e1)
    100. }

    101. var c = bt("20NvOoh6T39X3qwKO4cY5g5bVhg+1nfPHIQafFTmCXihnrqF2PjczO8O0auK1KJhDrJ30XMYfKJo2uz+xgwd3rwRPi5f")
    复制代码
    这段js比较复杂,调试的时候坑死我了,不在同一个地方,导致我来回复制,最终于才把这个算法整理到这一个js文件中,依然用 node跑一下,输出:
    1. [
    2.   '617574686f72697a6564',
    3.   'ef9a0678d77870843ef203d6333ce021',
    4.   '5790',
    5.   '1598533668'
    6. ]
    复制代码

这几个参数分别对应的是:buy_key sign token timestamp

有了这了两个js算法就可以完全的解析 这个接口返回的参数了。

python 代码仿写加密算法
计算 m4a路径加密算法
  1. class vt():
  2.     def __init__(self,t):
  3.         self._randomSeed = t
  4.         self.cg_hun()

  5.     def ran(self):
  6.         self._randomSeed = (211 * self._randomSeed + 30031) % 65536
  7.         return self._randomSeed / 65536

  8.     def cg_hun(self):
  9.         self._cgStr = ""
  10.         t = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\\:._-1234567890"
  11.         e = len(t)
  12.         n = 0
  13.         for i in range(e):
  14.             r = self.ran() * len(t)
  15.             o = int(r)
  16.             self._cgStr += t[o]
  17.             t = "".join(t.split(t[o]))

  18.     def cg_fun(self,t):
  19.         t = [int(i) if i else 0 for i in t.split("*")]
  20.         e = ""
  21.         n = 0;
  22.         for n in range(n,len(t)-1):
  23.             e += self._cgStr[t[n]]
  24.         return e

  25. def path_decode(seed,fileId):
  26.     c = vt(seed)
  27.     p = c.cg_fun(fileId)
  28.     return p

  29. if __name__ == '__main__':
  30.     result = path_decode(9583,"27*31*44*62*1*8*6*48*52*4*6*17*16*6*35*35*6*43*25*27*48*63*58*4*50*47*60*64*15*39*59*49*2*36*48*48*16*58*18*44*2*32*12*7*52*64*51*26*29*4*22*")
  31.     print(result)
复制代码


通过ep来计算url参数的算法:
  1. def yt(t, e):
  2.     r = [0 for i in range(256)]
  3.     o = 0
  4.     i = ""
  5.     for a in range(0,256):
  6.         r[a] = a;
  7.     for a in range(0,256):
  8.         o = (o + r[a] + ord(t[a % len(t)])) % 256
  9.         n = r[a]
  10.         r[a] = r[o]
  11.         r[o] = n

  12.     u = 0
  13.     o = 0
  14.     a = 0
  15.     for u in range(0,len(e)):
  16.         a = (a + 1) % 256
  17.         o = (o + r[a]) % 256
  18.         n = r[a]
  19.         r[a] = r[o]
  20.         r[o] = n
  21.         i += chr(ord(e[u]) ^ r[(r[a] + r[o]) % 256])
  22.     return i

  23. def bt(t):
  24.     def arg1(t,e):
  25.         n = [' ' for i in range(256)]
  26.         for r in range(0,len(t)):

  27.             if "a" <= t[r] and "z" >= t[r]:
  28.                 o = ord(t[r]) - 97
  29.             else:
  30.                 o = ord(t[r]) - ord("0") + 26
  31.             for i in range(0,36):
  32.                 if (e[i] == o):
  33.                     o = i
  34.                     break

  35.             if 25< o:
  36.                 n[r] = chr(o - 26 + ord("0"))
  37.             else:
  38.                 n[r] = chr(o + 97)

  39.         return "".join(n).strip()

  40.     a1 = arg1("d" + mt + "9", gt)
  41.     def arg2(t):
  42.         if not t:
  43.             return ""

  44.         e = n = r = o = i = a = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1];

  45.         o = len(t)
  46.         i = ""
  47.         r = 0
  48.         while r < o:
  49.             while True:
  50.                 e = a[255 & ord(t[r])]
  51.                 r += 1
  52.                 if not (r < o and -1 == e):
  53.                     break
  54.             if (-1 == e):
  55.                 break
  56.             while True:
  57.                 n = a[255 & ord(t[r])]
  58.                 r += 1
  59.                 if not (r < o and -1 == n):
  60.                     break
  61.             if (-1 == n):
  62.                 break
  63.             i += chr(e << 2 | (48 & n) >> 4)
  64.             while True:
  65.                 e = (255 & ord(t[r]))
  66.                 if 61 == e:
  67.                     return i
  68.                 r += 1

  69.                 e = a[e]
  70.                 if not (r < o and -1 == e):
  71.                     break
  72.             if (-1 == e):
  73.                 break
  74.             i += chr((15 & n) << 4 | (60 & e) >> 2);
  75.             while True:
  76.                 n = (255 & ord(t[r]))
  77.                 if (61 == n):
  78.                     return i
  79.                 r += 1
  80.                 n = a[n]
  81.                 if not (r < o and -1 == n):
  82.                     break
  83.             if (-1 == n):
  84.                 break
  85.             i += chr((3 & e) << 6 | n)

  86.         return i

  87.     a2 = arg2(t)
  88.     buy_key,sign,token,timestamp = yt(a1,a2).split('-')
  89.     data = dict(
  90.         buy_key=buy_key,
  91.         sign=sign,
  92.         token=token,
  93.         timestamp=timestamp,
  94.     )
  95.     return data

  96. mt = yt("xm", "Ä[üJ=†Û3áf÷N")
  97. gt = [19, 1, 4, 7, 30, 14, 28, 8, 24, 17, 6, 35, 34, 16, 9, 10, 13, 22, 32, 29, 31, 21, 18, 3, 2, 23, 25, 27, 11, 20, 5, 15, 12, 0, 33, 26]

  98. def ep_decode(ep):
  99.     data = bt(ep)
  100.     return data

  101. if __name__ == '__main__':
  102.     print(ep_decode('20NvOoh6T39X3qwKO4cY5g5bVhg+1nfPHIQafFTmCXihnrqF2PjczO8O0auK1KJhDrJ30XMYfKJo2uz+xgwd3rwRPi5f'))
复制代码



这个接口到此为止才算是完全可以解析。

免费接口分析
如果你没有充会员,免费的音频还是可以听的,我找到一个免费音频的接口

  1. https://www.ximalaya.com/revision/play/v1/audio?id=324681559&ptype=1
复制代码


返回值:
  1. <blockquote>{
复制代码


这个接口还是比较简单的,返回值里面直接包含 m4a音频地址,没有加密措施,另外 url中的数字依然是 trackId,值得一提的是免费音频的trackId不能用在付费接口,我猜测是版本迭代的问题,或者是客户端不同的问题,因为当时我不只是分析网页的接口,还抓包了电脑客户端的接口,具体对应的是网页还是客户端我也忘了。

解析整本书的接口
喜马拉雅接口主要关键的有两个参数,一个是前面我说的 trackId 另一个就是albumId,trackId 对应唯一的一个音频,而 albumId 对应的是唯一的一本书。

  1. https://www.ximalaya.com/revision/album/v1/getTracksList?albumId=30816438&pageNum=1&pageSize=1000
复制代码


返回值中就有每一集的trackId,其实喜马拉雅还有很多其他接口,搜索接口等等,一般的其他的接口需要在请求头中加入xm-sign,我也写了xm-sign的计算方法:
  1. import requests
  2. import time
  3. import hashlib
  4. import random
  5. import json
  6. from requests.packages.urllib3.exceptions import InsecureRequestWarning
  7. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

  8. # 获取sign签名

  9. def get_sign(headers):
  10.     serverTimeUrl = "https://www.ximalaya.com/revision/time"
  11.     response = requests.get(serverTimeUrl,headers=headers,verify=False)
  12.     serverTime = response.text
  13.     nowTime = str(round(time.time()*1000))

  14.     sign = str(hashlib.md5("himalaya-{}".format(serverTime).encode()).hexdigest()) + "({})".format(str(round(random.random()*100))) + serverTime + "({})".format(str(round(random.random()*100))) + nowTime
  15.     headers["xm-sign"] = sign
  16.     return headers

  17. def get_header():
  18.     headers = {
  19.             "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.90 Safari/537.36"
  20.     }   
  21.     headers = get_sign(headers)
  22.     return headers

  23. if __name__ == '__main__':
  24.         # 这是一个搜索接口
  25.     url = "https://www.ximalaya.com/revision/search/main?core=all&spellchecker=true&device=iPhone&kw=%E9%9B%AA%E4%B8%AD%E6%82%8D%E5%88%80%E8%A1%8C&page=1&rows=20&condition=relation&fq=&paidFilter=false"
  26.     s = requests.get(url,headers=get_header(),verify=False)
  27.     print(s.json())
复制代码

还有很多其他接口,我就懒得说了,因为我不想写了,有了这些就可以满足我下载整本书的需求了
最终整合
我写了 喜马拉雅 扫码登陆的脚本,因为我不能每次都去复制浏览器中的 cookie,这种重复劳动太傻了
  1. import requests
  2. import re
  3. from threading import Thread
  4. import time
  5. import requests
  6. from io import BytesIO
  7. import http.cookiejar as cookielib
  8. from PIL import Image
  9. import sys
  10. import psutil
  11. from base64 import b64decode
  12. import os

  13. requests.packages.urllib3.disable_warnings()

  14. class show_code(Thread):
  15.     def __init__(self,data):
  16.         Thread.__init__(self)
  17.         self.data = data

  18.     def run(self):
  19.         img = Image.open(BytesIO(self.data))  # 打开图片,返回PIL image对象
  20.         img.show()

  21. def is_login(session):
  22.     headers = {'User-Agent':"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"}
  23.     url = "https://www.ximalaya.com/revision/main/getCurrentUser"
  24.     try:
  25.         session.cookies.load(ignore_discard=True)
  26.     except Exception:
  27.         pass
  28.     response  = session.get(url,verify=False,headers=headers)
  29.     if response.json()['ret'] == 200:
  30.         print(response.json())
  31.         return session,True
  32.     else:
  33.         return session,False

  34. def login():
  35.     if not os.path.exists(".cookie"):
  36.         os.makedirs('.cookie')
  37.     if not os.path.exists('.cookie/xmly.txt'):
  38.         print("hello")
  39.         with open(".cookie/xmly.txt",'w') as f:
  40.             f.write("")
  41.     session = requests.session()
  42.     session.cookies = cookielib.LWPCookieJar(filename='.cookie/xmly.txt')
  43.     session,status = is_login(session)
  44.     if not status:
  45.         url = "https://passport.ximalaya.com/web/qrCode/gen?level=L"
  46.         response = session.get(url,verify=False)
  47.         data = response.json()
  48.         # with open('qrcode.jpg','wb') as f:
  49.             # f.write(b64decode(data['img']))
  50.         t= show_code(b64decode(data['img']))
  51.         t.start()
  52.         qrId = data['qrId']

  53.         url = 'https://passport.ximalaya.com/web/qrCode/check/%s/%s' % (qrId,int(time.time()*1000))
  54.         while 1:
  55.             response = session.get(url,verify=False)
  56.             data = response.json()
  57.             # code = re.findall("window.wx_code='(.*?)'",response.text)
  58.             # sys.exit()

  59.             if data['ret'] == 0:
  60.                 # for proc in psutil.process_iter():  # 遍历当前process
  61.                     # try:
  62.                     #     if proc.name() == "Microsoft.Photos.exe":  
  63.                     #         proc.kill()  # 关闭该process
  64.                     # except Exception as e:
  65.                     #     print(e)
  66.                 break
  67.             time.sleep(1)
  68.         session.cookies.save()
  69.     return session
  70. if __name__ == '__main__':
  71.     login()
复制代码
简单的一个扫码登陆脚本,如果cookie自动保存成文件,下次使用的时候直接调用:
  1. session = login()
复制代码
就能在保持登陆状态下,访问各种接口




点评回复

使用道具 举报

回答|共 1 个

事与愿违

发表于 2020-10-9 21:45:51 | 显示全部楼层

感谢分享
点评回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则