目录
效果展示(爬取数据)
词云展示
项目流程
歌单信息
1. 自动查询某个歌手的所有热门歌曲
2. 自动获取每一首歌的基础信息,专辑信息
3. 自动获取每一首歌的热门评论,最新评论
4. 对所有的热门评论进行统计形成词云
开发阶段核心步骤
根据设计图创建类,pom.xml添加依赖
1. 需求分析(模型+服务):
从以上对歌单数据的分析,我们可以得出关键模型(定义对象及其属性)是:
- 歌单
- 歌曲
要完成数据爬取,需要搭配的服务(定义操作、行为)是:
- 歌单服务接口
- 歌单服务实现类
2. 概要设计
设计规范:
service包存放服务接口,其子包imp1存放服务实现类。
model包存放模型。
3. 项目依赖
<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>
</dependencies>
模型设计
字段 | 作用、含义 |
id | 歌单的唯一id |
alias | 别名,包括英文名、艺名等 |
picUrl | 封面图 |
briefDesc | 艺人介绍 |
img1v1Url | 正方形封面图。适合展示歌单列表等场景 |
name | (艺人的)名称 |
模型图:
/**
● 歌单对象
*/
public class Artist { //表示一个歌手对象
private String id; //歌手的唯一标识符
private List alias; //歌手的别名列表
private String picUrl; //歌手的图片URL
private String briefDesc; //歌手的简介
private String img1v1Url; //歌手的小头像URL
private String name; //歌手的名字
// 储存歌手的歌曲列表,包含一组歌曲
private List songList;
/**
● 歌曲对象
*/
public class Song { //一首歌曲对象
private String id;
private String name;
private List singers; //歌曲的歌手列表
private String sourceUrl; //歌曲的资源URL
private Album album; //歌曲的专辑对象
private List hotComments;
private List comments;
服务设计
抓取:
项目目标是提供抓取歌单的服务,给其它类调用,是一个通用能力,让
具体的业务根据需要抓取歌单。那么就需要抓取歌单数据的方法。
- 既然是给其它类调用,那么应该定义成pub1ic
- 这个方法不需要返回值,只是提供抓取功能,所以方法返回值定义为void
- 方法的作用是开始执行抓取任务,可以命名为start注意方法名一定要代表功能含义,让别人阅读时能理解是做什么的
- 方法的参数当然是字符串类型的歌单D。本章第一节讲了,歌单其实是属于某位歌手的,所以这里命名为artistId
public void start(String artistId);
歌单:
抓取歌单的目的,是使用歌单。所以还需要提供查询方法查询歌单。方
法的参数也是歌单ID。
public Artist getArtist(String artistId);
歌曲:
歌单中有很多歌曲,还可以提供一个查询歌曲的方法。虽然歌单模型中已经包含歌曲了,但是提供一个通用、简单、易用的方法也是有意义的。方法参数是歌单ID和歌曲ID。
public Song getSong(String artistId, String songId);
/**
● 音乐抓取服务
*/
public interface SongCrawlerService { //定义名为songCrawlerService的接口(interface)
public void start(String artistId); //根据歌单id,抓取歌单数据
public Artist getArtist(String artistId); //根据歌单id查询歌单对象
public Song getSong(String artistId, String songId); //根据歌曲id查询歌曲对象
}
// 歌单数据仓库
private Map<String, Artist> artists; //Map类型的变量artists,用于保存歌单
//Map 是一种键值映射的数据结构,可以方便地根据键来查找对应的值
爬取歌单服务实现步骤
start方法实现
- 在SongCrawlerServiceImpl类的start()方法中按顺序实现这些步骤,就实现了歌单的抓取操作;
- 封装为一个私有方法,是因为包含了一系列子步骤:
- 因为这些方法不需要暴露给其他类调用的,所以用private修饰符;
@Override
public void start(String artistId) {
// 空字符串或者内容为空,则表示未输入参数
if (artistId == null || artistId.equals("")) {
return;
}
// 执行初始化
init();
// 初始化歌曲及歌单
initArtistHotSongs(artistId);
assembleSongDetail(artistId);
assembleSongComment(artistId);
assembleSongUrl(artistId);
generateWordCloud(artistId);
}
取得整体数据对象
// 歌单 API
private static final String ARTIEST_API_PREFIX = "http://neteaseapi.youkeda.com:3000/artists?id=";
//声明okHttpClient实例,用于发送HTTP请求
private OkHttpClient okHttpClient; //okHttpClient是一个网络请求库,可以用于发送HTTP请求并获取响应
// 歌单数据仓库
private Map<String, Artist> artists; //Map类型的变量artists,用于保存歌单
//Map 是一种键值映射的数据结构,可以方便地根据键来查找对应的值
构建填充属性的Artist实例
@Override
public Artist getArtist(String artistId) { //获取指定歌手ID对应的歌手对象
return artists.get(artistId);
}
构建一组填充了属性的Song实例
@Override
public Song getSong(String artistId, String songId) {
Artist artist = artists.get(artistId); //获取指定歌手ID对应的歌手对象,并赋值给artist
List<Song> songs = artist.getSongList(); //获取歌手对象的歌曲列表,赋值给songs
if (songs == null) { //歌手没有歌曲列表
return null;
}
for (Song song : songs) { //循环遍历songs列表中的每一首歌曲
if (song.getId().equals(songId)) { //判断当前的ID与传入的ID相等,则找到对应的歌曲
return song;
}
}
return null;
}
歌曲信息
歌曲详情信息(歌曲相关API)
// 歌曲详情 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=";
模型设计
/**
* 歌曲对象
*/
@Data
public class Song { //一首歌曲对象
private String id;
private String name;
private List<User> singers; //歌曲的歌手列表
private String sourceUrl; //歌曲的资源URL
private Album album; //歌曲的专辑对象
private List<Comment> hotComments;
private List<Comment> comments;
/**
歌曲评论
*/
@Data
public class Comment {
private String id;
private String content;
private String likedCount; //表示评论被点赞的次数
private String time; //表示评论的发表时间
private User commentUser; //发表该评论的用户对象
}
@Data
public class Album { //表示一个音乐专辑对象
private String id; //音乐专辑的唯一标识符
private String name; //音乐专辑的名称
private String picUrl; //音乐专辑的封面URL
}
/**
*储存一个用户的相关信息
*/
@Data
public class User { //表示一个用户对象
private String id;
private String nickName; //用户昵称
private String avatar; //用户头像
}
服务设计
- 把原来start()方法里完成的抓取步骤,挪到initArtistHotSongs()中,作为歌曲和歌单初始化的方法。
- 增加三个装配方法:
assembleSongDetail()组装歌曲的详细信息
assembleSongComment()组装歌曲的评论
assembleSongUrl()组装歌曲的音乐文件地址
start()方法的作为装配工,按顺序调用各个步骤:
@Override
public void start(String artistId) {
// 空字符串或者内容为空,则表示未输入参数
if (artistId == null || artistId.equals("")) {
return;
}
// 执行初始化
init();
// 初始化歌曲及歌单
initArtistHotSongs(artistId);
assembleSongDetail(artistId);
assembleSongComment(artistId);
assembleSongUrl(artistId);
//用于初始化一个艺术家的热门歌曲信息
private void initArtistHotSongs(String artistId) {
//传入艺术家API的前缀和ID,获取Map对象returnData
Map returnData = getSourceDataObj(ARTIEST_API_PREFIX, artistId);
//构建一个填充了属性的Artist实例artist
Artist artist = buildArtist(returnData);
List<Song> songs = buildSongs(returnData);
artist.setSongList(songs);
artists.put(artist.getId(), artist);//将artist的ID作为键,artist实例作为值,存入名为artist的Map中
}
服务实现
@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); //获取一个包含歌曲详细信息(API地址前缀、参数)的Map对象
List<Map> sourceSongs = (List<Map>) songsDetailObj.get("songs"); //获取原始数据中的歌曲列表
Map<String, Map> sourceSongsMap = new HashMap<>(); //创建临时的Map对象sourceSongsMap
for (Map songSourceData : sourceSongs) { //对每首歌的原始数据进行处理
String sId = songSourceData.get("id").toString(); //获取歌曲ID,转换为字符串类型
sourceSongsMap.put(sId, songSourceData); //原始歌曲数据对象放入一个临时的Map中
}
for (Song song : songs) { //再次遍历歌单中的歌曲,填入详情数据
String sId = song.getId(); //对于每首歌曲,获取它的ID
Map songSourceData = sourceSongsMap.get(sId);//从临时的Map中取得对应的歌曲源数据,使用id直接获取,比较方便
List<Map> singersData = (List<Map>) songSourceData.get("ar");//源歌曲数据中,ar字段是歌手列表
List<User> singers = new ArrayList<>();//创建List<User>集合singers,储存歌曲的所有歌手
for (Map singerData : singersData) {
User singer = new User(); //创建歌手对象
singer.setId(singerData.get("id").toString());
singer.setNickName(singerData.get("name").toString());
singers.add(singer); //将singer对象添加到singers集合中
}
song.setSingers(singers); //将singers集合赋值给歌曲对象的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) { //如果专辑数据中有PicUrl字段
album.setPicUrl(albumData.get("picUrl").toString());
}
song.setAlbum(album); //专辑对象放入歌曲
}
}
歌曲评论及音乐文件
服务实现
@SuppressWarnings("unchecked")
private void assembleSongComment(String artistId) {
Artist artist = getArtist(artistId); //根据传入的歌手ID获取对应的艺人对象artist
if (artist == null) { //取不到歌单说明artistId输入错误
return;
}
List<Song> songs = artist.getSongList(); //从artist中取出该艺人的歌曲列表songs
for (Song song : songs) {
String sIdsParam = song.getId() + "&limit=5"; //将歌曲ID和limit=5构成请求参数字符串sIdsParam
Map songsCommentObj = getSourceDataObj(S_C_API_PREFIX, sIdsParam); //从API接口中抓取该歌曲的热门评论储存
List<Map> hotCommentsObj = (List<Map>) songsCommentObj.get("hotComments"); //获取热门评论
List<Map> commontsObj = (List<Map>) songsCommentObj.get("comments"); // 获取最新评论
song.setHotComments(buildComments(hotCommentsObj));
song.setComments(buildComments(commontsObj)); //构建评论集合并赋值给对应的对象属性
}
} //通过在线API接口获取歌曲的评论数据并添加到对应歌曲对象的hotComment和comment属性中供后续使用
@SuppressWarnings("unchecked") //为每首歌曲添加音乐文件的URL地址
private void assembleSongUrl(String artistId) {
Artist artist = getArtist(artistId);
if (artist == null) { //取不到歌单说明参数输入错误
return;
}
List<Song> songs = artist.getSongList(); //从artist对象中取出该艺人的歌曲列表songs
String sIdsParam = buildManyIdParam(songs); //构建参数字符串sIdsParam,包含所有歌曲id
Map songsFileObj = getSourceDataObj(S_F_API_PREFIX, sIdsParam); //从在线API接口中抓取包含所有歌曲音乐文件信息的数据,储存在Map对象中
List<Map> datas = (List<Map>) songsFileObj.get("data"); //从songsFileObj中取出音乐文件列表datas
Map<String, Map> sourceSongsMap = new HashMap<>(); //临时的Map
// 遍历音乐文件列表
for (Map songFileData : datas) {
String sId = songFileData.get("id").toString();
sourceSongsMap.put(sId, songFileData); //原始音乐文件数据对象放入一个临时的Map中
}
// 再次遍历歌单中的歌曲,填入音乐文件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(); //取出url字段对应的字符串,作为该歌曲的音乐文件URL地址,赋值给sourceUrl属性
song.setSourceUrl(songFileUrl);
}
}
}
//通过在线API接口获取歌曲的音乐文件信息,以及构建歌曲ID和音乐文件数据对象之间的映射关系,从而便于后续为每首歌曲添加对应的音乐文件URL地址
制作图云
1.1. 对评论进行分词。例如:
老子要听一辈子周杰伦分词结果:
老子/要/听/一辈子周杰伦半夜听着周董的老歌,看着大家的评论,满满的回忆分词结果:半夜听着/周董/的/老歌/看着/大家/的/评论/满的回忆
1.2. 把分词以后的关键词出现次数进行统计,排序
1.3. 最后根据关键词的频率进行视觉显示
1.4. 在Java中,最常用的词云库为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>
generateWordCloud(artistId);
参考流程图
private void generateWordCloud(String artistId) { //生成艺人歌曲风格的词云图
Artist artist = getArtist(artistId);
if (artist == null) { //取不到歌单说明参数输入错误
return;
}
List<Song> songs = artist.getSongList(); //获取艺人对象artist歌曲列表
List<String> contents = new ArrayList<>(); //存取歌曲评论的额内容
for (Song song : songs) {
collectContent(song.getComments(), contents);
collectContent(song.getHotComments(), contents);
}//遍历歌曲所有的评论,包括普通评论和热门评论,把评论内容字符串存入集合
WordCloudUtil.generate(artistId, contents); //制作词云
}
词云工具类
//生成图云的工具类
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));
//ColorPalette是调色板,用于设置词云显示的多种颜色,越靠前设置表示词频越高的词语的颜色
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");
}
}