python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

版权声明:禁止转载至其它平台,转载至博客需带上此文链接。 https://blog.csdn.net/qq_41841569/article/details/85063493

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

今天我给大家介绍一下用Python爬取网易云音乐全部歌手的热门歌曲.由于歌手个人主页的网页源代码中还嵌入了一个子网页(框架源代码里面包含了我们需要的信息),因此我们不能使用requests库来爬取,而使用selenium,接下来,让我详细讲解整个爬取过程.

学习Python中有不明白推荐加入交流群
                号:960410445
                群里有志同道合的小伙伴,互帮互助,
                群里有不错的视频学习教程和PDF!

一,构造歌手个人主页的URL

前段时间我们获取了网易云音乐全部歌手的id号,今天我们就利用全部歌手的id号来构造歌手个人主页的URL,从而实现用爬取全部歌手的热门歌曲及其id号的目的.以歌手 薛之谦的个人主页 为例,来看一下他的主页的URL为:

https://music.163.com/#/artist?id=5781

因此只需要根据歌手对应的id就可以构造出歌手的个人主页,在歌手的个人主页我们能看到热门作品这一栏.网易云音乐全部歌手id号点击获取(csv文件)

二,分析网页源代码

现在我们就要用Python爬虫去爬取这些内容.如果你用requests库去爬取的话,返回的网页源代码中根本就没有这些信息.这时我们打开薛之谦的个人主页鼠标右键分别查看网页的源代码和查看框架的源代码.你会发现网页源代码和用requests库请求返回的源代码一摸一样(里面没有我们要爬取的信息),而在框架源代码中有我们要爬取的热门作品的信息,因此我们只需要将框架源代码爬取下来,然后再解析即可得到我们需要的歌手的热门作品的信息.

三,网页源代码和框架源代码的区别

网页源代码是指父级网页的源代码.另外网页中还有一种节点叫iframe,也就是子Frame,相当于网页的子页面,它的结构和外部网页的结构完全一致,框架源代码就是这个子网页的源代码.

四,获取框架源代码

这里我们使用selenium库来爬取,在selenium打开页面后,默认是在父级frame里面进行操作,而此时页面中还有子frame,它是不能获取到子frame里面的节点的,因此这时我们需要使用swith_to.frame()方法来切换到子frame中去,这时请求得到的代码就从网页源代码切换到了框架源代码,于是我们便能够提取我们需要的热门作品的信息了.通过歌手的个人主页的URL来爬取其框架源代码,具体爬取框架源代码的函数:

def get_html_src(url):
 # 可以任意选择浏览器,前提是要配置好相关环境,更多请参考selenium官方文档
 driver = webdriver.Chrome()
 driver.get(url)
 # 切换成frame
 driver.switch_to_frame("g_iframe")
 # 休眠3秒,等待加载完成!
 time.sleep(3)
 page_src = driver.page_source
 driver.close()
 return page_src

返回结果为歌手个人主页的框架源代码,里面包含了我们需要的信息.

五,解析源代码

我们使用bs4库进行解析,需要的信息包含在HTML5的下面代码片段中:

<span class="txt"><a href="/song?id=(d*)"><b title="(.*?)">

因此可定义下面函数对其进行解析:

def parse_html_page(html):
 
 # pattern = '<span class="txt"><a href="/song?id=(d*)"><b title="(.*?)">'
 # 这里是使用lxml解析器进行解析,lxml速度快,文档容错能力强,也能使用html5lib
 soup = BeautifulSoup(html, 'lxml')
 items = soup.find_all('span', 'txt')
 return items

六,写入csv文件

def write_to_csv(items, artist_name):
 
 with open("music163_songs.csv", "a") as csvfile:
 writer = csv.writer(csvfile)
 writer.writerow(["歌手名字", artist_name])
 
 for item in items:
 writer.writerow([item.a['href'].replace('/song?id=', ''), item.b['title']])
 
 print('歌曲id:', item.a['href'].replace('/song?id=', ''))
 song_name = item.b['title']
 print('歌曲名字:', song_name)
 
 csvfile.close()

七,读取csv文件,构造全部歌手的个人主页

# 获取歌手id和歌手姓名
def read_csv():
 
 with open("music163_artists.csv", "r", encoding="utf-8") as csvfile:
 
 reader = csv.reader(csvfile)
 for row in reader:
 artist_id, artist_name = row
 if str(artist_id) is "artist_id":
 continue
 else:
 yield artist_id, artist_name

# 当程序的控制流程离开with语句块后, 文件将自动关闭

八,程序主函数

# 主函数
def main():
 for readcsv in read_csv():
 artist_id, artist_name = readcsv
 url = "https://music.163.com/#/artist?id=" + str(artist_id)
 print("正在获取{}的热门歌曲...".format(artist_name))
 html = get_html_src(url)
 items = parse_html_page(html)
 print("{}的热门歌曲获取完成!".format(artist_name))
 print("开始将{}的热门歌曲写入文件".format(artist_name))
 write_to_csv(items, artist_name)
 print("{}的热门歌曲写入到本地成功!".format(artist_name))

