0HB 发表于 2021-8-25 01:18:21

【升级的爬虫】全自动爬取网易云 任意歌曲 热门 / 最新 评论

本帖最后由 0HB 于 2022-3-23 09:47 编辑

最近学习了小甲鱼老师的[爬虫] 爬取网易云音乐的精彩评论,发现居然只能实现对 “已知id” 的某一首歌曲进行爬取“热门评论”。{:9_241:}
不甘之情顿时涌上心头,遍历全网,经过整合,算是完成了“自动爬取网易云音乐任意歌曲 热门 / 最新 评论”的任务。{:9_231:}
吼吼!此时,我才记起可以翻翻看鱼C论坛里是否已有完美的解决方案,发现是没有的。正好,那么便分享出来,同鱼友们共适之!
注:1.本次分享不涉及教学(因为码力尚浅的我确也不明晰其中解密的过程,只是做了下代码的搬运工/整合者,具体原理可见下文中标出的原文链接)
      2.目的是最大程度完成课程›《极客Python之效率革命》›爬取网易云音乐的精彩评论的 任务,为方便学习而整合了代码(将一一标明作者与出处),侵权必删。
      3.欢迎鱼友转载/学习,还烦请记得标出原文链接中的作者(麻烦了{:9_221:} )

那么,事不宜迟,我们开始第一项
功能简介:
通过输入的歌名与歌手名,即可自动获得此歌曲的id,其后通过id 来得到 url并进行爬取,期间可以选择爬取“热门评价”或“最新评价”。最后的最后,将获得的数据保存在当前目录的新建文件夹“comments”里。


好,那么先来看看效果图:
下载想要歌曲的 热门评论


txt文本展示


想要歌曲的 最新评论


xt文本展示



使用方法:
运行WyyComment.py(其中引用了‘WyyGetID’ (获取网易云歌曲ID) 与 ‘WyyGetC.py’)(获取歌曲下评论)


接下来,是代码:

# 命名为:WyyComment.py

import WyyGetID# 这个模块 我们用来获得所需要下载 网易云歌曲 的 id
import WyyGetC# 这个模块 我们借已获得的 id 来获得所需要下载 网易云歌曲 的 评论
import re
import os, sys



def save(song_id, song_name, people_name):# 按格式保存爬取的最新评论
    data = []
    file = open('./comments/{0}.txt'.format(song_name + ' - ' + people_name + '_最新评论'), 'w+')
    with open("./comments/" + str(song_id) + ".txt") as f:
      for each_line in f:
            data.append(re.findall(r" '(.+?)',", each_line))
      file.write('{0} 的歌曲 {1} 最新评论如下\n'.format(people_name, song_name))
      for each_one in data:
            name = '用户名:' + each_one + '\n'
            try:
                content = '评价:' + ''.join(each_one.split("content': '")) + '\n' * 2
            except:
                pass
            file.write(name + content)
    os.remove("./comments/" + str(song_id) + ".txt")


def get(choice):
    print('自动爬取网易云音乐歌曲 {0} 评论'.format(choice))
    print('—————————程序开始—————————')

    s_name = input('请输入歌曲名:')
    p_name = input('请输入歌手名:')
    try:
      id1, p_name1 = WyyGetID.GetID(s_name, p_name)# 有一说一,p_name1更全面。
    except:
      print('未找到该歌手的歌曲,请重新尝试。')
      sys.exit()
    if choice == '最新':
      WyyGetC.GetNC(str(id1))
      save(id1, s_name, p_name1)
      print('已成功获取最新评论')

    elif choice == '热门':
      WyyGetC.GetHC(str(id1), s_name, p_name1)
      print('已成功获取热门评论')

    print('—————————程序结束—————————')



if __name__ == '__main__':
    while 1:
      choice = input('请选择获取你想要获取评论的类型("最新" 或 "热门"):')
      if choice in ['最新', '热门']:
            break
    get(choice)



# 应命名为: WyyGetID.py
获取id的功能 及 原理讲解 来自:【实战:爬取网易云音乐歌曲对应id并剔除无版权歌曲】 作者:如梦如幻uuu
(注:特别指出,其中依赖库:pycryptodome,原文中打错了,已纠正)

import requests
import random
import base64
from Crypto.Cipher import AES
import json
import binascii

