学习爬虫Part8-网易云音乐评论爬取

网易云音乐评论爬取

收集歌单id

每个歌单都有唯一的id,通过 https://music.163.com/# 这个链接就可以找到歌单,所以第一步我们要收集发现音乐下的多个歌单id

1

首先进入官网的“发现音乐”的“歌单”一栏,这里可以看到很多高分歌单,先到处点一下,可以发现链接是在改变的,说明部分数据不是动态加载的,可通过网页源码获得。最后发现链接有cat,order,offset,和limit四个对我们有用的参数,cat是分类,order是排序,offset=(页数-1)*35,limit=35。还有注意使用前要把链接的井号和一个斜杠去掉,否者会导致网页源码缺失。

2

先随便找一条链接requests一下先,可以发现目标信息是完整的,和F12看到的源码一样,那歌单id就可以放心提取了

1
2
3
4
5
6
7
8
def get_playlists(pages,order,cat):#页数(一页获取35个歌单id),排序,分类
playlist_ids = []
for page in range(pages):
url = 'http://music.163.com/discover/playlist/?order={}&cat={}&limit=35&offset={}'.format(order,cat,str(page*35))
print(url)
r = requests.get(url,headers=headers)
playlist_ids.extend(re.findall(r'playlist\?id=(\d+?)" class="msk"',r.text))
return playlist_ids

3

收集歌单內歌曲id

每个歌单都有多首歌曲,所以第二步我们要获取每个歌单下的所有歌曲id顺便把歌单名也获取。
歌单链接是http://music.163.com/playlist...,先随便找一个requests一下先,目标没缺失但是requests结果是和F12源码是不同的,筛选时请照着requests结果写(requests结果只有id和歌名,暂时够用那就这样吧)(网易云现在必须登录验证了)

另一种方法是通过API获取,包含更全的信息(包括歌手,所属专辑,歌单介绍等)。

5
5

1
2
3
4
5
6
7
def get_songs(playlist_id='778462085'):
r = requests.get('http://music.163.com/playlist?id={}'.format(playlist_id),headers=headers)
song_ids = re.findall(r'song\?id=(\d+?)".+?</a>',r.text)#歌id列表
song_titles = re.findall(r'song\?id=\d+?">(.+?)</a>',r.text)#歌名列表
list_title = re.search(r'>(.+?) - 歌单 - 网易云音乐',r.text).group(1)#歌单名
list_url = 'http://music.163.com/playlist?id='+playlist_id #歌单链接
return [song_ids, song_titles, list_title, list_url]#一次性返回这些信息给评论爬取器

请求动态数据(评论)

进入某首歌 https://music.163.com/#/song?id=721243 ,很自然就想到requests一下,然而这不会得到任何评论信息,因为评论区是动态加载的(翻页链接不变,动态标志),所以打开F12捉包吧,在xhr中查看response很快找到。

6

7

请求链接中“R_SO_4_”后接的是歌曲的id,同一首歌下不同页数的动态包的请求链接除csrf_token外是相同的。

请求类型为post,需要两个参数,无论是刷新还是评论翻页这两个参数都会变,应该是加密过的。

先不理加密先,尝试把第一页的两个参数传给请求链接是能获得数据的,对应第一页的评论,尝试把csrf_token参数去除,还是能获取数据,所以csrf_token参数可以不要。我们大胆一点,继续把这对参数传给不同歌曲的请求链接,发现都能获取对应的第一页评论;而把第二页的两个参数传给不同歌曲的请求链接,就会得到对应第二页评论,以此类推。所以得出结论,任一页数的两个参数对不同歌曲是通用的,第n页的参数post过去会得到第n页的评论。这样就成功绕过了加密问题。

如果你要大量爬取评论/各种信息时,加密算法就显得很重要。

1
2
3
4
5
6
7
8
def get_comments(arg):  # 接收get_songs方法返回的数据,爬取页数等
post_urls = [......] # 通过get_songs方法返回的数据构造每首歌的请求链接列表
data = [{}] # 手动写入或加密算法生成
for i in range(len(post_url)): # 爬每首歌评论
#for j in range(pages): # 如果每首歌要爬多页,那要再设一个循环
r = requests.post(post_urls[i],data=data,headers=headers)
print(r.json()) # 剩下解析json数据并写入容器。其中json数据可能会有坑,详看github中的代码。
''''''
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
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import json
import re
from Crypto.Cipher import AES#新的加密模块只接受bytes数据,否者报错,密匙明文什么的要先转码
import base64
import binascii
import random
import requests
from math import ceil

secret_key = b'0CoJUm6Qyw8W8jud'#第四参数,aes密匙
pub_key ="010001"#第二参数,rsa公匙组成
modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"#第三参数,rsa公匙组成
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36'}

#生成随机长度为16的字符串的二进制编码
def random_16():
return bytes(''.join(random.sample('1234567890DeepDarkFantasy',16)),'utf-8')

