这几天一直听听评书,发现喜马拉雅上的资源很多,不过很可惜都是付费的,所以我冲了一个月会员,简单写个爬虫,爬下来几10部,够我一年听的了
开始分析打开chrome控制台,点击播放,最先拿到的一个接口就是
私信小编01即可获取大量Python学习资源
https://mpay.ximalaya.com/mobile/track/pay/244130607/?device=pc
当然这个是付费的一部书,所以如果你浏览器不带会员的cookie是访问不到的,其中的数字244130607,这个在他们的接口中叫做trackId,每个音频文件对应唯一的一个trackId
也就是对应这个界面的后面的数字,通过这个唯一的trackId可以获取到音频文件,那么看一下这个接口返回的内容
{\n"ret":0,\n"msg":"0",\n"trackId":244130607,\n"uid":170217760,\n"albumId":30816438,\n"title":"《三体》第一季第十集聚会与大撕裂",\n"domain":"http://audiopay.cos.xmcdn.com",\n"totalLength":12780565,\n"sampleDuration":0,\n"sampleLength":0,\n"isAuthorized":true,\n"apiVersion":"1.0.0",\n"seed":9583,\n"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*",\n"buyKey":"617574686f72697a6564",\n"duration":1578,\n"ep":"20NvOoh6T39X3qwKO4cY5g5bVhg+1nfPHIQafFTmCXihnrqF2PjczO8O0auK1KJhDrJ30XMYfKJo2uz+xgwd3rwRPi5f",\n"highestQualityLevel":1,\n"downloadQualityLevel":1,\n"authorizedType":1\n}
这里,我充会员了,所以可以直接用浏览器中打开这个url,其中有用的字段有了只有几个seed和fileId两个通过js加密算法计算出m4a的路径,并拼接主域名,然后ep经过另一个加密算法得到url的访问参数buy_keysigntokentimestamp,最后将它们拼接到一起才是一个完整的音频的url
两个js加密算法经过我调试我分别找到了这两个加密的js算法
计算m4a的路径js算法:functionvt(t){\nthis._randomSeed=t,\nthis.cg_hun()\n}\nvt.prototype={\ncg_hun:function(){\nthis._cgStr="";\nvart="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\\\\:._-1234567890"\n,e=t.length\n,n=0;\nfor(n=0;n<e;n++){\nvarr=this.ran()*t.length\n,o=parseInt(r);\nthis._cgStr+=t.charAt(o),\nt=t.split(t.charAt(o)).join("")\n}\n},\ncg_fun:function(t){\nt=t.split("*");\nvare=""\n,n=0;\nfor(n=0;n<t.length-1;n++)\ne+=this._cgStr.charAt(t[n]);\nreturne\n},\nran:function(){\nthis._randomSeed=(211*this._randomSeed+30031)%65536;\nreturnthis._randomSeed/65536\n},\n\n};\n\nc=function(t,e){\nvarn=newvt(t).cg_fun(e);\nreturn"/"===n[0]?n:"/".concat(n)\n}\n\nconsole.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的路径输出
/group3/M04/9E/88/wKgMbF4ejn2TfGPRAMMEFYoRHXs027.m4a通过ep来计算url参数的js算法:
Z=function(){\nthrownewTypeError("Invalidattempttodestructurenon-iterableinstance")\n}\n\nJ=function(t,e){\nvarn=[]\n,r=!0\n,o=!1\n,i=void0;\ntry{\nfor(vara,u=t[Symbol.iterator]();!(r=(a=u.next()).done)&&(n.push(a.value),\n!e||n.length!==e);r=!0)\n;\n}catch(t){\no=!0,\ni=t\n}finally{\ntry{\nr||null==u.return||u.return()\n}finally{\nif(o)\nthrowi\n}\n}\nreturnn\n}\n\nQ=function(t){\nif(Array.isArray(t))\nreturnt\n}\n\ntt=function(t,e){\nreturnQ(t)||J(t,e)||Z()\n}\n\nfunctionyt(t,e){\nfor(varn,r=[],o=0,i="",a=0;256>a;a++)\nr[a]=a;\nfor(a=0;256>a;a++)\no=(o+r[a]+t.charCodeAt(a%t.length))%256,\nn=r[a],\nr[a]=r[o],\nr[o]=n;\nfor(varu=o=a=0;u<e.length;u++)\no=(o+r[a=(a+1)%256])%256,\nn=r[a],\nr[a]=r[o],\nr[o]=n,\ni+=String.fromCharCode(e.charCodeAt(u)^r[(r[a]+r[o])%256]);\nreturni\n}\n\nvarmt=yt("xm","?[ü\u0014J=??3áf÷\u0017N")\ngt=[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]\n\nbt=function(t){\n\nvare1=yt(\nfunction(t,e){\nfor(varn=[],r=0;r<t.length;r++){\nfor(varo="a"<=t[r]&&"z">=t[r]?t[r].charCodeAt()-97:t[r].charCodeAt()-"0".charCodeAt()+26,i=0;36>i;i++)\nif(e[i]==o){\no=i;\nbreak\n}\nn[r]=25<o?String.fromCharCode(o-26+"0".charCodeAt()):String.fromCharCode(o+97)\n}\nreturnn.join("")\n}("d"+mt+"9",gt)\n,\ne2=function(t){\nif(!t)\nreturn"";\nvare,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];\nfor(o=(t=t.toString()).length,\nr=0,\ni="";r<o;){\ndo{\ne=a[255&t.charCodeAt(r++)]\n}while(r<o&&-1==e);if(-1==e)\nbreak;\ndo{\nn=a[255&t.charCodeAt(r++)]\n}while(r<o&&-1==n);if(-1==n)\nbreak;\ni+=String.fromCharCode(e<<2|(48&n)>>4);\ndo{\nif(61==(e=255&t.charCodeAt(r++)))\nreturni;\ne=a[e]\n}while(r<o&&-1==e);if(-1==e)\nbreak;\ni+=String.fromCharCode((15&n)<<4|(60&e)>>2);\ndo{\nif(61==(n=255&t.charCodeAt(r++)))\nreturni;\nn=a[n]\n}while(r<o&&-1==n);if(-1==n)\nbreak;\ni+=String.fromCharCode((3&e)<<6|n)\n}\nreturni\n}(t)\n).split("-")\n\nconsole.log(e1)\n}\n\nvarc=bt("20NvOoh6T39X3qwKO4cY5g5bVhg+1nfPHIQafFTmCXihnrqF2PjczO8O0auK1KJhDrJ30XMYfKJo2uz+xgwd3rwRPi5f")
这段js比较复杂,调试的时候坑死我了,不在同一个地方,导致我来回复制,最终才把这个算法整理到这一个js文件中,依然用node跑一下,输出:
[\n'617574686f72697a6564',\n'ef9a0678d77870843ef203d6333ce021',\n'5790',\n'1598533668'\n]
这几个参数分别对应的是:buy_keysigntokentimestamp有了这两个js算法就可以完全地解析这个接口返回的参数了。
python代码仿写加密算法计算m4a路径加密算法classvt():\ndef__init__(self,t):\nself._randomSeed=t\nself.cg_hun()\n\ndefran(self):\nself._randomSeed=(211*self._randomSeed+30031)%65536\nreturnself._randomSeed/65536\n\ndefcg_hun(self):\nself._cgStr=""\nt="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\\\\:._-1234567890"\ne=len(t)\nn=0\nforiinrange(e):\nr=self.ran()*len(t)\no=int(r)\nself._cgStr+=t[o]\nt="".join(t.split(t[o]))\n\ndefcg_fun(self,t):\nt=[int(i)ifielse0foriint.split("*")]\ne=""\nn=0;\nforninrange(n,len(t)-1):\ne+=self._cgStr[t[n]]\nreturne\n\ndefpath_decode(seed,fileId):\nc=vt(seed)\np=c.cg_fun(fileId)\nreturnp\n\nif__name__=='__main__':\nresult=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*")\nprint(result)通过ep来计算url参数的算法:
defyt(t,e):\nr=[0foriinrange(256)]\no=0\ni=""\nforainrange(0,256):\nr[a]=a;\nforainrange(0,256):\no=(o+r[a]+ord(t[a%len(t)]))%256\nn=r[a]\nr[a]=r[o]\nr[o]=n\n\nu=0\no=0\na=0\nforuinrange(0,len(e)):\na=(a+1)%256\no=(o+r[a])%256\nn=r[a]\nr[a]=r[o]\nr[o]=n\ni+=chr(ord(e[u])^r[(r[a]+r[o])%256])\nreturni\n\ndefbt(t):\ndefarg1(t,e):\nn=[''foriinrange(256)]\nforrinrange(0,len(t)):\n\nif"a"<=t[r]and"z">=t[r]:\no=ord(t[r])-97\nelse:\no=ord(t[r])-ord("0")+26\nforiinrange(0,36):\nif(e[i]==o):\no=i\nbreak\n\nif25<o:\nn[r]=chr(o-26+ord("0"))\nelse:\nn[r]=chr(o+97)\n\nreturn"".join(n).strip()\n\na1=arg1("d"+mt+"9",gt)\ndefarg2(t):\nifnott:\nreturn""\n\ne=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];\n\no=len(t)\ni=""\nr=0\nwhiler<o:\nwhileTrue:\ne=a[255&ord(t[r])]\nr+=1\nifnot(r<oand-1==e):\nbreak\nif(-1==e):\nbreak\nwhileTrue:\nn=a[255&ord(t[r])]\nr+=1\nifnot(r<oand-1==n):\nbreak\nif(-1==n):\nbreak\ni+=chr(e<<2|(48&n)>>4)\nwhileTrue:\ne=(255&ord(t[r]))\nif61==e:\nreturni\nr+=1\n\ne=a[e]\nifnot(r<oand-1==e):\nbreak\nif(-1==e):\nbreak\ni+=chr((15&n)<<4|(60&e)>>2);\nwhileTrue:\nn=(255&ord(t[r]))\nif(61==n):\nreturni\nr+=1\nn=a[n]\nifnot(r<oand-1==n):\nbreak\nif(-1==n):\nbreak\ni+=chr((3&e)<<6|n)\n\nreturni\n\na2=arg2(t)\nbuy_key,sign,token,timestamp=yt(a1,a2).split('-')\ndata=dict(\nbuy_key=buy_key,\nsign=sign,\ntoken=token,\ntimestamp=timestamp,\n)\nreturndata\n\nmt=yt("xm","?[ü\u0014J=??3áf÷\u0017N")\ngt=[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]\n\ndefep_decode(ep):\ndata=bt(ep)\nreturndata\n\nif__name__=='__main__':\nprint(ep_decode('20NvOoh6T39X3qwKO4cY5g5bVhg+1nfPHIQafFTmCXihnrqF2PjczO8O0auK1KJhDrJ30XMYfKJo2uz+xgwd3rwRPi5f'))
这个接口到此为止才算是完全可以解析。
免费接口分析如果你没有充会员,免费的音频还是可以听的,我找到一个免费音频的接口
https://www.ximalaya.com/revision/play/v1/audio?id=324681559&ptype=1
{\n"ret":200,\n"data":{\n"trackId":324681559,\n"canPlay":true,\n"isPaid":false,\n"hasBuy":true,\n"src":"https://aod.cos.tx.xmcdn.com/group84/M03/4A/A6/wKg5Hl8s0cTwcp6xABQ0EbeuW5Q193.m4a",\n"albumIsSample":false,\n"sampleDuration":48,\n"isBaiduMusic":false,\n"firstPlayStatus":true,\n"isVipFree":false\n}\n}
这个接口还是比较简单的,返回值里面直接包含m4a音频地址,没有加密措施,另外url中的数字依然是trackId,值得一提的是免费音频的trackId不能用在付费接口,我猜测是版本迭代的问题,或者是客户端不同的问题,因为当时我不只是分析网页的接口,还抓包了电脑客户端的接口,具体对应的是网页还是客户端我也忘了。
解析整本书的接口喜马拉雅接口主要关键的有两个参数,一个是前面我说的trackId另一个就是albumId,trackId对应唯一的一个音频,而albumId对应的是唯一的一本书。
https://www.ximalaya.com/revision/album/v1/getTracksList?albumId=30816438&pageNum=1&pageSize=1000
返回值中就有每一集的trackId,其实喜马拉雅还有很多其他接口,搜索接口等等,一般的其他的接口需要在请求头中加入xm-sign,我也写了xm-sign的计算方法:
importrequests\nimporttime\nimporthashlib\nimportrandom\nimportjson\nfromrequests.packages.urllib3.exceptionsimportInsecureRequestWarning\nrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)\n\n#获取sign签名\n\ndefget_sign(headers):\nserverTimeUrl="https://www.ximalaya.com/revision/time"\nresponse=requests.get(serverTimeUrl,headers=headers,verify=False)\nserverTime=response.text\nnowTime=str(round(time.time()*1000))\n\nsign=str(hashlib.md5("himalaya-{}".format(serverTime).encode()).hexdigest())+"({})".format(str(round(random.random()*100)))+serverTime+"({})".format(str(round(random.random()*100)))+nowTime\nheaders["xm-sign"]=sign\nreturnheaders\n\ndefget_header():\nheaders={\n"User-Agent":"Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/75.0.3770.90Safari/537.36"\n}\nheaders=get_sign(headers)\nreturnheaders\n\nif__name__=='__main__':\n#这是一个搜索接口\nurl="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"\ns=requests.get(url,headers=get_header(),verify=False)\nprint(s.json())
还有很多其他接口,我就懒得说了,因为我不想写了,有了这些就可以满足我下载整本书的需求了
最终整合我写了喜马拉雅扫码登陆的脚本,因为我不能每次都去复制浏览器中的cookie,这种重复劳动太傻了
importrequests\nimportre\nfromthreadingimportThread\nimporttime\nimportrequests\nfromioimportBytesIO\nimporthttp.cookiejarascookielib\nfromPILimportImage\nimportsys\nimportpsutil\nfrombase64importb64decode\nimportos\n\nrequests.packages.urllib3.disable_warnings()\n\nclassshow_code(Thread):\ndef__init__(self,data):\nThread.__init__(self)\nself.data=data\n\ndefrun(self):\nimg=Image.open(BytesIO(self.data))#打开图片,返回PILimage对象\nimg.show()\n\ndefis_login(session):\nheaders={'User-Agent':"Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/84.0.4147.89Safari/537.36"}\nurl="https://www.ximalaya.com/revision/main/getCurrentUser"\ntry:\nsession.cookies.load(ignore_discard=True)\nexceptException:\npass\nresponse=session.get(url,verify=False,headers=headers)\nifresponse.json()['ret']==200:\nprint(response.json())\nreturnsession,True\nelse:\nreturnsession,False\n\ndeflogin():\nifnotos.path.exists(".cookie"):\nos.makedirs('.cookie')\nifnotos.path.exists('.cookie/xmly.txt'):\nprint("hello")\nwithopen(".cookie/xmly.txt",'w')asf:\nf.write("")\nsession=requests.session()\nsession.cookies=cookielib.LWPCookieJar(filename='.cookie/xmly.txt')\nsession,status=is_login(session)\nifnotstatus:\nurl="https://passport.ximalaya.com/web/qrCode/gen?level=L"\nresponse=session.get(url,verify=False)\ndata=response.json()\n#withopen('qrcode.jpg','wb')asf:\n#f.write(b64decode(data['img']))\nt=show_code(b64decode(data['img']))\nt.start()\nqrId=data['qrId']\n\nurl='https://passport.ximalaya.com/web/qrCode/check/%s/%s'%(qrId,int(time.time()*1000))\nwhile1:\nresponse=session.get(url,verify=False)\ndata=response.json()\n#code=re.findall("window.wx_code='(.*?)'",response.text)\n#sys.exit()\n\nifdata['ret']==0:\n#forprocinpsutil.process_iter():#遍历当前process\n#try:\n#ifproc.name()=="Microsoft.Photos.exe":\n#proc.kill()#关闭该process\n#exceptExceptionase:\n#print(e)\nbreak\ntime.sleep(1)\nsession.cookies.save()\nreturnsession\nif__name__=='__main__':\nlogin()
简单的一个扫码登陆脚本,如果cookie自动保存成文件,下次使用的时候直接调用:
session=login()
就能在保持登陆状态下,访问各种接口
好了,关于trackid是什么和Python爬虫之JavaScript逆向的问题到这里结束啦,希望可以解决您的问题哈!