# 这个模块 我们用来获得所需要下载 网易云歌曲 的 id
class Music_api():
    # 设置从JS文件提取的RSA的模数、协商的AES对称密钥、RSA的公钥等重要信息
    def __init__(self):
      self.modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
      self.nonce = '0CoJUm6Qyw8W8jud'
      self.pubKey = '010001'
      self.url = "https://music.163.com/weapi/cloudsearch/get/web?csrf_token="
      self.HEADER = {}
      self.setHeader()
      self.secKey = self.getRandom()

    # 生成16字节即256位的随机数
    def getRandom(self):
      string = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
      res = ""
      for i in range(16):
            res += string
      return res

    # AES加密,用seckey对text加密
    def aesEncrypt(self, text, secKey):
      pad = 16 - len(text) % 16
      text = text + pad * chr(pad)
      encryptor = AES.new(secKey.encode('utf-8'), 2, '0102030405060708'.encode('utf-8'))
      ciphertext = encryptor.encrypt(text.encode('utf-8'))
      ciphertext = base64.b64encode(ciphertext).decode("utf-8")
      return ciphertext

    # 快速模幂运算,求 x^y mod mo
    def quickpow(self, x, y, mo):
      res = 1
      while y:
            if y & 1:
                res = res * x % mo
            y = y // 2
            x = x * x % mo
      return res

      # rsa加密

    def rsaEncrypt(self, text, pubKey, modulus):
      text = text[::-1]
      a = int(binascii.hexlify(str.encode(text)), 16)
      b = int(pubKey, 16)
      c = int(modulus, 16)
      rs = self.quickpow(a, b, c)
      return format(rs, 'x').zfill(256)

    # 设置请求头
    def setHeader(self):
      self.HEADER = {
            'Accept': '*/*',
            'Accept-Encoding': 'gzip,deflate,sdch',
            'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
            'Connection': 'keep-alive',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Host': 'music.163.com',
            'Referer': 'https://music.163.com/search/',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36'
      }

    # 设置相应的请求参数,从而搜索列表
    # 总体的密码加密步骤为:
    # 首先用nonce对text加密生成密文1
    # 然后用随机数seckey加密密文1生成密文2
    # 随后,用公钥加密seckey生成密文3
    # 其中,密文2作为请求参数中的params,密文3作为encSeckey字段
    # 这样,接收方可以通过私钥解密密文3获得seckey(随机数)
    # 然后用seckey解密密文2获得密文1
    # 最终用统一协商的密钥nonce解密密文1最终获得text
    def search(self, s, offset, type="1"):
      text = {"hlpretag": "<span class=\"s-fc7\">",
                "hlposttag": "</span>",
                "#/discover": "",
                "s": s,
                "type": type,
                "offset": offset,
                "total": "true",
                "limit": "30",
                "csrf_token": ""}
      text = json.dumps(text)
      params = self.aesEncrypt(self.aesEncrypt(text, self.nonce), self.secKey)
      encSecKey = self.rsaEncrypt(self.secKey, self.pubKey, self.modulus)
      data = {
            'params': params,
            'encSecKey': encSecKey
      }
      result = requests.post(url=self.url,
                               data=data,
                               headers=self.HEADER).json()
      return result

    # 获取指定音乐列表(相当于主函数)
    def get_music_list(self, keywords):
      music_list = []
      for offset in range(1):
            result = Music_api().search(keywords, str(offset))
            result = result['result']['songs']
            for music in result:
                # if music['copyright'] == 1 and music['fee'] == 8:
                if (music['privilege']['fee'] == 0 or music['privilege']['payed']) and music['privilege']['pl'] > 0 and \
                        music['privilege']['dl'] == 0:
                  continue
                if music['privilege']['dl'] == 0 and music['privilege']['pl'] == 0:
                  continue
                  # if music['fee'] == 8:
                music_list.append(music)
      return music_list


def GetID(song_name, people_name):
    contents = Music_api().get_music_list(song_name)
    data_dict = {}
    for i in contents:# 测试
      data_dict['name']] = i['id']
      if people_name in data_dict.keys():
            return i['id'], i['ar']['name']


# 应命名为: WyyGetC.py

