网易云音乐上的所有!全部!随便爬!(Python爬虫&基于网易云音乐评论的用户推荐系统)

版权声明:本文为博主原创文章,有参考的地方都会在文中给出链接。如有转载,需征求博主同意。 https://blog.csdn.net/striver6/article/details/86743904

申明:本文纯属原创,有参考的地方都会在文中给出链接。如有转载,需征求本人同意。

 

一、目标网站介绍


网易云音乐是一款由网易开发的音乐产品,是网易杭州研究院的成果,依托专业音乐人、DJ、好友推荐及社交功能,在线音乐服务主打歌单、社交、大牌推荐和音乐指纹,以歌单、DJ节目、社交、地理位置为核心要素,主打发现和分享。

2017年11月17日,网易云用户突破4亿。知乎上有这样一个问题:你为什么用网易云。其中,有一条是这样说的:

成年人的生活里有太多无奈。
我之前把情绪和秘密写在QQ留言板里,被朋友们发现。
后来写在人人网上,被朋友们发现。
后来写在微博里,被朋友们发现。
后来写在知乎里,被朋友们发现。
后来淹没在网易云强大的评论区里。
我需要一个地方,一个可以光明正大写出来的地方。
不用担心被任何人看到,不需要任何解释。
网易云可以把所有的悲伤,变成段子。

出于大众对网易云音乐的喜爱,这次文本挖掘我放在了这里,希望能发现些有趣的东西。

 二、所需工具

  •  Anaconda 3.5.0
  • Pycharm
  • Node.js
  • Mongodb
  • Studio 3T
  • RStudio

 三、数据爬取

3.1 环境搭建

本文基于Scrapy框架爬取数据。使用pip install 来安装scrapy需要安装大量的依赖库:

  • wheel
pip install wheel
pip install D:\Downloads\Scrapy\lxml-4.3.0-cp36-cp36m-win_amd64.whl
pip install D:\Downloads\Scrapy\pyOpenSSL-18.0.0-py2.py3-none-any.whl
pip install D:\Downloads\Scrapy\Twisted-18.9.0-cp36-cp36m-win_amd64.whl

可执行文件,挑选与Python对应版本安装就好。

  • Scrapy 
pip install scrapy

这里我使用了Anaconda来安装scrapy,安装时只需要一条语句:

conda install scrapy

3.2 网站分析

网易云音乐首页:


爬取思路有两种:

1.基于网页原代码,利用正则表达式、XPath等获取数据;

2.基于每次请求的API,直接获取所需数据;

本文采用第二种,针对电视剧《旋风少女》插曲的评论做简单说明:

查看NetWork,发起请求,我们可以看到,数据保存在这里:

请求地址为:https://music.163.com/weapi/v1/resource/comments/R_SO_4_28936510?csrf_token=

但是Request HEaders里的Cookie值、FromData里的params、encSecKey都是加密过的。开始解密:

然后,我从一众解密大佬的海谈阔论中发现了这个神奇的存在,哈哈,众多泥石流里的一股清泉啊。没错,就是这个链接:

http://music.163.com/api/v1/resource/comments/R_SO_4_28936510

很好,狂喜。但是很不巧的是网易云设置了反爬虫,根本下不不了手,爬虫时会出现以下错误:

{"code":-460,"msg":"Cheating"}

这是网上存在的解决办法:

  • 更换动态IP的:

我跟网易云音乐爬虫不得不说的故事(文末附精彩评论)

  • 复制请求头的:

Python爬网易云音乐的那些事

说明:上面两种方法在现在是行不通的,网易云加强了反爬虫机制,对请求头中的Cookie值进行了加密,所以有了下面这些对请求头中的Cookie值进行解密的:

但是,这种解密方法繁琐复杂,本文采用@Binaryify团队开发的[NeteaseCloudMusicApi]获取请求,这是一个相当便利、好用的API,感谢Binaryify。

3.3 功能特性


此次爬虫,下面的所有功能,都可以实现,是的,都可以!只要你想要的,在网易云上面有的,都可以爬,随便爬!!!就是这么任性和嚣张!奈我何?!哈哈哈!

获取用户信息 , 歌单,收藏,mv, dj 数量
获取用户歌单
获取用户电台
获取用户关注列表
获取用户粉丝列表
获取用户动态
获取用户播放记录
获取精品歌单
获取歌单详情
搜索
搜索建议
获取歌词
歌曲评论
收藏单曲到歌单
专辑评论
歌单评论
mv 评论
电台节目评论
banner
获取歌曲详情
获取专辑内容
获取歌手单曲
获取歌手 mv
获取歌手专辑
获取歌手描述
获取相似歌手
获取相似歌单
相似 mv
获取相似音乐
获取最近 5 个听了这首歌的用户
获取每日推荐歌单
获取每日推荐歌曲
私人 FM
签到
喜欢音乐
垃圾桶
歌单 ( 网友精选碟 )
新碟上架
热门歌手
最新 mv
推荐 mv
推荐歌单
推荐新音乐
推荐电台
推荐节目
独家放送
mv 排行
获取 mv 数据
播放 mv/视频
排行榜
歌手榜
云盘
电台 - 推荐
电台 - 分类
电台 - 分类推荐
电台 - 订阅
电台 - 详情
电台 - 节目
给评论点赞
获取动态
获取热搜
发送私信
发送私信歌单
新建歌单
收藏/取消收藏歌单
歌单分类
收藏的歌手列表
订阅的电台列表
相关歌单推荐
付费精选接口
音乐是否可用检查接口
登录状态
获取视频数据
发送/删除评论
热门评论
视频评论
退出登录
所有榜单
所有榜单内容摘要
收藏视频
收藏 MV
视频详情
相关视频
关注用户
新歌速递
喜欢音乐列表(无序)
收藏的 MV 列表

本文以其中几个为例,进行数据爬取和文本挖掘:

3.3 歌单

我们找到歌单页面为下:

可以看到:
语种有:华语、欧美、日语、韩语、小语种;
风格有:流行、摇滚、民谣、电子、舞曲、说唱、轻音乐、说唱、爵士、乡村、古典、民族、英伦、金属、朋克、蓝调、雷鬼、世界音乐、拉丁、古风等;还有对歌单的场景、情感、主题分类。我们不对其进行帅选,爬取所有种类的歌单。当每页为35个歌单的时候,一共有38页;当每页为20个歌单的时候,一共有66页,我们把66页一共1306个的歌单信息爬取下来:

class MenuSpider(scrapy.Spider):
    name = 'menu'
    allowed_domains = ['localhost:3000']
    start_urls = ['http://localhost:3000/']
    allplaylist_url = 'http://localhost:3000/top/playlist?order=hot&cat=%E5%85%A8%E9%83%A8&limit=20&offset={offset}'

    def start_requests(self):
        for i in range(0, 66):
            yield Request(self.allplaylist_url.format(offset=i * 20), callback=self.parse_allplaylist)

    def parse_allplaylist(self, response):
        result = json.loads(response.text)
        item = MusicmenuItem()
        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
                yield item

3.4 歌曲

我们选择爬取歌曲评论信息的时候一并把歌曲信息爬下来。

3.5 歌曲评论

我们的目的是获取K个歌单下面的M首歌曲的前N页评论信息。

2018年12月30日下午3点27分:

元旦公休,周日,CS下雪,CSU美不胜收。
从前晚、昨天、昨晚01:00点,我一直在一个问题上挣扎,那就是爬取前4首歌的前3页评论时,为什么爬取的时候只会爬到7条记录:第一首第一页,第二首第一页,第三首第一页,第四首一、二、三页,现在这个问题终于解决。
其中,有包括:Scrapy的调试:pycharm下打开、执行并调试scrapy爬虫程序

  •  Crawer 1.0
# -*- coding: utf-8 -*-  
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8') #改变标准输出的默认编码  
  
