From 6ffd5466a344c93e72d3f104db1aa8983563302f Mon Sep 17 00:00:00 2001
From: Bilei <3354484301@qq.com>
Date: Sun, 31 May 2026 18:04:10 +0800
Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E8=AF=95=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
w11/.gitignore | 4 +
w11/.vscode/settings.json | 4 +
w11/README.md | 17 +
w11/pom.xml | 71 ++
.../java/com/example/datacollect/Main.java | 19 +
.../example/datacollect/command/Command.java | 6 +
.../datacollect/command/CommandContext.java | 42 ++
.../datacollect/command/CrawlCommand.java | 77 ++
.../datacollect/command/ExitCommand.java | 26 +
.../datacollect/command/HelpCommand.java | 34 +
.../datacollect/command/HistoryCommand.java | 37 +
.../datacollect/command/ListCommand.java | 25 +
.../controller/CrawlerController.java | 59 ++
.../exception/CrawlerException.java | 11 +
.../exception/NetworkException.java | 11 +
.../datacollect/exception/ParseException.java | 11 +
.../example/datacollect/model/Article.java | 75 ++
.../repository/ArticleRepository.java | 73 ++
.../datacollect/strategy/BlogStrategy.java | 33 +
.../datacollect/strategy/CrawlStrategy.java | 13 +
.../datacollect/strategy/NewsStrategy.java | 56 ++
.../strategy/RealCrawlStrategy.java | 178 +++++
.../datacollect/strategy/StrategyFactory.java | 46 ++
.../datacollect/strategy/WebCrawler.java | 244 +++++++
.../example/datacollect/test/CrawlerDemo.java | 118 ++++
.../example/datacollect/test/CrawlerTest.java | 83 +++
.../datacollect/test/RealCrawlTest.java | 53 ++
.../example/datacollect/test/ToutiaoTest.java | 37 +
.../example/datacollect/view/ConsoleView.java | 42 ++
...example.datacollect.strategy.CrawlStrategy | 3 +
w11/src/main/resources/logback.xml | 45 ++
...example.datacollect.strategy.CrawlStrategy | 3 +
w11/target/classes/logback.xml | 45 ++
w11/target/maven-archiver/pom.properties | 3 +
.../compile/default-compile/createdFiles.lst | 20 +
.../compile/default-compile/inputFiles.lst | 25 +
w11/test_crawler.bat | 26 +
w12/.vscode/launch.json | 29 +
w12/.vscode/settings.json | 3 +
...毕磊-高级程序设计实验报告.docx | Bin 0 -> 266488 bytes
.../hotsearch_20260528_140606.txt | 56 ++
.../hotsearch_20260531_121726.txt | 60 ++
w12/总代码/.vscode/settings.json | 4 +
w12/总代码/pom.xml | 76 ++
.../src/main/java/WeiboStarHotSearcha.java | 667 ++++++++++++++++++
.../weibo/hotsearch/ConsoleOutputHandler.java | 30 +
.../com/weibo/hotsearch/HotSearchFilter.java | 24 +
.../main/java/com/weibo/hotsearch/Main.java | 16 +
.../com/weibo/hotsearch/OutputHandler.java | 46 ++
.../java/com/weibo/hotsearch/StarFilter.java | 18 +
.../com/weibo/hotsearch/cli/CliHandler.java | 133 ++++
.../com/weibo/hotsearch/cli/CliParser.java | 101 +++
.../com/weibo/hotsearch/command/Command.java | 12 +
.../hotsearch/command/CommandInvoker.java | 41 ++
.../hotsearch/command/CommandResult.java | 47 ++
.../weibo/hotsearch/command/FetchCommand.java | 34 +
.../hotsearch/command/FilterCommand.java | 35 +
.../weibo/hotsearch/command/HelpCommand.java | 27 +
.../hotsearch/command/OutputCommand.java | 34 +
.../weibo/hotsearch/command/SaveCommand.java | 30 +
.../controller/HotSearchController.java | 69 ++
.../hotsearch/exception/CliException.java | 20 +
.../exception/DataParseException.java | 20 +
.../weibo/hotsearch/exception/ErrorCode.java | 47 ++
.../exception/HotSearchException.java | 30 +
.../hotsearch/exception/NetworkException.java | 20 +
.../com/weibo/hotsearch/model/AppContext.java | 49 ++
.../weibo/hotsearch/model/HotSearchItem.java | 69 ++
.../hotsearch/model/HotSearchResult.java | 75 ++
.../weibo/hotsearch/service/DataFetcher.java | 307 ++++++++
.../hotsearch/service/FilterService.java | 44 ++
.../hotsearch/service/OutputService.java | 66 ++
.../hotsearch/strategy/FilterStrategy.java | 12 +
.../strategy/FilterStrategyFactory.java | 30 +
.../strategy/PolicyFilterStrategy.java | 55 ++
.../strategy/SportsFilterStrategy.java | 54 ++
.../strategy/StarFilterStrategy.java | 76 ++
.../com/weibo/hotsearch/view/ConsoleView.java | 51 ++
.../com/weibo/hotsearch/view/TextView.java | 83 +++
.../java/com/weibo/hotsearch/view/View.java | 10 +
.../com/weibo/hotsearch/view/ViewFactory.java | 29 +
w12/总代码/src/run.bat | 4 +
.../target/classes/WeiboStarHotSearcha.class | Bin 0 -> 19727 bytes
.../hotsearch/ConsoleOutputHandler.class | Bin 0 -> 2044 bytes
.../com/weibo/hotsearch/HotSearchFilter.class | Bin 0 -> 823 bytes
.../classes/com/weibo/hotsearch/Main.class | Bin 0 -> 1351 bytes
.../com/weibo/hotsearch/OutputHandler.class | Bin 0 -> 2129 bytes
.../com/weibo/hotsearch/StarFilter.class | Bin 0 -> 853 bytes
.../com/weibo/hotsearch/cli/CliHandler.class | Bin 0 -> 4157 bytes
.../com/weibo/hotsearch/cli/CliParser.class | Bin 0 -> 4072 bytes
.../com/weibo/hotsearch/command/Command.class | Bin 0 -> 297 bytes
.../hotsearch/command/CommandInvoker.class | Bin 0 -> 3329 bytes
.../hotsearch/command/CommandResult.class | Bin 0 -> 1415 bytes
.../hotsearch/command/FetchCommand.class | Bin 0 -> 1388 bytes
.../hotsearch/command/FilterCommand.class | Bin 0 -> 1427 bytes
.../weibo/hotsearch/command/HelpCommand.class | Bin 0 -> 939 bytes
.../hotsearch/command/OutputCommand.class | Bin 0 -> 1401 bytes
.../weibo/hotsearch/command/SaveCommand.class | Bin 0 -> 1061 bytes
.../controller/HotSearchController.class | Bin 0 -> 4358 bytes
.../hotsearch/exception/CliException.class | Bin 0 -> 1048 bytes
.../exception/DataParseException.class | Bin 0 -> 1066 bytes
.../weibo/hotsearch/exception/ErrorCode.class | Bin 0 -> 2859 bytes
.../exception/HotSearchException.class | Bin 0 -> 1854 bytes
.../exception/NetworkException.class | Bin 0 -> 1060 bytes
.../weibo/hotsearch/model/AppContext.class | Bin 0 -> 1934 bytes
.../weibo/hotsearch/model/HotSearchItem.class | Bin 0 -> 2106 bytes
.../hotsearch/model/HotSearchResult.class | Bin 0 -> 2732 bytes
.../weibo/hotsearch/service/DataFetcher.class | Bin 0 -> 11051 bytes
.../hotsearch/service/FilterService.class | Bin 0 -> 2937 bytes
.../hotsearch/service/OutputService.class | Bin 0 -> 4055 bytes
.../hotsearch/strategy/FilterStrategy.class | Bin 0 -> 265 bytes
.../strategy/FilterStrategyFactory.class | Bin 0 -> 1794 bytes
.../strategy/PolicyFilterStrategy.class | Bin 0 -> 2268 bytes
.../strategy/SportsFilterStrategy.class | Bin 0 -> 2135 bytes
.../strategy/StarFilterStrategy.class | Bin 0 -> 2134 bytes
.../weibo/hotsearch/view/ConsoleView.class | Bin 0 -> 3367 bytes
.../com/weibo/hotsearch/view/TextView.class | Bin 0 -> 4095 bytes
.../com/weibo/hotsearch/view/View.class | Bin 0 -> 225 bytes
.../weibo/hotsearch/view/ViewFactory.class | Bin 0 -> 1612 bytes
.../hotsearch-1.0.0-jar-with-dependencies.jar | Bin 0 -> 4539875 bytes
w12/总代码/target/hotsearch-1.0.0.jar | Bin 0 -> 59194 bytes
.../target/maven-archiver/pom.properties | 3 +
.../compile/default-compile/createdFiles.lst | 37 +
.../compile/default-compile/inputFiles.lst | 37 +
124 files changed, 4495 insertions(+)
create mode 100644 w11/.gitignore
create mode 100644 w11/.vscode/settings.json
create mode 100644 w11/README.md
create mode 100644 w11/pom.xml
create mode 100644 w11/src/main/java/com/example/datacollect/Main.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/Command.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/CommandContext.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/CrawlCommand.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/ExitCommand.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/HelpCommand.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/HistoryCommand.java
create mode 100644 w11/src/main/java/com/example/datacollect/command/ListCommand.java
create mode 100644 w11/src/main/java/com/example/datacollect/controller/CrawlerController.java
create mode 100644 w11/src/main/java/com/example/datacollect/exception/CrawlerException.java
create mode 100644 w11/src/main/java/com/example/datacollect/exception/NetworkException.java
create mode 100644 w11/src/main/java/com/example/datacollect/exception/ParseException.java
create mode 100644 w11/src/main/java/com/example/datacollect/model/Article.java
create mode 100644 w11/src/main/java/com/example/datacollect/repository/ArticleRepository.java
create mode 100644 w11/src/main/java/com/example/datacollect/strategy/BlogStrategy.java
create mode 100644 w11/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java
create mode 100644 w11/src/main/java/com/example/datacollect/strategy/NewsStrategy.java
create mode 100644 w11/src/main/java/com/example/datacollect/strategy/RealCrawlStrategy.java
create mode 100644 w11/src/main/java/com/example/datacollect/strategy/StrategyFactory.java
create mode 100644 w11/src/main/java/com/example/datacollect/strategy/WebCrawler.java
create mode 100644 w11/src/main/java/com/example/datacollect/test/CrawlerDemo.java
create mode 100644 w11/src/main/java/com/example/datacollect/test/CrawlerTest.java
create mode 100644 w11/src/main/java/com/example/datacollect/test/RealCrawlTest.java
create mode 100644 w11/src/main/java/com/example/datacollect/test/ToutiaoTest.java
create mode 100644 w11/src/main/java/com/example/datacollect/view/ConsoleView.java
create mode 100644 w11/src/main/resources/META-INF/services/com.example.datacollect.strategy.CrawlStrategy
create mode 100644 w11/src/main/resources/logback.xml
create mode 100644 w11/target/classes/META-INF/services/com.example.datacollect.strategy.CrawlStrategy
create mode 100644 w11/target/classes/logback.xml
create mode 100644 w11/target/maven-archiver/pom.properties
create mode 100644 w11/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
create mode 100644 w11/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
create mode 100644 w11/test_crawler.bat
create mode 100644 w12/.vscode/launch.json
create mode 100644 w12/.vscode/settings.json
create mode 100644 w12/202506050225-毕磊-高级程序设计实验报告.docx
create mode 100644 w12/hotsearch_results/hotsearch_20260528_140606.txt
create mode 100644 w12/hotsearch_results/hotsearch_20260531_121726.txt
create mode 100644 w12/总代码/.vscode/settings.json
create mode 100644 w12/总代码/pom.xml
create mode 100644 w12/总代码/src/main/java/WeiboStarHotSearcha.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/ConsoleOutputHandler.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/HotSearchFilter.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/Main.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/OutputHandler.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/StarFilter.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/cli/CliHandler.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/cli/CliParser.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/Command.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/CommandInvoker.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/CommandResult.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/FetchCommand.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/FilterCommand.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/HelpCommand.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/OutputCommand.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/command/SaveCommand.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/controller/HotSearchController.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/exception/CliException.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/exception/DataParseException.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/exception/ErrorCode.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/exception/HotSearchException.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/exception/NetworkException.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/model/AppContext.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/model/HotSearchItem.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/model/HotSearchResult.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/service/DataFetcher.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/service/FilterService.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/service/OutputService.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/strategy/FilterStrategy.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/strategy/FilterStrategyFactory.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/strategy/PolicyFilterStrategy.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/strategy/SportsFilterStrategy.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/strategy/StarFilterStrategy.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/view/ConsoleView.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/view/TextView.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/view/View.java
create mode 100644 w12/总代码/src/main/java/com/weibo/hotsearch/view/ViewFactory.java
create mode 100644 w12/总代码/src/run.bat
create mode 100644 w12/总代码/target/classes/WeiboStarHotSearcha.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/ConsoleOutputHandler.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/HotSearchFilter.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/Main.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/OutputHandler.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/StarFilter.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/cli/CliHandler.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/cli/CliParser.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/Command.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/CommandInvoker.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/CommandResult.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/FetchCommand.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/FilterCommand.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/HelpCommand.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/OutputCommand.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/command/SaveCommand.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/controller/HotSearchController.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/exception/CliException.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/exception/DataParseException.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/exception/ErrorCode.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/exception/HotSearchException.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/exception/NetworkException.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/model/AppContext.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/model/HotSearchItem.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/model/HotSearchResult.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/service/DataFetcher.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/service/FilterService.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/service/OutputService.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/strategy/FilterStrategy.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/strategy/FilterStrategyFactory.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/strategy/PolicyFilterStrategy.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/strategy/SportsFilterStrategy.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/strategy/StarFilterStrategy.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/view/ConsoleView.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/view/TextView.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/view/View.class
create mode 100644 w12/总代码/target/classes/com/weibo/hotsearch/view/ViewFactory.class
create mode 100644 w12/总代码/target/hotsearch-1.0.0-jar-with-dependencies.jar
create mode 100644 w12/总代码/target/hotsearch-1.0.0.jar
create mode 100644 w12/总代码/target/maven-archiver/pom.properties
create mode 100644 w12/总代码/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
create mode 100644 w12/总代码/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
diff --git a/w11/.gitignore b/w11/.gitignore
new file mode 100644
index 0000000..0ebcf1a
--- /dev/null
+++ b/w11/.gitignore
@@ -0,0 +1,4 @@
+*.jar
+*.jar
+*.class
+*.log
\ No newline at end of file
diff --git a/w11/.vscode/settings.json b/w11/.vscode/settings.json
new file mode 100644
index 0000000..192fc44
--- /dev/null
+++ b/w11/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "git.ignoreLimitWarning": true,
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/w11/README.md b/w11/README.md
new file mode 100644
index 0000000..3ea02ec
--- /dev/null
+++ b/w11/README.md
@@ -0,0 +1,17 @@
+# DataCollect 教学项目 — 最小可运行版本
+
+这是一个最小可用的 Java CLI 演示工程,目标:打印帮助信息以验证运行环境。
+
+构建:
+```bash
+mvn -q package
+```
+
+运行(示例):
+```bash
+java -jar target/datacollect-cli-0.1.0-jar-with-dependencies.jar --help
+```
+
+项目结构(最小):
+- `src/main/java/com/example/datacollect/Main.java` — CLI 入口,打印帮助
+- `pom.xml` — Maven 构建配置,生成可执行 jar
diff --git a/w11/pom.xml b/w11/pom.xml
new file mode 100644
index 0000000..a24f629
--- /dev/null
+++ b/w11/pom.xml
@@ -0,0 +1,71 @@
+
+ 4.0.0
+ com.example
+ datacollect-cli
+ 0.1.0
+
+ 11
+ 11
+ 2.0.9
+ 1.4.11
+ 1.17.2
+ 4.12.0
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+
+
+ org.jsoup
+ jsoup
+ ${jsoup.version}
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.3.0
+
+
+
+ com.example.datacollect.Main
+
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
diff --git a/w11/src/main/java/com/example/datacollect/Main.java b/w11/src/main/java/com/example/datacollect/Main.java
new file mode 100644
index 0000000..a136ba6
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/Main.java
@@ -0,0 +1,19 @@
+package com.example.datacollect;
+
+import com.example.datacollect.controller.CrawlerController;
+import com.example.datacollect.repository.ArticleRepository;
+import com.example.datacollect.view.ConsoleView;
+
+public class Main {
+
+ public static void main(String[] args) {
+ ConsoleView view = new ConsoleView();
+ ArticleRepository repository = new ArticleRepository();
+ CrawlerController controller = new CrawlerController(view, repository);
+
+ view.printSuccess("Welcome to CLI Crawler (W10)! Type help for commands.");
+ while (true) {
+ controller.handle(view.readLine());
+ }
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/command/Command.java b/w11/src/main/java/com/example/datacollect/command/Command.java
new file mode 100644
index 0000000..4cdadd9
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/Command.java
@@ -0,0 +1,6 @@
+package com.example.datacollect.command;
+
+public interface Command {
+ String getName();
+ void execute(String[] args, CommandContext context);
+}
diff --git a/w11/src/main/java/com/example/datacollect/command/CommandContext.java b/w11/src/main/java/com/example/datacollect/command/CommandContext.java
new file mode 100644
index 0000000..cb1c0db
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/CommandContext.java
@@ -0,0 +1,42 @@
+package com.example.datacollect.command;
+
+import com.example.datacollect.repository.ArticleRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CommandContext {
+ private static final Logger logger = LoggerFactory.getLogger(CommandContext.class);
+ private final ArticleRepository repository;
+ private final List history;
+
+ public CommandContext(ArticleRepository repository) {
+ this.repository = repository;
+ this.history = new ArrayList<>();
+ logger.debug("CommandContext initialized");
+ }
+
+ public ArticleRepository getRepository() {
+ return repository;
+ }
+
+ public List getHistory() {
+ return new ArrayList<>(history);
+ }
+
+ public void addToHistory(String command) {
+ history.add(command);
+ logger.trace("Command added to history: {}", command);
+ }
+
+ public int getHistorySize() {
+ return history.size();
+ }
+
+ public void clearHistory() {
+ history.clear();
+ logger.debug("Command history cleared");
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/command/CrawlCommand.java b/w11/src/main/java/com/example/datacollect/command/CrawlCommand.java
new file mode 100644
index 0000000..fe02f56
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/CrawlCommand.java
@@ -0,0 +1,77 @@
+package com.example.datacollect.command;
+
+import com.example.datacollect.exception.CrawlerException;
+import com.example.datacollect.model.Article;
+import com.example.datacollect.strategy.CrawlStrategy;
+import com.example.datacollect.strategy.StrategyFactory;
+import com.example.datacollect.view.ConsoleView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class CrawlCommand implements Command {
+ private static final Logger logger = LoggerFactory.getLogger(CrawlCommand.class);
+ private static final int MAX_RETRIES = 3;
+ private static final long RETRY_DELAY_MS = 1000;
+ private final ConsoleView view;
+
+ public CrawlCommand(ConsoleView view) {
+ this.view = view;
+ }
+
+ @Override
+ public String getName() {
+ return "crawl";
+ }
+
+ @Override
+ public void execute(String[] args, CommandContext context) {
+ if (args.length < 3) {
+ view.printError("Usage: crawl ");
+ view.printError("Supported types: " + StrategyFactory.getSupportedTypes());
+ return;
+ }
+
+ String type = args[1];
+ String url = args[2];
+
+ if (!StrategyFactory.hasStrategy(type)) {
+ view.printError("Unknown strategy type: " + type);
+ view.printError("Supported types: " + StrategyFactory.getSupportedTypes());
+ return;
+ }
+
+ int attempt = 0;
+ Exception lastException = null;
+
+ while (attempt < MAX_RETRIES) {
+ try {
+ attempt++;
+ logger.info("Crawl attempt {} of {} for: {}", attempt, MAX_RETRIES, url);
+ CrawlStrategy strategy = StrategyFactory.getStrategy(type);
+ List articles = strategy.crawl(url);
+ context.getRepository().addAll(articles);
+ view.printSuccess("Crawled " + articles.size() + " articles using " + type + " strategy");
+ return;
+ } catch (CrawlerException e) {
+ lastException = e;
+ logger.warn("Crawl attempt {} failed: {}", attempt, e.getMessage());
+ if (attempt < MAX_RETRIES) {
+ try {
+ Thread.sleep(RETRY_DELAY_MS);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ } catch (Exception e) {
+ lastException = e;
+ logger.error("Crawl failed with unexpected error: {}", e.getMessage());
+ break;
+ }
+ }
+
+ view.printError("Crawl failed after " + attempt + " attempts: " + (lastException != null ? lastException.getMessage() : "Unknown error"));
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/command/ExitCommand.java b/w11/src/main/java/com/example/datacollect/command/ExitCommand.java
new file mode 100644
index 0000000..f8440d3
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/ExitCommand.java
@@ -0,0 +1,26 @@
+package com.example.datacollect.command;
+
+import com.example.datacollect.view.ConsoleView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ExitCommand implements Command {
+ private static final Logger logger = LoggerFactory.getLogger(ExitCommand.class);
+ private final ConsoleView view;
+
+ public ExitCommand(ConsoleView view) {
+ this.view = view;
+ }
+
+ @Override
+ public String getName() {
+ return "exit";
+ }
+
+ @Override
+ public void execute(String[] args, CommandContext context) {
+ logger.info("Exit command executed");
+ view.printSuccess("Bye!");
+ System.exit(0);
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/command/HelpCommand.java b/w11/src/main/java/com/example/datacollect/command/HelpCommand.java
new file mode 100644
index 0000000..8ff02f7
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/HelpCommand.java
@@ -0,0 +1,34 @@
+package com.example.datacollect.command;
+
+import com.example.datacollect.view.ConsoleView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HelpCommand implements Command {
+ private static final Logger logger = LoggerFactory.getLogger(HelpCommand.class);
+ private final ConsoleView view;
+
+ public HelpCommand(ConsoleView view) {
+ this.view = view;
+ }
+
+ @Override
+ public String getName() {
+ return "help";
+ }
+
+ @Override
+ public void execute(String[] args, CommandContext context) {
+ logger.debug("Help command executed");
+ view.printInfo("Commands:");
+ view.printInfo(" crawl - Crawl articles");
+ view.printInfo(" Types:");
+ view.printInfo(" blog - 模拟博客爬取(演示用)");
+ view.printInfo(" news - 模拟新闻爬取(演示用)");
+ view.printInfo(" real - 真实网页爬取(从目标网站获取真实内容)");
+ view.printInfo(" list - List all articles");
+ view.printInfo(" history - Show command history");
+ view.printInfo(" help - Show this help");
+ view.printInfo(" exit - Exit program");
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/command/HistoryCommand.java b/w11/src/main/java/com/example/datacollect/command/HistoryCommand.java
new file mode 100644
index 0000000..7ee207f
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/HistoryCommand.java
@@ -0,0 +1,37 @@
+package com.example.datacollect.command;
+
+import com.example.datacollect.view.ConsoleView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class HistoryCommand implements Command {
+ private static final Logger logger = LoggerFactory.getLogger(HistoryCommand.class);
+ private final ConsoleView view;
+
+ public HistoryCommand(ConsoleView view) {
+ this.view = view;
+ }
+
+ @Override
+ public String getName() {
+ return "history";
+ }
+
+ @Override
+ public void execute(String[] args, CommandContext context) {
+ List history = context.getHistory();
+ logger.debug("History command executed, {} entries", history.size());
+
+ if (history.isEmpty()) {
+ view.printInfo("暂无命令历史");
+ return;
+ }
+
+ view.printInfo("命令历史:");
+ for (int i = 0; i < history.size(); i++) {
+ System.out.println((i + 1) + ". " + history.get(i));
+ }
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/command/ListCommand.java b/w11/src/main/java/com/example/datacollect/command/ListCommand.java
new file mode 100644
index 0000000..9c136fa
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/command/ListCommand.java
@@ -0,0 +1,25 @@
+package com.example.datacollect.command;
+
+import com.example.datacollect.view.ConsoleView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ListCommand implements Command {
+ private static final Logger logger = LoggerFactory.getLogger(ListCommand.class);
+ private final ConsoleView view;
+
+ public ListCommand(ConsoleView view) {
+ this.view = view;
+ }
+
+ @Override
+ public String getName() {
+ return "list";
+ }
+
+ @Override
+ public void execute(String[] args, CommandContext context) {
+ logger.debug("List command executed");
+ view.display(context.getRepository().findAll());
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/controller/CrawlerController.java b/w11/src/main/java/com/example/datacollect/controller/CrawlerController.java
new file mode 100644
index 0000000..d6390d9
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/controller/CrawlerController.java
@@ -0,0 +1,59 @@
+package com.example.datacollect.controller;
+
+import com.example.datacollect.command.Command;
+import com.example.datacollect.command.CommandContext;
+import com.example.datacollect.command.CrawlCommand;
+import com.example.datacollect.command.ExitCommand;
+import com.example.datacollect.command.HelpCommand;
+import com.example.datacollect.command.HistoryCommand;
+import com.example.datacollect.command.ListCommand;
+import com.example.datacollect.repository.ArticleRepository;
+import com.example.datacollect.view.ConsoleView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CrawlerController {
+ private static final Logger logger = LoggerFactory.getLogger(CrawlerController.class);
+ private final Map commands = new HashMap<>();
+ private final ConsoleView view;
+ private final CommandContext context;
+
+ public CrawlerController(ConsoleView view, ArticleRepository repository) {
+ this.view = view;
+ this.context = new CommandContext(repository);
+ register(new HelpCommand(view));
+ register(new ListCommand(view));
+ register(new CrawlCommand(view));
+ register(new ExitCommand(view));
+ register(new HistoryCommand(view));
+ logger.info("CrawlerController initialized with {} commands", commands.size());
+ }
+
+ private void register(Command command) {
+ commands.put(command.getName(), command);
+ logger.debug("Registered command: {}", command.getName());
+ }
+
+ public void handle(String input) {
+ String text = input == null ? "" : input.trim();
+ if (text.isEmpty()) {
+ return;
+ }
+
+ context.addToHistory(text);
+
+ String[] args = text.split("\\s+");
+ String cmdName = args[0].toLowerCase();
+ Command command = commands.get(cmdName);
+ if (command == null) {
+ logger.warn("Unknown command received: {}", cmdName);
+ view.printError("Unknown command: " + cmdName);
+ return;
+ }
+ logger.debug("Executing command: {}", cmdName);
+ command.execute(args, context);
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/exception/CrawlerException.java b/w11/src/main/java/com/example/datacollect/exception/CrawlerException.java
new file mode 100644
index 0000000..bde38fd
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/exception/CrawlerException.java
@@ -0,0 +1,11 @@
+package com.example.datacollect.exception;
+
+public class CrawlerException extends Exception {
+ public CrawlerException(String message) {
+ super(message);
+ }
+
+ public CrawlerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/exception/NetworkException.java b/w11/src/main/java/com/example/datacollect/exception/NetworkException.java
new file mode 100644
index 0000000..b80f1bb
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/exception/NetworkException.java
@@ -0,0 +1,11 @@
+package com.example.datacollect.exception;
+
+public class NetworkException extends CrawlerException {
+ public NetworkException(String message) {
+ super(message);
+ }
+
+ public NetworkException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/exception/ParseException.java b/w11/src/main/java/com/example/datacollect/exception/ParseException.java
new file mode 100644
index 0000000..ef4c5a1
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/exception/ParseException.java
@@ -0,0 +1,11 @@
+package com.example.datacollect.exception;
+
+public class ParseException extends CrawlerException {
+ public ParseException(String message) {
+ super(message);
+ }
+
+ public ParseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/model/Article.java b/w11/src/main/java/com/example/datacollect/model/Article.java
new file mode 100644
index 0000000..f3b0ca8
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/model/Article.java
@@ -0,0 +1,75 @@
+package com.example.datacollect.model;
+
+import java.time.LocalDate;
+
+public class Article {
+ private String title;
+ private String url;
+ private String content;
+ private String author;
+ private LocalDate publishDate;
+
+ public Article(String title, String url, String content) {
+ this.title = title;
+ this.url = url;
+ this.content = content;
+ }
+
+ 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;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public LocalDate getPublishDate() {
+ return publishDate;
+ }
+
+ public void setPublishDate(LocalDate publishDate) {
+ this.publishDate = publishDate;
+ }
+
+ @Override
+ public String toString() {
+ return "Article{"
+ + "title='" + title + '\''
+ + ", url='" + url + '\''
+ + ", author='" + author + '\''
+ + ", publishDate=" + publishDate
+ + '}';
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/repository/ArticleRepository.java b/w11/src/main/java/com/example/datacollect/repository/ArticleRepository.java
new file mode 100644
index 0000000..e007d6e
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/repository/ArticleRepository.java
@@ -0,0 +1,73 @@
+package com.example.datacollect.repository;
+
+import com.example.datacollect.model.Article;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+public class ArticleRepository {
+ private static final Logger logger = LoggerFactory.getLogger(ArticleRepository.class);
+ private final List articles = new ArrayList<>();
+
+ public void add(Article article) {
+ if (article == null) {
+ logger.warn("Attempted to add null article to repository");
+ throw new IllegalArgumentException("Article cannot be null");
+ }
+ if (article.getUrl() == null || article.getUrl().isEmpty()) {
+ logger.warn("Attempted to add article with null or empty URL");
+ throw new IllegalArgumentException("Article URL cannot be null or empty");
+ }
+ if (findByUrl(article.getUrl()).isPresent()) {
+ logger.debug("Article with URL {} already exists, skipping", article.getUrl());
+ return;
+ }
+ articles.add(article);
+ logger.debug("Added article: {} ({})", article.getTitle(), article.getUrl());
+ }
+
+ public void addAll(List articleList) {
+ if (articleList == null) {
+ logger.warn("Attempted to add null article list to repository");
+ throw new IllegalArgumentException("Article list cannot be null");
+ }
+ int count = 0;
+ for (Article article : articleList) {
+ try {
+ add(article);
+ count++;
+ } catch (IllegalArgumentException e) {
+ logger.warn("Skipping invalid article: {}", e.getMessage());
+ }
+ }
+ logger.info("Added {} articles to repository (total: {})", count, articles.size());
+ }
+
+ public List findAll() {
+ return Collections.unmodifiableList(new ArrayList<>(articles));
+ }
+
+ public Optional findByUrl(String url) {
+ if (url == null || url.isEmpty()) {
+ logger.warn("findByUrl called with null or empty URL");
+ return Optional.empty();
+ }
+ return articles.stream()
+ .filter(a -> a.getUrl().equals(url))
+ .findFirst();
+ }
+
+ public int count() {
+ return articles.size();
+ }
+
+ public void clear() {
+ int size = articles.size();
+ articles.clear();
+ logger.info("Cleared {} articles from repository", size);
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/strategy/BlogStrategy.java b/w11/src/main/java/com/example/datacollect/strategy/BlogStrategy.java
new file mode 100644
index 0000000..585adcc
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/strategy/BlogStrategy.java
@@ -0,0 +1,33 @@
+package com.example.datacollect.strategy;
+
+import com.example.datacollect.exception.CrawlerException;
+import com.example.datacollect.exception.ParseException;
+import com.example.datacollect.model.Article;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BlogStrategy implements CrawlStrategy {
+ private static final Logger logger = LoggerFactory.getLogger(BlogStrategy.class);
+
+ @Override
+ public String getType() {
+ return "blog";
+ }
+
+ @Override
+ public List crawl(String url) throws CrawlerException {
+ logger.info("Starting blog crawl for: {}", url);
+ List articles = new ArrayList<>();
+ articles.add(new Article("Java编程入门教程", "https://www.oracle.com/java/technologies/get-started/", "Oracle官方Java入门教程,涵盖Java基础知识和开发环境配置"));
+ articles.add(new Article("Java最佳实践指南", "https://www.baeldung.com/java-best-practices", "Baeldung提供的Java编程最佳实践,包括代码规范、性能优化等"));
+ logger.info("Successfully crawled {} articles from {}", articles.size(), url);
+ return articles;
+ }
+
+ @Override
+ public void parse(String html, String url) throws ParseException {
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java b/w11/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java
new file mode 100644
index 0000000..7ab4013
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java
@@ -0,0 +1,13 @@
+package com.example.datacollect.strategy;
+
+import com.example.datacollect.exception.CrawlerException;
+import com.example.datacollect.exception.ParseException;
+import com.example.datacollect.model.Article;
+
+import java.util.List;
+
+public interface CrawlStrategy {
+ String getType();
+ List crawl(String url) throws CrawlerException;
+ void parse(String html, String url) throws ParseException;
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/strategy/NewsStrategy.java b/w11/src/main/java/com/example/datacollect/strategy/NewsStrategy.java
new file mode 100644
index 0000000..14abe34
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/strategy/NewsStrategy.java
@@ -0,0 +1,56 @@
+package com.example.datacollect.strategy;
+
+import com.example.datacollect.exception.CrawlerException;
+import com.example.datacollect.exception.ParseException;
+import com.example.datacollect.model.Article;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NewsStrategy implements CrawlStrategy {
+ private static final Logger logger = LoggerFactory.getLogger(NewsStrategy.class);
+
+ @Override
+ public String getType() {
+ return "news";
+ }
+
+ @Override
+ public List crawl(String url) throws CrawlerException {
+ logger.info("Starting news crawl for: {}", url);
+ List articles = new ArrayList<>();
+
+ String baseUrl = url.replaceAll("/$", "");
+
+ if (url.contains("toutiao.com")) {
+ articles.add(new Article("今日头条 - 热点新闻", "https://www.toutiao.com/", "今日头条热点新闻聚合平台"));
+ articles.add(new Article("今日头条科技", "https://www.toutiao.com/c/user/token/MS4wLjABAAAAlS0q8OYF0X0Kf2dJ7w0wFg/", "科技资讯频道"));
+ articles.add(new Article("今日头条娱乐", "https://www.toutiao.com/c/user/token/MS4wLjABAAAAj80G9D28h2a8q9F9x9x9x9/", "娱乐新闻频道"));
+ } else if (url.contains("sina.com") || url.contains("sina.cn")) {
+ articles.add(new Article("新浪新闻 - 国内新闻", "https://news.sina.com.cn/", "新浪国内新闻频道"));
+ articles.add(new Article("新浪财经", "https://finance.sina.com.cn/", "财经资讯"));
+ articles.add(new Article("新浪体育", "https://sports.sina.com.cn/", "体育新闻"));
+ } else if (url.contains("qq.com")) {
+ articles.add(new Article("腾讯新闻", "https://news.qq.com/", "腾讯新闻频道"));
+ articles.add(new Article("腾讯财经", "https://finance.qq.com/", "财经资讯"));
+ articles.add(new Article("腾讯科技", "https://tech.qq.com/", "科技新闻"));
+ } else if (url.contains("163.com")) {
+ articles.add(new Article("网易新闻", "https://news.163.com/", "网易新闻频道"));
+ articles.add(new Article("网易财经", "https://money.163.com/", "财经资讯"));
+ articles.add(new Article("网易科技", "https://tech.163.com/", "科技新闻"));
+ } else {
+ articles.add(new Article("科技新闻 - TechCrunch", "https://techcrunch.com/", "TechCrunch提供最新的科技新闻"));
+ articles.add(new Article("国际新闻 - BBC News", "https://www.bbc.com/news", "BBC News全球新闻"));
+ articles.add(new Article("商业新闻 - Reuters", "https://www.reuters.com/", "路透社商业新闻"));
+ }
+
+ logger.info("Successfully crawled {} articles from {}", articles.size(), url);
+ return articles;
+ }
+
+ @Override
+ public void parse(String html, String url) throws ParseException {
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/strategy/RealCrawlStrategy.java b/w11/src/main/java/com/example/datacollect/strategy/RealCrawlStrategy.java
new file mode 100644
index 0000000..5005b5b
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/strategy/RealCrawlStrategy.java
@@ -0,0 +1,178 @@
+package com.example.datacollect.strategy;
+
+import com.example.datacollect.exception.CrawlerException;
+import com.example.datacollect.exception.NetworkException;
+import com.example.datacollect.exception.ParseException;
+import com.example.datacollect.model.Article;
+import org.jsoup.nodes.Document;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
+
+public class RealCrawlStrategy implements CrawlStrategy {
+ private static final Logger logger = LoggerFactory.getLogger(RealCrawlStrategy.class);
+
+ private static final Set NEWS_KEYWORDS = Set.of("news", "article", "post", "story", "report", "blog");
+
+ @Override
+ public String getType() {
+ return "real";
+ }
+
+ @Override
+ public List crawl(String url) throws CrawlerException {
+ List articles = new ArrayList<>();
+ Set visitedUrls = new HashSet<>();
+
+ try {
+ logger.info("Starting real crawl for: {}", url);
+
+ Document doc = WebCrawler.fetchDocument(url);
+ String pageTitle = WebCrawler.extractTitle(doc);
+ String pageContent = WebCrawler.extractContent(doc);
+
+ logger.debug("Page title extracted: {}", pageTitle);
+ articles.add(new Article(pageTitle, url, pageContent));
+ visitedUrls.add(url);
+
+ List links = WebCrawler.extractLinks(doc, url);
+ logger.debug("Found {} links on page", links.size());
+
+ if (links.size() > 0) {
+ logger.debug("First 5 links:");
+ for (int i = 0; i < Math.min(5, links.size()); i++) {
+ logger.debug(" {}: {} (isArticle: {})", i + 1, links.get(i), isArticleLink(links.get(i)));
+ }
+ }
+
+ int count = 0;
+ for (String link : links) {
+ if (count >= 5) break;
+ if (visitedUrls.contains(link)) continue;
+ if (!isArticleLink(link)) continue;
+
+ try {
+ Document articleDoc = WebCrawler.fetchDocument(link);
+ String articleTitle = WebCrawler.extractTitle(articleDoc);
+ String articleContent = WebCrawler.extractContent(articleDoc);
+
+ articles.add(new Article(articleTitle, link, articleContent));
+ visitedUrls.add(link);
+ count++;
+
+ logger.debug("Crawled article: {} - {}", articleTitle, link);
+ } catch (NetworkException e) {
+ logger.warn("Failed to crawl article: {} - {}", link, e.getMessage());
+ }
+ }
+
+ if (articles.size() == 1 && "No Title".equals(pageTitle)) {
+ logger.warn("Only 1 article found with no title, trying alternative extraction");
+ articles = extractArticlesFromPage(doc, url);
+ }
+
+ logger.info("Successfully crawled {} articles from {}", articles.size(), url);
+
+ } catch (NetworkException e) {
+ logger.error("Failed to crawl {}: {}", url, e.getMessage());
+ throw e;
+ }
+
+ return articles;
+ }
+
+ private List extractArticlesFromPage(Document doc, String baseUrl) {
+ List articles = new ArrayList<>();
+
+ articles.add(new Article(doc.title(), baseUrl, WebCrawler.extractContent(doc)));
+
+ return articles;
+ }
+
+ private boolean isArticleLink(String url) {
+ String lowerUrl = url.toLowerCase();
+
+ // 排除非文章链接
+ if (lowerUrl.contains("/login") ||
+ lowerUrl.contains("/register") ||
+ lowerUrl.contains("/logout") ||
+ lowerUrl.contains("/search") ||
+ lowerUrl.contains("/about") ||
+ lowerUrl.contains("/contact") ||
+ lowerUrl.contains("/terms") ||
+ lowerUrl.contains("/privacy") ||
+ lowerUrl.contains("/sitemap") ||
+ lowerUrl.contains("/feed") ||
+ lowerUrl.contains("?") ||
+ lowerUrl.contains(".json") ||
+ lowerUrl.contains(".xml") ||
+ lowerUrl.contains("#") ||
+ lowerUrl.contains("/courses/") ||
+ lowerUrl.endsWith("/") ||
+ lowerUrl.endsWith("/start-here") ||
+ lowerUrl.endsWith("/home") ||
+ lowerUrl.endsWith("/index")) {
+ return false;
+ }
+
+ // 包含文章相关关键词的链接
+ if (lowerUrl.contains("/article/") ||
+ lowerUrl.contains("/posts/") ||
+ lowerUrl.contains("/blog/") ||
+ lowerUrl.contains("/news/") ||
+ lowerUrl.contains("/story/") ||
+ lowerUrl.contains("/post/") ||
+ lowerUrl.contains("/tutorial/") ||
+ lowerUrl.contains("/guide/") ||
+ lowerUrl.contains("/learn/") ||
+ lowerUrl.contains("/articles/") ||
+ lowerUrl.contains("/java-") ||
+ lowerUrl.contains("/spring-") ||
+ lowerUrl.contains("/kotlin-") ||
+ lowerUrl.contains("/maven-") ||
+ lowerUrl.contains("/gradle-") ||
+ lowerUrl.contains("/junit-") ||
+ lowerUrl.contains("/hibernate-") ||
+ lowerUrl.contains("/jdbc-") ||
+ lowerUrl.contains("/concurrent-") ||
+ lowerUrl.contains("/stream-") ||
+ lowerUrl.contains("/regex-") ||
+ lowerUrl.contains("/json-") ||
+ lowerUrl.contains("/xml-") ||
+ lowerUrl.contains("/security-") ||
+ lowerUrl.contains("/test-")) {
+ return true;
+ }
+
+ // 带数字ID或日期格式的链接
+ if (lowerUrl.matches(".*/\\d+\\.html?$") ||
+ lowerUrl.matches(".*/[\\w-]+\\.html?$") ||
+ lowerUrl.matches(".*/\\d+/\\d+/\\d+/.*") ||
+ lowerUrl.matches(".*/\\d{4}/\\d{2}/.*") ||
+ lowerUrl.matches(".*/\\d{4}/\\d{2}/\\d{2}/.*") ||
+ lowerUrl.matches(".*/\\d{2}-\\d{2}-.*")) {
+ return true;
+ }
+
+ // 对于 baeldung 这类技术博客,识别 /java-something 形式的链接
+ if (lowerUrl.matches(".*/java-[\\w-]+$") ||
+ lowerUrl.matches(".*/spring-[\\w-]+$") ||
+ lowerUrl.matches(".*/kotlin-[\\w-]+$")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void parse(String html, String url) throws ParseException {
+ if (html == null || html.isEmpty()) {
+ throw new ParseException("HTML content is null or empty for URL: " + url);
+ }
+ logger.debug("Parsing HTML content for URL: {}", url);
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/strategy/StrategyFactory.java b/w11/src/main/java/com/example/datacollect/strategy/StrategyFactory.java
new file mode 100644
index 0000000..5390ffa
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/strategy/StrategyFactory.java
@@ -0,0 +1,46 @@
+package com.example.datacollect.strategy;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+public class StrategyFactory {
+ private static final Logger logger = LoggerFactory.getLogger(StrategyFactory.class);
+ private static final Map strategies = new HashMap<>();
+
+ static {
+ loadStrategies();
+ }
+
+ private static synchronized void loadStrategies() {
+ if (!strategies.isEmpty()) {
+ return;
+ }
+ ServiceLoader loader = ServiceLoader.load(CrawlStrategy.class);
+ for (CrawlStrategy strategy : loader) {
+ strategies.put(strategy.getType().toLowerCase(), strategy);
+ logger.debug("Loaded strategy: {}", strategy.getType());
+ }
+ logger.info("Loaded {} strategies", strategies.size());
+ }
+
+ public static CrawlStrategy getStrategy(String type) {
+ CrawlStrategy strategy = strategies.get(type.toLowerCase());
+ if (strategy == null) {
+ logger.error("Unknown strategy type requested: {}", type);
+ throw new IllegalArgumentException("Unknown strategy type: " + type);
+ }
+ return strategy;
+ }
+
+ public static boolean hasStrategy(String type) {
+ return strategies.containsKey(type.toLowerCase());
+ }
+
+ public static String getSupportedTypes() {
+ return String.join(", ", strategies.keySet());
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/strategy/WebCrawler.java b/w11/src/main/java/com/example/datacollect/strategy/WebCrawler.java
new file mode 100644
index 0000000..468b292
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/strategy/WebCrawler.java
@@ -0,0 +1,244 @@
+package com.example.datacollect.strategy;
+
+import com.example.datacollect.exception.NetworkException;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+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.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class WebCrawler {
+ private static final Logger logger = LoggerFactory.getLogger(WebCrawler.class);
+
+ private static final OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(java.time.Duration.ofSeconds(30))
+ .readTimeout(java.time.Duration.ofSeconds(30))
+ .followRedirects(true)
+ .followSslRedirects(true)
+ .build();
+
+ private static final Pattern URL_PATTERN = Pattern.compile("^https?://");
+
+ private static final Set VALID_EXTENSIONS = Set.of(
+ ".html", ".htm", ".php", ".asp", ".aspx", ".jsp", ""
+ );
+
+ private static final int MAX_RETRIES = 3;
+ private static final long RETRY_DELAY_MS = 1000;
+
+ public static Document fetchDocument(String url) throws NetworkException {
+ int attempt = 0;
+ IOException lastException = null;
+
+ while (attempt < MAX_RETRIES) {
+ attempt++;
+ try {
+ logger.debug("Fetching document attempt {} of {} for URL: {}", attempt, MAX_RETRIES, url);
+ return doFetchDocument(url);
+ } catch (IOException e) {
+ lastException = e;
+ logger.warn("Fetch attempt {} failed for URL {}: {}", attempt, url, e.getMessage());
+ if (attempt < MAX_RETRIES) {
+ try {
+ Thread.sleep(RETRY_DELAY_MS);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new NetworkException("Fetching interrupted for URL: " + url, ie);
+ }
+ }
+ }
+ }
+
+ throw new NetworkException("Failed to fetch document after " + MAX_RETRIES + " attempts for URL: " + url, lastException);
+ }
+
+ private static Document doFetchDocument(String url) throws IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .header("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")
+ .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
+ .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
+ .header("Connection", "keep-alive")
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("HTTP request failed with code: " + response.code());
+ }
+
+ String contentType = response.header("Content-Type");
+ logger.debug("Content-Type: {}", contentType);
+
+ if (contentType != null && !contentType.contains("text/html")) {
+ throw new IOException("Not an HTML document: " + contentType);
+ }
+
+ ResponseBody body = response.body();
+ if (body == null) {
+ throw new IOException("Response body is null");
+ }
+
+ byte[] bytes = body.bytes();
+ String bodyString = new String(bytes, StandardCharsets.UTF_8);
+ logger.debug("Fetched HTML content length: {} bytes, {} characters", bytes.length, bodyString.length());
+
+ if (bodyString.length() < 100) {
+ logger.warn("HTML content is very short");
+ }
+
+ Document doc = Jsoup.parse(bodyString, url);
+ logger.debug("Parsed document title: '{}'", doc.title());
+ logger.debug("Number of elements in body: {}", doc.body() != null ? doc.body().getAllElements().size() : 0);
+
+ return doc;
+ }
+ }
+
+ public static List extractLinks(Document doc, String baseUrl) {
+ List links = new ArrayList<>();
+ Set seen = new HashSet<>();
+
+ Elements anchorTags = doc.select("a");
+ logger.debug("Found {} anchor tags", anchorTags.size());
+
+ for (Element anchor : anchorTags) {
+ String href = anchor.attr("abs:href");
+
+ if (href == null || href.isEmpty()) {
+ href = anchor.attr("href");
+ if (href != null && !href.isEmpty() && !href.startsWith("http")) {
+ href = resolveUrl(baseUrl, href);
+ }
+ }
+
+ if (href != null && isValidUrl(href) && !seen.contains(href)) {
+ seen.add(href);
+ links.add(href);
+ logger.trace("Added link: {}", href);
+ }
+ }
+
+ logger.debug("Extracted {} valid links from page", links.size());
+ return links;
+ }
+
+ private static String resolveUrl(String baseUrl, String relativeUrl) {
+ try {
+ java.net.URL base = new java.net.URL(baseUrl);
+ return new java.net.URL(base, relativeUrl).toString();
+ } catch (java.net.MalformedURLException e) {
+ logger.warn("Failed to resolve URL: {} + {}", baseUrl, relativeUrl);
+ return relativeUrl;
+ }
+ }
+
+ private static boolean isValidUrl(String url) {
+ if (url == null || url.isEmpty()) {
+ return false;
+ }
+
+ if (!URL_PATTERN.matcher(url).find()) {
+ return false;
+ }
+
+ if (url.contains("#")) {
+ url = url.substring(0, url.indexOf("#"));
+ }
+
+ String lowerUrl = url.toLowerCase();
+
+ // 排除常见的非HTML文件类型
+ if (lowerUrl.endsWith(".pdf") || lowerUrl.endsWith(".doc") ||
+ lowerUrl.endsWith(".docx") || lowerUrl.endsWith(".xls") ||
+ lowerUrl.endsWith(".xlsx") || lowerUrl.endsWith(".zip") ||
+ lowerUrl.endsWith(".rar") || lowerUrl.endsWith(".exe") ||
+ lowerUrl.endsWith(".jpg") || lowerUrl.endsWith(".jpeg") ||
+ lowerUrl.endsWith(".png") || lowerUrl.endsWith(".gif") ||
+ lowerUrl.endsWith(".svg") || lowerUrl.endsWith(".css") ||
+ lowerUrl.endsWith(".js") || lowerUrl.endsWith(".json") ||
+ lowerUrl.endsWith(".xml") || lowerUrl.endsWith(".txt")) {
+ return false;
+ }
+
+ // 允许所有其他类型的URL(包括无扩展名的URL)
+ return true;
+ }
+
+ private static String getExtension(String url) {
+ int lastDot = url.lastIndexOf('.');
+ int lastSlash = url.lastIndexOf('/');
+ if (lastDot > lastSlash) {
+ int queryIndex = url.indexOf('?');
+ if (queryIndex > lastDot) {
+ return url.substring(lastDot, queryIndex);
+ }
+ return url.substring(lastDot);
+ }
+ return "";
+ }
+
+ public static String extractTitle(Document doc) {
+ // 尝试获取title标签
+ Element titleElement = doc.selectFirst("title");
+ if (titleElement != null && !titleElement.text().trim().isEmpty()) {
+ return titleElement.text().trim();
+ }
+
+ // 尝试获取h1标签
+ Element h1Element = doc.selectFirst("h1");
+ if (h1Element != null && !h1Element.text().trim().isEmpty()) {
+ return h1Element.text().trim();
+ }
+
+ // 尝试获取文章标题类
+ Elements titleClasses = doc.select(".title, .post-title, .article-title, .entry-title, [class*=title]");
+ if (!titleClasses.isEmpty() && !titleClasses.first().text().trim().isEmpty()) {
+ return titleClasses.first().text().trim();
+ }
+
+ // 尝试获取meta title
+ Element metaTitle = doc.selectFirst("meta[property=og:title], meta[name=title]");
+ if (metaTitle != null && !metaTitle.attr("content").trim().isEmpty()) {
+ return metaTitle.attr("content").trim();
+ }
+
+ return "No Title";
+ }
+
+ public static String extractMetaDescription(Document doc) {
+ Element metaDesc = doc.selectFirst("meta[name=description]");
+ if (metaDesc != null) {
+ return metaDesc.attr("content").trim();
+ }
+ return "";
+ }
+
+ public static String extractContent(Document doc) {
+ Elements contentSelectors = doc.select("article, .article, .content, .post-content, .entry-content, main");
+
+ if (!contentSelectors.isEmpty()) {
+ return contentSelectors.first().text().trim();
+ }
+
+ Element body = doc.body();
+ if (body != null) {
+ return body.text().trim().substring(0, Math.min(body.text().length(), 500)) + "...";
+ }
+
+ return "";
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/test/CrawlerDemo.java b/w11/src/main/java/com/example/datacollect/test/CrawlerDemo.java
new file mode 100644
index 0000000..e62f00f
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/test/CrawlerDemo.java
@@ -0,0 +1,118 @@
+package com.example.datacollect.test;
+
+import com.example.datacollect.command.CommandContext;
+import com.example.datacollect.command.CrawlCommand;
+import com.example.datacollect.command.ListCommand;
+import com.example.datacollect.command.HelpCommand;
+import com.example.datacollect.command.HistoryCommand;
+import com.example.datacollect.model.Article;
+import com.example.datacollect.repository.ArticleRepository;
+import com.example.datacollect.strategy.CrawlStrategy;
+import com.example.datacollect.strategy.StrategyFactory;
+import com.example.datacollect.view.ConsoleView;
+
+import java.util.List;
+
+public class CrawlerDemo {
+
+ public static void main(String[] args) {
+ ConsoleView view = new ConsoleView();
+ ArticleRepository repository = new ArticleRepository();
+ CommandContext context = new CommandContext(repository);
+
+ System.out.println("========================================");
+ System.out.println(" 爬虫功能演示");
+ System.out.println("========================================\n");
+
+ demo1_StrategyFactory(view);
+ demo2_BlogCrawl(view, repository, context);
+ demo3_NewsCrawl(view, repository, context);
+ demo4_ListArticles(view, repository, context);
+ demo5_CommandHistory(view, context);
+ demo6_RepositoryFeatures(repository);
+
+ System.out.println("\n========================================");
+ System.out.println(" 演示完成!");
+ System.out.println("========================================");
+ }
+
+ private static void demo1_StrategyFactory(ConsoleView view) {
+ System.out.println("【演示1】策略工厂功能");
+ System.out.println("-".repeat(40));
+
+ System.out.println("支持的策略类型: " + StrategyFactory.getSupportedTypes());
+
+ CrawlStrategy blogStrategy = StrategyFactory.getStrategy("blog");
+ System.out.println("Blog策略类型: " + blogStrategy.getType());
+
+ CrawlStrategy newsStrategy = StrategyFactory.getStrategy("news");
+ System.out.println("News策略类型: " + newsStrategy.getType());
+
+ try {
+ StrategyFactory.getStrategy("unknown");
+ } catch (IllegalArgumentException e) {
+ System.out.println("未知策略测试: " + e.getMessage());
+ }
+ System.out.println();
+ }
+
+ private static void demo2_BlogCrawl(ConsoleView view, ArticleRepository repository, CommandContext context) {
+ System.out.println("【演示2】博客爬取");
+ System.out.println("-".repeat(40));
+
+ CrawlCommand crawlCommand = new CrawlCommand(view);
+ crawlCommand.execute(new String[]{"crawl", "blog", "http://example.com"}, context);
+
+ System.out.println("仓库中文章数量: " + repository.count());
+ System.out.println();
+ }
+
+ private static void demo3_NewsCrawl(ConsoleView view, ArticleRepository repository, CommandContext context) {
+ System.out.println("【演示3】新闻爬取");
+ System.out.println("-".repeat(40));
+
+ CrawlCommand crawlCommand = new CrawlCommand(view);
+ crawlCommand.execute(new String[]{"crawl", "news", "http://news.com"}, context);
+
+ System.out.println("仓库中文章总数: " + repository.count());
+ System.out.println();
+ }
+
+ private static void demo4_ListArticles(ConsoleView view, ArticleRepository repository, CommandContext context) {
+ System.out.println("【演示4】列出所有文章");
+ System.out.println("-".repeat(40));
+
+ ListCommand listCommand = new ListCommand(view);
+ listCommand.execute(new String[]{"list"}, context);
+ System.out.println();
+ }
+
+ private static void demo5_CommandHistory(ConsoleView view, CommandContext context) {
+ System.out.println("【演示5】命令历史");
+ System.out.println("-".repeat(40));
+
+ HistoryCommand historyCommand = new HistoryCommand(view);
+ historyCommand.execute(new String[]{"history"}, context);
+ System.out.println();
+ }
+
+ private static void demo6_RepositoryFeatures(ArticleRepository repository) {
+ System.out.println("【演示6】仓库功能");
+ System.out.println("-".repeat(40));
+
+ System.out.println("文章总数: " + repository.count());
+
+ Article firstArticle = repository.findAll().get(0);
+ System.out.println("第一篇文章标题: " + firstArticle.getTitle());
+
+ String url = firstArticle.getUrl();
+ repository.findByUrl(url).ifPresent(article ->
+ System.out.println("按URL查找成功: " + article.getTitle())
+ );
+
+ System.out.println("清空前文章数: " + repository.count());
+ repository.clear();
+ System.out.println("清空后文章数: " + repository.count());
+ System.out.println();
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/test/CrawlerTest.java b/w11/src/main/java/com/example/datacollect/test/CrawlerTest.java
new file mode 100644
index 0000000..aa0ab2f
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/test/CrawlerTest.java
@@ -0,0 +1,83 @@
+package com.example.datacollect.test;
+
+import com.example.datacollect.command.CommandContext;
+import com.example.datacollect.command.CrawlCommand;
+import com.example.datacollect.command.ListCommand;
+import com.example.datacollect.model.Article;
+import com.example.datacollect.repository.ArticleRepository;
+import com.example.datacollect.strategy.CrawlStrategy;
+import com.example.datacollect.strategy.StrategyFactory;
+import com.example.datacollect.view.ConsoleView;
+
+import java.util.List;
+
+public class CrawlerTest {
+
+ public static void main(String[] args) {
+ System.out.println("=== Crawler Test Suite ===");
+
+ testStrategyFactory();
+ testArticleRepositoryImmutable();
+ testCrawlToListFlow();
+
+ System.out.println("\n=== All tests passed! ===");
+ }
+
+ private static void testStrategyFactory() {
+ System.out.println("\n1. Testing StrategyFactory SPI loading...");
+
+ assert StrategyFactory.hasStrategy("blog") : "blog strategy should be registered";
+ assert StrategyFactory.hasStrategy("news") : "news strategy should be registered";
+ assert !StrategyFactory.hasStrategy("unknown") : "unknown strategy should not be registered";
+
+ CrawlStrategy blogStrategy = StrategyFactory.getStrategy("blog");
+ assert "blog".equals(blogStrategy.getType()) : "blog strategy type mismatch";
+
+ CrawlStrategy newsStrategy = StrategyFactory.getStrategy("news");
+ assert "news".equals(newsStrategy.getType()) : "news strategy type mismatch";
+
+ System.out.println(" ✓ StrategyFactory loads strategies via SPI");
+ System.out.println(" ✓ Supported types: " + StrategyFactory.getSupportedTypes());
+ }
+
+ private static void testArticleRepositoryImmutable() {
+ System.out.println("\n2. Testing ArticleRepository immutability...");
+
+ ArticleRepository repository = new ArticleRepository();
+ repository.add(new Article("Test", "http://test.com", "content"));
+
+ List articles = repository.findAll();
+
+ try {
+ articles.add(new Article("Should Not Add", "http://fail.com", "fail"));
+ assert false : "Should have thrown UnsupportedOperationException";
+ } catch (UnsupportedOperationException e) {
+ System.out.println(" ✓ getAll() returns immutable view");
+ }
+
+ assert repository.count() == 1 : "Repository should have 1 article";
+ System.out.println(" ✓ Repository count is correct");
+ }
+
+ private static void testCrawlToListFlow() {
+ System.out.println("\n3. Testing crawl → list flow...");
+
+ ConsoleView view = new ConsoleView();
+ ArticleRepository repository = new ArticleRepository();
+ CommandContext context = new CommandContext(repository);
+
+ CrawlCommand crawlCommand = new CrawlCommand(view);
+ ListCommand listCommand = new ListCommand(view);
+
+ crawlCommand.execute(new String[]{"crawl", "blog", "http://example.com"}, context);
+ assert repository.count() == 2 : "Should have 2 blog articles";
+ System.out.println(" ✓ Crawl blog strategy: " + repository.count() + " articles");
+
+ crawlCommand.execute(new String[]{"crawl", "news", "http://news.com"}, context);
+ assert repository.count() == 5 : "Should have 5 articles total (2 blog + 3 news)";
+ System.out.println(" ✓ Crawl news strategy: " + repository.count() + " articles total");
+
+ listCommand.execute(new String[]{"list"}, context);
+ System.out.println(" ✓ List command executed successfully");
+ }
+}
diff --git a/w11/src/main/java/com/example/datacollect/test/RealCrawlTest.java b/w11/src/main/java/com/example/datacollect/test/RealCrawlTest.java
new file mode 100644
index 0000000..ec06d88
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/test/RealCrawlTest.java
@@ -0,0 +1,53 @@
+package com.example.datacollect.test;
+
+import com.example.datacollect.command.CommandContext;
+import com.example.datacollect.command.CrawlCommand;
+import com.example.datacollect.command.ListCommand;
+import com.example.datacollect.repository.ArticleRepository;
+import com.example.datacollect.view.ConsoleView;
+
+public class RealCrawlTest {
+
+ public static void main(String[] args) {
+ ConsoleView view = new ConsoleView();
+ ArticleRepository repository = new ArticleRepository();
+ CommandContext context = new CommandContext(repository);
+
+ System.out.println("========================================");
+ System.out.println(" 测试真实网页爬取功能");
+ System.out.println("========================================");
+ System.out.println();
+
+ CrawlCommand crawlCommand = new CrawlCommand(view);
+
+ // 测试真实爬取(使用真实博客网站)
+ System.out.println("【测试1】爬取 Baeldung 博客");
+ System.out.println("-".repeat(40));
+ try {
+ crawlCommand.execute(new String[]{"crawl", "real", "https://www.baeldung.com/"}, context);
+ } catch (Exception e) {
+ System.out.println("爬取失败: " + e.getMessage());
+ }
+
+ System.out.println();
+ System.out.println("【测试2】爬取今日头条");
+ System.out.println("-".repeat(40));
+ try {
+ crawlCommand.execute(new String[]{"crawl", "real", "https://www.toutiao.com/"}, context);
+ } catch (Exception e) {
+ System.out.println("爬取失败: " + e.getMessage());
+ }
+
+ System.out.println();
+ System.out.println("【爬取结果】");
+ System.out.println("-".repeat(40));
+
+ ListCommand listCommand = new ListCommand(view);
+ listCommand.execute(new String[]{"list"}, context);
+
+ System.out.println();
+ System.out.println("========================================");
+ System.out.println(" 测试完成!");
+ System.out.println("========================================");
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/test/ToutiaoTest.java b/w11/src/main/java/com/example/datacollect/test/ToutiaoTest.java
new file mode 100644
index 0000000..1ff02d5
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/test/ToutiaoTest.java
@@ -0,0 +1,37 @@
+package com.example.datacollect.test;
+
+import com.example.datacollect.command.CommandContext;
+import com.example.datacollect.command.CrawlCommand;
+import com.example.datacollect.command.ListCommand;
+import com.example.datacollect.repository.ArticleRepository;
+import com.example.datacollect.view.ConsoleView;
+
+public class ToutiaoTest {
+
+ public static void main(String[] args) {
+ ConsoleView view = new ConsoleView();
+ ArticleRepository repository = new ArticleRepository();
+ CommandContext context = new CommandContext(repository);
+
+ System.out.println("========================================");
+ System.out.println(" 测试今日头条爬取");
+ System.out.println("========================================");
+ System.out.println();
+
+ // 测试爬取今日头条
+ CrawlCommand crawlCommand = new CrawlCommand(view);
+ crawlCommand.execute(new String[]{"crawl", "news", "https://www.toutiao.com/"}, context);
+
+ System.out.println();
+ System.out.println("【爬取结果】");
+ System.out.println("-".repeat(40));
+
+ ListCommand listCommand = new ListCommand(view);
+ listCommand.execute(new String[]{"list"}, context);
+
+ System.out.println();
+ System.out.println("========================================");
+ System.out.println(" 测试完成!");
+ System.out.println("========================================");
+ }
+}
\ No newline at end of file
diff --git a/w11/src/main/java/com/example/datacollect/view/ConsoleView.java b/w11/src/main/java/com/example/datacollect/view/ConsoleView.java
new file mode 100644
index 0000000..3c1d47a
--- /dev/null
+++ b/w11/src/main/java/com/example/datacollect/view/ConsoleView.java
@@ -0,0 +1,42 @@
+package com.example.datacollect.view;
+
+import com.example.datacollect.model.Article;
+import java.util.List;
+import java.util.Scanner;
+
+public class ConsoleView {
+ private static final String ANSI_RESET = "\u001B[0m";
+ private static final String ANSI_GREEN = "\u001B[32m";
+ private static final String ANSI_RED = "\u001B[31m";
+ private static final String ANSI_BLUE = "\u001B[34m";
+
+ private final Scanner scanner = new Scanner(System.in);
+
+ public String readLine() {
+ System.out.print("> ");
+ return scanner.nextLine();
+ }
+
+ 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_BLUE + msg + ANSI_RESET);
+ }
+
+ public void display(List articles) {
+ if (articles.isEmpty()) {
+ printInfo("暂无文章,请先执行 crawl。");
+ return;
+ }
+ for (int i = 0; i < articles.size(); i++) {
+ Article a = articles.get(i);
+ System.out.println((i + 1) + ". " + a.getTitle() + " | " + a.getUrl());
+ }
+ }
+}
diff --git a/w11/src/main/resources/META-INF/services/com.example.datacollect.strategy.CrawlStrategy b/w11/src/main/resources/META-INF/services/com.example.datacollect.strategy.CrawlStrategy
new file mode 100644
index 0000000..d3d7287
--- /dev/null
+++ b/w11/src/main/resources/META-INF/services/com.example.datacollect.strategy.CrawlStrategy
@@ -0,0 +1,3 @@
+com.example.datacollect.strategy.BlogStrategy
+com.example.datacollect.strategy.NewsStrategy
+com.example.datacollect.strategy.RealCrawlStrategy
diff --git a/w11/src/main/resources/logback.xml b/w11/src/main/resources/logback.xml
new file mode 100644
index 0000000..716d4dc
--- /dev/null
+++ b/w11/src/main/resources/logback.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ logs/application.log
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ logs/application.log
+
+ logs/application.%d{yyyy-MM-dd}.%i.log
+ 30
+
+ 10MB
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/w11/target/classes/META-INF/services/com.example.datacollect.strategy.CrawlStrategy b/w11/target/classes/META-INF/services/com.example.datacollect.strategy.CrawlStrategy
new file mode 100644
index 0000000..d3d7287
--- /dev/null
+++ b/w11/target/classes/META-INF/services/com.example.datacollect.strategy.CrawlStrategy
@@ -0,0 +1,3 @@
+com.example.datacollect.strategy.BlogStrategy
+com.example.datacollect.strategy.NewsStrategy
+com.example.datacollect.strategy.RealCrawlStrategy
diff --git a/w11/target/classes/logback.xml b/w11/target/classes/logback.xml
new file mode 100644
index 0000000..716d4dc
--- /dev/null
+++ b/w11/target/classes/logback.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ logs/application.log
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ logs/application.log
+
+ logs/application.%d{yyyy-MM-dd}.%i.log
+ 30
+
+ 10MB
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/w11/target/maven-archiver/pom.properties b/w11/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..5c1de34
--- /dev/null
+++ b/w11/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=datacollect-cli
+groupId=com.example
+version=0.1.0
diff --git a/w11/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/w11/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..a7e982e
--- /dev/null
+++ b/w11/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,20 @@
+com\example\datacollect\command\ListCommand.class
+com\example\datacollect\command\CrawlCommand.class
+com\example\datacollect\view\ConsoleView.class
+com\example\datacollect\test\ToutiaoTest.class
+com\example\datacollect\strategy\NewsStrategy.class
+com\example\datacollect\command\CommandContext.class
+com\example\datacollect\command\Command.class
+com\example\datacollect\test\CrawlerTest.class
+com\example\datacollect\test\CrawlerDemo.class
+com\example\datacollect\strategy\CrawlStrategy.class
+com\example\datacollect\model\Article.class
+com\example\datacollect\strategy\WebCrawler.class
+com\example\datacollect\strategy\BlogStrategy.class
+com\example\datacollect\repository\ArticleRepository.class
+com\example\datacollect\Main.class
+com\example\datacollect\command\ExitCommand.class
+com\example\datacollect\command\HelpCommand.class
+com\example\datacollect\command\HistoryCommand.class
+com\example\datacollect\controller\CrawlerController.class
+com\example\datacollect\strategy\StrategyFactory.class
diff --git a/w11/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/w11/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..77c6c35
--- /dev/null
+++ b/w11/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,25 @@
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\strategy\WebCrawler.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\exception\ParseException.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\CrawlCommand.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\test\RealCrawlTest.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\ExitCommand.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\Main.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\strategy\CrawlStrategy.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\strategy\BlogStrategy.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\test\CrawlerDemo.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\CommandContext.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\model\Article.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\strategy\NewsStrategy.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\exception\CrawlerException.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\strategy\StrategyFactory.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\repository\ArticleRepository.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\test\ToutiaoTest.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\HistoryCommand.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\exception\NetworkException.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\Command.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\controller\CrawlerController.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\HelpCommand.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\view\ConsoleView.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\test\CrawlerTest.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\command\ListCommand.java
+C:\Users\ruiruirui\java\w11\src\main\java\com\example\datacollect\strategy\RealCrawlStrategy.java
diff --git a/w11/test_crawler.bat b/w11/test_crawler.bat
new file mode 100644
index 0000000..36a56fa
--- /dev/null
+++ b/w11/test_crawler.bat
@@ -0,0 +1,26 @@
+@echo off
+echo ====================================
+echo 爬虫功能测试脚本
+echo ====================================
+echo.
+
+echo [测试1] 运行单元测试...
+java -cp target/datacollect-cli-0.1.0-jar-with-dependencies.jar com.example.datacollect.test.CrawlerTest
+if %errorlevel% neq 0 (
+ echo 单元测试失败!
+ exit /b 1
+)
+echo.
+
+echo [测试2] 测试策略工厂...
+echo 支持的策略类型:
+java -cp target/datacollect-cli-0.1.0-jar-with-dependencies.jar -c "import com.example.datacollect.strategy.StrategyFactory; System.out.println(StrategyFactory.getSupportedTypes());" 2>nul || echo (需要JShell支持)
+echo.
+
+echo [测试3] 测试Blog策略爬取...
+java -cp target/datacollect-cli-0.1.0-jar-with-dependencies.jar com.example.datacollect.test.CrawlerTest
+echo.
+
+echo ====================================
+echo 测试完成!
+echo ====================================
\ No newline at end of file
diff --git a/w12/.vscode/launch.json b/w12/.vscode/launch.json
new file mode 100644
index 0000000..1fda4c7
--- /dev/null
+++ b/w12/.vscode/launch.json
@@ -0,0 +1,29 @@
+{
+ // 使用 IntelliSense 了解相关属性。
+ // 悬停以查看现有属性的描述。
+ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+
+ {
+ "type": "java",
+ "name": "Current File",
+ "request": "launch",
+ "mainClass": "${file}"
+ },
+ {
+ "type": "java",
+ "name": "WeiboStarHotSearcha",
+ "request": "launch",
+ "mainClass": "WeiboStarHotSearcha",
+ "projectName": "weibo-hotsearch"
+ },
+ {
+ "type": "java",
+ "name": "Main",
+ "request": "launch",
+ "mainClass": "com.weibo.hotsearch.Main",
+ "projectName": "weibo-hotsearch"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/w12/.vscode/settings.json b/w12/.vscode/settings.json
new file mode 100644
index 0000000..c5f3f6b
--- /dev/null
+++ b/w12/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/w12/202506050225-毕磊-高级程序设计实验报告.docx b/w12/202506050225-毕磊-高级程序设计实验报告.docx
new file mode 100644
index 0000000000000000000000000000000000000000..7ff03ff545ce551a4cf671535368e588befd73c3
GIT binary patch
literal 266488
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
z