网易云音乐爬虫分析(Java后端,详细笔记过程)

目录

效果展示(爬取数据)

词云展示

项目流程

歌单信息

1. 自动查询某个歌手的所有热门歌曲

2. 自动获取每一首歌的基础信息,专辑信息

3. 自动获取每一首歌的热门评论,最新评论

4. 对所有的热门评论进行统计形成词云

开发阶段核心步骤

1. 需求分析(模型+服务):

2. 概要设计

3. 项目依赖

模型设计

模型图:

服务设计

抓取:

歌单:

歌曲:

爬取歌单服务实现步骤

start方法实现

取得整体数据对象

构建填充属性的Artist实例

构建一组填充了属性的Song实例

歌曲信息

歌曲详情信息(歌曲相关API)

模型设计

服务设计

服务实现

歌曲评论及音乐文件

服务实现

制作图云

1.1. 对评论进行分词。例如:

老子要听一辈子周杰伦分词结果:

1.2. 把分词以后的关键词出现次数进行统计,排序

1.3. 最后根据关键词的频率进行视觉显示

1.4. 在Java中,最常用的词云库为Kumo,引入依赖

词云工具类


效果展示(爬取数据)

词云展示

项目流程

歌单信息

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");
}
}

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳智麒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值