commit c306458c7ecf4119621e2677f4d84fbaf6000331 Author: Guoyiting <654525901@qq.com> Date: Sat May 30 00:11:55 2026 +0800 完成音乐爬虫项目 diff --git a/logs/crawler.log b/logs/crawler.log new file mode 100644 index 0000000..0c1d27c --- /dev/null +++ b/logs/crawler.log @@ -0,0 +1,65 @@ +2026-05-29 23:18:06.182 [main] INFO com.music.strategy.NetEaseStrategy - 开始爬取网易云热歌榜,限制 50 首 +2026-05-29 23:18:07.033 [main] ERROR com.music.strategy.NetEaseStrategy - 网易云爬取失败 +java.lang.NullPointerException: Cannot invoke "com.google.gson.JsonObject.getAsJsonArray(String)" because "result" is null + at com.music.strategy.NetEaseStrategy.crawl(NetEaseStrategy.java:35) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +2026-05-29 23:18:07.036 [main] ERROR com.music.command.CrawlCommand - 爬取异常 +com.music.exception.ParseException: 解析网易云数据失败: Cannot invoke "com.google.gson.JsonObject.getAsJsonArray(String)" because "result" is null + at com.music.strategy.NetEaseStrategy.crawl(NetEaseStrategy.java:79) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +Caused by: java.lang.NullPointerException: Cannot invoke "com.google.gson.JsonObject.getAsJsonArray(String)" because "result" is null + at com.music.strategy.NetEaseStrategy.crawl(NetEaseStrategy.java:35) + ... 3 common frames omitted +2026-05-29 23:19:31.271 [main] INFO com.music.strategy.NetEaseStrategy - 开始爬取网易云热歌榜,限制 50 首 +2026-05-29 23:19:56.780 [main] INFO com.music.strategy.NetEaseStrategy - 网易云爬取完成,共 50 首 +2026-05-29 23:19:56.805 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=netease, 数量=50 +2026-05-29 23:21:00.898 [main] INFO com.music.command.AnalyzeCommand - 分析报告已生成,共 50 首歌曲 +2026-05-29 23:21:21.127 [main] INFO com.music.command.SaveCommand - 数据已保存到文件: result.csv +2026-05-29 23:25:29.304 [main] INFO com.music.strategy.QQStrategy - 开始爬取 QQ 音乐热歌榜,限制 50 首 +2026-05-29 23:25:30.367 [main] INFO com.music.strategy.QQStrategy - QQ音乐爬取完成,共 20 首 +2026-05-29 23:25:30.368 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=qq, 数量=20 +2026-05-29 23:26:13.206 [main] INFO com.music.strategy.KuGouStrategy - 开始爬取酷狗热歌榜,限制 50 首 +2026-05-29 23:26:13.691 [main] ERROR com.music.strategy.KuGouStrategy - 酷狗爬取失败,使用模拟数据 +com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.JsonParser.parseReader(JsonParser.java:76) + at com.google.gson.JsonParser.parseString(JsonParser.java:51) + at com.music.strategy.KuGouStrategy.crawl(KuGouStrategy.java:39) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1659) + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1465) + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:551) + at com.google.gson.stream.JsonReader.peek(JsonReader.java:433) + at com.google.gson.JsonParser.parseReader(JsonParser.java:71) + ... 5 common frames omitted +2026-05-29 23:26:13.695 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=kugou, 数量=8 +2026-05-29 23:27:34.126 [main] INFO com.music.strategy.KuGouStrategy - 开始爬取酷狗热歌榜,限制 50 首 +2026-05-29 23:27:34.611 [main] ERROR com.music.strategy.KuGouStrategy - 酷狗爬取失败,使用模拟数据 +com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.JsonParser.parseReader(JsonParser.java:76) + at com.google.gson.JsonParser.parseString(JsonParser.java:51) + at com.music.strategy.KuGouStrategy.crawl(KuGouStrategy.java:39) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1659) + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1465) + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:551) + at com.google.gson.stream.JsonReader.peek(JsonReader.java:433) + at com.google.gson.JsonParser.parseReader(JsonParser.java:71) + ... 5 common frames omitted +2026-05-29 23:27:34.613 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=kugou, 数量=8 +2026-05-29 23:28:00.192 [main] INFO com.music.strategy.KuGouStrategy - 开始爬取酷狗热歌榜,限制 50 首 +2026-05-29 23:28:00.937 [main] INFO com.music.strategy.KuGouStrategy - 酷狗爬取完成,真实数据 22 首 +2026-05-29 23:28:00.939 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=kugou, 数量=22 +2026-05-29 23:28:29.618 [main] INFO com.music.command.AnalyzeCommand - 分析报告已生成,共 22 首歌曲 +2026-05-29 23:33:25.068 [main] INFO com.music.strategy.NetEaseStrategy - 开始爬取网易云热歌榜,限制 50 首 +2026-05-29 23:33:31.039 [main] INFO com.music.strategy.NetEaseStrategy - 网易云爬取完成,共 50 首 +2026-05-29 23:33:31.042 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=netease, 数量=50 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..69ca0ff --- /dev/null +++ b/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.musiccrawler + music-crawler + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + org.jsoup + jsoup + 1.16.1 + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + com.google.code.gson + gson + 2.10.1 + + + + ch.qos.logback + logback-classic + 1.4.14 + + + org.jsoup + jsoup + 1.16.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + \ No newline at end of file diff --git a/src/main/java/com/music/App.java b/src/main/java/com/music/App.java new file mode 100644 index 0000000..ff4d454 --- /dev/null +++ b/src/main/java/com/music/App.java @@ -0,0 +1,19 @@ +package com.music; + +import com.music.controller.CrawlerController; +import com.music.repository.SongRepository; +import com.music.service.AnalyzerService; +import com.music.strategy.StrategyFactory; +import com.music.view.ConsoleView; + +public class App { + public static void main(String[] args) { + ConsoleView view = new ConsoleView(); + SongRepository repository = new SongRepository(); + StrategyFactory factory = new StrategyFactory(); + AnalyzerService analyzer = new AnalyzerService(); + + CrawlerController controller = new CrawlerController(view, repository, factory, analyzer); + controller.start(); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/AnalyzeCommand.java b/src/main/java/com/music/command/AnalyzeCommand.java new file mode 100644 index 0000000..81dca40 --- /dev/null +++ b/src/main/java/com/music/command/AnalyzeCommand.java @@ -0,0 +1,36 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.service.AnalyzerService; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AnalyzeCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(AnalyzeCommand.class); + private final ConsoleView view; + private final AnalyzerService analyzerService; + + public AnalyzeCommand(ConsoleView view, AnalyzerService analyzerService) { + this.view = view; + this.analyzerService = analyzerService; + } + + @Override + public String getName() { + return "analyze"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + var songs = repository.getAll(); + if (songs.isEmpty()) { + view.printError("暂无数据,请先执行 crawl 命令爬取歌曲。"); + return; + } + view.printInfo("正在分析数据..."); + var stats = analyzerService.analyze(songs); + view.displayAnalysis(stats); + logger.info("分析报告已生成,共 {} 首歌曲", songs.size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/Command.java b/src/main/java/com/music/command/Command.java new file mode 100644 index 0000000..7f0cbeb --- /dev/null +++ b/src/main/java/com/music/command/Command.java @@ -0,0 +1,8 @@ +package com.music.command; + +import com.music.repository.SongRepository; + +public interface Command { + String getName(); + void execute(String[] args, SongRepository repository); +} diff --git a/src/main/java/com/music/command/CrawlCommand.java b/src/main/java/com/music/command/CrawlCommand.java new file mode 100644 index 0000000..e0a865c --- /dev/null +++ b/src/main/java/com/music/command/CrawlCommand.java @@ -0,0 +1,52 @@ +package com.music.command; + +import com.music.exception.CrawlerException; +import com.music.repository.SongRepository; +import com.music.strategy.CrawlStrategy; +import com.music.strategy.StrategyFactory; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CrawlCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(CrawlCommand.class); + private final ConsoleView view; + private final StrategyFactory factory; + + public CrawlCommand(ConsoleView view, StrategyFactory factory) { + this.view = view; + this.factory = factory; + } + + @Override + public String getName() { + return "crawl"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + if (args.length < 2) { + view.printError("用法: crawl (netease/qq/kugou)"); + return; + } + String platform = args[1]; + CrawlStrategy strategy = factory.getStrategy(platform); + if (strategy == null) { + view.printError("不支持的平台: " + platform + ",可选:netease, qq, kugou"); + return; + } + view.printInfo("正在爬取 " + platform + " 热歌榜..."); + try { + var songs = strategy.crawl(50); // 爬取前50首 + repository.addAll(songs); + view.printSuccess(String.format("成功爬取 %d 首歌曲", songs.size())); + logger.info("爬取完成,平台={}, 数量={}", platform, songs.size()); + } catch (CrawlerException e) { + view.printError("爬取失败: " + e.getMessage()); + logger.error("爬取异常", e); + } catch (Exception e) { + view.printError("未知错误: " + e.getMessage()); + logger.error("未知异常", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/ExitCommand.java b/src/main/java/com/music/command/ExitCommand.java new file mode 100644 index 0000000..b0f1fe1 --- /dev/null +++ b/src/main/java/com/music/command/ExitCommand.java @@ -0,0 +1,23 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; + +public class ExitCommand implements Command { + private final ConsoleView view; + + public ExitCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "exit"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + view.printSuccess("再见!"); + System.exit(0); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/HelpCommand.java b/src/main/java/com/music/command/HelpCommand.java new file mode 100644 index 0000000..e73b2ad --- /dev/null +++ b/src/main/java/com/music/command/HelpCommand.java @@ -0,0 +1,22 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; + +public class HelpCommand implements Command { + private final ConsoleView view; + + public HelpCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "help"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + view.printHelp(); + } +} diff --git a/src/main/java/com/music/command/HistoryCommand.java b/src/main/java/com/music/command/HistoryCommand.java new file mode 100644 index 0000000..75ceb60 --- /dev/null +++ b/src/main/java/com/music/command/HistoryCommand.java @@ -0,0 +1,32 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; +import java.util.List; + +public class HistoryCommand implements Command { + private final ConsoleView view; + private final List history; + + public HistoryCommand(ConsoleView view, List history) { + this.view = view; + this.history = history; + } + + @Override + public String getName() { + return "history"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + if (history.isEmpty()) { + view.println("没有命令历史。"); + return; + } + view.println("最近输入的命令:"); + for (int i = 0; i < history.size(); i++) { + view.println(" " + (i + 1) + ". " + history.get(i)); + } + } +} diff --git a/src/main/java/com/music/command/ListCommand.java b/src/main/java/com/music/command/ListCommand.java new file mode 100644 index 0000000..4e6ef22 --- /dev/null +++ b/src/main/java/com/music/command/ListCommand.java @@ -0,0 +1,22 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; + +public class ListCommand implements Command { + private final ConsoleView view; + + public ListCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "list"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + view.displaySongs(repository.getAll()); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/SaveCommand.java b/src/main/java/com/music/command/SaveCommand.java new file mode 100644 index 0000000..4f745e7 --- /dev/null +++ b/src/main/java/com/music/command/SaveCommand.java @@ -0,0 +1,38 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.util.CsvUtil; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SaveCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(SaveCommand.class); + private final ConsoleView view; + + public SaveCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "save"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + if (args.length < 2) { + view.printError("用法: save <文件名>"); + return; + } + String filename = args[1]; + try { + CsvUtil.saveToCsv(repository.getAll(), filename); + view.printSuccess("已保存到 " + filename); + logger.info("数据已保存到文件: {}", filename); + } catch (Exception e) { + view.printError("保存失败: " + e.getMessage()); + logger.error("保存CSV失败", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/controller/CrawlerController.java b/src/main/java/com/music/controller/CrawlerController.java new file mode 100644 index 0000000..4ab4bf1 --- /dev/null +++ b/src/main/java/com/music/controller/CrawlerController.java @@ -0,0 +1,59 @@ +package com.music.controller; + +import com.music.command.*; +import com.music.repository.SongRepository; +import com.music.service.AnalyzerService; +import com.music.strategy.StrategyFactory; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class CrawlerController { + private static final Logger logger = LoggerFactory.getLogger(CrawlerController.class); + private final Map commands = new HashMap<>(); + private final ConsoleView view; + private final SongRepository repository; + private final List history = new ArrayList<>(); + + public CrawlerController(ConsoleView view, SongRepository repository, + StrategyFactory factory, AnalyzerService analyzer) { + this.view = view; + this.repository = repository; + registerCommand(new HelpCommand(view)); + registerCommand(new ExitCommand(view)); + registerCommand(new ListCommand(view)); + registerCommand(new CrawlCommand(view, factory)); + registerCommand(new SaveCommand(view)); + registerCommand(new AnalyzeCommand(view, analyzer)); + registerCommand(new HistoryCommand(view, history)); + } + + private void registerCommand(Command cmd) { + commands.put(cmd.getName(), cmd); + } + + public void start() { + view.printSuccess("欢迎使用音乐爬虫系统 (CLI)"); + view.println("输入 help 查看所有命令。\n"); + while (true) { + String input = view.readLine().trim(); + if (input.isEmpty()) continue; + history.add(input); + String[] parts = input.split("\\s+"); + String cmdName = parts[0].toLowerCase(); + Command cmd = commands.get(cmdName); + if (cmd == null) { + view.printError("未知命令: " + cmdName + ",输入 help 查看帮助"); + continue; + } + try { + cmd.execute(parts, repository); + } catch (Exception e) { + view.printError("命令执行出错: " + e.getMessage()); + logger.error("命令执行异常", e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/exception/CrawlerException.java b/src/main/java/com/music/exception/CrawlerException.java new file mode 100644 index 0000000..95bb01e --- /dev/null +++ b/src/main/java/com/music/exception/CrawlerException.java @@ -0,0 +1,10 @@ +package com.music.exception; + +public class CrawlerException extends Exception { + public CrawlerException(String message) { + super(message); + } + public CrawlerException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/exception/NetworkException.java b/src/main/java/com/music/exception/NetworkException.java new file mode 100644 index 0000000..b40b835 --- /dev/null +++ b/src/main/java/com/music/exception/NetworkException.java @@ -0,0 +1,10 @@ +package com.music.exception; + +public class NetworkException extends CrawlerException { + private final String url; + public NetworkException(String url, String message, Throwable cause) { + super(message, cause); + this.url = url; + } + public String getUrl() { return url; } +} diff --git a/src/main/java/com/music/exception/ParseException.java b/src/main/java/com/music/exception/ParseException.java new file mode 100644 index 0000000..00050ba --- /dev/null +++ b/src/main/java/com/music/exception/ParseException.java @@ -0,0 +1,10 @@ +package com.music.exception; + +public class ParseException extends CrawlerException { + public ParseException(String message) { + super(message); + } + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/music/model/Song.java b/src/main/java/com/music/model/Song.java new file mode 100644 index 0000000..335b59a --- /dev/null +++ b/src/main/java/com/music/model/Song.java @@ -0,0 +1,55 @@ +package com.music.model; + +public class Song { + private String platform; // netease, qq, kugou + private String name; + private String artist; + private String album; + private Integer duration; // 秒 + private Integer popularity; + private String chartType; + private Integer rank; + + public Song() {} + + // 全参构造器(方便测试) + public Song(String platform, String name, String artist, String album, Integer duration, Integer rank) { + this.platform = platform; + this.name = name; + this.artist = artist; + this.album = album; + this.duration = duration; + this.rank = rank; + this.chartType = "热歌榜"; + } + + // Getters and Setters + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getArtist() { return artist; } + public void setArtist(String artist) { this.artist = artist; } + + public String getAlbum() { return album; } + public void setAlbum(String album) { this.album = album; } + + public Integer getDuration() { return duration; } + public void setDuration(Integer duration) { this.duration = duration; } + + public Integer getPopularity() { return popularity; } + public void setPopularity(Integer popularity) { this.popularity = popularity; } + + public String getChartType() { return chartType; } + public void setChartType(String chartType) { this.chartType = chartType; } + + public Integer getRank() { return rank; } + public void setRank(Integer rank) { this.rank = rank; } + + @Override + public String toString() { + return String.format("%d. %s - %s [%s]", rank, name, artist, platform); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/repository/SongRepository.java b/src/main/java/com/music/repository/SongRepository.java new file mode 100644 index 0000000..8db53be --- /dev/null +++ b/src/main/java/com/music/repository/SongRepository.java @@ -0,0 +1,36 @@ +package com.music.repository; + +import com.music.model.Song; +import java.util.*; + +public class SongRepository { + private final List songs = new ArrayList<>(); + + public void add(Song song) { + if (song == null) { + throw new IllegalArgumentException("歌曲不能为 null"); + } + if (song.getName() == null || song.getName().trim().isEmpty()) { + throw new IllegalArgumentException("歌曲名不能为空"); + } + songs.add(song); + } + + public void addAll(List songList) { + for (Song s : songList) { + add(s); + } + } + + public List getAll() { + return Collections.unmodifiableList(songs); + } + + public int size() { + return songs.size(); + } + + public void clear() { + songs.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/service/AnalyzerService.java b/src/main/java/com/music/service/AnalyzerService.java new file mode 100644 index 0000000..b68be77 --- /dev/null +++ b/src/main/java/com/music/service/AnalyzerService.java @@ -0,0 +1,55 @@ +package com.music.service; + +import com.music.model.Song; +import java.util.*; +import java.util.stream.Collectors; + +public class AnalyzerService { + + public Map analyze(List songs) { + Map result = new HashMap<>(); + + // 基础统计 + long uniqueSongs = songs.stream() + .map(s -> s.getName() + "|" + s.getArtist()) + .distinct() + .count(); + long duplicateCount = songs.size() - uniqueSongs; + long artistCount = songs.stream().map(Song::getArtist).distinct().count(); + + result.put("totalSongs", songs.size()); + result.put("uniqueSongs", uniqueSongs); + result.put("duplicateCount", duplicateCount); + result.put("artistCount", artistCount); + + // 热门歌手排行 + Map artistCountMap = songs.stream() + .collect(Collectors.groupingBy(Song::getArtist, Collectors.counting())); + List> topArtists = new ArrayList<>(artistCountMap.entrySet()); + topArtists.sort((a, b) -> b.getValue().compareTo(a.getValue())); + result.put("topArtists", topArtists); + + // 时长分析 + double avgDuration = songs.stream().mapToInt(Song::getDuration).average().orElse(0); + result.put("avgDuration", avgDuration); + + Song shortest = songs.stream().min(Comparator.comparingInt(Song::getDuration)).orElse(null); + Song longest = songs.stream().max(Comparator.comparingInt(Song::getDuration)).orElse(null); + result.put("shortestSong", shortest == null ? "无" : String.format("%s (%d秒)", shortest.getName(), shortest.getDuration())); + result.put("longestSong", longest == null ? "无" : String.format("%s (%d秒)", longest.getName(), longest.getDuration())); + + // 时长分布 + Map durationDist = songs.stream() + .collect(Collectors.groupingBy(s -> { + int min = s.getDuration() / 60; + if (min < 3) return "3分钟以下"; + else if (min < 4) return "3-4分钟"; + else if (min < 5) return "4-5分钟"; + else if (min < 6) return "5-6分钟"; + else return "6分钟以上"; + }, Collectors.counting())); + result.put("durationDistribution", durationDist); + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/CrawlStrategy.java b/src/main/java/com/music/strategy/CrawlStrategy.java new file mode 100644 index 0000000..1eb558c --- /dev/null +++ b/src/main/java/com/music/strategy/CrawlStrategy.java @@ -0,0 +1,11 @@ +package com.music.strategy; + +import com.music.model.Song; +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import java.util.List; + +public interface CrawlStrategy { + boolean supports(String platform); + List crawl(int limit) throws NetworkException, ParseException; +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/KuGouStrategy.java b/src/main/java/com/music/strategy/KuGouStrategy.java new file mode 100644 index 0000000..3eb74f0 --- /dev/null +++ b/src/main/java/com/music/strategy/KuGouStrategy.java @@ -0,0 +1,120 @@ +package com.music.strategy; + +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import com.music.model.Song; +import com.music.util.RetryUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class KuGouStrategy implements CrawlStrategy { + private static final Logger logger = LoggerFactory.getLogger(KuGouStrategy.class); + + @Override + public boolean supports(String platform) { + return "kugou".equalsIgnoreCase(platform); + } + + @Override + public List crawl(int limit) throws NetworkException, ParseException { + logger.info("开始爬取酷狗热歌榜,限制 {} 首", limit); + try { + // 使用酷狗网页版排行榜(相对稳定) + String url = "https://www.kugou.com/yy/rank/home/1-6666.html"; + Document doc = RetryUtils.retry(() -> fetchDocument(url), 3, 1000); + + // 解析歌曲列表(根据酷狗网页结构调整选择器) + Elements songItems = doc.select("#rankWrap .pc_temp_songlist li"); + if (songItems.isEmpty()) { + // 备用选择器 + songItems = doc.select(".song-list li"); + } + if (songItems.isEmpty()) { + logger.warn("未找到歌曲列表,可能网页结构已变化,使用模拟数据"); + return getMockSongs(limit); + } + + List songs = new ArrayList<>(); + int rank = 1; + for (Element item : songItems) { + if (rank > limit) break; + + // 歌曲名和歌手:通常在 a 标签内,格式如 "歌曲名 - 歌手" + Element nameLink = item.select(".pc_temp_songname a").first(); + if (nameLink == null) nameLink = item.select("a").first(); + if (nameLink == null) continue; + + String fullText = nameLink.text(); + String name = fullText; + String artist = "未知歌手"; + if (fullText.contains("-")) { + String[] parts = fullText.split("-", 2); + if (parts.length == 2) { + name = parts[0].trim(); + artist = parts[1].trim(); + } + } + + // 时长(格式如 03:45) + String durationStr = item.select(".pc_temp_time").text(); + int durationSeconds = parseDuration(durationStr); + + Song song = new Song(); + song.setPlatform("kugou"); + song.setRank(rank); + song.setChartType("热歌榜"); + song.setName(name); + song.setArtist(artist); + song.setAlbum("酷狗热歌榜"); + song.setDuration(durationSeconds); + songs.add(song); + + rank++; + } + logger.info("酷狗爬取完成,真实数据 {} 首", songs.size()); + return songs.isEmpty() ? getMockSongs(limit) : songs; + } catch (Exception e) { + logger.error("酷狗爬取失败,使用模拟数据", e); + return getMockSongs(limit); + } + } + + private Document fetchDocument(String url) throws Exception { + return Jsoup.connect(url) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + .header("Referer", "https://www.kugou.com/") + .timeout(10000) + .get(); + } + + private int parseDuration(String durationStr) { + if (durationStr == null || durationStr.isEmpty()) return 0; + try { + String[] parts = durationStr.split(":"); + if (parts.length == 2) { + return Integer.parseInt(parts[0]) * 60 + Integer.parseInt(parts[1]); + } + } catch (NumberFormatException e) { + logger.warn("时长解析失败: {}", durationStr); + } + return 0; + } + + private List getMockSongs(int limit) { + List songs = new ArrayList<>(); + String[] names = {"海阔天空", "老男孩", "逆战", "夜曲", "青花瓷", "演员", "消愁", "童话"}; + String[] artists = {"Beyond", "筷子兄弟", "张杰", "周杰伦", "周杰伦", "薛之谦", "毛不易", "光良"}; + for (int i = 0; i < Math.min(limit, names.length); i++) { + Song song = new Song("kugou", names[i], artists[i], "酷狗精选", 220 + i * 10, i + 1); + songs.add(song); + } + return songs; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/NetEaseStrategy.java b/src/main/java/com/music/strategy/NetEaseStrategy.java new file mode 100644 index 0000000..8c4a80f --- /dev/null +++ b/src/main/java/com/music/strategy/NetEaseStrategy.java @@ -0,0 +1,142 @@ +package com.music.strategy; // 注意你的包名是 com.music + +import com.google.gson.*; +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import com.music.model.Song; +import com.music.util.RetryUtils; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class NetEaseStrategy implements CrawlStrategy { + private static final Logger logger = LoggerFactory.getLogger(NetEaseStrategy.class); + + @Override + public boolean supports(String platform) { + return "netease".equalsIgnoreCase(platform); + } + + @Override + public List crawl(int limit) throws NetworkException, ParseException { + logger.info("开始爬取网易云热歌榜,限制 {} 首", limit); + List songs = new ArrayList<>(); + try { + // 使用更稳定的接口:歌单详情 API (歌单 ID: 3778678 是官方热歌榜) + String jsonData = RetryUtils.retry(() -> fetchJsonFromUrl(), 3, 1000); + + // 调试:打印前500字符,查看返回结构 + if (jsonData.length() > 500) { + logger.debug("API返回预览: {}", jsonData.substring(0, 500)); + } else { + logger.debug("API返回: {}", jsonData); + } + + JsonObject root = JsonParser.parseString(jsonData).getAsJsonObject(); + int code = root.get("code").getAsInt(); + if (code != 200) { + throw new ParseException("网易云API返回错误码: " + code); + } + + // 新版网易云API返回的数据在 "playlist" -> "tracks" 下 + JsonObject playlist = root.getAsJsonObject("playlist"); + if (playlist == null) { + // 兼容旧版结构:直接 result.tracks + JsonObject result = root.getAsJsonObject("result"); + if (result == null) { + throw new ParseException("JSON中既没有 playlist 也没有 result 字段,请检查API返回"); + } + parseTracks(result.getAsJsonArray("tracks"), songs, limit); + } else { + JsonArray tracks = playlist.getAsJsonArray("tracks"); + parseTracks(tracks, songs, limit); + } + + logger.info("网易云爬取完成,共 {} 首", songs.size()); + return songs; + } catch (Exception e) { + logger.error("网易云爬取失败", e); + if (e instanceof NetworkException) throw (NetworkException) e; + if (e instanceof ParseException) throw (ParseException) e; + throw new ParseException("解析网易云数据失败: " + e.getMessage(), e); + } + } + + private void parseTracks(JsonArray tracks, List songs, int limit) { + if (tracks == null) { + logger.warn("tracks 数组为空"); + return; + } + int count = 0; + for (int i = 0; i < tracks.size() && count < limit; i++) { + JsonObject track = tracks.get(i).getAsJsonObject(); + Song song = new Song(); + song.setPlatform("netease"); + song.setRank(i + 1); + song.setChartType("热歌榜"); + song.setName(track.get("name").getAsString()); + + // 歌手 + if (track.has("artists")) { + JsonArray artists = track.getAsJsonArray("artists"); + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < artists.size(); j++) { + sb.append(artists.get(j).getAsJsonObject().get("name").getAsString()); + if (j < artists.size() - 1) sb.append("/"); + } + song.setArtist(sb.toString()); + } else { + song.setArtist("未知歌手"); + } + + // 专辑 + if (track.has("album")) { + JsonObject album = track.getAsJsonObject("album"); + song.setAlbum(album.has("name") ? album.get("name").getAsString() : "未知专辑"); + } else { + song.setAlbum("未知专辑"); + } + + // 时长(毫秒转秒) + song.setDuration(track.get("duration").getAsInt() / 1000); + songs.add(song); + count++; + try { Thread.sleep(100); } catch (InterruptedException ignored) {} + } + } + + private String fetchJsonFromUrl() throws Exception { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .addInterceptor(chain -> { + Request original = chain.request(); + Request request = original.newBuilder() + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .header("Cookie", "os=pc; appver=2.0.2;") + .header("Referer", "https://music.163.com/") + .method(original.method(), original.body()) + .build(); + return chain.proceed(request); + }) + .build(); + // 使用官方热歌榜歌单 ID: 3778678 的详情接口 + String url = "https://music.163.com/api/playlist/detail?id=3778678"; + Request request = new Request.Builder() + .url(url) + .get() + .build(); + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new NetworkException(url, "HTTP " + response.code(), null); + } + return response.body().string(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/QQStrategy.java b/src/main/java/com/music/strategy/QQStrategy.java new file mode 100644 index 0000000..8c79586 --- /dev/null +++ b/src/main/java/com/music/strategy/QQStrategy.java @@ -0,0 +1,104 @@ +package com.music.strategy; + +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import com.music.model.Song; +import com.music.util.RetryUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class QQStrategy implements CrawlStrategy { + private static final Logger logger = LoggerFactory.getLogger(QQStrategy.class); + + @Override + public boolean supports(String platform) { + return "qq".equalsIgnoreCase(platform); + } + + @Override + public List crawl(int limit) throws NetworkException, ParseException { + logger.info("开始爬取 QQ 音乐热歌榜,限制 {} 首", limit); + List songs = new ArrayList<>(); + try { + // 使用重试工具包装网络请求 + String url = "https://y.qq.com/n/ryqq/toplist/4"; // QQ音乐热歌榜 + Document doc = RetryUtils.retry(() -> fetchDocument(url), 3, 1000); + + // 解析歌曲列表:选择器基于 QQ 音乐网页结构 + Elements songItems = doc.select(".songlist__list li"); + if (songItems.isEmpty()) { + logger.warn("未找到歌曲列表,网页结构可能已变化"); + return songs; // 返回空列表,不抛异常 + } + + int rank = 1; + for (Element item : songItems) { + if (rank > limit) break; + + // 歌曲名 + String name = item.select(".songlist__songname").text(); + if (name.isEmpty()) { + // 备用选择器 + name = item.select(".songlist__songname_txt").text(); + } + // 歌手 + String artist = item.select(".songlist__artist").text(); + if (artist.isEmpty()) { + artist = item.select(".songlist__artist_name").text(); + } + // 时长(格式如 03:45) + String durationStr = item.select(".songlist__time").text(); + int durationSeconds = parseDuration(durationStr); + + Song song = new Song(); + song.setPlatform("qq"); + song.setRank(rank); + song.setChartType("热歌榜"); + song.setName(name.isEmpty() ? "未知歌曲" : name); + song.setArtist(artist.isEmpty() ? "未知歌手" : artist); + song.setAlbum("QQ音乐专辑"); // 网页上未直接展示专辑,可留空或后续补充 + song.setDuration(durationSeconds); + songs.add(song); + + logger.debug("QQ音乐: 排名{} {} - {}", rank, name, artist); + rank++; + } + logger.info("QQ音乐爬取完成,共 {} 首", songs.size()); + return songs; + } catch (Exception e) { + logger.error("QQ音乐爬取失败", e); + if (e instanceof NetworkException) throw (NetworkException) e; + if (e instanceof ParseException) throw (ParseException) e; + throw new ParseException("解析QQ音乐数据失败: " + e.getMessage(), e); + } + } + + private Document fetchDocument(String url) throws Exception { + return Jsoup.connect(url) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + .header("Referer", "https://y.qq.com/") + .timeout(10000) + .get(); + } + + private int parseDuration(String durationStr) { + if (durationStr == null || durationStr.isEmpty()) return 0; + // 格式: "03:45" -> 225秒 + try { + String[] parts = durationStr.split(":"); + if (parts.length == 2) { + return Integer.parseInt(parts[0]) * 60 + Integer.parseInt(parts[1]); + } + } catch (NumberFormatException e) { + logger.warn("时长解析失败: {}", durationStr); + } + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/StrategyFactory.java b/src/main/java/com/music/strategy/StrategyFactory.java new file mode 100644 index 0000000..c6ea8c2 --- /dev/null +++ b/src/main/java/com/music/strategy/StrategyFactory.java @@ -0,0 +1,23 @@ +package com.music.strategy; + +import java.util.ArrayList; +import java.util.List; + +public class StrategyFactory { + private final List strategies = new ArrayList<>(); + + public StrategyFactory() { + strategies.add(new NetEaseStrategy()); + strategies.add(new QQStrategy()); + strategies.add(new KuGouStrategy()); + } + + public CrawlStrategy getStrategy(String platform) { + for (CrawlStrategy s : strategies) { + if (s.supports(platform)) { + return s; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/util/CsvUtil.java b/src/main/java/com/music/util/CsvUtil.java new file mode 100644 index 0000000..859fc08 --- /dev/null +++ b/src/main/java/com/music/util/CsvUtil.java @@ -0,0 +1,28 @@ +package com.music.util; + +import com.music.model.Song; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.List; + +public class CsvUtil { + public static void saveToCsv(List songs, String filename) throws Exception { + try (PrintWriter out = new PrintWriter(new FileWriter(filename))) { + out.println("排名,歌曲名称,歌手,专辑,时长(秒),平台"); + for (Song s : songs) { + out.printf("%d,\"%s\",\"%s\",\"%s\",%d,%s\n", + s.getRank(), + escape(s.getName()), + escape(s.getArtist()), + escape(s.getAlbum()), + s.getDuration(), + s.getPlatform()); + } + } + } + + private static String escape(String str) { + if (str == null) return ""; + return str.replace("\"", "\"\""); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/util/RetryUtils.java b/src/main/java/com/music/util/RetryUtils.java new file mode 100644 index 0000000..2b9bec3 --- /dev/null +++ b/src/main/java/com/music/util/RetryUtils.java @@ -0,0 +1,26 @@ +package com.music.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RetryUtils { + private static final Logger logger = LoggerFactory.getLogger(RetryUtils.class); + + @FunctionalInterface + public interface ThrowingAction { + T call() throws Exception; + } + + public static T retry(ThrowingAction action, int maxRetries, long waitMillis) throws Exception { + for (int i = 0; i < maxRetries; i++) { + try { + return action.call(); + } catch (Exception e) { + if (i == maxRetries - 1) throw e; + logger.warn("重试 {}/{},等待 {}ms,异常: {}", i + 1, maxRetries, waitMillis, e.getMessage()); + Thread.sleep(waitMillis); + } + } + throw new IllegalStateException("Unreachable"); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/view/ConsoleView.java b/src/main/java/com/music/view/ConsoleView.java new file mode 100644 index 0000000..b210120 --- /dev/null +++ b/src/main/java/com/music/view/ConsoleView.java @@ -0,0 +1,115 @@ +package com.music.view; + +import com.music.model.Song; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +public class ConsoleView { + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_RESET = "\u001B[0m"; + + private final Scanner scanner = new Scanner(System.in); + + public void printSuccess(String msg) { + System.out.println(ANSI_GREEN + msg + ANSI_RESET); + } + + public void printError(String msg) { + System.out.println(ANSI_RED + msg + ANSI_RESET); + } + + public void printInfo(String msg) { + System.out.println(ANSI_CYAN + msg + ANSI_RESET); + } + + public void printHelp() { + println("\n可用命令:"); + println(" crawl - 爬取歌曲 (platform: netease, qq, kugou)"); + println(" list - 显示已爬取的所有歌曲"); + println(" save - 保存到 CSV 文件 (例如 save data.csv)"); + println(" analyze - 显示数据分析报告"); + println(" history - 显示本次会话输入的命令历史"); + println(" help - 显示本帮助"); + println(" exit - 退出程序\n"); + } + + public void displaySongs(List songs) { + if (songs.isEmpty()) { + println("暂无歌曲数据,请先执行 crawl 命令。"); + return; + } + System.out.printf("%-4s %-30s %-20s %-10s %-10s%n", + "排名", "歌曲名", "歌手", "时长(秒)", "平台"); + for (Song s : songs) { + System.out.printf("%-4d %-30s %-20s %-10d %-10s%n", + s.getRank(), + truncate(s.getName(), 28), + truncate(s.getArtist(), 18), + s.getDuration(), + s.getPlatform()); + } + } + + public void displayAnalysis(Map stats) { + println("\n" + "=".repeat(60)); + println("📊 音乐数据分析报告"); + println("=".repeat(60)); + + // 基础统计 + println("\n📋 【基础统计】"); + println("-".repeat(40)); + println(" 总歌曲数: " + stats.get("totalSongs") + " 首"); + println(" 去重后: " + stats.get("uniqueSongs") + " 首"); + println(" 重复歌曲: " + stats.get("duplicateCount") + " 首"); + println(" 涉及歌手: " + stats.get("artistCount") + " 位"); + + // 热门歌手排行 + println("\n🎤 【热门歌手上榜次数排行】"); + println("-".repeat(40)); + @SuppressWarnings("unchecked") + List> topArtists = (List>) stats.get("topArtists"); + int rank = 1; + for (Map.Entry entry : topArtists) { + System.out.printf(" %d. %s: 上榜 %d 次\n", rank++, entry.getKey(), entry.getValue()); + if (rank > 15) break; + } + + // 时长分析 + println("\n⏱️ 【歌曲时长分析】"); + println("-".repeat(40)); + System.out.printf(" 平均时长: %.1f 秒 (%.1f 分钟)\n", stats.get("avgDuration"), (Double)stats.get("avgDuration") / 60); + System.out.println(" 最短歌曲: " + stats.get("shortestSong")); + System.out.println(" 最长歌曲: " + stats.get("longestSong")); + + // 时长分布 + println("\n📈 【歌曲时长分布】"); + println("-".repeat(40)); + @SuppressWarnings("unchecked") + Map durationDist = (Map) stats.get("durationDistribution"); + durationDist.forEach((range, count) -> { + double percentage = count * 100.0 / (Integer)stats.get("totalSongs"); + System.out.printf(" %s: %d 首 (%.1f%%) ", range, count, percentage); + int bar = (int)(percentage / 2); + for (int i = 0; i < bar; i++) System.out.print("█"); + System.out.println(); + }); + println(""); + } + + private String truncate(String str, int maxLen) { + if (str == null) return ""; + if (str.length() <= maxLen) return str; + return str.substring(0, maxLen - 3) + "..."; + } + + public void println(String msg) { + System.out.println(msg); + } + + public String readLine() { + return scanner.nextLine(); + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..4dea16e --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + UTF-8 + %highlight(%level) %d{HH:mm:ss} %logger{20} - %msg%n + + + + logs/crawler.log + + logs/crawler.%d{yyyy-MM-dd}.log + 7 + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/target/classes/com/music/App.class b/target/classes/com/music/App.class new file mode 100644 index 0000000..951bac3 Binary files /dev/null and b/target/classes/com/music/App.class differ diff --git a/target/classes/com/music/command/AnalyzeCommand.class b/target/classes/com/music/command/AnalyzeCommand.class new file mode 100644 index 0000000..4b70962 Binary files /dev/null and b/target/classes/com/music/command/AnalyzeCommand.class differ diff --git a/target/classes/com/music/command/Command.class b/target/classes/com/music/command/Command.class new file mode 100644 index 0000000..543631e Binary files /dev/null and b/target/classes/com/music/command/Command.class differ diff --git a/target/classes/com/music/command/CrawlCommand.class b/target/classes/com/music/command/CrawlCommand.class new file mode 100644 index 0000000..4c08c3f Binary files /dev/null and b/target/classes/com/music/command/CrawlCommand.class differ diff --git a/target/classes/com/music/command/ExitCommand.class b/target/classes/com/music/command/ExitCommand.class new file mode 100644 index 0000000..6717265 Binary files /dev/null and b/target/classes/com/music/command/ExitCommand.class differ diff --git a/target/classes/com/music/command/HelpCommand.class b/target/classes/com/music/command/HelpCommand.class new file mode 100644 index 0000000..756d18c Binary files /dev/null and b/target/classes/com/music/command/HelpCommand.class differ diff --git a/target/classes/com/music/command/HistoryCommand.class b/target/classes/com/music/command/HistoryCommand.class new file mode 100644 index 0000000..7fa34d6 Binary files /dev/null and b/target/classes/com/music/command/HistoryCommand.class differ diff --git a/target/classes/com/music/command/ListCommand.class b/target/classes/com/music/command/ListCommand.class new file mode 100644 index 0000000..ddc7ecf Binary files /dev/null and b/target/classes/com/music/command/ListCommand.class differ diff --git a/target/classes/com/music/command/SaveCommand.class b/target/classes/com/music/command/SaveCommand.class new file mode 100644 index 0000000..c0a3a8f Binary files /dev/null and b/target/classes/com/music/command/SaveCommand.class differ diff --git a/target/classes/com/music/controller/CrawlerController.class b/target/classes/com/music/controller/CrawlerController.class new file mode 100644 index 0000000..e894173 Binary files /dev/null and b/target/classes/com/music/controller/CrawlerController.class differ diff --git a/target/classes/com/music/exception/CrawlerException.class b/target/classes/com/music/exception/CrawlerException.class new file mode 100644 index 0000000..15cfb0e Binary files /dev/null and b/target/classes/com/music/exception/CrawlerException.class differ diff --git a/target/classes/com/music/exception/NetworkException.class b/target/classes/com/music/exception/NetworkException.class new file mode 100644 index 0000000..0801760 Binary files /dev/null and b/target/classes/com/music/exception/NetworkException.class differ diff --git a/target/classes/com/music/exception/ParseException.class b/target/classes/com/music/exception/ParseException.class new file mode 100644 index 0000000..725f4d6 Binary files /dev/null and b/target/classes/com/music/exception/ParseException.class differ diff --git a/target/classes/com/music/model/Song.class b/target/classes/com/music/model/Song.class new file mode 100644 index 0000000..407ac71 Binary files /dev/null and b/target/classes/com/music/model/Song.class differ diff --git a/target/classes/com/music/repository/SongRepository.class b/target/classes/com/music/repository/SongRepository.class new file mode 100644 index 0000000..d2926d7 Binary files /dev/null and b/target/classes/com/music/repository/SongRepository.class differ diff --git a/target/classes/com/music/service/AnalyzerService.class b/target/classes/com/music/service/AnalyzerService.class new file mode 100644 index 0000000..a6835f5 Binary files /dev/null and b/target/classes/com/music/service/AnalyzerService.class differ diff --git a/target/classes/com/music/strategy/CrawlStrategy.class b/target/classes/com/music/strategy/CrawlStrategy.class new file mode 100644 index 0000000..5624776 Binary files /dev/null and b/target/classes/com/music/strategy/CrawlStrategy.class differ diff --git a/target/classes/com/music/strategy/KuGouStrategy.class b/target/classes/com/music/strategy/KuGouStrategy.class new file mode 100644 index 0000000..0048ed6 Binary files /dev/null and b/target/classes/com/music/strategy/KuGouStrategy.class differ diff --git a/target/classes/com/music/strategy/NetEaseStrategy.class b/target/classes/com/music/strategy/NetEaseStrategy.class new file mode 100644 index 0000000..7737be0 Binary files /dev/null and b/target/classes/com/music/strategy/NetEaseStrategy.class differ diff --git a/target/classes/com/music/strategy/QQStrategy.class b/target/classes/com/music/strategy/QQStrategy.class new file mode 100644 index 0000000..7b76eec Binary files /dev/null and b/target/classes/com/music/strategy/QQStrategy.class differ diff --git a/target/classes/com/music/strategy/StrategyFactory.class b/target/classes/com/music/strategy/StrategyFactory.class new file mode 100644 index 0000000..29b7113 Binary files /dev/null and b/target/classes/com/music/strategy/StrategyFactory.class differ diff --git a/target/classes/com/music/util/CsvUtil.class b/target/classes/com/music/util/CsvUtil.class new file mode 100644 index 0000000..2c26a37 Binary files /dev/null and b/target/classes/com/music/util/CsvUtil.class differ diff --git a/target/classes/com/music/util/RetryUtils$ThrowingAction.class b/target/classes/com/music/util/RetryUtils$ThrowingAction.class new file mode 100644 index 0000000..bdf9931 Binary files /dev/null and b/target/classes/com/music/util/RetryUtils$ThrowingAction.class differ diff --git a/target/classes/com/music/util/RetryUtils.class b/target/classes/com/music/util/RetryUtils.class new file mode 100644 index 0000000..5fba799 Binary files /dev/null and b/target/classes/com/music/util/RetryUtils.class differ diff --git a/target/classes/com/music/view/ConsoleView.class b/target/classes/com/music/view/ConsoleView.class new file mode 100644 index 0000000..a366cc5 Binary files /dev/null and b/target/classes/com/music/view/ConsoleView.class differ diff --git a/target/classes/logback.xml b/target/classes/logback.xml new file mode 100644 index 0000000..4dea16e --- /dev/null +++ b/target/classes/logback.xml @@ -0,0 +1,23 @@ + + + + UTF-8 + %highlight(%level) %d{HH:mm:ss} %logger{20} - %msg%n + + + + logs/crawler.log + + logs/crawler.%d{yyyy-MM-dd}.log + 7 + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file