#aes加密
def aes_encrypt(text,key):
pad = 16 - len(text)%16#对长度不是16倍数的字符串进行补全,然后在转为bytes数据
try: #如果接到bytes数据(如第一次aes加密得到的密文)要解码再进行补全
text = text.decode()
except:
pass
text = text + pad * chr(pad)
try:
text = text.encode()
except:
pass
encryptor = AES.new(key,AES.MODE_CBC,b'0102030405060708')
ciphertext = encryptor.encrypt(text)
ciphertext = base64.b64encode(ciphertext)#得到的密文还要进行base64编码
return ciphertext

#rsa加密
def rsa_encrypt(ran_16,pub_key,modulus):
text = ran_16[::-1]#明文处理,反序并hex编码
rsa = int(binascii.hexlify(text), 16) ** int(pub_key, 16) % int(modulus, 16)
return format(rsa, 'x').zfill(256)

#返回加密后内容
def encrypt_data(data):
ran_16 = random_16()
text = json.dumps(data)
params = aes_encrypt(text,secret_key)
params = aes_encrypt(params,ran_16)
encSecKey = rsa_encrypt(ran_16,pub_key,modulus)
return {'params':params.decode(),
'encSecKey':encSecKey }


#以上是加密算法

#获取歌单id
def get_playlists(pages,order,cat):
playlist_ids = []
for page in range(pages):
url = 'http://music.163.com/discover/playlist/?order={}&cat={}&limit=35&offset={}'.format(order,cat,str(page*35))
r = requests.get(url,headers=headers)
playlist_ids.extend(re.findall(r'playlist\?id=(\d+?)" class="msk"',r.text))
return playlist_ids

#获取歌曲id
def get_songs(playlist_id='778462085'):
r = requests.get('http://music.163.com/playlist?id={}'.format(playlist_id),headers=headers)
song_ids = re.findall(r'song\?id=(\d+?)".+?</a>',r.text)
song_titles = re.findall(r'song\?id=\d+?">(.+?)</a>',r.text)
list_title = re.search(r'>(.+?) - 歌单 - 网易云音乐',r.text).group(1)
list_url = 'http://music.163.com/playlist?id='+playlist_id
return [song_ids, song_titles, list_title, list_url]




#以非常简陋的txt保存评论
def save_comments(some,pages,f):
f.write('\n\n\n歌单《{}》\t\t链接{}'.format(some[2], some[3]).center(200) + '\n\n\n')
post_urls = ['http://music.163.com/weapi/v1/resource/comments/R_SO_4_' + deep + '?csrf_token=' for deep in some[0]]
song_urls = ['http://music.163.com/song?id=' + dark for dark in some[0]]

for i in range(len(post_urls)):
f.write('歌曲「{}」\t\t链接{}\n\n'.format(some[1][i],song_urls[i]))
for j in range(pages):
if j == 0: #第一页会包括精彩评论和最新评论
data = {'rid':"", 'offset':'0', 'total':"true", 'limit':"20", 'csrf_token':""}
enc_data = encrypt_data(data)
r = requests.post(post_urls[i], headers=headers, data=enc_data)
content = r.json()
if content['hotComments']:#判断第一页有没有精彩评论
f.write('\n\n********' + '精彩评论\n\n')
comment(content, 'hotComments', f)

f.write('\n\n********'+'最新评论\n\n')
comment(content,'comments',f)

else: #非第一页只有普通评论
data = {'rid':"", 'offset':str(j*20), 'total':"false", 'limit':"20", 'csrf_token':""}
enc_data = encrypt_data(data)
r = requests.post(post_urls[i],headers=headers,data=enc_data)
content = r.json()
comment(content,'comments',f)


#提取json数据中的评论,因为评论分两种,所以设一个参数接收种类
def comment(content,c_type,f):
for each in content[c_type]:
if each['beReplied']:#判断有没有回复内容
if each['beReplied'][0]['content']:#有时回复内容会被删掉,也判断一下
f.write('' + each['content'] + '\n')
f.write('\t回复:\n' + each['beReplied'][0]['content'] + '\n' + '-' * 50 + '\n')
else:
f.write('' + each['content'] + '\n' + '-' * 60 + '\n')




#single_crawl接收两个个参数,是歌单id字符串默认'778462085',爬取页数默认为1
def single_crawl(playlist_id='778462085',pages=1):
with open('playlist_id{}.txt'.format(playlist_id), 'w', encoding='utf-8') as s:
save_comments(get_songs(playlist_id),pages,s)


#multiple_crawl接收四个参数,nums是必需整形参数,是爬多前少个歌单;order,cat是可选参数,分别是排序和分类,排序有new和hot,分类动手找。默认排序hot,默认分类ACG
def multiple_crawl(nums,order='hot',cat='ACG',pages=1):
with open('{}_{}_{}lists.txt'.format(order, cat, nums), 'w', encoding='utf-8') as m:
playlist_ids = get_playlists(ceil(nums/35),order,cat)
print(playlist_ids)
for i in range(nums):
save_comments(get_songs(playlist_ids[i]),pages,m)
#长时间爬取会被服务器踢出,适当时候可以休眠一段时间,或ip代理

multiple_crawl(2)
Donate? comment?