孤竹翊算是我最喜欢的唱歌的人了。喜欢了大概有5年,快6年了吧从初中听见《蜀相》以来就一直好喜欢她的歌。
建议你,可以一边动手一边听歌。这样最好了。
嗯,说说如何批量下载她的歌吧,过一段时间准备做一个webapp。
于是先来爬取她的歌好了。
首先,说说,看完这个东西会得到什么好了(如果你不会的话)。
1.应对异步加载
2.应对JS代码
3.简单的前端知识
4.使用抓包工具
5.使用异步编程以及子进程
6.处理内存泄露(如果发生)
7.听竹子的歌
一般来说都是用 <audio>标签实现的音频文件播放。而,为了美化,通常不会让这个东西给用户看见。
所以我们来找一找这个 <audio>
找到咯,然后呢。
把src属性找出来就好了。
话不多说了,直接看看代码吧。
from requests import get,post
from bs4 import BeautifulSoup
if __name__ == '__main__':
a=get('http://5sing.kugou.com/echoriath/fc/1.html').text
print(a)
但是呢,看看response的html。
所以说,整个html中是没有audio存在的。
这个标签是后来才创建出来的哦,还有,src属性也是后来才加上的哦。
第二部分,看看 audio 标签是被什么创建的吧。
这里用到抓包工具fiddler 你也可以用其他的东西,但是我用这个。
这方面的教程有很多,随便找一下就可以了。
我在这里只说说一些坑
1.关于FILLDER的HTTPS
2.记得要清除浏览器缓存(要不然浏览器不会发出请求的,它会保存静态文件),还有就是设置FILLDER,要它禁止缓存。
好了,现在把他们整个5sing的前端代码全部下载下来就好了(别下图片,太多了)。
大概有50多个吧。
如果你没有装vsode,那么应该装上它(它很美,很好看,也很方便),还有要装node.js 。
然后,你要用npm下载一个 js-beautify
否则你看到的js代码都是一团乱的,而不是接下来整理的效果。
使用vsode的全局搜索功能(ctrl+shift+f)找到 audio
看,结果出来了。
这一行代码就是使得audio出现的东西了(得会会被jquery插入html)
唯一需要关注的东西是,"src="=source 这句,如果可以找到source就是找到了下载地址了。
一路往上看,啊,发现,它们是在一个函数了里面。
看看谁调用了这个函数吧。
发现了,上一段语句是在另一个函数里面的。
那么是谁又在调用它呢。
对了,记得看好一点,它们的参数,我们找到最上一层,就可以找到source 这个变量的来源了。
好了,接下来添加两个语句
输出file到底是什么,还有就是查看调用栈。嗯,要用,fiddler才行。自己看如何做到自己看教程吧。
这时候,另外一个JS文件出现了。就是 listen.min.newm.js
好了。记得要用js-beautify啊。
看到1946行吧
调用了之前(并不是,我跳过了两个函数,照着之前方法向上找就好了。)说的函数。看传进去的参数。
只要得到了songObj 可以说事情就了了。
下面就解决了。
按照之前的方法,全局搜索,找到了。globals.js文件。
但是里面没有 globals.ticket 这个东西,它在
html 文件里面。
所以很无聊的加密了一下。我们把加密过程复制下来就好了。
如果是没有前端经验的人看了,以后会觉得奇怪,为什么JS文件有那么多个,可以互相引用呢。其实浏览器都支持了 AMD 和 CommonJS 这两个东西,使得他们可以相互引用,其中,AMD(也就是5SING代码中的)是异步的(现在html<script>标签也支持异步了加上 async就可以了)。做爬虫的话,前端知识是很必要的,当然后端也是如此,还要懂得密码学的东西和人工智能(这个我是完全不懂了)
感谢5sing的程序员们没有写 ‘au'+'dio’要不然就搜索不到了。当然还有最后的三个办法(骑驴找马吧算是)————用火焰图查看调用栈,不过这个很费力气就是了。
或者是另外自己定义一个子元素改变事件。
最后是直接改他们的JQuery源码,加入console.trace查看调用栈以及触发创建事件。总之放个钩子。
定义事件的实现方式会在其他文章里面写。
第三部分
把加密有关的文件都放在一个js文件里面就好了。然后,让它接受输入。
看js代码
{
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode:function(input){
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = globals.base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
globals.base64._keyStr.charAt(enc1) + globals.base64._keyStr.charAt(enc2) +
globals.base64._keyStr.charAt(enc3) + globals.base64._keyStr.charAt(enc4);
}
return output;
},
decode:function(input){
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = globals.base64._keyStr.indexOf(input.charAt(i++));
enc2 = globals.base64._keyStr.indexOf(input.charAt(i++));
enc3 = globals.base64._keyStr.indexOf(input.charAt(i++));
enc4 = globals.base64._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = globals.base64._utf8_decode(output);
return output;
},
_utf8_encode: function (input) {
input = input.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < input.length; n++) {
var c = input.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
},
_utf8_decode: function (input) {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while ( i < input.length ) {
c = input.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if((c > 191) && (c < 224)) {
c2 = input.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = input.charCodeAt(i+1);
c3 = input.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
};
以上代码不完整。还要加上输入的东西
看python代码
from requests import get
import re
import json
import subprocess
text=get('http://5sing.kugou.com/fc/16236596.html').text
globalsReExp=re.compile(r'("ticket"\s*:\s*"(?:\w|=)+")')
aj=json.loads('{%s}'%(globalsReExp.search(text).group()))
aac=subprocess.Popen('node fortest.js',stdin=subprocess.PIPE,stdout=subprocess.PIPE,cwd='你的路径')
aac.stdin.write(aj['ticket'].encode())
aac.stdin.close()
ccc=aac.wait()
resulet =aac.stdout.read()
print(resulet.decode())
如何获取播放列表是很简单的,就不说了。
好了,准备第四部分,用协程或者多进程加快爬虫速度(其实,我无论怎么搞都是一样的,因为我用的是校园网,只有2M)
按照直接的写法是这样的。
from requests import get
import re
import json
import subprocess
from lxml import etree
from time import clock
def getList(url):
text = get(url).text
#可以不要编码,这样速度也会快,反正都可以找到的。
html = etree.HTML(text)
for each in html.xpath('//div[@class="song_list"]/ul/li/strong[@class="lt list_name"]/a'):
name = each.xpath('./text()')
url = each.xpath('./@href')
yield url
def getTicket(url):
text = get(url).text
#可以不要编码,这样速度也会快,反正都可以找到的。
globalsReExp = re.compile(r'("ticket"\s*:\s*"(?:\w|=|\+)+")')
aj = json.loads('{%s}' % (globalsReExp.search(text).group()))
aac = subprocess.Popen('node fortest.js', stdin=subprocess.PIPE, stdout=subprocess.PIPE,
cwd='你的JS路径')
aac.stdin.write(aj['ticket'].encode())
aac.stdin.close()
aac.wait()
resulet = aac.stdout.read()
return resulet.decode()
if __name__ == '__main__':
startTime=clock()
songList=getList('http://5sing.kugou.com/echoriath/fc/1.html')
for each in songList:
print(getTicket(each[0]))
print(clock()-startTime)
输出结果
{"songName":"流光断代史【词:suixinsuiyuan】","songUrl":"http://data.5sing.kgimg.com/G108/M04/12/01/rA0DAFn4VFmAY9YlAKjELjKjvb4408.mp3"}
{"songName":"愿你 171019","songUrl":"http://data.5sing.kgimg.com/G107/M0A/17/03/S5QEAFnyhnqACGgKAFa5LA6wRhk579.mp3"}
{"songName":"颂歌【词:雨霁天青】","songUrl":"http://data.5sing.kgimg.com/G086/M0A/1C/15/9oYBAFlqL-CAIobGAM9awCoY2WY199.mp3"}
{"songName":"初恋之歌【词:SUIXINSUIYUAN】","songUrl":"http://data.5sing.kgimg.com/G113/M08/06/13/sQ0DAFlguV2AbTYgAIn_u1mOrsA360.mp3"}
{"songName":"我说的不朽【词:雨霁天青】","songUrl":"http://data.5sing.kgimg.com/G088/M05/1C/0D/OJQEAFjE1EOAUnToALRjsSnvyn8627.mp3"}
{"songName":"WZ · 咲","songUrl":"http://data.5sing.kgimg.com/G091/M04/11/13/-4YBAFi6t8CAP3mpAKWEhYPbu7A148.mp3"}
{"songName":"……【词:W君】","songUrl":"http://data.5sing.kgimg.com/G065/M00/04/09/IZQEAFfbnL-AUqR3ADKG2XxygvI801.mp3"}
{"songName":"在路上【词:稗子】","songUrl":"http://data.5sing.kgimg.com/G070/M08/09/17/5oYBAFdqd4SAWMEIAG6SUdsnB24361.mp3"}
{"songName":"有一天【SS·贵鬼 | 词:suixinsuiyuan】","songUrl":"http://data.5sing.kgimg.com/G058/M0A/04/1F/eg0DAFbF1H2AIQ6FAH2notXT3Kc138.mp3"}
{"songName":"(黑)历史 · 相憾(记蜀丞相)【词:浅措】","songUrl":"http://data.5sing.kgimg.com/G053/M00/01/15/1YYBAFbEPJSAbATiADhb2yA0M4Y075.mp3"}
11.898513153268777
花了12秒,额,貌似还算快了吧。其实我前几次都是20秒以上的,可能是缓存了还是网络变好了什么的快了。
然后,要知道,计算是费时间的。所以,要是可以一边计算一边下载就好了,这样就不会浪费时间了啊。
下面就是 asyncio 咯。3.4以后才有啊,3.5之后才出现了async/await (JS现在也可以了,所以有人说JS学了PYTHON,现在以及不用加分号了,import关键字也是更像PYTHON而不是JAVA)关键字,之前貌似也有twisted可以用,但是我不会啊。额,常用的scrapy框架就是twisted搞的,所以很多时候都装不上,主要因为没有装py3的twisted。
细心的人发现了,我在getList 里面用的是 yield ,这个函数是个生成器函数了。说白了就是会在执行的时候中断,执行其他函数了。额,关于asyncio 的内容还是请百度,教程很多的。要是有过JS经验的人会听见一个叫做EVENTLOOP的东西大概会很耳熟了。
import asyncio
import time
import string
from requests import get
async def xiazai(url):
response = get(url)
print(response.statusCode)
async def shuchu(a):
await download('http://5sing.kugou.com/echoriath/fc/1.html')
return
async def ganhuo(zifu):
res= await shuchu(zifu)
print(res)
if __name__ == '__main__':
a=time.time()
# for each in range(0,5):
# get('http://5sing.kugou.com/echoriath/fc/1.html')
# time.sleep(1)
loop=asyncio.get_event_loop()
tasks=[]
for eachChar in ['1','3','5','7','9']:
tasks.append(asyncio.ensure_future(ganhuo(eachChar)))
loop.run_until_complete(asyncio.wait(tasks))
print(time.time()-a)
试用试下,发现其实呢,get这个函数其实是无法异步的哦, 会一直到下载完了才会下一个的,还有,subprocess也不是异步(但是它可以,看下面)。
所以如果不是用aiohttp的话就不行了啊。所以不可以用requests咯。
下载aiohttp吧,然后自己看教程就好了。
import asyncio
import time
import string
from requests import get
import subprocess
import aiohttp
async def xiazai(url):
async with aiohttp.ClientSession() as session:
async with session.get('http://5sing.kugou.com/echoriath/fc/1.html') as r:
aaa=await r.read()
print(aaa)
session.close()
async def shuchu(a):
await download('http://5sing.kugou.com/echoriath/fc/1.html')
return
async def ganhuo(zifu):
res= await shuchu(zifu)
print(res)
if __name__ == '__main__':
a=time.time()
# for each in range(0,5):
# get('http://5sing.kugou.com/echoriath/fc/1.html')
# time.sleep(1)
loop=asyncio.get_event_loop()
tasks=[]
for eachChar in ['1','3','5','7','9']:
tasks.append(asyncio.ensure_future(xiazai(eachChar)))
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
print(time.time()-a)
只用了1秒多吧。中间就算用了 asyncio.sleep(1)模拟也就是2秒多的样子吧(我猜的)。
然后,关键是实现一个东西。subprocess.Popen这个玩意要实现异步才可以,因为这个是io以外时间用的最多的了(不过貌似,node.js比python性能好很多诶,如果用python实现里面的js代码肯定更慢了。)
稍加改造,就得到了一个,异步IO的爬虫了。
接下来看看如何做到subprocess.Popen的异步吧。
第五部分了
asyncio.create_subprocess_exec
这个可以实现子进程的异步操作。
如果在windows 下,你得这样做才行了。因为,windows的select只可以用在socket上,而对于eventloop的默认实现方式是基于select的,在windos上得要用基于proactor的才行咯。可以去看下关于JS的事件循环和reactor和proactor以及一些关于系统的内容,网络上都有,就不多说了。
import sys,asyncio
from subprocess import PIPE
import subprocess
import sys
async def acfun():
sp=await asyncio.create_subprocess_shell('node acfun.js',cwd='D:\\PythonProject\\debug',stdin=subprocess.PIPE, stdout=subprocess.PIPE)
sp.stdin.write('cca'.encode())
sp.stdin.close()
await sp.wait()
acc=sp.stdout
aa=await acc.read(3)
print(aa)
if __name__=='__main__':
if sys.platform =='win32':
loop = asyncio.ProactorEventLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(acfun())
loop.close()
这样子我们就实现了异步的子进程,以及异步的网络io,最后是下载歌曲了,也要异步或者说吧所有的songUrl收集起来在一起下载。
第六部分了,这是。会把前面的东西全部组合起来用。是不是对python的异步有点感觉了呢。其实不妨试试看用JS的爬虫啊,解析JS,肯定方便啊,而且人家速度也快,回调写起来比较方便了,以及还有一个promise。
import asyncio
import aiohttp
import subprocess
import json
from requests import get
import re
from lxml import etree
BASEURLOFPAGE='http://5sing.kugou.com'
async def downloader(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as result:
html = await result.read()
session.close()
return html
async def analysis(text):
html = etree.HTML(text)
songLinks = []
xpathExp='//div[@class="song_list"]/ul/li/strong[@class="lt list_name"]/a'
for each in html.xpath(xpathExp):
name = each.xpath('./text()')[0]
print(name)
url = each.xpath('./@href')[0]
spReturn = await jsCodeExec(url)
songLinks.append(json.loads(spReturn.decode()))
return songLinks
def getNextPage(html):
tree = etree.HTML(html)
xpathExp='//span[@class="page_list"]/a[text()="下一页"]/@href'
suffix = tree.xpath(xpathExp)
if(len(suffix)==0):
return False
href = ''.join([BASEURLOFPAGE,suffix[0]])
return href
async def jsCodeExec(url):
globalsReExp = re.compile(b'("ticket"\s*:\s*"(?:\w|=)+")')
content = await downloader(url)
aj = json.loads('{%s}' % ((globalsReExp.search(content).group())).decode())
sp= await asyncio.create_subprocess_shell('node fortest.js',cwd='[你的路径]',
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
sp.stdin.write(aj['ticket'].encode())
sp.stdin.close()
await sp.wait()
spOut = sp.stdout
songInfo = await spOut.read()
return songInfo
async def instuctor(entry):
allUrls=[]
html = get(entry).text
# html = html.decode()
while True:
nextPageUrl = getNextPage(html)
print(nextPageUrl)
if(nextPageUrl):
urlsHub = await analysis(html)
allUrls.extend(urlsHub)
html = get(nextPageUrl).text
else:
urlsHub=await analysis(html)
allUrls.extend(urlsHub)
break
print('结束了')
print(allUrls)
return allUrls
if __name__ == '__main__':
ENTRY='http://5sing.kugou.com/echoriath/fc/1.html'
loop = asyncio.ProactorEventLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(instuctor(ENTRY))
loop.close()
要用倒是可以的但是,事实上没有任何的用处,并不会变快的(我猜大概不会),而且没有应对反爬的任何手段例如,ip池,控制速度(我就不用了,反正网速太慢了。),和UA以及请求头之类的。而且这个东西除了输出名字以外没有任何的log产生,除了问题也不好找。
可以看出来一点,我一直在用循环一步一步往下走,虽然,用了异步但是,其实我的loop里面只有一个任务而已。我再怎么await都不会产生任何效果来着。你可以看到,我在使用 aiohttp的时候也用了requests.get,这是因为我决定,一个一个网页地走(没原因,网速慢而已)
如果要真的产生效果,要这样子做。
async def analysis(text):
html = etree.HTML(text)
songLinks = []
xpathExp='//div[@class="song_list"]/ul/li/strong[@class="lt list_name"]/a'
jsTasks=[]
for each in html.xpath(xpathExp):
name = each.xpath('./text()')[0]
url = each.xpath('./@href')[0]
jsTasks.append(jsCodeExec(url))
# spReturn = await jsCodeExec(url)
# songLinks.append(json.loads(spReturn.decode()))
dones, pendings = await asyncio.wait(jsTasks)
for each in dones:
songLinks.append(each.result())
return songLinks
一共是12秒(2M网络下)。
当然还可以更快,但是我的网络就这样了,刚才听歌都卡住了。
基于同步的方式(单线程/进程),大概是45秒(和网络也有关系吧),而且一旦在下载的时候卡住就麻烦了,花费的时间就更多了。
第七部分咯。关于内存的事情
也许已经发现了吧,我在instructor这个函数里面用的是循环来着的,而不是去递归,虽然我本身比较喜欢递归,但是,如果说爬取的网站深度很大,而且每一页都是好几百个链接怎么办啊。基于Python 解释器的特点,会把上一层的东西保存下来的,所以会产生无法回收的垃圾,然后产生内存溢出。
刚才用了11s就爬完全部了,CPU一直是100%(平时都是10%左右的)。好快啊。我以前低估我的电脑了呵呵,虽然很卡,鼠标有点卡住了。当然,不是上面那段代码,是修改过了的。