获得 最新评论的功能及 讲解 来自:   【python爬取网易云音乐歌曲评论信息】       作者:NewJune
# 参考地址:https://www.zhihu.com/question/36081767
# 完美兼容win10、python3.6(亲测Mac、python3.8,神州行!),由于python3.6下pycrypto库已经停止维护,可以安装pyCryptodome库代替,pyCyrpto库的后续分支,有一个叫pyCryptodome的库,是前代的延伸版。
import sys
import codecs
import requests, json, os
import base64
from Crypto.Cipher import AES

# 这个模块 我们借已获得的 id 来获得所需要下载 网易云歌曲 的 评论

class Spider():

    def __init__(self, idNum):
      # user-Agent字段直接从浏览器中复制过来即可,请求头中其他字段非必须项,也可以从浏览器中找到所有字段都放到Request Headers
      self.header = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0',
            'Referer': 'http://music.163.com/'}
      self.url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_' + idNum + '?csrf_token='# 每一次的base_url只有歌曲id不同,构造url即可。

    def __get_jsons(self, url, page):
      # 获取两个参数
      music = WangYiYun()
      text = music.create_random_16()
      params = music.get_params(text, page)

      encSecKey = music.get_encSEcKey(text)
      fromdata = {'params': params, 'encSecKey': encSecKey}
      jsons = requests.post(url, data=fromdata, headers=self.header)
      # print(jsons.raise_for_status())
      # 打印返回来的内容,是个json格式的
      # print(jsons.content)
      return jsons.text

    def json2list(self, jsons):
      '''把json转成字典,并把他重要的信息获取出来存入列表'''
      # 可以用json.loads()把它转成字典
      # print(json.loads(jsons.text))
      users = json.loads(jsons)
      comments = []
      for user in users['comments']:
            # print(user['user']['nickname']+' : '+user['content']+'   点赞数:'+str(user['likedCount']))
            name = user['user']['nickname']
            content = user['content']
            # 点赞数
            likedCount = user['likedCount']
            # 提取所需json中所需的字段构造字典
            user_dict = {'name': name, 'content': content, 'likedCount': likedCount}
            # 将提取的字典信息追加到列表中
            comments.append(user_dict)
      return comments

    def run(self, idNum):
      self.page = 1
      while True:
            jsons = self.__get_jsons(self.url, self.page)
            comments = self.json2list(jsons)
            non_bmp_map = dict.fromkeys(range(0x10000, sys.maxunicode + 1), 0xfffd)

            ##            print(str(comments).translate(non_bmp_map))
            print('self.page = ' + str(self.page))# 控制台打印正在爬取的页码数
            print(idNum)# 打印正在爬取的歌曲id
            # 在该脚本同级目录下生成“comments”文件夹
            dirName = u'{}'.format('comments')
            if not os.path.exists(dirName):
                os.makedirs(dirName)
            with open("./comments/" + idNum + ".txt", "a", encoding='utf-8') as f:# 结果写入txt文件
                ##                print(len(comments))
                for ii in range(len(comments)):
                  f.write(str(comments).translate(non_bmp_map))
                  f.write('\n')
                ##                  print(ii)
                f.close()
            # 当这一页的评论数少于20条时,证明已经获取完
            ##            self.write2sql(comments)
            if len(comments) < 100:# 当limits设置为100时,默认每次服务器请求结果100条comments,当小于此数,意味爬到最后一页。
                print('评论已经获取完')
                break
            self.page += 1


