Browse Source

上传文件至 'w11'

main
Chengwuyi 3 weeks ago
parent
commit
3e554c82e3
  1. 36
      w11/Article.java
  2. 49
      w11/ArticleRepository.java
  3. 195
      w11/Commands.java
  4. 66
      w11/ConsoleView.java
  5. 57
      w11/Main.java

36
w11/Article.java

@ -0,0 +1,36 @@
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* Model: 文章实体只存放数据无任何 I/O 代码
*/
public class Article {
private String title;
private String url;
private String content;
private String author; // 作业1:新增 author
private LocalDate publishDate; // 作业1:新增 publishDate
public Article(String title, String url, String content,
String author, LocalDate publishDate) {
this.title = title;
this.url = url;
this.content = content;
this.author = author;
this.publishDate = publishDate;
}
// ── getters ──
public String getTitle() { return title; }
public String getUrl() { return url; }
public String getContent() { return content; }
public String getAuthor() { return author; }
public LocalDate getPublishDate() { return publishDate; }
@Override
public String toString() {
String dateStr = (publishDate != null)
? publishDate.format(DateTimeFormatter.ISO_LOCAL_DATE) : "unknown";
return String.format("[%s] %s (by %s, %s)", url, title, author, dateStr);
}
}

49
w11/ArticleRepository.java

@ -0,0 +1,49 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 仓库层封装 List<Article>对外只暴露安全接口
* - getAll() 返回不可变视图切断外部对内部列表的直接引用
* - add() / addAll() 均进行 null 防御
*/
public class ArticleRepository {
private final List<Article> store = new ArrayList<>();
/** 添加单篇文章,忽略 null */
public void add(Article article) {
if (article == null) return;
store.add(article);
}
/**
* 必做1addAll 批量添加跳过 null 条目
* 传入 null 列表本身也安全处理
*/
public void addAll(List<Article> articles) {
if (articles == null) return;
for (Article a : articles) {
add(a); // 复用 add() 的 null 防御
}
}
/** 返回只读视图,防止外部 clear()/add() 破坏内部状态 */
public List<Article> getAll() {
return Collections.unmodifiableList(store);
}
/** 按 URL 查找,找不到返回 null */
public Article findByUrl(String url) {
if (url == null) return null;
return store.stream()
.filter(a -> url.equals(a.getUrl()))
.findFirst()
.orElse(null);
}
public int size() { return store.size(); }
public boolean isEmpty() { return store.isEmpty(); }
/** 清空(仅供测试使用) */
void clear() { store.clear(); }
}

195
w11/Commands.java