用过网易云音乐听歌的朋友都知道,网易云音乐每首歌曲后面都有很多评论,热门歌曲的评论更是接近百万或者是超过百万条.现在我就来分享一下如何爬取网易云音乐歌曲的全部评论,由于网易云音乐的评论都做了混淆加密处理,因此我们需要深入了解它的加密过程之后才能爬取到网易云音乐歌曲的全部评论.

一,首先分析数据的请求方式

网易云音乐歌曲页面的URL形式为

https://music.163.com/#/song?id=歌曲id号

这里我用Delacey的Dream it possible 为例进行讲解,它的URL为

https://music.163.com/#/song?id=38592976.

接下来开始分析数据的请求方式.

由于网易云音乐的评论是通过Ajax传输,我们打开浏览器的开发者工具(检查元素),选中控制面板中的Network,再点击XHR(捕获ajax数据),然后点击左上角的重新加载,会看到下面图片中的数据请求列表

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

点击R_SO_4_38592976?csrf_token=cdee144903c5a32e6752f50180329fc9这一行,再点击Preview

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

发现我们所需要的数据就在这json格式的数据中,其中comments中是第一页的全部评论,一共20条,hotcomments是精彩评论一共有15条,每首歌曲只有第一页评论才有精彩评论.接着看一下它的请求头,点击Headers

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

我们发现的它是个post请求,向下滑你会发现这个post请求还带有数据

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

这些数据都是经过加密处理的,因此我们需要分析它的加密过程来生成相应的参数,然后把加密后的参数加到post请求中才能获取到我们需要的评论数据.

二,分析加密过程

通过断点调试发现params和encSecKey是由js脚本中的window.asrsea()函数生成的.

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

我们发现window.asrsea()函数有4个参数,在浏览器的js控制台分别对这四个参数进行调试:

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

后面三个参数是定值,只有第一个参数是控制评论页面偏移量的参数,它是一个变量.笔者经过分析发现第一个参数的形式是:

1

{"rid":"R_SO_4_38592976","offset":"0","total":"True","limit":"20","csrf_token":""}

下面我来详细讲解这个变量的发现过程:

首先找到core_dfe56728795d119e4d476fd09ea2dc51.js这个js脚本,然后将断点打在第12973行,点击第一页评论,页面加载到断点处便停止了

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

然后按下电脑的Esc键打开js控制台,输入i1x,查看第一个变量:

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

这是第一页的i1x的值,接下来看第二页的(需要点击第2页,然后输入i1x的值):

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

再看第3页:

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

再看第4页:

python爬虫综合篇,采集网易云音乐全部歌手的热门歌曲以及评论!

通过这几页的分析,我们可以得到i1x值的变化规律,且可以得到它的一般形式:

{"rid":"R_SO_4_38592976","offset":"0","total":"True","limit":"20","csrf_token":""}

offset和limit是必选参数,其他参数是可选的,其他参数不影响data数据的生成,offset (页面偏移量) = (页数-1) * 20, 注意limit最大值为100,当设为100时,获取第二页时,默认前一页是20个评论,也就是说第二页最新评论有80个,有20个是第一页显示的.因此我们可以构造第一个参数为:

# 偏移量,page是页数
offset = (page-1) * 20
msg = '{"offset":' + str(offset) + ',"total":"True","limit":"20","csrf_token":""}'

接下来,我们来看一下window.asrsea()函数的整个加密过程:

!function() {
 // 函数a生成长度为16的随机字符串
 function a(a) {
 var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
 for (d = 0; a > d; d += 1)
 e = Math.random() * b.length,
 e = Math.floor(e),
 c += b.charAt(e);
 return c
 }
 // 函数b实现AES加密
 function b(a, b) {
 var c = CryptoJS.enc.Utf8.parse(b)
 , d = CryptoJS.enc.Utf8.parse("0102030405060708")
 , e = CryptoJS.enc.Utf8.parse(a)
 , f = CryptoJS.AES.encrypt(e, c, {
 iv: d,
 mode: CryptoJS.mode.CBC
 });
 return f.toString()
 }
 // 函数c实现RSA加密
 function c(a, b, c) {
 var d, e;
 return setMaxDigits(131),
 d = new RSAKeyPair(b,"",c),
 e = encryptedString(d, a)
 }
 function d(d, e, f, g) {
 var h = {}
 , i = a(16);
 return h.encText = b(d, g),
 h.encText = b(h.encText, i),
 h.encSecKey = c(i, e, f),
 h
 }
 function e(a, b, d, e) {
 var f = {};
 return f.encText = c(a + e, b, d),
 f
 }
 window.asrsea = d,
 window.ecnonasr = e
}();

window.asrsea()函数就是上面的d函数,现在我们来看函数d:

