You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
7.9 KiB
195 lines
7.9 KiB
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);
|
|
}
|
|
}
|
|
|