@ -0,0 +1,195 @@
import java.util.function.Supplier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
// ─────────────────────────────────────────────
// Command 接口
// ─────────────────────────────────────────────
interface Command {
String getName();
void execute(String[] args, List<Article> articles);
}
// ─────────────────────────────────────────────
// HelpCommand
// ─────────────────────────────────────────────
class HelpCommand implements Command {
private final ConsoleView view;
HelpCommand(ConsoleView v) { this.view = v; }
@Override public String getName() { return "help"; }
@Override
public void execute(String[] args, List<Article> articles) {
view.printInfo("可用命令:");
view.printInfo(" crawl <url> 抓取 URL(别名: c)");
view.printInfo(" list 列出所有文章");
view.printInfo(" analyze 策略命中统计");
view.printInfo(" history 历史命令记录");
view.printInfo(" help 显示帮助");
view.printInfo(" exit 退出");
}
}
// ─────────────────────────────────────────────
// ListCommand
// ─────────────────────────────────────────────
class ListCommand implements Command {
private final ConsoleView view;
private final ArticleRepository repo;
ListCommand(ConsoleView v, ArticleRepository r) { view = v; repo = r; }
@Override public String getName() { return "list"; }
@Override
public void execute(String[] args, List<Article> ignored) {
view.display(repo.getAll());
}
}
// ─────────────────────────────────────────────
// CrawlCommand(W11升级:异常体系 + 指数退避 + 断路器)
// ─────────────────────────────────────────────
class CrawlCommand implements Command {
private final ConsoleView view;
private final ArticleRepository repo;
private final StrategySelector selector;
// 选做:断路器,连续失败3次熔断,冷却10秒
private final CircuitBreaker breaker = new CircuitBreaker(3, 10_000);
private static final Pattern URL_PAT =
Pattern.compile("^https?://[^\\s/$.?#].[^\\s]*$");
CrawlCommand(ConsoleView v, ArticleRepository r, StrategySelector s) {
view = v; repo = r; selector = s;
}
@Override public String getName() { return "crawl"; }
public String getAlias() { return "c"; }
@Override
public void execute(String[] args, List<Article> ignored) {
if (args.length < 2 || args[1].isBlank()) {
view.printError("用法:crawl <url>");
return;
}
String url = args[1].trim();
// 必做1:URL 格式校验改为抛出 UrlFormatException(Unchecked)
try {
validateUrl(url);
} catch (UrlFormatException e) {
view.printError(e.getMessage());
return;
}
if (repo.findByUrl(url) != null) {
view.printWarning("已抓取过,跳过:" + url);
return;
}
ParseStrategy strategy = selector.select(url);
if (strategy == null) {
view.printError("无可用策略:" + url);
return;
}
// 必做2:指数退避重试 + 选做:断路器保护
final ParseStrategy finalStrategy = strategy;
boolean success = RetryUtils.retryWithFastFail(() ->
breaker.call(() -> {
Article article = finalStrategy.parse(url);
repo.add(article);
view.printSuccess("已抓取 [" + finalStrategy.getName() + "]:" + article.getTitle());
return true;
}), 3
);
if (!success) {
view.printError("抓取失败(已重试/熔断):" + url);
if (breaker.getState() == CircuitBreaker.State.OPEN) {
view.printWarning("断路器已熔断,请稍后再试。");
}
}
}
/** 必做1:集中校验,校验失败抛 UrlFormatException */
private void validateUrl(String url) {
if (!URL_PAT.matcher(url).matches()) {
throw new UrlFormatException(url);
}
}
}
// ─────────────────────────────────────────────
// AnalyzeCommand
// ─────────────────────────────────────────────
class AnalyzeCommand implements Command {
private final ConsoleView view;
private final ArticleRepository repo;
private final StrategySelector selector;
AnalyzeCommand(ConsoleView v, ArticleRepository r, StrategySelector s) {
view = v; repo = r; selector = s;
}
@Override public String getName() { return "analyze"; }
@Override
public void execute(String[] args, List<Article> ignored) {
List<Article> all = repo.getAll();
if (all.isEmpty()) { view.printWarning("暂无文章。"); return; }
Map<String, Integer> hits = new HashMap<>();
int unmatched = 0;
for (Article a : all) {
ParseStrategy s = selector.select(a.getUrl());
if (s == null) unmatched++;
else hits.merge(s.getName(), 1, Integer::sum);
}
view.printBold("── 策略分析报告(共 " + all.size() + " 篇)──");
for (ParseStrategy s : selector.getAll()) {
view.printInfo(String.format(" %-25s priority=%-4d 命中 %d 篇",
s.getName(), s.getPriority(), hits.getOrDefault(s.getName(), 0)));
}
if (unmatched > 0) view.printWarning("无策略匹配:" + unmatched + " 篇");
}
}
// ─────────────────────────────────────────────
// HistoryCommand
// ─────────────────────────────────────────────
class HistoryCommand implements Command {
private final ConsoleView view;
private final List<String> history;
HistoryCommand(ConsoleView v, List<String> h) { view = v; history = h; }
@Override public String getName() { return "history"; }
@Override
public void execute(String[] args, List<Article> ignored) {
if (history.isEmpty()) { view.printInfo("暂无历史记录。"); return; }
view.printBold("── 历史命令(共 " + history.size() + " 条)──");
for (int i = 0; i < history.size(); i++)
view.printInfo(String.format("%3d %s", i + 1, history.get(i)));
}
}
// ─────────────────────────────────────────────
// ExitCommand
// ─────────────────────────────────────────────
class ExitCommand implements Command {
private final ConsoleView view;
ExitCommand(ConsoleView v) { this.view = v; }
@Override public String getName() { return "exit"; }
@Override
public void execute(String[] args, List<Article> ignored) {
view.printSuccess("Bye!");
System.exit(0);
}
}