class MusiccommentsSpider(scrapy.Spider):  
    name = 'musiccomments'  
    allowed_domains = ['localhost:3000']  
    start_urls = ['http://localhost:3000/comment/music?id=296883/']  
    comment_url = 'http://localhost:3000/comment/music?id={id}&offset={offset}&limit=20'  
    playlist_url = 'http://localhost:3000/playlist/detail?id=2571516884'  
    num_comment = 0  
    song_id=0  
  
    def start_requests(self):  
        yield Request(self.playlist_url, callback=self.parse_playlist)  
        #yield Request(self.comment_url.format(offset=self.num_comment), callback=self.parse_comment,dont_filter=True)  
  
    def parse_playlist(self, response):  
        result = json.loads(response.text)  
        item = WangyiyunPlaylistItem()  
        #print(result.get('playlist').get('tracks').get('name'))  
   
        for field in item.fields:  
            if field in result.keys():  
                item[field] = result.get(field)  
                yield item  
  
        #global song_id  
        for i in range(0, 4):  
        #for i in range(0,(result.get('playlist').get('trackCount'))-1):  
            self.num_comment=0  
            self.song_id = result.get('playlist').get('tracks')[i].get('id')  
            yield Request(self.comment_url.format(id=self.song_id,offset=self.num_comment), callback=self.parse_comment,dont_filter=True)  
  
    def parse_comment(self, response):  
        result = json.loads(response.text)  
        item = WangyiyunCommentItem()  
        #print(response.text.encode('utf-8','ignore'))  
        for field in item.fields:  
            if field in result.keys():  
                item[field] = result.get(field)  
                yield item  
  
        self.num_comment = self.num_comment + 1  
        if self.num_comment <= 2:  
            yield Request(self.comment_url.format(id=self.song_id, offset=self.num_comment * 20),  
                          callback=self.parse_comment, dont_filter=True)  


究其原因,是yield的缘故,传送门:python中yield的用法详解——最简单,最清晰的解释。一定要明白语句的执行顺序和yield的特殊用法,双重循环省事更多,何必非要搞函数递归?

  • Crawer 2.0
# -*- coding: utf-8 -*-
import time
import scrapy
from scrapy import Spider,Request
import io
import sys
import json
from wangyiyun.items import WangyiyunCommentItem
from wangyiyun.items import WangyiyunPlaylistItem

sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8') #改变标准输出的默认编码

class MusiccommentsSpider(scrapy.Spider):
    name = 'musiccomments'
    allowed_domains = ['localhost:3000']
    start_urls = ['http://localhost:3000/comment/music?id=296883/']
    comment_url = 'http://localhost:3000/comment/music?id={id}&offset={offset}&limit=20'
    playlist_url = 'http://localhost:3000/playlist/detail?id=2571516884'
    num_comment = 0
    song_id=0

    def start_requests(self):
        yield Request(self.playlist_url, callback=self.parse_playlist)
        #yield Request(self.comment_url.format(offset=self.num_comment), callback=self.parse_comment,dont_filter=True)

    def parse_playlist(self, response):
        result = json.loads(response.text)
        item = WangyiyunPlaylistItem()
        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
                yield item

        for i in range(0, 4):
            for j in range(0,3):
                yield Request(
                    self.comment_url.format(id=result.get('playlist').get('tracks')[i].get('id'), offset=j * 20),
                    callback=self.parse_comment, dont_filter=True)

    def parse_comment(self, response):
        result = json.loads(response.text)
        item = WangyiyunCommentItem()
        #print(response.text.encode('utf-8','ignore'))
        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
                yield item


是不是简洁了很多?
接下来,连接数据库,提取上面所爬歌单的id,根据id值获得每个歌单下面的歌曲id,再根据歌曲id值获得每首歌下面的评论信息。执行翻页操作,从而最终获取1306个歌单下面的前30首歌曲的前10页评论:

  • Crawer 3.0
# -*- coding: utf-8 -*-
import time
import scrapy
from scrapy import Spider,Request
import io
import sys
import json
import pandas as pd
import pymongo
from wangyiyun.items import WangyiyunCommentItem
from wangyiyun.items import WangyiyunPlaylistItem
from wangyiyun.items import WangyiyunAllPlaylistItem

sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8') #改变标准输出的默认编码

class MusiccommentsSpider(scrapy.Spider):
    name = 'musiccomments'
    allowed_domains = ['localhost:3000']
    start_urls = ['http://localhost:3000/comment/music?id=296883/']
    allplaylist_url = 'http://localhost:3000/top/playlist?order=hot&cat=%E5%85%A8%E9%83%A8&limit=20&offset={offset}'
    playlist_url = 'http://localhost:3000/playlist/detail?id={id}'
    comment_url = 'http://localhost:3000/comment/music?id={id}&offset={offset}&limit=20'
    num_comment = 0
    num_page = 0
    song_id = 0
    results = ''


    def start_requests(self):
        client = pymongo.MongoClient(host='localhost', port=27017)
        db = client['music']
        collection = db['menu']
        # 将数据库数据转为dataFrame
        menu = pd.DataFrame(list(collection.find()))
        num = menu['playlists']
        result = pd.DataFrame(num.iloc[0])
        for i in range(1, 66):
            data2 = pd.DataFrame(num.iloc[i])
            result = pd.concat([result, data2], ignore_index=True)
        print(result.shape)
        id = result['id']
        for i in range(0, 1000):
            yield Request(self.playlist_url.format(id=id.iloc[i]), callback=self.parse_playlist)
            
    def parse_playlist(self, response):
        result = json.loads(response.text)
        item = WangyiyunPlaylistItem()
        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
                yield item

        for j in range(0, 30):
            for k in range(0,10):
                yield Request(
                    self.comment_url.format(id=result.get('playlist').get('tracks')[j].get('id'), offset=k * 20),
                    callback=self.parse_comment, dont_filter=True)

    def parse_comment(self, response):
        result = json.loads(response.text)
        item = WangyiyunCommentItem()
        #print(response.text.encode('utf-8','ignore'))
        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
                yield item

获取了13943个文档。由于爬取时歌曲信息和评论信息保存到了一张表里,去掉其中的歌曲信息,评论信息约有12000*20=24万条。

说明:由于爬取的时候IP被封,这24万条评论并非所有,这一问题在后文中处理。

并且,IP被封还导致我网易云音乐的评论是看不了的,加载不出来的。。。网易云音乐做得真够绝!一直都是这样子。。。。

3.6 网易云音乐用户

爬取思路:我们从网易云音乐的大V云音乐小秘书开始,爬取其关注和粉丝,再爬取其关注者的关注和粉丝,粉丝的关注和粉丝,以此类推,不断递归,雪球会越滚越大,涉及到的用户会越来越多,怎么样?是不是感觉很爽很刺激?代码奉上:

# -*- coding: utf-8 -*-
import time
import scrapy
import json
from scrapy import Spider, Request
from WangyiyunUser.items import WangyiyunuserItem

class UserSpider(scrapy.Spider):
    name = 'user'
    allowed_domains = ['localhost:3000']
    start_urls = ['http://localhost:3000/']
    user_url = 'http://localhost:3000/user/detail?uid={uid}'
    follows_url = 'http://localhost:3000/user/follows?uid={uid}&offset={offset}&limit={limit}'
    followers_url = 'http://localhost:3000/user/followeds?uid={uid}&offset={offset}&limit={limit}'
    start_uid = '9003'
    follows_next_page=0
    followers_next_page=0

    def start_requests(self):
        yield Request(self.user_url.format(uid=self.start_uid), self.parse_user,dont_filter = True)
        
        yield Request(self.follows_url.format(uid=self.start_uid, limit=30, offset=0),
                      self.parse_follows,dont_filter = True)
        
        yield Request(self.followers_url.format(uid=self.start_uid, limit=30, offset=0),self.parse_followers,dont_filter = True)
       

    def parse_user(self, response):
        result = json.loads(response.text)
        item = WangyiyunuserItem()

        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
        yield item
        #print(result.get('profile').get('userId'))

        yield Request(
          self.follows_url.format(uid=result.get('profile').get('userId'), limit=30, offset=0),
            self.parse_follows,dont_filter = True)
     
        yield Request(
            self.followers_url.format(uid=result.get('profile').get('userId'), limit=30, offset=0),
            self.parse_followers,dont_filter = True)
       
    def parse_follows(self, response):
        results = json.loads(response.text)
        print('正在判断关注者:')
        if 'follow' in results.keys():
            for result in results.get('follow'):
                yield Request(self.user_url.format(uid=result.get('userId')),
                              self.parse_user,dont_filter = True)
                

        if results.get('more') == True:
            self.follows_next_page = self.follows_next_page+1
            yield Request(self.follows_url.format(uid=self.start_uid, limit=30, offset=self.follows_next_page*30),
                          self.parse_follows,dont_filter = True)
            

    def parse_followers(self, response):
        results = json.loads(response.text)

        if 'followeds' in results.keys():
            for result in results.get('followeds'):
                yield Request(self.user_url.format(uid=result.get('userId')),
                              self.parse_user,dont_filter = True)
               

        if results.get('more') == True:
            self.followers_next_page=self.followers_next_page+1
            yield Request(self.followers_url.format(uid=self.start_uid, limit=30, offset=self.followers_next_page*30),
                          self.parse_followers,dont_filter = True)