# 找出post的两个参数params和encSecKey
class WangYiYun():

    def __init__(self):
      # 在网易云获取的三个参数

      self.second_param = '010001'
      self.third_param = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
      self.fourth_param = '0CoJUm6Qyw8W8jud'

    def create_random_16(self):
      '''获取随机十六个字母拼接成的字符串'''
      return (''.join(map(lambda xx: (hex(ord(xx))), str(os.urandom(16)))))

    def aesEncrypt(self, text, key):

      # 偏移量
      iv = '0102030405060708'
      # 文本

      pad = 16 - len(text) % 16
      text = text + pad * chr(pad)# 补齐文本长度

      encryptor = AES.new(bytearray(key, 'utf-8'), AES.MODE_CBC, bytearray(iv, 'utf-8'))

      # encryptor = AES.new(key, 2, iv)

      ciphertext = encryptor.encrypt(bytearray(text, 'utf-8'))
      ##      print(bytearray(key,'utf-8'))
      ciphertext = base64.b64encode(ciphertext)
      return ciphertext

    def get_params(self, text, page):
      '''获取网易云第一个参数'''
      # 第一个参数
      if page == 1:
            self.first_param = '{rid: "", offset: "0", total: "true", limit: "100", csrf_token: ""}'
            # rid: "R_SO_4_557581284",经测试该值可以置空,不影响结果的执行。
      else:
            self.first_param = '{rid: "", offset:%s, total: "false", limit: "100", csrf_token: ""}' % str(
                (page - 1) * 40)# limit参数可以灵活设置,默认为20,设置为100,爬取效率可以提高

      params = self.aesEncrypt(self.first_param, self.fourth_param).decode('utf-8')
      params = self.aesEncrypt(params, text)

      return params

    def rsaEncrypt(self, pubKey, text, modulus):
      '''进行rsa加密'''
      text = text[::-1]
      rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16)
      return format(rs, 'x').zfill(256)

    def get_encSEcKey(self, text):
      '''获取第二个参数'''
      pubKey = self.second_param
      moudulus = self.third_param
      encSecKey = self.rsaEncrypt(pubKey, text, moudulus)
      return encSecKey


def GetNC(id):# Get New Comments & 获取最新评论
    idPs = # 列表内 id 可以给多个值,会增加循环依次获取。
    for jj in range(len(idPs)):
      idNum = idPs
      spider = Spider(idNum)# 根据Spider类实例化spider对象
      spider.run(idNum)# 调用spider对象的run方法


"""
获取网易云音乐任意歌曲 最新 评论,以上
"""


def GetHC(ID, s_name, p_name):# Get Hot Comments & 获取热门评论
    header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'}
    url = 'http://music.163.com/api/v1/resource/comments/R_SO_4_' + str(ID)
    req = requests.get(url, headers=header)
    req = req.json()
    dirName = u'{}'.format('comments')
    if not os.path.exists(dirName):
      os.makedirs(dirName)
    with open('./comments/{0}.txt'.format(s_name + ' - ' + p_name + '_热门评论'), 'w+') as file:
      file.write('{0} 的歌曲 {1} 热门评论如下\n'.format(p_name, s_name))
      for each_one in req['hotComments']:
            name = each_one['user']['nickname']
            comment = each_one['content']
            liked_count = str(each_one['likedCount'])
            file.write('用户名:{0}      总获赞:{1}\n评价:{2} \n\n'.format(name, liked_count, comment))



写在最后:哈哈,路遥知码力,大家都要加油啊!{:9_225:}

0HB 发表于 2021-8-25 01:20:57

使用时,把三个文件放在同一目录下,分别命好名字,下载好依赖的库,就可以愉快的运行 WyyComment.py 了。奥,时间到了,生而..{:10_245:}

burntlime 发表于 2021-8-25 07:35:45

学习

burntlime 发表于 2021-8-25 07:39:37

学习

burntlime 发表于 2021-8-25 07:43:56

{:10_256:}

burntlime 发表于 2021-8-25 07:49:25

{:10_277:}

0HB 发表于 2021-8-25 08:45:46

鱼币中奖概率设置太低了,第一次发分享帖,下次注意{:10_256:}

Agcl 发表于 2021-8-25 09:04:25

看看

Max472 发表于 2021-8-25 09:14:39

厉害

495693466 发表于 2021-8-25 09:29:10

学习

yobdc 发表于 2021-8-25 09:33:51

学会了

Rebecca2021 发表于 2021-8-25 09:34:43

666

trolwy 发表于 2021-8-25 10:02:24

赚鱼币做作业

hornwong 发表于 2021-8-25 11:04:56

{:5_95:}

hornwong 发表于 2021-8-25 11:05:36

学习!

不大不小甲鱼 发表于 2021-8-25 11:43:27

学习

yobdc 发表于 2021-8-25 11:50:12

我版本太低了

小伤口 发表于 2021-8-25 12:11:11

太好了,学习学习,正好需要这个,感谢(O)

donaldl8 发表于 2021-8-25 14:36:19

asddd

huagn100 发表于 2021-8-25 14:52:58


页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 【升级的爬虫】全自动爬取网易云 任意歌曲 热门 / 最新 评论