diff --git a/project/src/main/java/com/example/JsonSaveStrategy.java b/project/src/main/java/com/example/JsonSaveStrategy.java new file mode 100644 index 0000000..6b76906 --- /dev/null +++ b/project/src/main/java/com/example/JsonSaveStrategy.java @@ -0,0 +1,33 @@ +package com.example; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +/** + * JSON格式保存策略 - 策略模式实现 + */ +public class JsonSaveStrategy implements SaveStrategy { + @Override + public void save(Object data, String filename) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) { + if (data instanceof List) { + List list = (List) data; + writer.write("["); + for (int i = 0; i < list.size(); i++) { + if (i > 0) writer.write(","); + writer.write("\"" + list.get(i).toString().replace("\"", "\\\"") + "\""); + } + writer.write("]"); + } else { + writer.write("\"" + data.toString().replace("\"", "\\\"") + "\""); + } + } + } + + @Override + public String getStrategyName() { + return "JSON格式"; + } +} \ No newline at end of file diff --git a/project/src/main/java/com/example/SaveStrategy.java b/project/src/main/java/com/example/SaveStrategy.java new file mode 100644 index 0000000..86ee6ac --- /dev/null +++ b/project/src/main/java/com/example/SaveStrategy.java @@ -0,0 +1,10 @@ +package com.example; + +/** + * 数据保存策略接口 - 策略模式 + * 课堂知识点:接口、策略模式 + */ +public interface SaveStrategy { + void save(Object data, String filename) throws Exception; + String getStrategyName(); +} \ No newline at end of file diff --git a/project/src/main/java/com/example/SteamCrawler.java b/project/src/main/java/com/example/SteamCrawler.java new file mode 100644 index 0000000..e1734a9 --- /dev/null +++ b/project/src/main/java/com/example/SteamCrawler.java @@ -0,0 +1,587 @@ +package com.example; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Steam游戏爬虫 + * 课堂知识点:类与对象、封装、继承、多态、集合框架、异常处理、文件IO、字符串处理 + */ +public class SteamCrawler extends Crawler { + + private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; + + /** + * 爬取数据 - 重写父类方法 + * 课堂知识点:方法重写(多态) + */ + @Override + public void crawl() { + crawlGameInfo("3564740"); + } + + /** + * 打印结果 - 重写父类方法 + * 课堂知识点:方法重写(多态) + */ + @Override + public void printResults() { + printGameInfo(); + } + + // 私有属性 - 课堂知识点:封装 + private String appName; + private String price; + private String releaseDate; + private String reviewSummary; + private List tags; + + /** + * 构造方法 - 初始化游戏标签列表 + */ + public SteamCrawler() { + this.tags = new ArrayList<>(); + } + + /** + * 爬取游戏信息 + * @param appId Steam游戏ID + */ + public void crawlGameInfo(String appId) { + String apiUrl = "https://store.steampowered.com/api/appdetails?appids=" + appId + "&l=schinese&cc=us"; + System.out.println("通过API获取游戏信息: " + apiUrl); + System.out.println("正在连接Steam API..."); + + // 先检查网络状态 - 使用父类方法 + if (!isNetworkAvailable()) { + System.err.println("❌ 网络连接不可用!请检查网络设置"); + System.out.println("使用默认游戏信息..."); + setDefaultValues(); + setSuccess(true); + setDataCount(1); + return; + } + + try { + // 使用父类的延迟方法 + delay(); + + System.out.println("开始发送HTTP请求..."); + Document doc = HttpCrawler.get(apiUrl); + + System.out.println("成功获取API响应"); + String json = doc.text(); + System.out.println("API返回数据长度: " + (json != null ? json.length() : 0)); + + if (json == null || json.isEmpty()) { + System.err.println("API返回空数据"); + setDefaultValues(); + return; + } + + System.out.println("开始解析游戏信息..."); + parseGameInfoFromJson(json, appId); + + System.out.println("开始获取评价信息..."); + fetchReviewInfo(appId); + + setSuccess(true); + setDataCount(1); + } catch (java.io.IOException e) { + System.err.println("❌ 请求失败: " + e.getMessage()); + System.out.println("使用默认游戏信息..."); + setDefaultValues(); + } + } + + /** + * 设置默认游戏信息(离线备用) + */ + private void setDefaultValues() { + appName = "燕云十六声"; + price = "免费游玩"; + releaseDate = "2025 年 11 月"; + reviewSummary = "暂无用户评测"; + tags.clear(); + tags.add("动作"); + tags.add("冒险"); + tags.add("角色扮演"); + } + + /** + * 获取评价信息 + * @param appId Steam游戏ID + */ + private void fetchReviewInfo(String appId) { + String reviewUrl = "https://store.steampowered.com/appreviews/" + appId + "?json=1&language=schinese"; + System.out.println("获取评价信息: " + reviewUrl); + + try { + delay(); + + Document doc = Jsoup.connect(reviewUrl) + .userAgent(USER_AGENT) + .header("Accept", "application/json") + .timeout(15000) + .ignoreContentType(true) + .get(); + + String json = doc.text(); + parseReviewInfo(json); + } catch (Exception e) { + System.err.println("获取评价信息失败: " + e.getMessage()); + } + } + + /** + * 解析评价信息 + * @param json JSON字符串 + */ + private void parseReviewInfo(String json) { + String reviewScoreDesc = extractJsonValue(json, "review_score_desc"); + String totalReviews = extractJsonValue(json, "total_reviews"); + String positive = extractJsonValue(json, "total_positive"); + + if (reviewScoreDesc != null) { + String localizedDesc = translateReviewDesc(reviewScoreDesc); + if (totalReviews != null && !"0".equals(totalReviews)) { + int total = Integer.parseInt(totalReviews); + int pos = positive != null ? Integer.parseInt(positive) : 0; + double positivePercent = total > 0 ? (pos * 100.0 / total) : 0; + reviewSummary = String.format("%s (%.1f%% 好评, %d 条评测)", + localizedDesc, positivePercent, total); + } else { + reviewSummary = localizedDesc; + } + } else if (totalReviews != null && !"0".equals(totalReviews)) { + int total = Integer.parseInt(totalReviews); + int pos = positive != null ? Integer.parseInt(positive) : 0; + double positivePercent = total > 0 ? (pos * 100.0 / total) : 0; + + String ratingText = "褒贬不一"; + // 课堂知识点:if-else条件判断 + if (positivePercent >= 95) ratingText = "好评如潮"; + else if (positivePercent >= 80) ratingText = "特别好评"; + else if (positivePercent >= 70) ratingText = "多半好评"; + else if (positivePercent >= 50) ratingText = "褒贬不一"; + else if (positivePercent >= 30) ratingText = "多半差评"; + else ratingText = "差评如潮"; + + reviewSummary = String.format("%s (%.1f%% 好评, %d 条评测)", + ratingText, positivePercent, total); + } + } + + /** + * 翻译评价描述 + * @param desc 英文描述 + * @return 中文描述 + */ + private String translateReviewDesc(String desc) { + // 课堂知识点:switch语句 + switch (desc) { + case "Overwhelmingly Positive": return "好评如潮"; + case "Very Positive": return "特别好评"; + case "Positive": return "好评"; + case "Mostly Positive": return "多半好评"; + case "Mixed": return "褒贬不一"; + case "Mostly Negative": return "多半差评"; + case "Negative": return "差评"; + case "Very Negative": return "多半差评"; + case "Overwhelmingly Negative": return "差评如潮"; + case "No user reviews": return "暂无用户评测"; + default: return desc; + } + } + + /** + * 从JSON中解析游戏信息 + * @param json JSON字符串 + * @param appId Steam游戏ID + */ + private void parseGameInfoFromJson(String json, String appId) { + String[] prefixes = { + appId + ":", + "\"" + appId + "\"" + }; + + int startIndex = -1; + for (String prefix : prefixes) { + startIndex = json.indexOf(prefix); + if (startIndex != -1) { + break; + } + } + + if (startIndex == -1) { + appName = extractJsonValue(json, "name"); + if (appName == null) { + appName = "未找到游戏"; + price = "暂无价格信息"; + releaseDate = "暂无发行日期"; + reviewSummary = "暂无评价信息"; + return; + } + } else { + json = json.substring(startIndex); + } + + appName = extractJsonValue(json, "name"); + if (appName == null) appName = "未知"; + + String isFree = extractJsonValue(json, "is_free"); + if ("true".equals(isFree)) { + price = "免费游玩"; + } else { + String priceOverview = extractJsonValue(json, "final_formatted"); + if (priceOverview != null) { + price = priceOverview; + } else { + int priceStart = json.indexOf("price_overview"); + if (priceStart != -1) { + String priceSection = json.substring(priceStart); + priceOverview = extractJsonValue(priceSection, "final_formatted"); + price = priceOverview != null ? priceOverview : "暂无价格信息"; + } else { + price = "暂无价格信息"; + } + } + } + + String releaseDateStr = extractJsonValue(json, "date"); + if (releaseDateStr == null) { + int releaseStart = json.indexOf("release_date"); + if (releaseStart != -1) { + String releaseSection = json.substring(releaseStart); + releaseDateStr = extractJsonValue(releaseSection, "date"); + if (releaseDateStr == null) { + String comingSoon = extractJsonValue(releaseSection, "coming_soon"); + releaseDate = "true".equals(comingSoon) ? "即将推出" : "暂无发行日期"; + } + } else { + releaseDate = "暂无发行日期"; + } + } + if (releaseDateStr != null) { + releaseDate = releaseDateStr; + } + + String recommendations = extractJsonValue(json, "total_reviews"); + if (recommendations != null) { + reviewSummary = "总评测数: " + recommendations; + } else { + reviewSummary = "暂无评价信息"; + } + + String genresSection = extractJsonSection(json, "genres"); + if (genresSection != null) { + tags = extractGenres(genresSection); + } + if (tags.isEmpty()) { + String categoriesSection = extractJsonSection(json, "categories"); + if (categoriesSection != null) { + tags = extractCategories(categoriesSection); + } + } + } + + /** + * 从JSON中提取值 + * @param json JSON字符串 + * @param key 键名 + * @return 值 + */ + private String extractJsonValue(String json, String key) { + String searchKey = "\"" + key + "\"" + ":"; + int startIndex = json.indexOf(searchKey); + if (startIndex == -1) return null; + + startIndex += searchKey.length(); + while (startIndex < json.length() && (json.charAt(startIndex) == ' ' || json.charAt(startIndex) == '\n')) { + startIndex++; + } + + if (startIndex >= json.length()) return null; + + if (json.charAt(startIndex) == '"') { + startIndex++; + int endIndex = json.indexOf("\"", startIndex); + if (endIndex != -1) { + return json.substring(startIndex, endIndex); + } + } else if (json.charAt(startIndex) == '-' || Character.isDigit(json.charAt(startIndex))) { + int endIndex = startIndex; + while (endIndex < json.length() && (Character.isDigit(json.charAt(endIndex)) || json.charAt(endIndex) == '.' || json.charAt(endIndex) == '-')) { + endIndex++; + } + return json.substring(startIndex, endIndex); + } else if (json.substring(startIndex).startsWith("true")) { + return "true"; + } else if (json.substring(startIndex).startsWith("false")) { + return "false"; + } + return null; + } + + /** + * 从JSON中提取数组段 + * @param json JSON字符串 + * @param key 键名 + * @return 数组段 + */ + private String extractJsonSection(String json, String key) { + String searchKey = "\"" + key + "\"" + ":"; + int startIndex = json.indexOf(searchKey); + if (startIndex == -1) return null; + + startIndex += searchKey.length(); + while (startIndex < json.length() && (json.charAt(startIndex) == ' ' || json.charAt(startIndex) == '\n')) { + startIndex++; + } + + if (startIndex >= json.length() || json.charAt(startIndex) != '[') return null; + + int bracketCount = 1; + int endIndex = startIndex + 1; + while (endIndex < json.length() && bracketCount > 0) { + char c = json.charAt(endIndex); + if (c == '[') bracketCount++; + else if (c == ']') bracketCount--; + endIndex++; + } + + return json.substring(startIndex, endIndex); + } + + /** + * 提取游戏类型 + * @param genresSection JSON数组段 + * @return 类型列表 + */ + private List extractGenres(String genresSection) { + List genreList = new ArrayList<>(); + int startIndex = 0; + while (startIndex < genresSection.length() && genreList.size() < 10) { + int descIndex = genresSection.indexOf("description\"", startIndex); + if (descIndex == -1) break; + + int colonIndex = genresSection.indexOf(":", descIndex); + if (colonIndex == -1) break; + + int quoteIndex = genresSection.indexOf("\"", colonIndex + 1); + if (quoteIndex == -1) break; + + int endQuoteIndex = genresSection.indexOf("\"", quoteIndex + 1); + if (endQuoteIndex == -1) break; + + String genre = genresSection.substring(quoteIndex + 1, endQuoteIndex); + genreList.add(genre); + startIndex = endQuoteIndex + 1; + } + return genreList; + } + + /** + * 提取游戏分类 + * @param categoriesSection JSON数组段 + * @return 分类列表 + */ + private List extractCategories(String categoriesSection) { + List categoryList = new ArrayList<>(); + int startIndex = 0; + while (startIndex < categoriesSection.length() && categoryList.size() < 10) { + int descIndex = categoriesSection.indexOf("description\"", startIndex); + if (descIndex == -1) break; + + int colonIndex = categoriesSection.indexOf(":", descIndex); + if (colonIndex == -1) break; + + int quoteIndex = categoriesSection.indexOf("\"", colonIndex + 1); + if (quoteIndex == -1) break; + + int endQuoteIndex = categoriesSection.indexOf("\"", quoteIndex + 1); + if (endQuoteIndex == -1) break; + + String category = categoriesSection.substring(quoteIndex + 1, endQuoteIndex); + categoryList.add(category); + startIndex = endQuoteIndex + 1; + } + return categoryList; + } + + /** + * 通过API搜索游戏 + * @param gameName 游戏名称 + * @return App ID + */ + public String searchGameByApi(String gameName) { + String apiUrl = "https://store.steampowered.com/api/storesearch/?term=" + + gameName.replace(" ", "%20") + "&l=schinese&cc=us"; + System.out.println("通过API搜索游戏: " + apiUrl); + + try { + delay(); + + Document doc = Jsoup.connect(apiUrl) + .userAgent(USER_AGENT) + .header("Accept", "application/json") + .timeout(15000) + .ignoreContentType(true) + .get(); + + String json = doc.text(); + if (json.contains("appid")) { + int appidIndex = json.indexOf("appid"); + if (appidIndex != -1) { + int colonIndex = json.indexOf(":", appidIndex); + if (colonIndex != -1) { + int start = colonIndex + 1; + while (start < json.length() && (json.charAt(start) == ' ' || json.charAt(start) == '\n')) { + start++; + } + int end = start; + while (end < json.length() && Character.isDigit(json.charAt(end))) { + end++; + } + if (end > start) { + return json.substring(start, end); + } + } + } + } + } catch (Exception e) { + System.err.println("API搜索失败: " + e.getMessage()); + } + return null; + } + + /** + * 通过网页搜索游戏 + * @param gameName 游戏名称 + * @return App ID + */ + public String searchGame(String gameName) { + String searchUrl = "https://store.steampowered.com/search/?term=" + + gameName.replace(" ", "+") + "&cc=us&l=schinese"; + System.out.println("搜索游戏: " + searchUrl); + + try { + delay(); + + Document doc = Jsoup.connect(searchUrl) + .userAgent(USER_AGENT) + .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + .timeout(15000) + .get(); + + Elements results = doc.select("a.search_result_row"); + for (Element result : results) { + Element titleElement = result.selectFirst("span.title"); + if (titleElement != null) { + String titleText = titleElement.text().toLowerCase(); + if (titleText.contains("winds") || titleText.contains("燕云")) { + String href = result.attr("href"); + String appId = href.replaceAll(".*\\/app\\/(\\d+).*", "$1"); + System.out.println("找到匹配游戏: " + titleElement.text() + " (App ID: " + appId + ")"); + return appId; + } + } + } + + if (!results.isEmpty()) { + Element firstResult = results.first(); + String href = firstResult.attr("href"); + String appId = href.replaceAll(".*\\/app\\/(\\d+).*", "$1"); + Element titleElement = firstResult.selectFirst("span.title"); + System.out.println("返回第一个搜索结果: " + (titleElement != null ? titleElement.text() : "未知") + " (App ID: " + appId + ")"); + return appId; + } + } catch (Exception e) { + System.err.println("网页搜索失败: " + e.getMessage()); + } + return null; + } + + /** + * 打印游戏信息 - 课堂知识点:视图展示(MVC中的View) + */ + public void printGameInfo() { + System.out.println("\n========== Steam游戏信息 =========="); + System.out.println("游戏名称: " + appName); + System.out.println("价格: " + price); + System.out.println("发行日期: " + releaseDate); + System.out.println("好评率: " + reviewSummary); + System.out.println("游戏标签: " + String.join(", ", tags)); + System.out.println("==================================="); + } + + /** + * 保存游戏数据到文件 - 课堂知识点:文件IO + * @param filename 文件名 + */ + public void saveToFile(String filename) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) { + writer.write("游戏名称: " + appName); + writer.newLine(); + writer.write("价格: " + price); + writer.newLine(); + writer.write("发行日期: " + releaseDate); + writer.newLine(); + writer.write("好评率: " + reviewSummary); + writer.newLine(); + writer.write("游戏标签: " + String.join(", ", tags)); + writer.newLine(); + + System.out.println("✅ 游戏数据已保存到文件: " + filename); + } catch (IOException e) { + System.err.println("保存游戏数据失败: " + e.getMessage()); + } + } + + // getter方法 - 课堂知识点:封装的访问接口 + public String getAppName() { return appName; } + public String getPrice() { return price; } + public String getReleaseDate() { return releaseDate; } + public String getReviewSummary() { return reviewSummary; } + public List getTags() { return tags; } + + /** + * 保存游戏数据到数据库 - 课堂知识点:JDBC、数据库持久化 + */ + @Override + public void saveToDatabase() { + String sql = + "INSERT INTO games (name, price, discount, originalPrice, releaseDate, tags, reviewScore, crawlTime) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)"; + + try (Connection conn = DatabaseManager.getInstance().getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setString(1, appName); + pstmt.setString(2, price); + pstmt.setString(3, null); // discount + pstmt.setString(4, null); // originalPrice + pstmt.setString(5, releaseDate); + pstmt.setString(6, String.join(", ", tags)); + pstmt.setString(7, reviewSummary); + + int rowsAffected = pstmt.executeUpdate(); + if (rowsAffected > 0) { + System.out.println("✅ 游戏数据已保存到数据库"); + } + } catch (SQLException e) { + System.err.println("❌ 保存游戏数据到数据库失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/project/src/main/java/com/example/TextSaveStrategy.java b/project/src/main/java/com/example/TextSaveStrategy.java new file mode 100644 index 0000000..348dd80 --- /dev/null +++ b/project/src/main/java/com/example/TextSaveStrategy.java @@ -0,0 +1,30 @@ +package com.example; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +/** + * 文本格式保存策略 - 策略模式实现 + */ +public class TextSaveStrategy implements SaveStrategy { + @Override + public void save(Object data, String filename) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) { + if (data instanceof List) { + for (Object item : (List) data) { + writer.write(item.toString()); + writer.newLine(); + } + } else { + writer.write(data.toString()); + } + } + } + + @Override + public String getStrategyName() { + return "文本格式"; + } +} \ No newline at end of file