最终在封IP之前获得了14974条用户详情。

四、数据分析

 4.1 歌单

 4.1.1 播放数量最多的TOP20歌单

 导入包,设置中文字体支持

import pymongo
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from pylab import mpl  
#设置图片显示字体对汉字的支持
mpl.rcParams['font.sans-serif'] = ['SimHei'] 
client = pymongo.MongoClient(host='localhost', port=27017)

从数据库导入数据:

db = client['music']
collection = db['menu']
# 将数据库数据转为dataFrame
data = pd.DataFrame(list(collection.find()))

查看数据维度:

print(data.shape)

每页的歌单保存为一个文档,66页的歌单,一共有66个文档。
取其中一页歌单,查看内容:

num=data['playlists']
print(num.iloc[0])

为方便观察,对其做json解析。限于篇幅,取其中一个歌单的内容显示如下:

[{
    'name': '你是个成熟的成年人了,你该戒掉情绪了。',
    'id': 2468145627,
    'trackNumberUpdateTime': 1540176152147,
    'status': 0,
    'userId': 49341371,
    'createTime': 1539449979814,
    'updateTime': 1540176152147,
    'subscribedCount': 58406,
    'trackCount': 43,
    'cloudTrackCount': 0,
    'coverImgUrl': 'http://p2.music.126.net/qXN1QIV_mGUV1BusdRX1CA==/109951163602371733.jpg',
    'coverImgId': 109951163602371730,
    'description': '-\n\n你要做一个不动声色的大人了。不准情绪化,不准偷偷想念,不准回头看。去过自己另外的生活。\n\n以前的你,哭着哭着就笑了。\n\n现在的你,笑着笑着就哭了。\n\n到了一定的年纪,眼泪越来越少,因为身边再也没有一个能帮你擦眼泪的人。\n\n在成年人的世界里,最让人想哭的三个字是:不要哭。\n\n-\n\n人生还有眼泪也冲刷不干净的巨大悲伤,还有难忘的痛苦让你们即使想哭也不能流泪。\n\n怀揣着痛苦和悲伤,即使如此也要带上它们笑着前行。\n\n-\n\n如果可以,往后请让笑容比眼泪多。就算要哭,每一滴眼泪的名字也应该是——喜极而泣。\n\n-\n\n封面:站酷插画师Y_jianjian',
    'tags': ['华语', '流行', '治愈'],
    'playCount': 4370831,
    'trackUpdateTime': 1546744947197,
    'specialType': 0,
    'totalDuration': 0,
    'creator': {
        'defaultAvatar': False,
        'province': 440000,
        'authStatus': 0,
        'followed': False,
        'avatarUrl': 'http://p1.music.126.net/NbMIANWbmL6PVBmduaDzqA==/109951163776645958.jpg',
        'accountStatus': 0,
        'gender': 1,
        'city': 441500,
        'birthday': 845481600000,
        'userId': 49341371,
        'userType': 200,
        'nickname': '一点波澜-',
        'signature': '激不起一点波澜。',
        'description': '',
        'detailDescription': '',
        'avatarImgId': 109951163776645950,
        'backgroundImgId': 109951163373553600,
        'backgroundUrl': 'http://p1.music.126.net/O3aDHMNl_VW7GgK2VnGz9Q==/109951163373553592.jpg',
        'authority': 0,
        'mutual': False,
        'expertTags': ['华语', '流行', '欧美'],
        'experts': None,
        'djStatus': 10,
        'vipType': 11,
        'remarkName': None,
        'avatarImgIdStr': '109951163776645958',
        'backgroundImgIdStr': '109951163373553592',
        'avatarImgId_str': '109951163776645958'
    },
    'tracks': None,
    'subscribers': [{
        'defaultAvatar': False,
        'province': 440000,
        'authStatus': 0,
        'followed': False,
        'avatarUrl': 'http://p1.music.126.net/_XE9wV7-4JlWUPf51pnM_w==/109951163772509594.jpg',
        'accountStatus': 0,
        'gender': 2,
        'city': 440700,
        'birthday': -2209017600000,
        'userId': 515582083,
        'userType': 0,
        'nickname': '小心我锤爆你',
        'signature': '',
        'description': '',
        'detailDescription': '',
        'avatarImgId': 109951163772509600,
        'backgroundImgId': 109951163772503820,
        'backgroundUrl': 'http://p1.music.126.net/DQiWNBKXye4i_pbaSzUi9A==/109951163772503826.jpg',
        'authority': 0,
        'mutual': False,
        'expertTags': None,
        'experts': None,
        'djStatus': 0,
        'vipType': 0,
        'remarkName': None,
        'avatarImgIdStr': '109951163772509594',
        'backgroundImgIdStr': '109951163772503826',
        'avatarImgId_str': '109951163772509594'
    }],
    'subscribed': None,
    'commentThreadId': 'A_PL_0_2468145627',
    'newImported': False,
    'adType': 0,
    'highQuality': False,
    'privacy': 0,
    'ordered': True,
    'anonimous': False,
    'shareCount': 520,
    'coverImgId_str': '109951163602371733',
    'commentCount': 403,
    'alg': 'alg_sq_topn_lr'
}, 

我们将第一个文档,即第一页歌单数据转化为数据框,并查看其数据维度:

print(pd.DataFrame(num.iloc[0]).shape)

结果:

(20, 33)

一共有20行,33列。即第一页的20个歌单中,每个歌单都有33个字段。
将66页的所有歌单合并到一起,查看数据维度:

result=pd.DataFrame(num.iloc[0])
for i in range(1,66):
    data2=pd.DataFrame(num.iloc[i])
    result=pd.concat([result,data2],ignore_index=True)
print(result.shape)

结果:

(1306, 33)

所有全部歌单一共有1306个,每个歌单33个字段。
查看行名:

print(result.columns.values.tolist())

33个字段为下所示:

['adType', 'alg', 'anonimous', 'cloudTrackCount', 'commentCount', 'commentThreadId', 'coverImgId', 'coverImgId_str', 'coverImgUrl', 'createTime', 'creator', 'description', 'highQuality', 'id', 'name', 'newImported', 'ordered', 'playCount', 'privacy', 'shareCount', 'specialType', 'status', 'subscribed', 'subscribedCount', 'subscribers', 'tags', 'totalDuration', 'trackCount', 'trackNumberUpdateTime', 'trackUpdateTime', 'tracks', 'updateTime', 'userId']

播放数量最多的TOP20歌单:

result1=result.sort_values(by=['playCount'],ascending = False)
result1['playCount']
result2=pd.concat([result1['name'],result1['playCount']],axis=1,ignore_index=False)
print((pd.DataFrame(result2)).shape)
print((pd.DataFrame(result2)).head)
data=result2
data.index=data['name']
colors = '#6D6D6D' # 设置标题颜色为灰色
color_line = '#CC2824'
fontsize_title = 20
from IPython.core.pylabtools import figsize # import figsize
figsize(12.5, 4) # 设置 figsize
plt.rcParams['savefig.dpi'] = 200 #图片像素
#plt.rcParams['figure.dpi'] = 200 #分辨率
# 默认的像素:[6.0,4.0],分辨率为100,图片尺寸为 600&400
# 指定dpi=200,图片尺寸为 1200*800
# 指定dpi=300,图片尺寸为 1800*1200
# 设置figsize可以在不改变分辨率情况下改变比例
# 我们使用R语言中ggplot的风格
plt.style.use('ggplot')
data[1:21].plot(kind='barh',color=color_line).invert_yaxis()
#使用 pd.Series把dataframe转成Series
#data = pd.Series(data['playCount'].values)
#for y,x in enumerate(list(data['playCount'].values[1:21])):
for y,x in enumerate(list(data.iloc[:,1][1:21].values)):
    plt.text(x+1600000,y+0.3,'%s' %round(x,1),ha='center',color=colors)
    #plt.text(x-20,y+0.3,'%s' %x,color=colors)
plt.xlabel('播放数量')
plt.ylabel('歌单名称')
plt.title('播放数量最多的TOP20歌单', color = colors, fontsize=fontsize_title)
plt.tight_layout()
plt.savefig('播放数量最多的TOP20歌单.png',dpi=200)
plt.show()

结果:

显示为文本:

name
听说你也在找好听的华语歌                  41094772
那些喜欢到循环播放的歌                   37185064
失恋必听歌单 | 因为你突然听懂了很多歌          31393852
2018年度最热新歌TOP100              30460302
别急,甜甜的恋爱马上就轮到你了               27107300
温柔暴击 | 沉溺于男友音的甜蜜乡             18519378
最是粤语最为情深 也唯独你最难忘怀             17338788
提神醒脑 疯狂抖腿魔性摇头.GIF             15403695
Hip-hop | 少女心狙击手              12368797
你最想对暗恋的人 说的一句话是什么             11561604
风月无憾 | 你是我有关青春 最美的句读           9827816
新的一年,希望你喜欢的人也喜欢你               8841251
【2018年度电影精选| Ready Story 】     8747021
"若是心怀旧梦 就别再无疾而终"               8706438
“长大”这两个字,孤独得连偏旁都没有             8587671
如何用手机铃声惊艳四座? 日语篇               8059432
再见大侠:武侠小说泰斗金庸逝世                7993862
KTV必点:有没有一首歌,唱着唱着就泪奔           7890135
你放弃过一个爱了很久的人吗?                 7818393
欧美·耳朵怀孕 | 盘点那些流行歌手             7497257


播放量第一的为《听说你也在找好听的华语歌 》,播放数高达4109万多。第二为《那些喜欢到循环播放的歌》,播放量为3718万多。前20个歌单的播放量最少的也在749万以上。究其原因,有以下几点:

----  歌单名本身很具诱惑力,很适合没有固定听歌意向的群体,较为大众化,是绝大多数人的首选。像《别急,甜甜的恋爱马上就轮到你了》《温柔暴击 | 沉溺于男友音的甜蜜乡》《你最想对暗恋的人 说的一句话是什么》《风月无憾 | 你是我有关青春 最美的句读》等,光听名字就很吸引人。

----  创建人鹿白川,有网易云达人认证,粉丝数也高达9万多,实打实的大V。标签:音乐(华语、流行、欧美)、资讯(生活);个人介绍:话少慢热不喜交际 / 不推广 勿扰;请问都这个水平了还需要推广么??? 其创建的歌单播放量动辄上百万,上千万,看来属于网易云音乐中的主力人物。他的主页:

---- 歌单下的歌曲本身质量就很高,这也是最重要的一点。

相反,查看播放量最少的20首:

中岛美嘉 Special Live 2019 广州/上海        85
霉霉丨泰勒斯威·夫特的Style        82
C95东方专辑收录(一专一首)        80
CLUB MIAMI HOHHOT 百大DJ-DENIZ KOYU        78
战歌舞曲        76
vlog ❤        74
*我们自雪乡去往雾夏*(贰)        73
伴读喵自习室        71
细腻莫扎特        69
【开业歌曲】新店开业喜庆歌曲大全100首        67
邓丽君1982年香港伊丽莎白体育馆演唱会        65
⚡︎ 2019年精选电音 待制作 敬请期待        62
 一周日语新歌(12/29~01/04)        60
祺鑫时光轴2018年下        58
【纯音乐】你是否听过,那属于夏日的美好        49
方灿的Spotify歌单        42
冷门纯音| 轻叩心扉,静享惬意好时光        31
【我爱学习】学习解压轻音乐        29
【轻音乐】如何优雅地睡觉        24
思念的解药 即是归乡        19

恰恰和上面三点条件都相反,也难怪不太受欢迎。

4.2 网易云用户

4.2.1 数据库简单查询分析

筛选同在湖南的网易云小伙伴:

select * from users2 where profile.city = 650100

从数据库里得到了315条记录:

筛选地处长沙等级在5级以上的:

select * from users2 where profile.city = 430100 and level>5

得到了69条记录:

其中挑选了一个id为1389014,昵称叫猜猜寻的进行分析

4.2.2 关注者的地区分布

导入之前爬取到的用户数据:

import pymongo
import pandas as pd
import numpy as np
client = pymongo.MongoClient(host='localhost', port=27017)
db = client['music']
collection = db['users3']
# 将数据库数据转为dataFrame
user = pd.DataFrame(list(collection.find()))

查看数据维度:

print(user.shape)

有14974个用户,每个用户下面有13个字段信息:

(14974, 13)

查看字段名称:

['_id', 'adValid', 'bindings', 'code', 'createDays', 'createTime', 'level', 'listenSongs', 'mobileSign', 'pcSign', 'peopleCanSeeMyPlayRecord', 'profile', 'userPoint']

我们选取其中的profile进行分析:

profile=pd.DataFrame(user['profile'])

profile下面有41个字段,我们将14974个用户的profile字段下面的其中两个字段:province和userId提取出来,合并成一个数据框。

result=pd.DataFrame([[9003,110000]])
for i in range(1,14974):
    province=profile.iloc[i][0].get('province')
    userId=profile.iloc[i][0].get('userId')
    #列合并
    userInfo=pd.DataFrame([[userId,province]])
    #行合并
    result=pd.concat([result,userInfo],ignore_index=True)
#列命名
result.columns = ['userId','province']

统计每个省份所包含的网易云音乐用户数量。

data = result.groupby(result['province']).count()
data.to_csv("userProvinceInfo.csv", sep=',', header=True, index=False)

由于原始数据中province的值为行政区划代码,需要根据行政区划代码表还原用户的所在省份信息。

install.packages("xlsx")
library(xlsx) 
userInfo=read.csv("userProvinceInfo.csv")
code=read.table("clipboard",header = TRUE)
pro=matrix(0,nrow(userInfo),1)
for(i in 3:36){
  pro[i-2,1]=as.matrix(code[which(userInfo[i,2]==code[,1]),2])[1,1]
}
pro=pro[-c(35,36,37),]
data=cbind(userInfo[-c(35,36,37),1],pro)
colnames(data)<-c("userCounts","Province")
write.xlsx(x = data, file = "data.xlsx",
           sheetName = "data", row.names = FALSE)

利用得到的最终数据,基于可视化工具Power Map 2016,制作演示视频,并将其转化为GIF(简书不能上传视频)。但是GIF不能超过10M,下面是压缩后的,很模糊。原文件大小70M,高清,会看得很舒服。制作好的原演示视频链接:网易云音乐中国分布演示视频
是不是炫酷到爆炸,有型到喷汁??!!
在第二个深入镜头,你可以看到交通路线,有没有百度地图的即视感?!再深入点,我可以找到你的位置。

五、文本相似计算原理

5.1 TF-IDF

我们在判断一个词的重要性的时候,不能仅仅依靠词频来处理,因此引入了TF-IDF值。TF-IDF是Term Frequency -  Inverse Document Frequency的缩写,即“词频-逆文本频率”。它由两部分组成,TF和IDF。

TF是词频,我们后面做的词条—文档矩阵给出了文本中各个词的出现频率统计,并作为文本特征,这个很好理解。而IDF,即“逆文本频率”,则反映了一个词在所有文本中出现的频率,如果一个词在很多的文本中出现,那么它的IDF值应该低。反过来,如果一个词在比较少的文本中出现,那么它的IDF值应该高。比如一些专业的名词如“Machine Learning”。这样的词IDF值应该高。一个极端的情况,如果一个词在所有的文本中都出现,那么它的IDF值应该为0。

上面是从定性上说明的IDF的作用,那么如何对一个词的IDF进行定量分析呢?这里直接给出一个词x的IDF的基本公式如下:

 IDF(x)=log(N/N(x))                          (1)

其中,N代表语料库中文本的总数,而N(x)代表语料库中包含词x的文本总数。
当然,在一些特殊的情况IDF计算会有一些小问题,比如某一个生僻词在语料库中没有,这样我们的分母为0, IDF没有意义了。所以常用的IDF我们需要做一些平滑,使语料库中没有出现的词也可以得到一个合适的IDF值。平滑的方法有很多种,最常见的IDF平滑后的公式之一为:

IDF(x)=log((N+1)/(N(x)+1))+1                  (2)

有了IDF的定义,我们就可以计算某一个词的TF-IDF值了:

TF−IDF(x)=TF(x)∗IDF(x)                        (3)

其中TF(x)指词x在当前文本中的词频。

5.2 Doc2Bow模型

Word2vec和Doc2Bow同属两位学术大牛Quoc Le 和 Tomas Mikolov发明的。

在2014年的《Distributed Representations of Sentences and Documents》所提出文章向量(Documents vector),或者称句向量(Sentences vector),当然在文章中,统一称这种向量为Paragraph Vector。

Word2vec涉及到很多数学模型,囊括了词向量的理解、sigmoid函数、逻辑回归、Bayes公式、Huffman编码、n-gram模型、浅层神经网络、激活函数、最大似然及其梯度推导、随机梯度下降法、词向量与模型参数的更新公式、CBOW模型和 Skip-gram模型、Hierarchical Softmax算法和Negative Sampling算法等。当然还会结合google发布的C源码(好像才700+行),讲述相关部分的实现细节,比如Negative Sampling算法如何随机采样、参数更新的细节、sigmod的快速近似计算、词典的hash存储、低频与高频词的处理、窗口内的采样方式、自适应学习、参数初始化、w2v实际上含有两中方法等,用C代码仅仅700+行实现,并加入了诸多技巧。原文可以参见以下链接:

--------------------- 
作者:林海山波 
来源:CSDN 
原文:https://blog.csdn.net/lenbow/article/details/52120230 
版权声明:本文为博主原创文章,转载请附上博文链接!


5.3 计算相似度

本文使用欧氏距离度量相似度。欧几里得度量(euclidean metric)(也称欧式距离)是一个通常采用的距离定义,指在m维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。

0ρ = sqrt( (x1-x2)^2+ (y1-y2)^2 )
similarity = 1/(op + 1) 

最终的similarity就是相似度评价的值。


六、相似用户推荐(Python之Gensim版本)

6.1 Gensim

用Python做过文本挖掘的想必知道Gensim,首先我们看维基百科对gensim的解释:

Gensim  is a robust open-source vector space modeling and topic modeling toolkit implemented in Python. It uses NumPy ,SciPy and optionally Cython for performance. Gensim is specifically designed to handle large text collections, using data streaming and efficient incremental algorithms, which differentiates it from most other scientific software packages that only target batch and in-memory processing.

Gensim includes implementations of tf-idf, random projections, word2vec and document2vec algorithms,hierarchical Dirichlet processes (HDP), latent semantic analysis (LSA, LSI, SVD) and latent Dirichlet allocation (LDA), including distributed parallel versions.

也就是说,gensim是一个python的函数包,gensim包含了TF-IDF、随机投影、word2vec和document2vec算法的实现,分层Dirchlet过程(HDP),潜在语义分析(LSA)和潜在Dirichlet分配即主题模型(LDA),包括分布式并行版本。主要是用来主题建模、文档索引以及使用大规模语料数据的相似性检索,是文本挖掘在NLP领域里处理相当有效、得力的一个函数包。

6.2 数据预处理

说明:为了更广泛地使大家熟悉多种语言,接下来我用R语言来处理。首先,以之前我们爬到的陈百强大哥的经典歌曲《偏偏喜欢你》的1024页的评论文本信息为例展开说明,R语言连接Mongodb数据库

library(jsonlite)
library(mongolite)
library("openssl")
con <- mongo(collection = "陈百强",db = "comments", url = "mongodb://localhost")
mydata <- con$find()
dim(mydata)

得到了1025个文档,2列,第一是id,弃,第二列是comments,留。

1025    2

我们查看它的第二列即评论:

lengths(mydata['comments'][3,])

没错,一条评论信息下面有16个字段,但是R语言对这种json格式的数据处理不是很成熟,目前有rjson包,但是存在bug,其替代品RJSONIO也没有那么完善。这一阶段的数据处理我们改用Python。(这是我花费了一下午时间后的结论。多么痛的领悟。。。)

import pymongo
import pandas as pd
import numpy as np
client = pymongo.MongoClient(host='localhost', port=27017)
db = client['comments']
collection = db['陈百强']
# 将数据库数据转为dataFrame
data = pd.DataFrame(list(collection.find()))
num=data['comments']
result=pd.DataFrame(num.iloc[0])
for i in range(1,1025):
    data2=pd.DataFrame(num.iloc[i])
    result=pd.concat([result,data2],ignore_index=True)

查看其维度:

print(result.shape)

故有《偏偏喜欢你》下面有20500条评论,每条评论有16个字段。

(20500, 16)

我们选我们想要的文本字段和user字段下面的userID。

text.rename(columns={'w':'userID', 'x':'context'}, inplace = True)
text['context']=result['content']
for i in range(0,20500):
    text.iloc[i,0]=result['user'].iloc[i]['userId']

得到20500行2列的数据:

userID                                            context
0       328901978                                            都是有故事的人
1      1724358802                                       每天都要听至爱Danny
2       451250610                                 对呀,你就从来没喜欢过我。一点都煤油
3       554348857                                            沁入心脾的温柔
4      1296389446                                           最近好喜欢这首歌
5       303721726                                             今日点歌③.
6      1654009353                                          偏偏对你没有抵抗力
7      1654009353  那一次我们擦肩而过,我害怕我们会像电影里的情景一样错过,所以我没有走,我一直看着你走,可是你...
8       357343565                                “我喜欢你”珍贵,奈何听的人不珍惜……
9       293896875                                         偏偏喜欢你  王雷雷
10     1510414811                                         对啊,这是为什么呢。
11     1574359988                                              对,好浪漫
12     1333924430                            小时候有个小的收音机,好开心,睡觉都舍不得放下
13       64188725                                                 好听
14      482121141                                     这些歌曲都是经典作品[亲亲]
15      427963594                                      偏偏喜欢你,我的翩翩夫人。
16     1382017788                                  以爱情  以时光\n\n纪贯新~❤
17       46656982  我和他来一家店吃饭,店里在放这首歌,他问我,你知道这首歌是什么吗?我听了一下说不知道,他说:...
18      425557078                   幸福到疼……\r\r有一个人曾让我知道\r寄生于世原来是那么的好
19      568096298                             超级喜欢的一首歌,偏偏喜欢你【大赞】[爱心]
20      412381224                                         他可能会单手开法拉利
21      301593622                            [多多大笑][多多大笑][多多大笑]笑着活下去
22     1455906926                                             再见吧大渣男
23     1670031755                                       [憨笑]我比你还俗 更穷
24      301593622                                 现在一心只想搞钱,我比较俗,太穷了!
25     1730181168                                         哈哈哈哈这个年代感哦
26      279684331                                        偏偏喜欢的就是得不到!
27      621542625                                               阿Lan
28     1328014893                             曾经他为我唱过的歌,到如今听到都心里隐隐作痛
29       51335946                                     爱,直至成伤;之后,就是永远
...           ...                                                ...
20470    62338668                                               nice
20471    62338668  愛上你永遠沒有結果 很好 我現在要放棄了 努力忘記你 不想讓自己為了一個沒結果的人那麼難受 ...
20472    68320201  忽然想起和一个朋友的对话,她因为家庭原因极度缺乏安全感。\n我怕…\n你怕什么?\n怕他喜欢...
20473   246752393                                                張國榮
20474   134205876        还记得那时地方电视台点播的一些曲子吗?重点中间无广告,那才是乐趣。某某点歌给谁谁谁……
20475   122104993  班固 《西都赋》“愿宾摅怀旧之蓄念,发思古之幽情。” 元稹 《赠吴渠州从姨兄士则》诗“泪因生...
20476   100442347                中午广播无意听到这首歌,然后特意关注了聊大广播台微博找到歌单![大哭]
20477   118207115                                               别咒王杰
20478   116595775                      我爸只会唱一首歌,就这首歌把我妈娶到手了。[憨笑][憨笑]
20479    60109810                                    2000年生却偏偏喜欢80年代
20480    63357164                                    情爱 是什么 只有呵呵[大哭]
20481    62338668                                               [可爱]
20482    95087236                                         对啊,为何偏偏喜欢你
20483    68320201                                               [大笑]
20484    79282042                                       [皱眉][皱眉][皱眉]
20485    83512955                             听过这首歌的人都是有故事的男人和女人[跳舞]
20486    67061365    上班特别累的时候听到香港的经典感觉整个世界都属于我,要是来一杯纯正的香港奶茶就更好了。[大笑]
20487    71401843                                     当年的录像厅前面就放的这首歌
20488   103882075                                           他还没死[大哭]
20489    62338668                                               我01的
20490    62338668                                               nice
20491    62338668  愛上你永遠沒有結果 很好 我現在要放棄了 努力忘記你 不想讓自己為了一個沒結果的人那麼難受 ...
20492    68320201  忽然想起和一个朋友的对话,她因为家庭原因极度缺乏安全感。\n我怕…\n你怕什么?\n怕他喜欢...
20493   246752393                                                張國榮
20494   134205876        还记得那时地方电视台点播的一些曲子吗?重点中间无广告,那才是乐趣。某某点歌给谁谁谁……
20495   122104993  班固 《西都赋》“愿宾摅怀旧之蓄念,发思古之幽情。” 元稹 《赠吴渠州从姨兄士则》诗“泪因生...
20496   100442347                中午广播无意听到这首歌,然后特意关注了聊大广播台微博找到歌单![大哭]
20497   118207115                                               别咒王杰
20498   116595775                      我爸只会唱一首歌,就这首歌把我妈娶到手了。[憨笑][憨笑]
20499    60109810                                    2000年生却偏偏喜欢80年代

[20500 rows x 2 columns]

将userID一列保存为列名,重命名行名、列名,并保存数据到文件“DealtedData.csv”中

names=pd.DataFrame(data.iloc[:,0])
data.rename(index=names.iloc[:,0],columns={2:'context'}, inplace = True)
data.to_csv(r'DealtedData.csv',encoding='gbk',index=True,header=True)

 6.3 文本预处理


导入数据:

import os
os.chdir('G:\\项目\\网易云音乐评论\\文本挖掘')  # 打印当前工作目录
import pandas as pd
data = pd.read_csv(r"DealtedData.csv",encoding='gbk', sep=',',index_col=0,header=0)
data.head

数据格式为下:

<bound method NDFrame.head of                                                        context
328901978                                              都是有故事的人
1724358802                                        每天都要听至爱Danny
451250610                                   对呀,你就从来没喜欢过我。一点都煤油
554348857                                              沁入心脾的温柔
1296389446                                            最近好喜欢这首歌
303721726                                               今日点歌③.
1654009353                                           偏偏对你没有抵抗力
1654009353   那一次我们擦肩而过,我害怕我们会像电影里的情景一样错过,所以我没有走,我一直看着你走,可是你...
357343565                                  “我喜欢你”珍贵,奈何听的人不珍惜……
293896875                                           偏偏喜欢你  王雷雷
1510414811                                          对啊,这是为什么呢。
1574359988                                               对,好浪漫
1333924430                             小时候有个小的收音机,好开心,睡觉都舍不得放下
64188725                                                    好听
482121141                                       这些歌曲都是经典作品[亲亲]
427963594                                        偏偏喜欢你,我的翩翩夫人。
1382017788                           以爱情  以时光\r\r\n\r\r\n纪贯新~?
46656982     我和他来一家店吃饭,店里在放这首歌,他问我,你知道这首歌是什么吗?我听了一下说不知道,他说:...
425557078                                               幸福到疼……
有一个人曾让我知道                                                  NaN
寄生于世原来是那么的好                                                NaN
568096298                               超级喜欢的一首歌,偏偏喜欢你【大赞】[爱心]
412381224                                           他可能会单手开法拉利
301593622                              [多多大笑][多多大笑][多多大笑]笑着活下去
1455906926                                              再见吧大渣男
1670031755                                        [憨笑]我比你还俗 更穷
301593622                                   现在一心只想搞钱,我比较俗,太穷了!
1730181168                                          哈哈哈哈这个年代感哦
279684331                                          偏偏喜欢的就是得不到!
621542625                                                 阿Lan
...                                                        ...
62338668                                                  nice
62338668     愛上你永遠沒有結果 很好 我現在要放棄了 努力忘記你 不想讓自己為了一個沒結果的人那麼難受 ...
68320201     忽然想起和一个朋友的对话,她因为家庭原因极度缺乏安全感。\r\r\n我怕…\r\r\n你怕什...
246752393                                                  張國榮
134205876          还记得那时地方电视台点播的一些曲子吗?重点中间无广告,那才是乐趣。某某点歌给谁谁谁……
122104993    班固?《西都赋》“愿宾摅怀旧之蓄念,发思古之幽情。” 元稹?《赠吴渠州从姨兄士则》诗“泪因生...
100442347                  中午广播无意听到这首歌,然后特意关注了聊大广播台微博找到歌单![大哭]
118207115                                                 别咒王杰
116595775                        我爸只会唱一首歌,就这首歌把我妈娶到手了。[憨笑][憨笑]
60109810                                       2000年生却偏偏喜欢80年代
63357164                                       情爱 是什么 只有呵呵[大哭]
62338668                                                  [可爱]
95087236                                            对啊,为何偏偏喜欢你
68320201                                                  [大笑]
79282042                                          [皱眉][皱眉][皱眉]
83512955                                听过这首歌的人都是有故事的男人和女人[跳舞]
67061365       上班特别累的时候听到香港的经典感觉整个世界都属于我,要是来一杯纯正的香港奶茶就更好了。[大笑]
71401843                                        当年的录像厅前面就放的这首歌
103882075                                             他还没死[大哭]
62338668                                                  我01的
62338668                                                  nice
62338668     愛上你永遠沒有結果 很好 我現在要放棄了 努力忘記你 不想讓自己為了一個沒結果的人那麼難受 ...
68320201     忽然想起和一个朋友的对话,她因为家庭原因极度缺乏安全感。\r\r\n我怕…\r\r\n你怕什...
246752393                                                  張國榮
134205876          还记得那时地方电视台点播的一些曲子吗?重点中间无广告,那才是乐趣。某某点歌给谁谁谁……
122104993    班固?《西都赋》“愿宾摅怀旧之蓄念,发思古之幽情。” 元稹?《赠吴渠州从姨兄士则》诗“泪因生...
100442347                  中午广播无意听到这首歌,然后特意关注了聊大广播台微博找到歌单![大哭]
118207115                                                 别咒王杰
116595775                        我爸只会唱一首歌,就这首歌把我妈娶到手了。[憨笑][憨笑]
60109810                                       2000年生却偏偏喜欢80年代

[20523 rows x 1 columns]>

isnull检测是否含有NaN值,有就返回True。np.any()检测df数据中是否含有等于Ture的值

import numpy as np
print(np.any(data.isnull())== True)  
False

显示为TRUE,说明评论里有NAN值,用空str填充:

data=data.fillna(' ')   

设置训练集和测试集合:

from gensim import corpora, models, similarities
import jieba
# 文本集和搜索词
trains = list(data['context'].iloc[0:20523])
tests= '喜欢你'


6.4 对训练集进行分词

trains = [jieba.lcut(text) for text in trains]
trains

可以得到list形式的分词表:

[['都', '是', '有', '故事', '的', '人'],
 ['每天', '都', '要', '听', '至爱', 'Danny'],
 ['对', '呀', ',', '你', '就', '从来', '没', '喜欢', '过', '我', '。', '一点', '都', '煤油'],
 ['沁入', '心脾', '的', '温柔'],
 ['最近', '好', '喜欢', '这', '首歌'],
 ['今日', '点歌', '③', '.'],
 ['偏偏', '对', '你', '没有', '抵抗力'],..


6.5 提取词典特征数

dictionary = corpora.Dictionary(trains)
feature_cnt = len(dictionary.token2id)

查看字典里面的词语:

print(tfidf_vec.vocabulary_)

可见有在《偏偏喜欢你》的评论文本里一个有13651个词语。
即词典特征数为13651,feature_cnt的值。

Dictionary(13651 unique tokens: ['人', '故事', '是', '有', '的']...)

6.6 构建语料库

corpus = [dictionary.doc2bow(text) for text in trains]
corpus

我们得到的语料库形式为下,其中第一行评论['都', '是', '有', '故事', '的', '人']转化为了稀疏向量[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1)]。当我们使用doc2bow函数时,每一个词语会对应一个id,如:‘都’对应0,'是'对应1,'有'对应2,'故事'对应3,'的'对应4,'人'对应5, 后面的1代表在本句评论中,‘都’出现了一次,‘是’一次,‘有’一次,‘故事一次’......

[[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1)],
 [(5, 1), (6, 1), (7, 1), (8, 1), (9, 1), (10, 1)],
 [(5, 1),
  (11, 1),
  (12, 1),
  (13, 1),
  (14, 1),
  (15, 1),
  (16, 1),
  (17, 1),
  (18, 1),
  (19, 1),
  (20, 1),
  (21, 1),
  (22, 1),
  (23, 1)],
 [(4, 1), (24, 1), (25, 1), (26, 1)],
 [(16, 1), (27, 1), (28, 1), (29, 1), (30, 1)],...

6.7 使用TF-IDF模型处理语料库

tfidf = models.TfidfModel(corpus)
for i in tfidf[corpus]:
    print(i)

可以得到训练文本的TF-IDF值,以稀疏向量的形式保存:

[(0, 0.32184672062922548), (1, 0.73766014971005966), (2, 0.29497170498614139), (3, 0.29304380441485145), (4, 0.13870813169109303), (5, 0.40018654163321249)]
[(5, 0.19879141119607197), (6, 0.43291398548320181), (7, 0.1986860979407834), (8, 0.45262734719390507), (9, 0.68525962106956162), (10, 0.24318516977110011)]
[(5, 0.16060211474573619), (11, 0.098574217529621325), (12, 0.36067639144364333), (13, 0.42534037149410231), (14, 0.063581328755947963), (15, 0.20711773583505186), (16, 0.074812488712874464), (17, 0.248665060688224), (18, 0.16081534912048612), (19, 0.077507465039391904), (20, 0.27324683263788124), (21, 0.59516351798480016), (22, 0.27437250176801881), (23, 0.055533166748108702)]
[(4, 0.060614261626705232), (24, 0.64806776302591229), (25, 0.64806776302591229), (26, 0.39540139142475095)]
[(16, 0.1603400740935198), (27, 0.3795919475458821), (28, 0.78372267212523294), (29, 0.31693482880130575), (30, 0.33990013458011531)]
[(31, 0.22648623811135946), (32, 0.63332275807290517), (33, 0.50068929396716033), (34, 0.54490044866907528)]

6.8 将测试集转换为稀疏向量

kw_vector = dictionary.doc2bow(jieba.lcut(tests))
print(tfidf[kw_vector])

可以得到测试文本的TF-IDF值,以稀疏向量的形式保存:

[(14, 0.64759350572190955), (16, 0.76198599156861602)]

6.9 相似度计算

index = similarities.SparseMatrixSimilarity(tfidf[corpus], num_features=feature_cnt)
similarity = index[tfidf[kw_vector]]
for i in range(len(similarity)):
    print('tests 与 trains%d 相似度为:%.2f' % (i + 1, similarity[i]))

得到相似度为下:

tests 与 trains1 相似度为:0.00
tests 与 trains2 相似度为:0.00
tests 与 trains3 相似度为:0.10
tests 与 trains4 相似度为:0.00
tests 与 trains5 相似度为:0.12
tests 与 trains6 相似度为:0.00
tests 与 trains7 相似度为:0.06
tests 与 trains8 相似度为:0.04
tests 与 trains9 相似度为:0.09
tests 与 trains10 相似度为:0.15
tests 与 trains11 相似度为:0.00
tests 与 trains12 相似度为:0.00
tests 与 trains13 相似度为:0.00
tests 与 trains14 相似度为:0.00
tests 与 trains15 相似度为:0.00
tests 与 trains16 相似度为:0.14
tests 与 trains17 相似度为:0.00
tests 与 trains18 相似度为:0.09
tests 与 trains19 相似度为:0.00
tests 与 trains20 相似度为:0.00

将相似度保存到文件“sim.csv”中

similarity = list(sim)
sim_file = open("sim.csv",'w')
for i in similarity:
    sim_file.write(str(i)+'\n')
sim_file.close()
print(similarity)

将相似度转化为数据框,并降序排列:

similarity=pd.DataFrame(similarity)
similarity.iloc[np.argsort(-similarity.iloc[:,0]),:]

得到相似度最高的前18个:

    0
4536    1.000000
1803    0.784898
6485    0.784898
6134    0.784898
398        0.784898
1614    0.784898
8376    0.784898
3948    0.784898
1362    0.761986
9629    0.761986
5481    0.761986
324        0.761986
1632    0.761986
6405    0.761986
5091    0.761986
986        0.642364
9395    0.617391
3679    0.600096

查看相似度为1的评论:

data['context'].iloc[4536]

得到评论信息为:

'喜欢你'

同样,取得相似度为第2的评论信息:

'我喜欢你'

因此,我们得到与用户ID为76654704最相似的用户ID为380345546。

七、相似用户推荐(Python之sklearn版本)

7.1 sklearn

Scikit-learn (formerly scikits.learn) is a free software machine learning library for the Python programming language.It features various classification ,regression  and clustering  algorithms including support vector machines,random forests , gradient boosting, k-means and DBSCAN , and is designed to interoperate with the Python numerical and scientific libraries NumPy and SciPy.

7.2 文本预处理

导入之前处理过好的数据:

import os
os.chdir('G:\\项目\\网易云音乐评论\\文本挖掘')  
import pandas as pd
data = pd.read_csv(r"DealtedData.csv",encoding='gbk', sep=',',index_col=0,header=0)

分词:

import jieba
def chinese_word_cut(mytext):
    return " ".join(jieba.cut(mytext))
d=pd.DataFrame(data['context'].astype(str))
d["title_cutted"] = d['context'].apply(chinese_word_cut)
d.title_cutted.head()

可得分析结果为:

328901978                        都 是 有 故事 的 人
1724358802                  每天 都 要 听 至爱 Danny
451250610     对 呀 , 你 就 从来 没 喜欢 过 我 。 一点 都 煤油
554348857                          沁入 心脾 的 温柔
1296389446                       最近 好 喜欢 这 首歌
Name: title_cutted, dtype: object

接下来我们对这些本文做向量化,所谓文本向量化,指的就是形成一个28197(文档个数)*n(文本中所有词的数量)的0-1矩阵,特定词在这个文档出现记为1,否则为0。若选取所有词的话,这必然是一个很大的矩阵,因此在之前的操作中,本文从所有的词中选取了1000关键词。

from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
vectorizer = CountVectorizer()
count = vectorizer.fit_transform(d.title_cutted)
print(vectorizer.get_feature_names())  

查看所有文本的关键字:

[...'不是', '不是故意', '不曾', '不會', '不會來', '不服', '不服输', '不期而遇', '不来', '不染', '不欠', '不欢', '不欢而散', '不止', '不正', '不死', '不死心', '不求', '不浮', '不溜提', '不满', '不潮', '不点', '不烦', '不然', '不燥', '不爱', '不理', '不甘', '不甘示弱', '不生', '不用', '不用谢', '不由得', '不由自主', '不留', '不疼', '不痛', '不相误', '不看', '不眠', '不知', '不知不觉', '不知好歹', '不知情', '不知所措', '不知所谓', '不禁', '不离', '不算', '不算什么', '不管', '不管怎样', '不纯', '不经意', '不绕', '不羁', '不老', '不耐烦', '不肯', '不胜枚举', '不能', '不能不', '不能不要', '不能治愈', '不能自已', '不能自拔', '不腻', '不至于', '不舍', '不苦', '不蓝', '不行', '不衰', '不要', '不要脸', '不见', '不論', '不计', '不让', '不记', '不讲道理', '不许', '不论', '不论什么', '不识抬举', '不识曲', '不该', '不说', '不谢', '不负', '不败', '不起', '不足', '不躁', '不输给', '不辞而别', '不过', '不远', '不远千里', '不适', '不通', '不配', '不错', '不长', '不问', '不间断', '不需', '不靠', '不须', '不顾', '不高', '与其', '与其说是', '与否', '与子偕老', '与我无关', '与生俱来', '丑小鸭', '专一', '专业', '专业课', '专场', '专注', '专用', '专辑', '专门', '世上', '世事', '世人', '世俗', '世情', '世無雙', '世界', '世界杯', '世界观', '世纪', '世荣', '世间', '业生', '东京', '东北', '东南亚', '东方', '东方人', '东方文化', '东流', '东渡', '东皇钟', '东西', '东邪西毒', '东风破', '丝滑', '丢失', '丢掉', '丢脸', '两三次', '两个', '两人', '两人同', '两件', '两伙', '两位', '两位数', '两分钟', '两千块', '两千多', '两口', '两句', '两只', '两周', '两场', '两块钱', '两天', '两岁半', '两年', '两情相悦', '两月', '两条', '两栋', '两次', '两段', '两点', '两百天', '两百斤', '两盒', '两码事', '两种', '两秒', '两级', '两行', '两遍', '两集', '两面', '两首', '两首歌', '严厉', '严肃', '严谨', '严重', '丧失', '个个', '个人', '个人感觉', '个人观点', '个别', '个头', '个子', '个币', '个性', '个赞', '丫头', '丫点', '中会', '中医', '中午', '中华', '中华文化', '中华民谣', '中国', '中国香港', '中央', '中央空调', '中学', '中学毕业', '中层', '中山', '中岛美雪', '中带', '中广核', '中庙', '中式', '中心', '中意', '中文', '中文系', '中曲', '中有', '中泪点', '中环', '中秋', '中秋节', '中老年人', '中考', '中过', '中途', '中间', '中高音', '丰城', '临窗', '临近', '丹丹', '丹妮', '丹尼', '为了', '为什么', '为何', '为先', '为君', '为啥', '为国争光', '为数不多', '为止', '为此', '为活', '为琦琦', '为罗琦琦', '为萝莉', '为难', '主任', '主动', '主叫', '主唱', '主持人', '主时', '主流'...]

查看所有文本的关键字和其位置:

print(vectorizer.vocabulary_)
{...'故事': 6694, '每天': 7759, '至爱': 10103, 'danny': 252, '从来': 1885, '喜欢': 3777, '一点': 803, '煤油': 8436, '沁入': 7880, '心脾': 5636, '温柔': 8233, '最近': 7194, '首歌': 11920, '今日': 1863, '点歌': 8383, '偏偏': 2226, '没有': 7941, '抵抗力': 6369, '一次': 787, '我们': 6052, '擦肩而过': 6630, '害怕': 4776, '电影': 8799, '情景': 5858, '一样': 779, '错过': 11479, '所以': 6182, '一直': 826, '看着': 9095, '可是': 3350, '回头': 3848, '一眼': 828, '珍贵': 8683, '奈何': 4376, '珍惜': 8680, '王雷雷': 8642, '这是': 11085, '为什么': 1453, '浪漫': 8115, '小时候': 4912, '有个': 7208, '收音机': 6650, '好开心': 4462, '睡觉': 9183, '舍不得': 10111, '放下': 6662, '好听': 4444, '这些': 11063, '歌曲': 7650, '经典作品': 9647, '亲亲': 1735, '翩翩': 9789, '夫人': 4345, '爱情': 8485, '时光': 6969, '纪贯': 9594, '一家': 716, '吃饭': 3397, '店里': 5259, '知道': 9202, '什么': 1842, '一下': 607, '爱心': 8482, '幸福': 5221, 'nan': 444, '超级': 10823, '一首歌': 907, '大赞': 4234, '可能': 3361, '单手': 3036, '法拉利': 7987, '多多': 4104, '大笑': 4222, '活下去': 8045, '再见': 2488, '大渣': 4213,...}

查看词频矩阵的结果:

[[0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 ..., 
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]]

0充斥了整个矩阵,查看其维度:

count.shape
(20523, 12111)

20523行,12111列,指的是有20523个文档,即用户评论,语料库里一共有12111个词。查看其数据类型:

type(count)
scipy.sparse.csr.csr_matrix

妥妥的稀疏矩阵。

7.3 计算tf-idf值:

利用TfidfTransformer函数统计CountVectorizer中每个词语的tf-idf权值:

tfidf_vec = TfidfVectorizer() 
tfidf_matrix = tfidf_vec.fit_transform(d.title_cutted)
print(tfidf_matrix.toarray())

得到tf-idf数组:

[[ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 ..., 
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]]

剩余步骤类似于上,找对应函数,得到用户相似度。

八、相似用户推荐(R语言版本)

8.1 读取资料库

设置工作目录,导入函数包:

library(xml2,lib="G:\\R语言\\R语言学习\\安装包")
library(Rcpp,lib="G:\\R语言\\R语言学习\\安装包")
library(slam,lib="G:\\R语言\\R语言学习\\安装包")
library(NLP,lib="G:\\R语言\\R语言学习\\安装包")
library(tm,lib="G:\\R语言\\R语言学习\\安装包")
#只有RJava配置成功了,Rwordseg安装才可能成功,前者是后者的依赖包
#install.packages("rJava",lib="G:\\R语言\\R语言学习\\安装包")
library(rJava,lib="G:\\R语言\\R语言学习\\安装包")
#手动下载安装包Rwordseg,然后本地安装
library(Rwordseg,lib="G:\\R语言\\R语言学习\\安装包")


导入数据,其中,clipboard指的是HuXiu.txt用notepad打开后复制,防止中文乱码

csv <- read.table("clipboard",header=T, stringsAsFactors=F,quote = "",encoding="utf-8")
mystopwords<-unlist(read.table("StopWords.txt",stringsAsFactors=F,quote = ""))
head(csv)
dim(csv)
colnames(csv)<-c("text")

8.2 定义数据预处理函数

  • 移除数字函数
removeNumbers = function(x) { ret = gsub("[0-90123456789]","",x) }
  • segmentCN分词函数

中文分词,也可以考虑使用 rmmseg4j、rsmartcn 

wordsegment<- function(x) { 
  library(Rwordseg) 
  segmentCN(x)
} 
  • 去除停止词函数 
removeStopWords = function(x,words) {     
  ret = character(0) 
  index <- 1 
  it_max <- length(x) 
  while (index <= it_max) { 
    if (length(words[words==x[index]]) <1) ret <- c(ret,x[index]) 
    index <- index +1 
  } 
  ret 
} 

8.3移除数字

sample.words <- lapply(data[,1], removeNumbers) 
dim(as.matrix(sample.words))
head(sample.words)

8.4 中文分词

sample.words <- lapply(sample.words, wordsegment) 
dim(as.matrix(sample.words))
sample.words[1:6]


8.5 移除停止词

先处理中文分词,再处理 stopwords,防止全局替换丢失信息(下面这句运行时间较长 ):

sample.words <- lapply(sample.words, removeStopWords, mystopwords) 
head(sample.words)
text<-sample.words[,1]
colnames(sample.words)<-c("text")
write.csv(as.matrix(sample.words),"delateddata.csv")

8.6 构建语料库

corpus = Corpus(VectorSource(sample.words)) 
meta(corpus,"cluster") <- csv$type 
unique_type <- unique(csv$type) 
corpus

8.7建立文档-词条矩阵 

(sample.dtm <- DocumentTermMatrix(corpus, control = list(wordLengths = c(2, Inf))))

接下来,就是计算tf-idf值,相似度,得到相似用户,完成用户推荐。

九 网易云音乐年度总结

9.1 说明

灵感:新浪热搜#网易云音乐年度总结#,有这样的总结报告:

这份报告前些几日刷爆了我的朋友圈,我想的是,既然大家这么喜爱,那我就做一个呗。只要有数据,一切可行!

但是,有绝大部分数据网易云官方并没有提供给用户本人,而是存放到网易云官方数据库里。还有一部分只有用户本人可以看到,属于隐私信息。文本对无限制权限的选项做一些统计分析,权限列举如下:

  • 某首歌听了多少次(用户本人)
  • 某一天听某首歌特别多次(网易云官方)
  • 2018年年度歌手(网易云官方)
  • 每首歌多少次显示出来(网易云官方)
  • 播放前100歌曲(无限制)
  • 2018年你在网易云听到的最多的歌词(无限制)
  • 晚上12点6点之前以后仍在听歌的记录(网易云官方)
  • 听歌天数最多的歌曲(网易云官方)
  • 曾经喜欢现在冷落的歌(网易云官方)
  • 听了多少首歌(无限制)
  • 花了多少时间(网易云官方)

最后要做的也只能做的是图9.2:
一个用户播放记录里面歌词出现次数最多的词语
以及这个词语分别出现在哪首歌的哪句歌词中

注:后台时间记录是使用了13位时间戳,需要转化。13位的时间戳的默认精度是毫秒,10位时间戳的精度是秒。 由于时间、精力关系,9.2、9.3暂且放在这里,留着以后做或者删掉。

9.2 播放记录出现次数最多的词语

9.3 词语所对应的歌曲歌词

十、感想

码代码时,有一个很重要的点就是,你需要有推倒重来的勇气。当前面你已经做了很多工作时,你发现有的方法不太适用、有些繁杂、有更好的、简洁的、可以替代的、功能更全的solutions时,要想到:upgrade,才能不断跨越。

我很喜欢新鲜感,喜欢尝试一些有趣好玩的东西。潇潇洒洒一万五千余字,这篇文章花了我不少时间,暂停更。

附:

Github源码地址:https://github.com/striver6/WangyiyunMusic.git

Written By LXP

猜你喜欢

转载自blog.csdn.net/striver6/article/details/86743904