function d(d, e, f, g) {
 var h = {}
 , i = a(16);
 return h.encText = b(d, g), // 第一次AES加密
 h.encText = b(h.encText, i), // 第二次AES加密
 h.encSecKey = c(i, e, f), // RSA加密
 h
 }

参数h.encText是经过两次AES加密得到的,h.encSecKey是经过一次RSA加密得到的,其中i是随机生成的长度为16的随机字符串.

三,生成加密参数

首先我们需要生成长度为16的随机字符串,这里我们仿照上面的javascript的实现,用Python生成16位长的随机字符串:

# 生成随机字符串
def generate_random_strs(length):
 string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 # 控制次数参数i
 i = 0
 # 初始化随机字符串
 random_strs = ""
 while i < length:
 e = random.random() * len(string)
 # 向下取整
 e = math.floor(e)
 random_strs = random_strs + list(string)[e]
 i = i + 1
 return random_strs

接着用Python实现AES加密,这里要用到pycrypto库,先安装好这个库:

pip install pycrypto

然后导入加密模块:

from Crypto.Cipher import AES

由于AES加密的明文长度必须是16的倍数,因此我们需要对明文进行必要的填充,以满足它的长度是16的倍数:

# msg是需要加密的明文,如果不是16的倍数则进行填充(paddiing)
padding = 16 - len(msg) % 16
# 这里使用padding对应的单字符进行填充
msg = msg + padding * chr(padding)

AES加密的模式是AES.MODE_CBC,初始化向量iv=’0102030405060708′,具体的AES加密:

# AES加密
def AESencrypt(msg, key):
 # 如果不是16的倍数则进行填充(paddiing)
 padding = 16 - len(msg) % 16
 # 这里使用padding对应的单字符进行填充
 msg = msg + padding * chr(padding)
 # 用来加密或者解密的初始向量(必须是16位)
 iv = '0102030405060708'
 
 cipher = AES.new(key, AES.MODE_CBC, iv)
 # 加密后得到的是bytes类型的数据
 encryptedbytes = cipher.encrypt(msg)
 # 使用Base64进行编码,返回byte字符串
 encodestrs = base64.b64encode(encryptedbytes)
 # 对byte字符串按utf-8进行解码
 enctext = encodestrs.decode('utf-8')
 
 return enctext

然后是RSA加密.首先我简单介绍一下RSA的加密过程.在RSA中,明文,密钥和密文都是数字.RSA的加密过程可以用下列的公式来表达,这个公式非常的重要,你只有理解了这个公式,才能用Python实现RSA加密.

密文 = 明文E mod N (RSA加密)

RSA的密文是对代表明文的数字的E次方求mod N 的结果, 通俗的讲就是将明文和自己做E次乘法,然后将其结果除以N 求余数,这个余数就是密文.

下面来看具体的RSA加密代码实现:

# RSA加密
def RSAencrypt(randomstrs, key, f):
 # 随机字符串逆序排列
 string = randomstrs[::-1]
 # 将随机字符串转换成byte类型数据
 text = bytes(string, 'utf-8')
 seckey = int(codecs.encode(text, encoding='hex'), 16)**int(key, 16) % int(f, 16)
 return format(seckey, 'x').zfill(256)

RSA加密后得到的字符串长为256,这里不够长我们用x字符填充.

最后就是获取那两个加密参数:

# 获取参数
def get_params(page):
 # msg也可以写成msg = {"offset":"页面偏移量=(页数-1) * 20", "limit":"20"},offset和limit这两个参数必须有(js)
 # limit最大值为100,当设为100时,获取第二页时,默认前一页是20个评论,也就是说第二页最新评论有80个,有20个是第一页显示的
 # 偏移量
 offset = (page-1) * 20
 # offset和limit是必选参数,其他参数是可选的,其他参数不影响data数据的生成,最好还是保留
 msg = '{"offset":' + str(offset) + ',"total":"True","limit":"20","csrf_token":""}'
 key = '0CoJUm6Qyw8W8jud'
 f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
 e = '010001'
 enctext = AESencrypt(msg, key)
 # 生成长度为16的随机字符串
 i = generate_random_strs(16)
 
 # 两次AES加密之后得到params的值
 encText = AESencrypt(enctext, i)
 # RSA加密之后得到encSecKey的值
 encSecKey = RSAencrypt(i, e, f)
 return encText, encSecKey

四,获取全部评论

上面我们获取到了两个参数encText和encSecKey,利用这两个参数来构造post表单数据(Form Data),即data的值:

params, encSecKey = get_params(page)
data = {'params': params, 'encSecKey': encSecKey}

歌曲评论的URL为:

url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_' + str(songid) + '?csrf_token='

然后把data加到post的参数中去就能获取到json格式的评论数据.

html = requests.post(url, headers=headers, data=data)

猜你喜欢

转载自blog.csdn.net/qq_41841569/article/details/85063493