第一章 项目分析
###1.1 项目介绍
项目流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-236YV5rp-1604305644009)(https://style.youkeda.com/img/ham/course/j12/2.1-1.svg)]
一 项目需求:我们在网易云听歌的时候,经常会被下面的评论所打动。那么我们怎么才能知道每个歌手的歌曲评论中出现最频繁的词语是什么?
如图片示例
从上面这些词语中,我们可以很迅速的了解歌曲评论的大概内容,实际上,统计这些词语的过程也是目前最流行的大数据领域的一种应用。
二 功能描述
抓取单曲:采用爬虫+数据分析的方式,抓取指定歌手的热门单曲
抓取评论:我们可以看到张信哲的热门歌曲有50首,我再抓取每首歌的热门评论。
分词:我们纪录热门评论内容,对评论用工具分词,词汇出现的频率越高字体越大,呈现词云效果。
三 技术方案
梳理需求,然后整合成如下所示技术方案
1 自动查询某个歌手的所有热门歌曲
2 自动获取每一首歌的基础信息,热门信息
3 自动获取每一首歌热门评论,最新评论
4 对所有热门评论进行统计形成 词云
第二章 获取热门歌单
认识api
信息描述 信息说明
API地址 http://neteaseapi.youkeda.com:3000/artists?id=xxx
请求方式 GET
参数说明 id -歌手ID
从上面可以看出,只要我们有了歌手id,我们就 可以查询到歌手所有的单曲信息。
例如 我们找到周杰伦的id 6452,拼接好url http://neteaseapi.youkeda.com:3000/artists?id=6452 ,在网页中查找这个地址,我们
可以得到json格式的信息 从中,我们主要分析两个重要数据:
1 artist 是歌手的歌单(也叫“专辑”,这里统一称歌单)数据,里面包含了歌手信息,歌手名称,别名,简介,歌曲书,专辑数等
2 hotSongs 值的格式是:[],表示歌曲数据集合。一个歌单包含一组歌曲。每个歌曲有id,名称等属性。
开发阶段的核心步骤:
1需求分析:得到关键模型 歌曲和歌单
完成数据爬取需要搭配的服务 歌单服务接口和实现类 (模型+服务 是java通用的一种思路)
2概要设计:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fwm6HxVI-1604305644025)(https://style.youkeda.com/img/ham/course/j12/2-2-1.svg)]
注意:service提供服务接口, impl提供实现类 ,Model存放模型
3项目依赖:添加必要的依赖库
2.2模型设计
歌单及歌曲模型详细设计,分析api返回的对象
第一层是artist歌单,第二层是歌单的各个字段,我们来看看里面比较重要的字段
字段 含义
id 歌单的唯一id
alias 别名 明星的各种名字,花名艺名英文名等
picUrl 封面图
briefDesc 艺人介绍
img1v1Url 正方形封面图 展示歌单列表
name 艺人的名称
并列第一层是歌曲字段:歌曲中比较有用的字段
字段 含义
id 歌曲的id
name 歌曲的名字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OjZ0MD7t-1604305644026)(https://style.youkeda.com/img/ham/course/j12/2-2-2.svg)]
2.3服务设计
服务的接口和实现类,组成了操作数据的核心能力。
分析:
一 抓取:抓取歌单的办法
给其他类调用,应该定义为public
不需要返回值,只是抓取功能所以方法返回值定义为void
方法的作用是开始抓取任务,可以命名为start
方法的参数是字符串型的歌单ID,歌单属于某位歌手,我们命名为artistId
public void start(String artistId);
二 歌单
抓取歌单的目的是为了使用歌单,所以我们还要提供查询方法查询歌单,方法的参数也是歌单Id
public Artist getArtist(String artistId);
三 歌曲
歌单中有很多歌曲,我们还可以提供一个查询歌曲的办法,方法参数为歌单ID和歌曲ID
public Song getSong(String artistId, String songId);
制作模型图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9xXbOjby-1604305644028)(https://style.youkeda.com/img/ham/course/j12/2-2-3.svg)]
小知识:对于服务实现类,我们往往需要一个初始化实例变量的方法来提高扩展性.
2.4 爬取歌单实现步骤(一)
start()方法的实现思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97thkS6Z-1604305644029)(https://style.youkeda.com/img/ham/course/j12/2-2-4.svg)]
1 实现类中的start()方法按照如上顺序实现步骤,就可以实现歌单的抓取
2 封装为一个私有方法,是因为包含了一系列的子步骤。
2.5爬取歌单实现步骤(二)
构建填充了属性的artist实例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QO3KW4aQ-1604305644030)(https://style.youkeda.com/img/ham/course/j12/2-2-4-2.svg)]
在start方法中添加一个私有方法,方法的作用:在抓取的源数据对象中解析artist数据字段,创建歌单对象并填充属性值
2.6 爬取歌单实现步骤(三)
在start方法中构建填充了属性的song实例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8g2vmoIP-1604305644031)(https://style.youkeda.com/img/ham/course/j12/2-2-4-3.svg)]
在start方法中添加一个私有方法,方法的作用:解析hotSongs字段,创建歌曲集合,并为歌曲对象填充属性。
2.7 爬取歌单实现步骤(四)
回顾一下之前的流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpAgKj5I-1604305644032)(https://style.youkeda.com/img/ham/course/j12/2-2-4.svg)]
对于start方法,我们已经完成了前三个步骤,即完成了歌单的歌曲对象,我们来完成最后的两个步骤,将歌曲对象存入本地的数据仓库。
2.8 查询歌单和歌曲服务实现
我们来看看任务详细设计图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfrig4E0-1604305644033)(https://style.youkeda.com/img/ham/course/j12/2-2-3.svg)]
实现类的start方法已经完毕了,我们现在来看看剩下两个方法的完成。
getArtist方法 目的:根据歌单id查询歌单对象
这个比较简单,只需要用参数artistId 从本地 artists取出歌单对象即可
getSong方法:根据歌曲id查询歌曲对象
我们可以通过第一个参数获取歌单对象,然后再遍历歌单中的歌曲,找到id相同的歌曲返回即可。
第三章 歌曲信息
本章的目标 :1 获取每首歌的详细信息:音乐文件地址 所在专辑 所属歌手
2 获取每首歌的热门评论和最新的10条评论
3.1 歌曲详细信息
我们先来看看一首歌的详细信息
信息描述 信息说明
api地址 http://neteaseapi.youkeda.com:3000/song/detail?ids=xxx,xxx
请求方式 GET
参数说明 ids - 歌曲id,如果查询多个歌曲id之间用逗号隔开
获取每首歌的评论
信息描述 信息说明
api地址 http://neteaseapi.youkeda.com:3000/comment/music?id=xxx&limit=xxx
请求方式 GET
参数说明 id - 歌曲id limit - 评论条数
获取每首歌的音乐文件地址
信息描述 信息说明
api地址 http://neteaseapi.youkeda.com:3000/song/url?id=xxx,xxx
请求方式 GET
参数说明 id - 歌曲id,如果查询多个歌曲id之间用逗号隔开,注意不是ids
模型设计:由于业务需求,我们之前使用的歌曲模型需要扩展属性。
我们新增加了用户模型(user)评论模型(comment)专辑模型(album)并且将歌曲模型进行了扩展 如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xVypgcFa-1604305644035)(https://style.youkeda.com/img/ham/course/j12/3-1-1-1.svg)]
3.2服务设计
当我们完成歌曲模型的拓展之后就要开始相关接口和方法的设计了。
我们在抓取歌单的时候,已经抓取到歌曲的基础信息了,那么我们只需要在抓取过程中拓展,抓取歌曲的详细信息和评论信息即可。
我们来看看uml图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9TadSzT-1604305644036)(https://style.youkeda.com/img/ham/course/j12/3-2-1-1.svg)]
我们来看看完成过程:
1 我们把start() 方法里完成的抓取步骤,挪到 initArtistHotSongs() 中,作为歌曲和歌单初始化的方法。
2 增加三个新的装配方法:assembleSongDetail() 组装歌曲的详细信息
assembleSongComment() 组装歌曲的评论
assembleSongUrl() 组装歌曲的音乐文件地址
3 start() 方法的作为装配工,按顺序调用各个步骤:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TAvB1rm-1604305644037)(https://style.youkeda.com/img/ham/course/j12/3-2-2.svg)]
4 start() 方法变化较大,相当于做了一次重构
上面的过程相当于是把代码进行了一次重构,使得代码更加清晰,更容易理解和维护(把相对独立的操作封装起来)
什么是重构?
在不改变代码接口的情况下,对代码作出修改,以改进程序的内部结构。本质上说,重构就是在代码写好之后改进它的设计。
重构有什么好处?
1 提高代码质量(性能 可读性 可重用性)
2 修改bug
3 增加新功能
3.3歌曲详细信息
我们来回忆一下爬取歌单,爬取评论 爬取歌曲信息,大概都是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QuO4cxt9-1604305644038)(https://style.youkeda.com/img/ham/course/j12/3-3-1.svg)]
我们再来看看歌曲详细信息的api:
信息描述 信息说明
api地址 http://neteaseapi.youkeda.com:3000/song/detail?ids=xxx,xxx
请求方式 GET
参数说明 ids - 歌曲id,如果查询多个歌曲id之间用逗号隔开
我们发现它的参数ids要复杂一些,所以我们必须要组装ids的值。组装时,我们要把歌单遍历,然后找出所有的歌曲Id.
小技巧:String sIdsParam = String.join(",", songIds);这个用法可以使一个歌单中所有歌曲的id,组装成用逗号分割的字符串。
这里我们还要重构一个方法(private Map getArtistObj(String artistId))来使得爬取歌曲详情时候复用,避免重复代码。
调用歌曲详情 API 返回的结果中,歌曲存放在一个集合中,使用起来不方便。可以把源歌曲详情对象放在一个 Map 中,可以很方便的读取.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFrynkMH-1604305644039)(https://style.youkeda.com/img/ham/course/j12/3-2-1.svg)]
3.4歌曲评论及音乐文件
抓取信息的实现:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wrEcO2qI-1604305644040)(https://style.youkeda.com/img/ham/course/j12/3-3-1.svg)]
我们来看看api是否支持批量参数的区别:
如果支持(歌曲详情API、歌曲文件API):就要选调用API获取批量数据,再对歌曲进行循环遍历,解析出需要的源数据
如果不支持(歌曲评论API):先对歌曲进行循环遍历,每次遍历都调用一次 API ,再解析数据。这么做效率比较低
方法重构:歌曲详情API、歌曲文件API 都支持批量查询,对应的 assembleSongDetail() 方法 、 assembleSongUrl() 方法,
都需要组装 xxxx,xxxx格式的参数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jr2QytIr-1604305644042)(https://style.youkeda.com/img/ham/course/j12/3-2-1.svg)]
这一节我们就开发完剩下的assembleSongComment() 和 assembleSongUrl() 方法,并且完成方法的重构。
第四章 图云
我们在前面三章抓取了歌单信息,最后一章我们来把它组成图云,类似下图。
图云实现的理论思路:1 先对评论进行分词
2 把分词以后关键词出现的次数进行统计排序
3 根据关键词的频率进行视觉显示
但是上面的思路实现起来十分困难,设计到非常复杂的技术(词性标注,自然语言分析,歧义词分析,图片绘制,图文智能排版等),所以
我们可以使用别人已经写好的工具:kumo,使用时添加依赖即可。
<dependency>
<groupId>com.kennycason</groupId>
<artifactId>kumo-core</artifactId>
<version>1.17</version>
</dependency>
<!-- 下面tokenizers是为了中文分词引入 -->
<dependency>
<groupId>com.kennycason</groupId>
<artifactId>kumo-tokenizers</artifactId>
<version>1.17</version>
</dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VjB0B96s-1604305644044)(https://style.youkeda.com/img/ham/course/j12/4-1-1-1.svg)]
我们参照流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9O9PQxy-1604305644045)(https://style.youkeda.com/img/ham/course/j12/4-1-2.svg)]
第五章 代码
我们把代码贴在下面 注意不同的类和包的位置 我们使用idea创建maven工程
最后只需要在测试类 输入歌手的id和歌曲的id(网易云音乐的地址栏中可以找到)我们就能自动生成热门歌曲的图云了,当然还可以用一些if条件做验证这里就不验证了
模型类
1 Album
package com.youkeda.music.model;
/**
*
*/
public class Album {
private String id;
private String name;
private String picUrl;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPicUrl() {
return picUrl;
}
public void setPicUrl(String picUrl) {
this.picUrl = picUrl;
}
}
2 Artist
package com.youkeda.music.model;
import java.util.List;
/**
* 歌单对象
*/
public class Artist {
private String id;
private List<String> alias;
private String picUrl;
private String briefDesc;
private String img1v1Url;
private String name;
// 包含一组歌曲
private List<Song> songList;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getAlias() {
return alias;
}
public void setAlias(List<String> alias) {
this.alias = alias;
}
public String getPicUrl() {
return picUrl;
}
public void setPicUrl(String picUrl) {
this.picUrl = picUrl;
}
public String getBriefDesc() {
return briefDesc;
}
public void setBriefDesc(String briefDesc) {
this.briefDesc = briefDesc;
}
public String getImg1v1Url() {
return img1v1Url;
}
public void setImg1v1Url(String img1v1Url) {
this.img1v1Url = img1v1Url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Song> getSongList() {
return songList;
}
public void setSongList(List<Song> songList) {
this.songList = songList;
}
}
3 Comment
package com.youkeda.music.model;
/**
*
*/
public class Comment {
private String id;
private String content;
private String likedCount;
private String time;
private User commentUser;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getLikedCount() {
return likedCount;
}
public void setLikedCount(String likedCount) {
this.likedCount = likedCount;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public User getCommentUser() {
return commentUser;
}
public void setCommentUser(User commentUser) {
this.commentUser = commentUser;
}
}
4 Song
package com.youkeda.music.model;
import java.util.List;
/**
* 歌曲对象
*/
public class Song {
private String id;
private String name;
private List<User> singers;
private String sourceUrl;
private Album album;
List<Comment> hotComments;
List<Comment> comments;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSourceUrl() {
return sourceUrl;
}
public void setSourceUrl(String sourceUrl) {
this.sourceUrl = sourceUrl;
}
public Album getAlbum() {
return album;
}
public void setAlbum(Album album) {
this.album = album;
}
public List<Comment> getHotComments() {
return hotComments;
}
public void setHotComments(List<Comment> hotComments) {
this.hotComments = hotComments;
}
public List<Comment> getComments() {
return comments;
}
public void setComments(List<Comment> comments) {
this.comments = comments;
}
public List<User> getSingers() {
return singers;
}
public void setSingers(List<User> singers) {
this.singers = singers;
}
}
5 User
package com.youkeda.music.model;
/**
*
*/
public class User {
private String id;
private String nickName;
private String avatar;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
}
接口和实现类
1 接口类
package com.youkeda.music.service;
import com.youkeda.music.model.Artist;
import com.youkeda.music.model.Song;
/**
* 音乐抓取服务
*/
public interface SongCrawlerService {
/**
* 根据歌单id,抓取歌单数据
*/
public void start(String artistId);
/**
* 根据歌单id查询歌单对象
*/
public Artist getArtist(String artistId);
/**
* 根据歌曲id查询歌曲对象
*/
public Song getSong(String artistId, String songId);
}
2实现类
package com.youkeda.music.service.impl;
import com.alibaba.fastjson.JSON;
import com.youkeda.music.model.Album;
import com.youkeda.music.model.Artist;
import com.youkeda.music.model.Comment;
import com.youkeda.music.model.Song;
import com.youkeda.music.model.User;
import com.youkeda.music.service.SongCrawlerService;
import com.youkeda.music.util.WordCloudUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
/**
* 音乐抓取服务的实现
*/
public class SongCrawlerServiceImpl implements SongCrawlerService {
// 歌单 API
private static final String ARTIEST_API_PREFIX = "http://neteaseapi.youkeda.com:3000/artists?id=";
// 歌曲详情 API
private static final String S_D_API_PREFIX = "http://neteaseapi.youkeda.com:3000/song/detail?ids=";
// 歌曲评论 API
private static final String S_C_API_PREFIX = "http://neteaseapi.youkeda.com:3000/comment/music?id=";
// 歌曲音乐文件 API
private static final String S_F_API_PREFIX = "http://neteaseapi.youkeda.com:3000/song/url?id=";
// okHttpClient 实例
private OkHttpClient okHttpClient;
// 歌单数据仓库
private Map<String, Artist> artists;
private void init() {
//1. 构建 okHttpClient 实例
okHttpClient = new OkHttpClient();
artists = new HashMap<>();
}
@Override
public void start(String artistId) {
// 参数判断,未输入参数则直接返回
if (artistId == null || artistId.equals("")) {
return;
}
// 执行初始化
init();
// 初始化歌曲及歌单
initArtistHotSongs(artistId);
assembleSongDetail(artistId);
assembleSongComment(artistId);
assembleSongUrl(artistId);
generateWordCloud(artistId);
}
@Override
public Artist getArtist(String artistId) {
return artists.get(artistId);
}
@Override
public Song getSong(String artistId, String songId) {
Artist artist = artists.get(artistId);
List<Song> songs = artist.getSongList();
if (songs == null) {
return null;
}
for (Song song : songs) {
if (song.getId().equals(songId)) {
return song;
}
}
return null;
}
@SuppressWarnings("unchecked")
private Map getSourceDataObj(String prefix, String postfix) {
// 构建歌单url
String aUrl = prefix + postfix;
// 调用 okhttp3 获取返回数据
String content = getPageContentSync(aUrl);
// 反序列化成 Map 对象
Map returnData = JSON.parseObject(content, Map.class);
return returnData;
}
@SuppressWarnings("unchecked")
private Artist buildArtist(Map returnData) {
// 从 Map 对象中取得 歌单 数据。歌单也是一个子 Map 对象。
Map artistData = (Map) returnData.get("artist");
Artist artist = new Artist();
artist.setId(artistData.get("id").toString());
if (artistData.get("picUrl") != null) {
artist.setPicUrl(artistData.get("picUrl").toString());
}
artist.setBriefDesc(artistData.get("briefDesc").toString());
artist.setImg1v1Url(artistData.get("img1v1Url").toString());
artist.setName(artistData.get("name").toString());
artist.setAlias((List) artistData.get("alias"));
return artist;
}
private List<Song> buildSongs(Map returnData) {
// 从 Map 对象中取得一组 歌曲 数据
List songsData = (List) returnData.get("hotSongs");
List<Song> songs = new ArrayList<>();
for (int i = 0; i < songsData.size(); i++) {
Map songData = (Map) songsData.get(i);
Song songObj = new Song();
songObj.setId(songData.get("id").toString());
songObj.setName(songData.get("name").toString());
songs.add(songObj);
}
return songs;
}
/**
* 根据输入的url,读取页面内容并返回
*/
private String getPageContentSync(String url) {
//2.定义一个request
Request request = new Request.Builder().url(url).build();
//3.使用client去请求
Call call = okHttpClient.newCall(request);
String result = null;
try {
//4.获得返回结果
result = call.execute().body().string();
System.out.println("call " + url + " , content's size=" + result.length());
} catch (IOException e) {
System.out.println("request " + url + " error . ");
e.printStackTrace();
}
return result;
}
private void initArtistHotSongs(String artistId) {
// 取得整体数据对象。
Map returnData = getSourceDataObj(ARTIEST_API_PREFIX, artistId);
// 构建填充了属性的 Artist 实例
Artist artist = buildArtist(returnData);
// 构建一组填充了属性的 Song 实例
List<Song> songs = buildSongs(returnData);
// 歌曲填入歌单
artist.setSongList(songs);
// 存入本地
artists.put(artist.getId(), artist);
}
@SuppressWarnings("unchecked")
private void assembleSongDetail(String artistId) {
Artist artist = getArtist(artistId);
// 取不到歌单说明参数输入错误
if (artist == null) {
return;
}
// 删除其它语句,保留必要的语句
List<Song> songs = artist.getSongList();
String sIdsParam = buildManyIdParam(songs);
// 抓取结果
Map songsDetailObj = getSourceDataObj(S_D_API_PREFIX, sIdsParam);
// 原始数据中的 songs 是歌曲列表
List<Map> sourceSongs = (List<Map>) songsDetailObj.get("songs");
// 临时的 Map
Map<String, Map> sourceSongsMap = new HashMap<>();
// 遍历歌曲列表
for (Map songSourceData : sourceSongs) {
String sId = songSourceData.get("id").toString();
// 原始歌曲数据对象放入一个临时的 Map 中
sourceSongsMap.put(sId, songSourceData);
}
// 再次遍历歌单中的歌曲,填入详情数据
for (Song song : songs) {
String sId = song.getId();
// 从临时的Map中取得对应的歌曲源数据,使用id直接获取,比较方便
Map songSourceData = sourceSongsMap.get(sId);
// 源歌曲数据中,ar 字段是歌手列表
List<Map> singersData = (List<Map>) songSourceData.get("ar");
// 歌手集合
List<User> singers = new ArrayList<>();
for (Map singerData : singersData) {
// 歌手对象
User singer = new User();
singer.setId(singerData.get("id").toString());
singer.setNickName(singerData.get("name").toString());
// 歌手集合放入歌手对象
singers.add(singer);
}
// 歌手集合放入歌曲
song.setSingers(singers);
// 专辑
Map albumData = (Map) songSourceData.get("al");
Album album = new Album();
album.setId(albumData.get("id").toString());
album.setName(albumData.get("name").toString());
if (albumData.get("picUrl") != null) {
album.setPicUrl(albumData.get("picUrl").toString());
}
// 专辑对象放入歌曲
song.setAlbum(album);
}
}
@SuppressWarnings("unchecked")
private void assembleSongComment(String artistId) {
Artist artist = getArtist(artistId);
// 取不到歌单说明参数输入错误
if (artist == null) {
return;
}
List<Song> songs = artist.getSongList();
for (Song song : songs) {
String sIdsParam = song.getId() + "&limit=5";
// 抓取结果
Map songsCommentObj = getSourceDataObj(S_C_API_PREFIX, sIdsParam);
// 热门评论列表
List<Map> hotCommentsObj = (List<Map>) songsCommentObj.get("hotComments");
// 热门评论列表
List<Map> commontsObj = (List<Map>) songsCommentObj.get("comments");
song.setHotComments(buildComments(hotCommentsObj));
song.setComments(buildComments(commontsObj));
}
}
@SuppressWarnings("unchecked")
private void assembleSongUrl(String artistId) {
Artist artist = getArtist(artistId);
// 取不到歌单说明参数输入错误
if (artist == null) {
return;
}
// 删除其它语句,保留必要的语句
List<Song> songs = artist.getSongList();
String sIdsParam = buildManyIdParam(songs);
// 抓取结果
Map songsFileObj = getSourceDataObj(S_F_API_PREFIX, sIdsParam);
// 原始数据中的 data 是音乐文件列表
List<Map> datas = (List<Map>) songsFileObj.get("data");
// 临时的 Map
Map<String, Map> sourceSongsMap = new HashMap<>();
// 遍历音乐文件列表
for (Map songFileData : datas) {
String sId = songFileData.get("id").toString();
// 原始音乐文件数据对象放入一个临时的 Map 中
sourceSongsMap.put(sId, songFileData);
}
// 再次遍历歌单中的歌曲,填入音乐文件URL
for (Song song : songs) {
String sId = song.getId();
// 从临时的Map中取得对应的音乐文件源数据,使用id直接获取,比较方便
Map songFileData = sourceSongsMap.get(sId);
// 源音乐文件数据中,url 字段就是文件地址
if (songFileData != null && songFileData.get("url") != null) {
String songFileUrl = songFileData.get("url").toString();
song.setSourceUrl(songFileUrl);
}
}
}
private void generateWordCloud(String artistId) {
Artist artist = getArtist(artistId);
// 取不到歌单说明参数输入错误
if (artist == null) {
return;
}
List<Song> songs = artist.getSongList();
List<String> contents = new ArrayList<>();
for (Song song : songs) {
// 遍历歌曲所有的评论,包括普通评论和热门评论,把评论内容字符串存入集合
collectContent(song.getComments(), contents);
collectContent(song.getHotComments(), contents);
}
// 制作词云
WordCloudUtil.generate(artistId, contents);
}
private void collectContent(List<Comment> comments, List<String> contents) {
for (Comment comment : comments) {
contents.add(comment.getContent());
}
}
private List<Comment> buildComments(List<Map> commontsObj) {
List<Comment> comments = new ArrayList<>();
for (Map sourceComment : commontsObj) {
Comment commont = new Comment();
commont.setContent(sourceComment.get("content").toString());
commont.setId(sourceComment.get("commentId").toString());
commont.setLikedCount(sourceComment.get("likedCount").toString());
commont.setTime(sourceComment.get("time").toString());
User user = new User();
Map sourceUserData = (Map) sourceComment.get("user");
user.setId(sourceUserData.get("userId").toString());
user.setNickName(sourceUserData.get("nickname").toString());
user.setAvatar(sourceUserData.get("avatarUrl").toString());
commont.setCommentUser(user);
comments.add(commont);
}
return comments;
}
private String buildManyIdParam(List<Song> songs) {
// 收集一个歌单中所有歌曲的id,放入一个list
List<String> songIds = new ArrayList<>();
for (Song song : songs) {
songIds.add(song.getId());
}
// 一个歌单中所有歌曲的id,组装成用逗号分割的字符串,形如:347230,347231。记住这个用法,很方便
String sIdsParam = String.join(",", songIds);
return sIdsParam;
}
}
测试类
package com.youkeda.music.test;
import com.youkeda.music.model.Artist;
import com.youkeda.music.model.Song;
import com.youkeda.music.service.SongCrawlerService;
import com.youkeda.music.service.impl.SongCrawlerServiceImpl;
/**
* 检查服务是否可以正确返回对象
*/
public class SongCrawlerTest {
private static final String SA_DING_DING = "宋东野";
private static final String A_ID = "5073";
private static final String ZUO_SHOU_ZHI_YUE = "平淡日子里的刺";
private static final String S_ID = "27808295";
public static void main(String[] args) {
SongCrawlerService songService = new SongCrawlerServiceImpl();
songService.start(A_ID);
Artist artist = songService.getArtist(A_ID);
System.out.println("歌单名称:" + artist.getName());
Song song = songService.getSong(A_ID, S_ID);
System.out.println("歌曲名称:" + song.getName());
System.out.println("歌曲所属专辑名称:" + song.getAlbum().getName());
System.out.println("歌曲的歌手名称:" + song.getSingers().get(0).getNickName());
System.out.println("歌曲音乐为文件地址:" + song.getSourceUrl());
System.out.println("歌曲热门评论:" + song.getHotComments().get(0).getContent());
System.out.println("歌曲服务运行成功。非常棒!");
System.exit(0);
}
}
工具类
package com.youkeda.music.util;
import com.kennycason.kumo.CollisionMode;
import com.kennycason.kumo.WordCloud;
import com.kennycason.kumo.WordFrequency;
import com.kennycason.kumo.bg.CircleBackground;
import com.kennycason.kumo.font.FontWeight;
import com.kennycason.kumo.font.KumoFont;
import com.kennycason.kumo.font.scale.SqrtFontScalar;
import com.kennycason.kumo.nlp.FrequencyAnalyzer;
import com.kennycason.kumo.nlp.tokenizers.ChineseWordTokenizer;
import com.kennycason.kumo.palette.ColorPalette;
import java.awt.Color;
import java.awt.Dimension;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
/**
* 生成图云的工具类
*/
public class WordCloudUtil {
/**
* 生成词云
*
* @param artistId 歌单id
* @param texts 文本
*/
public static void generate(String artistId, List<String> texts) {
FrequencyAnalyzer frequencyAnalyzer = new FrequencyAnalyzer();
//设置返回的词数
frequencyAnalyzer.setWordFrequenciesToReturn(500);
//设置返回的词语最小出现频次
frequencyAnalyzer.setMinWordLength(4);
//引入中文解析器
frequencyAnalyzer.setWordTokenizer(new ChineseWordTokenizer());
//输入文章数据,进行分词
final List<WordFrequency> wordFrequencyList = frequencyAnalyzer.load(texts);
//设置图片分辨率大小
Dimension dimension = new Dimension(600, 600);
//此处的设置采用内置常量即可,生成词云对象
WordCloud wordCloud = new WordCloud(dimension, CollisionMode.PIXEL_PERFECT);
//设置边界及字体
wordCloud.setPadding(2);
// 设置字体,字体必须支持中文,不能随便改
wordCloud.setKumoFont(new KumoFont("阿里巴巴普惠体 Light", FontWeight.PLAIN));
//设置词云显示的三种颜色,越靠前设置表示词频越高的词语的颜色
wordCloud.setColorPalette(
new ColorPalette(new Color(0x4055F1), new Color(0x408DF1), new Color(0x40AAF1),
new Color(0x40C5F1), new Color(0x40D3F1), new Color(0xFFFFFF)));
wordCloud.setFontScalar(new SqrtFontScalar(10, 70));
//设置背景图层为圆形
wordCloud.setBackground(new CircleBackground(300));
//生成词云
wordCloud.build(wordFrequencyList);
//输出到图片文件,用当前的毫秒数作为文件名
Long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
//输出到图片文件
wordCloud.writeToFile("wordCloud-" + artistId + ".png");
}
}
依赖配置文件 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.youkeda.course</groupId>
<artifactId>app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<resource.delimiter>@</resource.delimiter>
<maven.compiler.source>${java.version}</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>com.kennycason</groupId>
<artifactId>kumo-core</artifactId>
<version>1.17</version>
</dependency>
<!-- 下面tokenizers是为了中文分词引入 -->
<dependency>
<groupId>com.kennycason</groupId>
<artifactId>kumo-tokenizers</artifactId>
<version>1.17</version>
</dependency>
</dependencies>
</project>