66
w11/ConsoleView.java

@ -0,0 +1,66 @@
import java.util.List;
import java.util.Scanner;
/**
* View: 所有 I/O 集中在这里颜色常量统一管理
* 选做暗色主题只需改 THEME_PRIMARY / THEME_SECONDARY 两个常量即可
*/
public class ConsoleView {
// ── ANSI 基础色 ──
private static final String ANSI_RESET = "\033[0m";
private static final String ANSI_GREEN = "\033[32m";
private static final String ANSI_RED = "\033[31m";
private static final String ANSI_YELLOW = "\033[33m";
private static final String ANSI_CYAN = "\033[36m";
private static final String ANSI_BOLD = "\033[1m";
// ── 选做:主题常量(暗色主题只改这两行)──
private static final String THEME_PRIMARY = ANSI_CYAN; // 暗色主题可换 ANSI_GREEN
private static final String THEME_SECONDARY = ANSI_YELLOW; // 暗色主题可换 ANSI_CYAN
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 + "[ERROR] " + msg + ANSI_RESET);
}
public void printInfo(String msg) {
System.out.println(THEME_PRIMARY + msg + ANSI_RESET);
}
public void printWarning(String msg) {
System.out.println(THEME_SECONDARY + "[WARN] " + msg + ANSI_RESET);
}
public void printBold(String msg) {
System.out.println(ANSI_BOLD + msg + ANSI_RESET);
}
/** 展示文章列表 */
public void display(List<Article> articles) {
if (articles.isEmpty()) {
printWarning("暂无文章,请先使用 crawl <url> 抓取。");
return;
}
printBold("── 文章列表 (" + articles.size() + " 篇) ──");
for (int i = 0; i < articles.size(); i++) {
Article a = articles.get(i);
if (a == null) {
printWarning((i + 1) + ". [null 条目,已跳过]");
continue;
}
System.out.printf(THEME_PRIMARY + "%2d. %s" + ANSI_RESET + "%n", i + 1, a);
}
}
/** 读取一行用户输入,显示提示符 */
public String readLine() {
System.out.print(ANSI_BOLD + "> " + ANSI_RESET);
return scanner.nextLine().trim();
}
}

57
w11/Main.java

@ -0,0 +1,57 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// ─────────────────────────────────────────────
// Controller
// ─────────────────────────────────────────────
class CrawlerController {
private final ConsoleView view;
private final ArticleRepository repo;
private final Map<String, Command> commands = new HashMap<>();
private final List<String> history = new ArrayList<>();
CrawlerController(ConsoleView view, ArticleRepository repo, StrategySelector selector) {
this.view = view;
this.repo = repo;
CrawlCommand crawl = new CrawlCommand(view, repo, selector);
reg(crawl.getName(), crawl);
reg(crawl.getAlias(), crawl);
reg(new ListCommand(view, repo));
reg(new AnalyzeCommand(view, repo, selector));
reg(new HistoryCommand(view, history));
reg(new HelpCommand(view));
reg(new ExitCommand(view));
}
private void reg(Command cmd) { commands.put(cmd.getName(), cmd); }
private void reg(String key, Command cmd) { commands.put(key, cmd); }
public void handle(String input) {
if (input == null || input.isBlank()) return;
history.add(input);
String[] parts = input.trim().split("\\s+");
Command cmd = commands.get(parts[0].toLowerCase());
if (cmd == null) view.printError("未知命令:" + parts[0] + "(输入 help)");
else cmd.execute(parts, repo.getAll());
}
}
// ─────────────────────────────────────────────
// Main 入口
// ─────────────────────────────────────────────
public class Main {
public static void main(String[] args) {
ConsoleView view = new ConsoleView();
ArticleRepository repo = new ArticleRepository();
StrategySelector selector = new StrategySelector();
CrawlerController ctrl = new CrawlerController(view, repo, selector);
view.printSuccess("Welcome to CLI Crawler W10!");
view.printInfo("输入 help 查看命令。");
while (true) ctrl.handle(view.readLine());
}
}
Loading…
Cancel
Save