Compare commits
2 Commits
598d13c54e
...
df3213139c
| Author | SHA1 | Date |
|---|---|---|
|
|
df3213139c | 4 weeks ago |
|
|
8adb77d146 | 4 weeks ago |
@ -0,0 +1 @@ |
|||||
|
*.log filter=lfs diff=lfs merge=lfs -text |
||||
@ -0,0 +1 @@ |
|||||
|
.log logs/target/ .vscode/ |
||||
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"java.configuration.updateBuildConfiguration": "interactive" |
||||
|
} |
||||
@ -0,0 +1,325 @@ |
|||||
|
# 大宗商品爬虫系统 |
||||
|
|
||||
|
## 项目概述 |
||||
|
|
||||
|
本项目为Java语言开发的大宗商品数据爬虫与可视化分析系统,核心目标是通过一套统一框架,爬取金投网、东方财富网、同花顺财经3个不同网站的相关数据,实现海量数据采集、存储、分析与可视化。 |
||||
|
|
||||
|
## 技术架构 |
||||
|
|
||||
|
### 分层架构 |
||||
|
- **控制层(Controller)**: CrawlerController,统一调度爬虫策略 |
||||
|
- **模型层(Model)**: 封装业务实体(行情、指数、舆情数据) |
||||
|
- **视图层(View)**: 基于JFreeChart实现可视化图表,支持HTML报告 |
||||
|
- **策略层(Strategy)**: 统一爬虫策略接口,各站点实现具体策略 |
||||
|
- **工厂层(Factory)**: 爬虫策略工厂,动态创建策略实例 |
||||
|
- **仓储层(Repository)**: 数据持久化操作封装 |
||||
|
- **命令层(Command)**: 基于Command模式实现命令管理 |
||||
|
- **监控层(Monitor)**: WebSocket实时数据广播 |
||||
|
|
||||
|
### 设计模式 |
||||
|
- **策略模式**: 站点爬取逻辑解耦 |
||||
|
- **工厂模式**: 策略实例创建与管理 |
||||
|
- **MVC模式**: 分层架构 |
||||
|
- **命令模式**: 命令封装与执行 |
||||
|
- **仓储模式**: 数据访问层抽象 |
||||
|
|
||||
|
### 技术栈 |
||||
|
- Java 1.8+ |
||||
|
- OkHttp3 (网络请求) |
||||
|
- Jsoup (网页解析) |
||||
|
- SQLite + MyBatis (数据持久化) |
||||
|
- JFreeChart (可视化图表) |
||||
|
- Apache POI (Excel导出) |
||||
|
- Gson (JSON处理) |
||||
|
- Apache PDFBox (PDF报告,中文黑体支持) |
||||
|
- Java-WebSocket (实时监控) |
||||
|
- SLF4J + Logback (日志) |
||||
|
|
||||
|
## 快速开始 |
||||
|
|
||||
|
### 环境要求 |
||||
|
- JDK 1.8+ |
||||
|
- Maven 3.6+ |
||||
|
- Windows系统(PDF报告生成需要黑体字体) |
||||
|
|
||||
|
### 构建项目 |
||||
|
|
||||
|
```bash |
||||
|
cd commodity-crawler |
||||
|
mvn clean package -DskipTests |
||||
|
``` |
||||
|
|
||||
|
### 运行爬虫 |
||||
|
|
||||
|
```bash |
||||
|
# 启动交互式菜单(推荐) |
||||
|
java -jar target/commodity-crawler-1.0.0.jar |
||||
|
|
||||
|
# 爬取所有站点(默认30页) |
||||
|
java -jar target/commodity-crawler-1.0.0.jar -s all -p 30 |
||||
|
|
||||
|
# 爬取指定站点 |
||||
|
java -jar target/commodity-crawler-1.0.0.jar -s jintou -p 5 |
||||
|
|
||||
|
# 爬取并生成分析图表 |
||||
|
java -jar target/commodity-crawler-1.0.0.jar -s all -p 30 -a |
||||
|
|
||||
|
# 爬取并导出CSV |
||||
|
java -jar target/commodity-crawler-1.0.0.jar -s all -e csv |
||||
|
|
||||
|
# 生成PDF分析报告 |
||||
|
java -jar target/commodity-crawler-1.0.0.jar --report |
||||
|
|
||||
|
# 爬取+导出+图表+报告+监控 |
||||
|
java -jar target/commodity-crawler-1.0.0.jar -s all -e json -a -r -m |
||||
|
``` |
||||
|
|
||||
|
### 命令行参数 |
||||
|
|
||||
|
| 参数 | 说明 | 默认值 | |
||||
|
|------|------|--------| |
||||
|
| -s, --site | 指定爬取站点 (jintou/eastmoney/tonghuashun/all) | all | |
||||
|
| -p, --pages | 指定爬取页数 | 30 | |
||||
|
| -a, --analyze | 执行数据分析并生成可视化图表 | false | |
||||
|
| -e, --export [格式] | 导出数据 (excel/csv/json,默认excel) | - | |
||||
|
| -r, --report | 生成PDF分析报告 | false | |
||||
|
| -m, --monitor | 启动WebSocket实时监控服务 | false | |
||||
|
| -h, --help | 显示帮助信息 | - | |
||||
|
|
||||
|
## 功能特性 |
||||
|
|
||||
|
### 1. 数据爬取 |
||||
|
- 支持金投网、东方财富网、同花顺财经三个站点 |
||||
|
- 模拟数据生成(避免真实网络请求) |
||||
|
- 支持多线程并发爬取 |
||||
|
- 完善的重试机制 |
||||
|
- **默认爬取30页数据** |
||||
|
|
||||
|
### 2. 数据导出 |
||||
|
- **Excel格式** (.xlsx) - 支持格式化表格输出,Apache POI实现 |
||||
|
- **CSV格式** (.csv) - 支持UTF-8编码,Excel兼容 |
||||
|
- **JSON格式** (.json) - 结构化数据输出,Gson处理 |
||||
|
|
||||
|
导出目录:`./output/excel/` |
||||
|
|
||||
|
### 3. 可视化分析 |
||||
|
- 价格趋势对比分析(多折线图) |
||||
|
- 波动特征分析(柱状图) |
||||
|
- 相关性分析(散点图) |
||||
|
- 舆情联动分析 |
||||
|
|
||||
|
图表输出目录:`./output/charts/` |
||||
|
|
||||
|
### 4. PDF报告生成 |
||||
|
- 自动生成专业分析报告(中文) |
||||
|
- 黑体(simhei.ttf)字体支持 |
||||
|
- 包含封面、目录、市场概览、数据表格 |
||||
|
- **报告内容**:市场概览、价格趋势分析、波动率分析、相关性分析、情绪分析、数据统计表 |
||||
|
- 报告输出目录:`./output/report/` |
||||
|
|
||||
|
### 5. 实时监控大屏 |
||||
|
- WebSocket实时数据推送 |
||||
|
- 多商品实时价格监控面板 |
||||
|
- 支持ECharts可视化 |
||||
|
- 市场情绪分析 |
||||
|
- 监控页面:`src/main/resources/webapp/monitor.html` |
||||
|
|
||||
|
## 爬取站点 |
||||
|
|
||||
|
### 1. 金投网 (jintou) |
||||
|
- 爬取内容:黄金、白银、原油历史行情 |
||||
|
- 数据字段:交易日期、品种、开盘价、收盘价、最高价、最低价、成交量、涨跌幅 |
||||
|
|
||||
|
### 2. 东方财富网 (eastmoney) |
||||
|
- 爬取内容:大宗商品板块指数、相关概念股行情 |
||||
|
- 数据字段:指数名称、日期、指数值、涨跌幅、概念股名称、股价、换手率 |
||||
|
|
||||
|
### 3. 同花顺财经 (tonghuashun) |
||||
|
- 爬取内容:大宗商品相关财经新闻、市场评论 |
||||
|
- 数据字段:新闻标题、内容、发布时间、关联商品、舆情倾向 |
||||
|
|
||||
|
## 异常处理 |
||||
|
|
||||
|
系统设计了完整的异常处理层次: |
||||
|
|
||||
|
- **BaseCrawlException**: 自定义异常父类 |
||||
|
- **NetworkException**: 网络异常(支持重试机制) |
||||
|
- **ParseException**: 网页解析异常 |
||||
|
- **DbException**: 数据库异常 |
||||
|
- **ParamException**: 参数异常 |
||||
|
|
||||
|
### 重试机制 |
||||
|
- 重试次数:可配置(默认3次) |
||||
|
- 重试间隔:递增间隔(1s、3s、5s) |
||||
|
- 续爬能力:断网后自动重试未完成任务 |
||||
|
|
||||
|
## 配置说明 |
||||
|
|
||||
|
配置文件:`src/main/resources/application.properties` |
||||
|
|
||||
|
```properties |
||||
|
# 数据库配置 |
||||
|
db.driver=org.sqlite.JDBC |
||||
|
db.url=jdbc:sqlite:./data/example_db.sqlite |
||||
|
|
||||
|
# 爬虫配置 |
||||
|
crawl.page.count=30 # 默认爬取页数(已从10调整为30) |
||||
|
crawl.retry.count=3 # 重试次数 |
||||
|
crawl.retry.delay.initial=1000 # 初始重试间隔(ms) |
||||
|
crawl.retry.delay.multiplier=2 # 重试间隔倍数 |
||||
|
crawl.request.interval=2000 # 请求间隔(ms) |
||||
|
|
||||
|
# 线程池配置 |
||||
|
thread.pool.core.size=5 |
||||
|
thread.pool.max.size=10 |
||||
|
|
||||
|
# 输出配置 |
||||
|
output.chart.dir=./output/charts/ |
||||
|
output.excel.dir=./output/excel/ |
||||
|
output.report.dir=./output/report/ |
||||
|
output.log.dir=./logs/ |
||||
|
|
||||
|
# WebSocket配置 |
||||
|
websocket.port=8080 |
||||
|
``` |
||||
|
|
||||
|
## 项目结构 |
||||
|
|
||||
|
``` |
||||
|
commodity-crawler/ |
||||
|
├── src/main/java/com/example/crawler/ |
||||
|
│ ├── CrawlMain.java # 主启动类 |
||||
|
│ ├── InteractiveCLI.java # 交互式命令行界面 |
||||
|
│ ├── command/ # 命令模式实现 |
||||
|
│ │ ├── Command.java # 命令接口 |
||||
|
│ │ ├── CommandInvoker.java # 命令调用者 |
||||
|
│ │ ├── CrawlCommand.java # 爬取命令 |
||||
|
│ │ ├── ExportDataCommand.java # 数据导出命令 |
||||
|
│ │ ├── GenerateChartCommand.java # 图表生成命令 |
||||
|
│ │ ├── GenerateReportCommand.java # PDF报告命令 |
||||
|
│ │ ├── MonitorCommand.java # 实时监控命令 |
||||
|
│ │ └── ViewDataCommand.java # 数据查看命令 |
||||
|
│ ├── controller/ |
||||
|
│ │ └── CrawlerController.java # 爬虫控制器 |
||||
|
│ ├── exception/ # 异常类 |
||||
|
│ ├── mapper/ # MyBatis Mapper |
||||
|
│ ├── model/ # 数据模型 |
||||
|
│ │ ├── MarketData.java # 行情数据模型 |
||||
|
│ │ ├── IndexData.java # 指数数据模型 |
||||
|
│ │ └── NewsData.java # 舆情数据模型 |
||||
|
│ ├── monitor/ # 实时监控模块 |
||||
|
│ │ ├── DataBroadcaster.java # WebSocket服务器 |
||||
|
│ │ └── PriceSnapshot.java # 价格快照 |
||||
|
│ ├── repository/ # 仓储层 |
||||
|
│ │ ├── MarketDataRepository.java |
||||
|
│ │ ├── IndexDataRepository.java |
||||
|
│ │ └── NewsDataRepository.java |
||||
|
│ ├── strategy/ # 策略层 |
||||
|
│ │ ├── CrawlStrategy.java # 爬虫策略接口 |
||||
|
│ │ ├── CrawlStrategyFactory.java |
||||
|
│ │ ├── JinTouCrawlStrategy.java |
||||
|
│ │ ├── EastMoneyCrawlStrategy.java |
||||
|
│ │ └── TongHuaShunCrawlStrategy.java |
||||
|
│ ├── util/ # 工具类 |
||||
|
│ │ ├── exporter/ # 数据导出器 |
||||
|
│ │ │ ├── DataExporter.java |
||||
|
│ │ │ ├── CsvExporter.java |
||||
|
│ │ │ ├── JsonExporter.java |
||||
|
│ │ │ └── DataExporterFactory.java |
||||
|
│ │ ├── ExcelExporter.java # Excel导出 |
||||
|
│ │ ├── PdfReportGenerator.java # PDF报告生成器(中文支持) |
||||
|
│ │ ├── ChartGenerator.java # 图表生成器 |
||||
|
│ │ ├── DateTypeHandler.java # MyBatis日期类型处理器 |
||||
|
│ │ └── MyBatisUtil.java # MyBatis工具类 |
||||
|
│ └── visualization/ # 可视化模块 |
||||
|
├── src/main/resources/ |
||||
|
│ ├── application.properties # 配置文件 |
||||
|
│ ├── logback.xml # 日志配置 |
||||
|
│ ├── mybatis-config.xml # MyBatis配置(含驼峰映射) |
||||
|
│ ├── schema.sql # 数据库初始化脚本 |
||||
|
│ ├── mapper/ # MyBatis XML配置 |
||||
|
│ │ ├── MarketDataMapper.xml |
||||
|
│ │ ├── IndexDataMapper.xml |
||||
|
│ │ └── NewsDataMapper.xml |
||||
|
│ └── webapp/ # Web资源 |
||||
|
│ ├── echarts.min.js # ECharts库(本地化,支持离线) |
||||
|
│ └── monitor.html # 监控大屏页面(ECharts可视化) |
||||
|
└── pom.xml # Maven配置 |
||||
|
``` |
||||
|
|
||||
|
## 核心特性说明 |
||||
|
|
||||
|
### MyBatis字段映射 |
||||
|
系统已配置`mapUnderscoreToCamelCase=true`,自动将数据库下划线命名字段映射到Java驼峰命名: |
||||
|
- `index_name` → `indexName` |
||||
|
- `index_value` → `indexValue` |
||||
|
- `change_rate` → `changeRate` |
||||
|
- `stock_name` → `stockName` |
||||
|
- `stock_price` → `stockPrice` |
||||
|
- `turnover_rate` → `turnoverRate` |
||||
|
|
||||
|
### 自定义日期类型处理器 |
||||
|
`DateTypeHandler`支持: |
||||
|
- Unix时间戳(毫秒,13位) |
||||
|
- Unix时间戳(秒,10位) |
||||
|
- 日期字符串格式 |
||||
|
- MySQL TIMESTAMP类型 |
||||
|
|
||||
|
### PDF报告中文支持 |
||||
|
- 使用黑体(simhei.ttf)字体 |
||||
|
- 支持中文完整显示 |
||||
|
- 包含8页完整报告内容 |
||||
|
|
||||
|
### 实时监控WebSocket |
||||
|
- 端口:8080 |
||||
|
- 推送频率:每2秒更新 |
||||
|
- 数据格式:JSON |
||||
|
- 监控页面自动连接 |
||||
|
|
||||
|
### 完整离线支持 |
||||
|
系统支持**完整离线运行**,所有核心功能均不依赖网络: |
||||
|
- ✅ 数据爬取(使用模拟数据生成) |
||||
|
- ✅ 数据存储(SQLite本地数据库) |
||||
|
- ✅ 数据导出(Excel/CSV/JSON本地文件) |
||||
|
- ✅ 图表生成(JFreeChart本地生成) |
||||
|
- ✅ PDF报告(本地字体和PDFBox生成) |
||||
|
- ✅ 实时监控页面(ECharts库本地化) |
||||
|
|
||||
|
## 扩展说明 |
||||
|
|
||||
|
### 新增爬虫站点 |
||||
|
1. 实现 `CrawlStrategy` 接口 |
||||
|
2. 在 `CrawlStrategyFactory` 中添加分支 |
||||
|
3. 无需修改原有代码 |
||||
|
|
||||
|
### 新增导出格式 |
||||
|
1. 实现 `DataExporter` 接口 |
||||
|
2. 在 `DataExporterFactory` 中注册 |
||||
|
3. 自动集成到导出命令 |
||||
|
|
||||
|
### 新增分析维度 |
||||
|
1. 在 `ChartGenerator` 中添加新方法 |
||||
|
2. 实现图表生成逻辑 |
||||
|
|
||||
|
## 输出文件说明 |
||||
|
|
||||
|
| 类型 | 目录 | 说明 | |
||||
|
|------|------|------| |
||||
|
| 图表 | `./output/charts/` | 价格趋势、波动率、相关性、舆情分析图 | |
||||
|
| 数据 | `./output/excel/` | Excel、CSV、JSON格式导出文件 | |
||||
|
| 报告 | `./output/report/` | 中文PDF分析报告 | |
||||
|
| 数据库 | `./data/` | SQLite数据库文件 | |
||||
|
| 日志 | `./logs/` | 系统运行日志 | |
||||
|
|
||||
|
## 注意事项 |
||||
|
|
||||
|
1. 请确保遵守目标网站的 robots.txt 协议 |
||||
|
2. 合理设置请求间隔,避免触发反爬机制 |
||||
|
3. 首次运行会自动创建SQLite数据库文件 |
||||
|
4. PDF报告需要Windows系统黑体字体支持 |
||||
|
5. 建议定期清理输出目录中的历史文件 |
||||
|
6. 系统支持完整离线运行,所有核心功能均可在断网环境下正常使用 |
||||
|
|
||||
|
## License |
||||
|
|
||||
|
MIT License |
||||
@ -0,0 +1,300 @@ |
|||||
|
@startuml大宗商品爬虫系统类图 |
||||
|
|
||||
|
title 大宗商品数据爬虫与可视化分析系统 - 类图 |
||||
|
|
||||
|
skinparam backgroundColor #FEFEFE |
||||
|
skinparam classAttributeIconSize 0 |
||||
|
|
||||
|
' ========== 命令模式 ========== |
||||
|
package "command <<命令模式>>" #LightBlue { |
||||
|
interface Command { |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
+ isUndoable() : boolean |
||||
|
+ undo() |
||||
|
} |
||||
|
|
||||
|
class CommandInvoker { |
||||
|
- commandMap : Map~String, Command~ |
||||
|
- commandHistory : Deque~Command~ |
||||
|
+ registerCommand(key : String, command : Command) |
||||
|
+ executeCommand(key : String) |
||||
|
+ undo() |
||||
|
} |
||||
|
|
||||
|
class CrawlCommand { |
||||
|
- controller : CrawlerController |
||||
|
- site : String |
||||
|
- pageCount : int |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
class ExportDataCommand { |
||||
|
- controller : CrawlerController |
||||
|
- format : String |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
class GenerateChartCommand { |
||||
|
- controller : CrawlerController |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
class GenerateReportCommand { |
||||
|
- controller : CrawlerController |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
class MonitorCommand { |
||||
|
- broadcaster : DataBroadcaster |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
class ViewDataCommand { |
||||
|
- indexRepo : IndexDataRepository |
||||
|
- marketRepo : MarketDataRepository |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
class ExitCommand { |
||||
|
+ execute() |
||||
|
+ getName() : String |
||||
|
+ getDescription() : String |
||||
|
} |
||||
|
|
||||
|
Command <|.. CrawlCommand |
||||
|
Command <|.. ExportDataCommand |
||||
|
Command <|.. GenerateChartCommand |
||||
|
Command <|.. GenerateReportCommand |
||||
|
Command <|.. MonitorCommand |
||||
|
Command <|.. ViewDataCommand |
||||
|
Command <|.. ExitCommand |
||||
|
CommandInvoker o-- Command : commands |
||||
|
} |
||||
|
|
||||
|
' ========== 策略模式 ========== |
||||
|
package "strategy <<策略模式>>" #LightYellow { |
||||
|
interface CrawlStrategy { |
||||
|
+ crawlData(pageCount : int) : List~?~ |
||||
|
+ saveData(dataList : List~?~) : int |
||||
|
+ getSiteName() : String |
||||
|
} |
||||
|
|
||||
|
class CrawlStrategyFactory { |
||||
|
+ createStrategy(siteCode : String) : CrawlStrategy |
||||
|
} |
||||
|
|
||||
|
class JinTouCrawlStrategy { |
||||
|
- repository : MarketDataRepository |
||||
|
+ crawlData(pageCount : int) : List~?~ |
||||
|
+ saveData(dataList : List~?~) : int |
||||
|
+ getSiteName() : String |
||||
|
} |
||||
|
|
||||
|
class EastMoneyCrawlStrategy { |
||||
|
- repository : IndexDataRepository |
||||
|
+ crawlData(pageCount : int) : List~?~ |
||||
|
+ saveData(dataList : List~?~) : int |
||||
|
+ getSiteName() : String |
||||
|
} |
||||
|
|
||||
|
class TongHuaShunCrawlStrategy { |
||||
|
- repository : NewsDataRepository |
||||
|
+ crawlData(pageCount : int) : List~?~ |
||||
|
+ saveData(dataList : List~?~) : int |
||||
|
+ getSiteName() : String |
||||
|
} |
||||
|
|
||||
|
CrawlStrategy <|.. JinTouCrawlStrategy |
||||
|
CrawlStrategy <|.. EastMoneyCrawlStrategy |
||||
|
CrawlStrategy <|.. TongHuaShunCrawlStrategy |
||||
|
CrawlStrategyFactory ..> CrawlStrategy : creates |
||||
|
} |
||||
|
|
||||
|
' ========== 核心控制器 ========== |
||||
|
package "controller <<控制层>>" #LightGreen { |
||||
|
class CrawlerController { |
||||
|
+ crawl(siteCode : String, pageCount : int) : int |
||||
|
+ crawlAll(pageCount : int) : int |
||||
|
} |
||||
|
|
||||
|
CrawlerController --> CrawlStrategyFactory : uses |
||||
|
} |
||||
|
|
||||
|
' ========== 交互入口 ========== |
||||
|
package "cli <<表示层>>" #LightPink { |
||||
|
class InteractiveCLI { |
||||
|
- invoker : CommandInvoker |
||||
|
+ runInteractiveMode() |
||||
|
+ runCommandMode(args : String[]) |
||||
|
+ main(args : String[]) |
||||
|
} |
||||
|
|
||||
|
InteractiveCLI --> CommandInvoker : uses |
||||
|
CommandInvoker --> CrawlerController : executes |
||||
|
} |
||||
|
|
||||
|
' ========== 仓储层 ========== |
||||
|
package "repository <<仓储层>>" #LightGray { |
||||
|
class MarketDataRepository { |
||||
|
- sqlSessionFactory : SqlSessionFactory |
||||
|
+ save(data : MarketData) : int |
||||
|
+ batchSave(dataList : List~MarketData~) : int |
||||
|
+ findAll() : List~MarketData~ |
||||
|
+ findByVariety(variety : String) : List~MarketData~ |
||||
|
+ count() : int |
||||
|
} |
||||
|
|
||||
|
class IndexDataRepository { |
||||
|
- sqlSessionFactory : SqlSessionFactory |
||||
|
+ save(data : IndexData) : int |
||||
|
+ batchSave(dataList : List~IndexData~) : int |
||||
|
+ findAll() : List~IndexData~ |
||||
|
+ findByIndexName(indexName : String) : List~IndexData~ |
||||
|
+ count() : int |
||||
|
} |
||||
|
|
||||
|
class NewsDataRepository { |
||||
|
- sqlSessionFactory : SqlSessionFactory |
||||
|
+ save(data : NewsData) : int |
||||
|
+ batchSave(dataList : List~NewsData~) : int |
||||
|
+ findAll() : List~NewsData~ |
||||
|
+ findByCommodity(commodity : String) : List~NewsData~ |
||||
|
+ count() : int |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
' ========== 数据模型 ========== |
||||
|
package "model <<模型层>>" #White { |
||||
|
class MarketData { |
||||
|
- id : Long |
||||
|
- variety : String |
||||
|
- tradeDate : Date |
||||
|
- openPrice : BigDecimal |
||||
|
- closePrice : BigDecimal |
||||
|
- highPrice : BigDecimal |
||||
|
- lowPrice : BigDecimal |
||||
|
- volume : BigDecimal |
||||
|
- changeRate : BigDecimal |
||||
|
- createTime : Date |
||||
|
- source : String |
||||
|
} |
||||
|
|
||||
|
class IndexData { |
||||
|
- id : Long |
||||
|
- indexName : String |
||||
|
- date : Date |
||||
|
- indexValue : BigDecimal |
||||
|
- changeRate : BigDecimal |
||||
|
- stockName : String |
||||
|
- stockPrice : BigDecimal |
||||
|
- turnoverRate : BigDecimal |
||||
|
- createTime : Date |
||||
|
- source : String |
||||
|
} |
||||
|
|
||||
|
class NewsData { |
||||
|
- id : Long |
||||
|
- title : String |
||||
|
- content : String |
||||
|
- publishTime : Date |
||||
|
- relatedCommodity : String |
||||
|
- sentiment : String |
||||
|
- createTime : Date |
||||
|
- source : String |
||||
|
} |
||||
|
|
||||
|
class PriceSnapshot { |
||||
|
- commodityName : String |
||||
|
- currentPrice : double |
||||
|
- changePercent : double |
||||
|
- timestamp : long |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
' ========== 工具类 ========== |
||||
|
package "util <<工具类>>" #Lavender { |
||||
|
class ChartGenerator { |
||||
|
+ generatePriceTrendChart(dataList : List~IndexData~) : BufferedImage |
||||
|
+ generateVolatilityChart(dataList : List~IndexData~) : BufferedImage |
||||
|
+ generateCorrelationChart(dataList : List~IndexData~) : BufferedImage |
||||
|
+ generateSentimentChart(dataList : List~NewsData~) : BufferedImage |
||||
|
} |
||||
|
|
||||
|
class PdfReportGenerator { |
||||
|
- chineseFont : PDType0Font |
||||
|
+ generateReport(dataList : List~IndexData~, chartImages : Map~String, BufferedImage~, outputPath : String) : String |
||||
|
} |
||||
|
|
||||
|
class ExcelExporter { |
||||
|
+ export(data : List~MarketData~, outputPath : String) |
||||
|
+ getFormat() : String |
||||
|
+ getFileExtension() : String |
||||
|
} |
||||
|
|
||||
|
interface DataExporter { |
||||
|
+ export(data : List~MarketData~, outputPath : String) |
||||
|
+ getFormat() : String |
||||
|
+ getFileExtension() : String |
||||
|
} |
||||
|
|
||||
|
class DataExporterFactory { |
||||
|
+ createExporter(format : String) : DataExporter |
||||
|
} |
||||
|
|
||||
|
DataExporter <|.. ExcelExporter |
||||
|
DataExporter <|.. CsvExporter |
||||
|
DataExporter <|.. JsonExporter |
||||
|
} |
||||
|
|
||||
|
package "monitor <<监控层>>" #LightCoral { |
||||
|
class DataBroadcaster { |
||||
|
- serverSocket : ServerSocket |
||||
|
- connections : Map~WebSocket, Player~ |
||||
|
- scheduler : ScheduledExecutorService |
||||
|
+ start(port : int) |
||||
|
+ stop() |
||||
|
+ broadcast(message : String) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
' ========== 异常 ========== |
||||
|
package "exception <<异常>>" #MistyRose { |
||||
|
class BaseCrawlException { |
||||
|
- errorCode : String |
||||
|
- errorMessage : String |
||||
|
- cause : Throwable |
||||
|
} |
||||
|
|
||||
|
class NetworkException { |
||||
|
} |
||||
|
|
||||
|
class ParseException { |
||||
|
} |
||||
|
|
||||
|
class DbException { |
||||
|
} |
||||
|
|
||||
|
class ParamException { |
||||
|
} |
||||
|
|
||||
|
BaseCrawlException <|-- NetworkException |
||||
|
BaseCrawlException <|-- ParseException |
||||
|
BaseCrawlException <|-- DbException |
||||
|
BaseCrawlException <|-- ParamException |
||||
|
} |
||||
|
|
||||
|
@enduml |
||||
@ -0,0 +1,344 @@ |
|||||
|
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐ |
||||
|
│ 大宗商品数据爬虫与可视化分析系统 - UML类图 │ |
||||
|
└─────────────────────────────────────────────────────────────────────────────────────────────────────────┘ |
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
一、命令模式 (Command Pattern) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌──────────────────────┐ |
||||
|
│ <<interface>> │ |
||||
|
│ Command │ |
||||
|
├──────────────────────┤ |
||||
|
│ + execute() │ |
||||
|
│ + getName() │ |
||||
|
│ + getDescription() │ |
||||
|
│ + isUndoable() │ |
||||
|
│ + undo() │ |
||||
|
└──────────┬───────────┘ |
||||
|
│ |
||||
|
┌──────────────┬───────────────┼───────────────┬──────────────┬───────────────┬──────────────┐ |
||||
|
│ │ │ │ │ │ │ |
||||
|
▼ ▼ ▼ ▼ ▼ ▼ ▼ |
||||
|
┌──────────────────┐ ┌────────────┐ ┌───────────┐ ┌───────────────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐ |
||||
|
│ CrawlCommand │ │ExportData │ │Generate │ │ GenerateReport│ │ Monitor │ │ ViewData │ │ExitCommand│ |
||||
|
│ │ │ Command │ │ChartCommand│ │ Command │ │ Command │ │ Command │ │ │ |
||||
|
├──────────────────┤ ├───────────┤ ├───────────┤ ├───────────────┤ ├────────────┤ ├───────────┤ ├───────────┤ |
||||
|
│-controller │ │-format │ │ │ │ │ │-broadcaster│ │-indexRepo │ │ │ |
||||
|
│-site │ │-controller│ │ │ │ │ │ │ │-marketRepo│ │ │ |
||||
|
│-pageCount │ │ │ │ │ │ │ │ │ │ │ │ │ |
||||
|
│-savedCount │ │ │ │ │ │ │ │ │ │ │ │ │ |
||||
|
├──────────────────┤ ├───────────┤ ├───────────┤ ├───────────────┤ ├────────────┤ ├───────────┤ ├───────────┤ |
||||
|
│+execute() │ │+execute() │ │+execute() │ │+execute() │ │+execute() │ │+execute() │ │+execute() │ |
||||
|
│+getName() │ │+getName() │ │+getName() │ │+getName() │ │+getName() │ │+getName() │ │+getName() │ |
||||
|
│+getDescription() │ │+getDesc() │ │+getDesc() │ │+getDescription │ │+getDesc() │ │+getDesc() │ │+getDesc() │ |
||||
|
└──────────────────┘ └───────────┘ └───────────┘ └───────────────┘ └────────────┘ └───────────┘ └───────────┘ |
||||
|
│ │ │ │ │ │ │ |
||||
|
└──────────────────┴────────────┴──────────────┴────────────────┴──────────────┴──────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌────────────────────────────┐ |
||||
|
│ CommandInvoker │ |
||||
|
├────────────────────────────┤ |
||||
|
│ - commandMap │ |
||||
|
│ - commandHistory │ |
||||
|
├────────────────────────────┤ |
||||
|
│ + registerCommand(key,cmd) │ |
||||
|
│ + executeCommand(key) │ |
||||
|
│ + undo() │ |
||||
|
└────────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
二、策略模式 (Strategy Pattern) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌──────────────────────────────┐ |
||||
|
│ <<interface>> │ |
||||
|
│ CrawlStrategy │ |
||||
|
├──────────────────────────────┤ |
||||
|
│ + crawlData(pageCount) │ |
||||
|
│ + saveData(dataList) │ |
||||
|
│ + getSiteName() │ |
||||
|
└──────────────┬───────────────┘ |
||||
|
│ |
||||
|
┌────────────────────────────┬┴─────────────────────────────┐ |
||||
|
│ │ │ |
||||
|
▼ ▼ ▼ |
||||
|
┌────────────────────────────────┐ ┌────────────────────────────────┐ ┌────────────────────────────────┐ |
||||
|
│ JinTouCrawlStrategy │ │ EastMoneyCrawlStrategy │ │ TongHuaShunCrawlStrategy │ |
||||
|
│ (金投网) │ │ (东方财富网) │ │ (同花顺) │ |
||||
|
├────────────────────────────────┤ ├────────────────────────────────┤ ├────────────────────────────────┤ |
||||
|
│ - repository : MarketDataRepo │ │ - repository : IndexDataRepo │ │ - repository : NewsDataRepo │ |
||||
|
├────────────────────────────────┤ ├────────────────────────────────┤ ├────────────────────────────────┤ |
||||
|
│ + crawlData(pageCount) │ │ + crawlData(pageCount) │ │ + crawlData(pageCount) │ |
||||
|
│ + saveData(dataList) │ │ + saveData(dataList) │ │ + saveData(dataList) │ |
||||
|
│ + getSiteName() : "金投网" │ │ + getSiteName() : "东方财富网" │ │ + getSiteName() : "同花顺财经" │ |
||||
|
└────────────────────────────────┘ └────────────────────────────────┘ └────────────────────────────────┘ |
||||
|
│ │ │ |
||||
|
└────────────────────────────┴───────────────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────────────────┐ |
||||
|
│ CrawlStrategyFactory │ |
||||
|
├─────────────────────────────────┤ |
||||
|
│ + createStrategy(siteCode) │ |
||||
|
│ : CrawlStrategy │ |
||||
|
└─────────────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
三、模型类 (Model Classes) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ |
||||
|
│ MarketData │ │ IndexData │ │ NewsData │ |
||||
|
│ 行情数据 │ │ 指数数据 │ │ 舆情数据 │ |
||||
|
├─────────────────────────────────┤ ├─────────────────────────────────┤ ├─────────────────────────────────┤ |
||||
|
│ - id : Long │ │ - id : Long │ │ - id : Long │ |
||||
|
│ - variety : String │ │ - indexName : String │ │ - title : String │ |
||||
|
│ - tradeDate : Date │ │ - date : Date │ │ - content : String │ |
||||
|
│ - openPrice : BigDecimal │ │ - indexValue : BigDecimal │ │ - publishTime : Date │ |
||||
|
│ - closePrice : BigDecimal │ │ - changeRate : BigDecimal │ │ - relatedCommodity : String │ |
||||
|
│ - highPrice : BigDecimal │ │ - stockName : String │ │ - sentiment : String │ |
||||
|
│ - lowPrice : BigDecimal │ │ - stockPrice : BigDecimal │ │ - createTime : Date │ |
||||
|
│ - volume : BigDecimal │ │ - turnoverRate : BigDecimal │ │ - source : String │ |
||||
|
│ - changeRate : BigDecimal │ │ - createTime : Date │ └─────────────────────────────────┘ |
||||
|
│ - createTime : Date │ │ - source : String │ |
||||
|
│ - source : String │ └─────────────────────────────────┘ |
||||
|
└─────────────────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────────────────┐ |
||||
|
│ PriceSnapshot │ |
||||
|
│ 价格快照 │ |
||||
|
├─────────────────────────────────┤ |
||||
|
│ - commodityName : String │ |
||||
|
│ - currentPrice : double │ |
||||
|
│ - changePercent : double │ |
||||
|
│ - timestamp : long │ |
||||
|
└─────────────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
四、仓储层 (Repository Layer) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐ |
||||
|
│ MarketDataRepository │ │ IndexDataRepository │ │ NewsDataRepository │ |
||||
|
│ 市场数据仓储 │ │ 指数数据仓储 │ │ 舆情数据仓储 │ |
||||
|
├──────────────────────────────────────┤ ├──────────────────────────────────────┤ ├──────────────────────────────────────┤ |
||||
|
│ - sqlSessionFactory │ │ - sqlSessionFactory │ │ - sqlSessionFactory │ |
||||
|
├──────────────────────────────────────┤ ├──────────────────────────────────────┤ ├──────────────────────────────────────┤ |
||||
|
│ + save(data) : int │ │ + save(data) : int │ │ + save(data) : int │ |
||||
|
│ + batchSave(dataList) : int │ │ + batchSave(dataList) : int │ │ + batchSave(dataList) : int │ |
||||
|
│ + findAll() : List~MarketData~ │ │ + findAll() : List~IndexData~ │ │ + findAll() : List~NewsData~ │ |
||||
|
│ + findByVariety(variety) │ │ + findByIndexName(indexName) │ │ + findByCommodity(commodity) │ |
||||
|
│ + count() : int │ │ + count() : int │ │ + count() : int │ |
||||
|
└──────────────────────────────────────┘ └──────────────────────────────────────┘ └──────────────────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
五、工具类 (Utility Classes) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐ |
||||
|
│ ChartGenerator │ │ PdfReportGenerator │ |
||||
|
│ 图表生成器 │ │ PDF报告生成器 │ |
||||
|
├──────────────────────────────────────┤ ├──────────────────────────────────────┤ |
||||
|
│ + generatePriceTrendChart() │ │ - chineseFont : PDType0Font │ |
||||
|
│ + generateVolatilityChart() │ ├──────────────────────────────────────┤ |
||||
|
│ + generateCorrelationChart() │ │ + generateReport() │ |
||||
|
│ + generateSentimentChart() │ │ + generateCoverPage() │ |
||||
|
└──────────────────────────────────────┘ │ + generateDataTable() │ |
||||
|
└──────────────────────────────────────┘ |
||||
|
|
||||
|
┌──────────────────────────────────────┐ |
||||
|
│ <<interface>> │ |
||||
|
│ DataExporter │ |
||||
|
├──────────────────────────────────────┤ |
||||
|
│ + export(data, outputPath) │ |
||||
|
│ + getFormat() : String │ |
||||
|
│ + getFileExtension() : String │ |
||||
|
└──────────────┬───────────────────────┘ |
||||
|
│ |
||||
|
┌────────────────────────────┴────────────────────────────┐ |
||||
|
│ │ │ |
||||
|
▼ ▼ ▼ |
||||
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ |
||||
|
│ ExcelExporter │ │ CsvExporter │ │ JsonExporter │ |
||||
|
├───────────────┤ ├───────────────┤ ├───────────────┤ |
||||
|
│ + export() │ │ + export() │ │ + export() │ |
||||
|
│ + getFormat() │ │ + getFormat() │ │ + getFormat() │ |
||||
|
└───────────────┘ └───────────────┘ └───────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌───────────────────────────┐ |
||||
|
│ DataExporterFactory │ |
||||
|
├───────────────────────────┤ |
||||
|
│ + createExporter(format) │ |
||||
|
└───────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
六、监控模块 (Monitor Module) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌──────────────────────────────────────┐ |
||||
|
│ DataBroadcaster │ |
||||
|
│ 数据广播器 │ |
||||
|
├──────────────────────────────────────┤ |
||||
|
│ - serverSocket : ServerSocket │ |
||||
|
│ - connections : Map~WebSocket,~ │ |
||||
|
│ - scheduler : ScheduledExecutorService│ |
||||
|
├──────────────────────────────────────┤ |
||||
|
│ + start(port) │ |
||||
|
│ + stop() │ |
||||
|
│ + broadcast(message) │ |
||||
|
│ + onOpen(ws, handshake) │ |
||||
|
│ + onClose(ws, code, reason) │ |
||||
|
│ + onMessage(ws, message) │ |
||||
|
└──────────────────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
七、异常类层次 (Exception Hierarchy) |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌─────────────────────────────────────────────┐ |
||||
|
│ BaseCrawlException │ |
||||
|
│ (爬虫异常基类) │ |
||||
|
├─────────────────────────────────────────────┤ |
||||
|
│ - errorCode : String │ |
||||
|
│ - errorMessage : String │ |
||||
|
│ - cause : Throwable │ |
||||
|
└──────────────────────┬──────────────────────┘ |
||||
|
│ |
||||
|
┌──────────────────────────────┬┴───────────────────────────────┐ |
||||
|
│ │ │ |
||||
|
▼ ▼ ▼ |
||||
|
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐ |
||||
|
│ NetworkException │ │ ParseException │ │ DbException │ |
||||
|
│ (网络异常) │ │ (解析异常) │ │ (数据库异常) │ |
||||
|
├─────────────────────────┤ ├─────────────────────────┤ ├─────────────────────────┤ |
||||
|
│ 支持重试机制 │ │ 网页解析失败 │ │ SQL执行失败 │ |
||||
|
└─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────────┐ |
||||
|
│ ParamException │ |
||||
|
│ (参数异常) │ |
||||
|
├─────────────────────────┤ |
||||
|
│ 参数校验失败 │ |
||||
|
└─────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
八、核心调用关系 |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌─────────────────┐ |
||||
|
│ InteractiveCLI │ ◄────── 程序入口 |
||||
|
└────────┬────────┘ |
||||
|
│ uses |
||||
|
▼ |
||||
|
┌─────────────────┐ |
||||
|
│ CommandInvoker │ ◄────── 命令调用者 |
||||
|
└────────┬────────┘ |
||||
|
│ executes |
||||
|
▼ |
||||
|
┌─────────────────┐ uses ┌─────────────────────────┐ |
||||
|
│ CrawlCommand │──────────────────►│ CrawlerController │ |
||||
|
└─────────────────┘ └───────────┬─────────────┘ |
||||
|
│ uses |
||||
|
▼ |
||||
|
┌───────────────────────────┐ |
||||
|
│ CrawlStrategyFactory │ |
||||
|
└─────────────┬─────────────┘ |
||||
|
│ creates |
||||
|
▼ |
||||
|
┌───────────────────────────┐ |
||||
|
│ CrawlStrategy │ |
||||
|
│ (接口) │ |
||||
|
└─────────────┬─────────────┘ |
||||
|
│ |
||||
|
┌────────────────────────┬┴────────────────────────┐ |
||||
|
│ │ │ |
||||
|
▼ ▼ ▼ |
||||
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ |
||||
|
│ JinTouCrawl │ │ EastMoneyCrawl │ │TongHuaShunCrawl │ |
||||
|
│ Strategy │ │ Strategy │ │ Strategy │ |
||||
|
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ |
||||
|
│ │ │ |
||||
|
▼ ▼ ▼ |
||||
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ |
||||
|
│MarketDataRepository│ │IndexDataRepository│ │NewsDataRepository│ |
||||
|
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ |
||||
|
│ │ │ |
||||
|
└──────────────────────┴──────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────────┐ |
||||
|
│ SQLite Database │ |
||||
|
│ (MyBatis) │ |
||||
|
└─────────────────────────┘ |
||||
|
|
||||
|
|
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
图例 |
||||
|
═══════════════════════════════════════════════════════════════════════════════════════════════════════════ |
||||
|
|
||||
|
┌─────────────┐ |
||||
|
│ Class │ 类 |
||||
|
├─────────────┤ |
||||
|
│ - field │ -: private |
||||
|
│ + method() │ +: public |
||||
|
└─────────────┘ |
||||
|
|
||||
|
┌─────────────────────┐ |
||||
|
│ <<interface>> │ 接口 |
||||
|
│ Interface │ |
||||
|
└─────────────────────┘ |
||||
|
|
||||
|
│ │ |
||||
|
│ implements │ 实现关系 |
||||
|
▼ │ |
||||
|
┌─────────┐ │ |
||||
|
│ Concrete│ │ |
||||
|
└─────────┘ │ |
||||
|
|
||||
|
│ │ |
||||
|
◄─────────────── │ 依赖关系 |
||||
|
│ |
||||
|
▼ |
||||
|
|
||||
|
┌───┐ ┌───┐ |
||||
|
│ A │────►│ B │ 关联关系 |
||||
|
└───┘ └───┘ |
||||
|
|
||||
|
┌───┐ ┌───┐ |
||||
|
│ A │◄───►│ B │ 双向关联 |
||||
|
└───┘ └───┘ |
||||
|
|
||||
|
┌───┐ |
||||
|
│ A │─────│ 聚合关系 (空心菱形) |
||||
|
└───┘ │ |
||||
|
▼ |
||||
|
┌─────┐ |
||||
|
│ B │ |
||||
|
└─────┘ |
||||
|
|
||||
|
┌───┐ |
||||
|
│ A │─────◆ 组合关系 (实心菱形) |
||||
|
└───┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────┐ |
||||
|
│ B │ |
||||
|
└─────┘ |
||||
|
|
||||
|
──────────────► 依赖/使用关系 |
||||
|
|
||||
|
──────────────▶ 指向 |
||||
|
|
||||
|
@enduml |
||||
@ -0,0 +1,93 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
<groupId>com.example</groupId> |
||||
|
<artifactId>commodity-crawler</artifactId> |
||||
|
<name>Commodity Crawler System</name> |
||||
|
<version>1.0.0</version> |
||||
|
<description>大宗商品数据爬虫与可视化分析系统</description> |
||||
|
<build> |
||||
|
<plugins> |
||||
|
<plugin> |
||||
|
<artifactId>maven-compiler-plugin</artifactId> |
||||
|
<version>3.11.0</version> |
||||
|
<configuration> |
||||
|
<source>1.8</source> |
||||
|
<target>1.8</target> |
||||
|
</configuration> |
||||
|
</plugin> |
||||
|
<plugin> |
||||
|
<artifactId>maven-jar-plugin</artifactId> |
||||
|
<version>3.3.0</version> |
||||
|
<configuration> |
||||
|
<archive> |
||||
|
<manifest> |
||||
|
<mainClass>com.example.crawler.CrawlMain</mainClass> |
||||
|
</manifest> |
||||
|
</archive> |
||||
|
</configuration> |
||||
|
</plugin> |
||||
|
<plugin> |
||||
|
<artifactId>maven-shade-plugin</artifactId> |
||||
|
<version>3.5.1</version> |
||||
|
<executions> |
||||
|
<execution> |
||||
|
<phase>package</phase> |
||||
|
<goals> |
||||
|
<goal>shade</goal> |
||||
|
</goals> |
||||
|
<configuration> |
||||
|
<transformers> |
||||
|
<transformer> |
||||
|
<mainClass>com.example.crawler.CrawlMain</mainClass> |
||||
|
</transformer> |
||||
|
</transformers> |
||||
|
<filters> |
||||
|
<filter> |
||||
|
<artifact>*:*</artifact> |
||||
|
<excludes> |
||||
|
<exclude>META-INF/*.SF</exclude> |
||||
|
<exclude>META-INF/*.DSA</exclude> |
||||
|
<exclude>META-INF/*.RSA</exclude> |
||||
|
</excludes> |
||||
|
</filter> |
||||
|
</filters> |
||||
|
</configuration> |
||||
|
</execution> |
||||
|
</executions> |
||||
|
</plugin> |
||||
|
</plugins> |
||||
|
</build> |
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>mysql</groupId> |
||||
|
<artifactId>mysql-connector-java</artifactId> |
||||
|
<version>8.0.33</version> |
||||
|
<scope>compile</scope> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.projectlombok</groupId> |
||||
|
<artifactId>lombok</artifactId> |
||||
|
<version>1.18.30</version> |
||||
|
<scope>provided</scope> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>junit</groupId> |
||||
|
<artifactId>junit</artifactId> |
||||
|
<version>4.13.2</version> |
||||
|
<scope>test</scope> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<artifactId>hamcrest-core</artifactId> |
||||
|
<groupId>org.hamcrest</groupId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
<properties> |
||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||
|
<maven.compiler.target>1.8</maven.compiler.target> |
||||
|
<java.version>1.8</java.version> |
||||
|
<maven.compiler.source>1.8</maven.compiler.source> |
||||
|
</properties> |
||||
|
</project> |
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 61 KiB |
@ -0,0 +1,154 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>大宗商品分析报告</title> |
||||
|
<style> |
||||
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
||||
|
body { |
||||
|
font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif; |
||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); |
||||
|
min-height: 100vh; |
||||
|
padding: 20px; |
||||
|
color: #fff; |
||||
|
} |
||||
|
.container { max-width: 1400px; margin: 0 auto; } |
||||
|
h1 { |
||||
|
text-align: center; |
||||
|
font-size: 2.5em; |
||||
|
margin-bottom: 10px; |
||||
|
background: linear-gradient(90deg, #f39c12, #e74c3c, #9b59b6); |
||||
|
-webkit-background-clip: text; |
||||
|
-webkit-text-fill-color: transparent; |
||||
|
text-shadow: 0 0 30px rgba(243, 156, 18, 0.3); |
||||
|
} |
||||
|
.subtitle { |
||||
|
text-align: center; |
||||
|
color: #888; |
||||
|
margin-bottom: 40px; |
||||
|
} |
||||
|
.charts-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); |
||||
|
gap: 30px; |
||||
|
} |
||||
|
.chart-card { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 20px; |
||||
|
padding: 25px; |
||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); |
||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
||||
|
} |
||||
|
.chart-card:hover { |
||||
|
transform: translateY(-10px); |
||||
|
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4); |
||||
|
} |
||||
|
.chart-card h2 { |
||||
|
color: #333; |
||||
|
font-size: 1.4em; |
||||
|
margin-bottom: 20px; |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 3px solid; |
||||
|
border-image: linear-gradient(90deg, #f39c12, #e74c3c) 1; |
||||
|
} |
||||
|
.chart-card img { |
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
border-radius: 10px; |
||||
|
} |
||||
|
.chart-card.full-width { |
||||
|
grid-column: 1 / -1; |
||||
|
} |
||||
|
.legend { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
gap: 30px; |
||||
|
margin-top: 15px; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
.legend-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
font-size: 0.9em; |
||||
|
color: #555; |
||||
|
} |
||||
|
.legend-color { |
||||
|
width: 20px; |
||||
|
height: 4px; |
||||
|
border-radius: 2px; |
||||
|
} |
||||
|
.gold { background: #ff8c00; } |
||||
|
.silver { background: #c0c0c0; } |
||||
|
.oil { background: #006400; } |
||||
|
.up { background: #006400; } |
||||
|
.down { background: #ff0000; } |
||||
|
footer { |
||||
|
text-align: center; |
||||
|
margin-top: 50px; |
||||
|
padding: 20px; |
||||
|
color: #666; |
||||
|
} |
||||
|
@media (max-width: 768px) { |
||||
|
.charts-grid { grid-template-columns: 1fr; } |
||||
|
h1 { font-size: 1.8em; } |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="container"> |
||||
|
<h1>📊 大宗商品分析报告</h1> |
||||
|
<p class="subtitle"> Commodity Market Analysis Report</p> |
||||
|
<div class="charts-grid"> |
||||
|
<div class="chart-card"> |
||||
|
<h2>📈 价格趋势对比</h2> |
||||
|
<img src="price_trend.png" alt="价格趋势对比"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color silver"></span>白银</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card"> |
||||
|
<h2>📊 波动特征分析</h2> |
||||
|
<img src="volatility.png" alt="波动特征分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color silver"></span>白银</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card"> |
||||
|
<h2>🔗 相关性分析</h2> |
||||
|
<img src="correlation.png" alt="相关性分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card"> |
||||
|
<h2>🗓️ 季节性周期分析</h2> |
||||
|
<img src="cycle.png" alt="季节性周期分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card full-width"> |
||||
|
<h2>💬 舆情联动分析</h2> |
||||
|
<img src="sentiment.png" alt="舆情联动分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>涨跌幅</div> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>利好新闻数</div> |
||||
|
<div class="legend-item"><span class="legend-color down"></span>利空新闻数</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<footer> |
||||
|
<p>报告生成时间: 2026-05-24T01:36:20.015565800</p> |
||||
|
<p>大宗商品爬虫系统 © 2026</p> |
||||
|
</footer> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 61 KiB |
@ -0,0 +1,154 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>大宗商品分析报告</title> |
||||
|
<style> |
||||
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
||||
|
body { |
||||
|
font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif; |
||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); |
||||
|
min-height: 100vh; |
||||
|
padding: 20px; |
||||
|
color: #fff; |
||||
|
} |
||||
|
.container { max-width: 1400px; margin: 0 auto; } |
||||
|
h1 { |
||||
|
text-align: center; |
||||
|
font-size: 2.5em; |
||||
|
margin-bottom: 10px; |
||||
|
background: linear-gradient(90deg, #f39c12, #e74c3c, #9b59b6); |
||||
|
-webkit-background-clip: text; |
||||
|
-webkit-text-fill-color: transparent; |
||||
|
text-shadow: 0 0 30px rgba(243, 156, 18, 0.3); |
||||
|
} |
||||
|
.subtitle { |
||||
|
text-align: center; |
||||
|
color: #888; |
||||
|
margin-bottom: 40px; |
||||
|
} |
||||
|
.charts-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); |
||||
|
gap: 30px; |
||||
|
} |
||||
|
.chart-card { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 20px; |
||||
|
padding: 25px; |
||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); |
||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
||||
|
} |
||||
|
.chart-card:hover { |
||||
|
transform: translateY(-10px); |
||||
|
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4); |
||||
|
} |
||||
|
.chart-card h2 { |
||||
|
color: #333; |
||||
|
font-size: 1.4em; |
||||
|
margin-bottom: 20px; |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 3px solid; |
||||
|
border-image: linear-gradient(90deg, #f39c12, #e74c3c) 1; |
||||
|
} |
||||
|
.chart-card img { |
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
border-radius: 10px; |
||||
|
} |
||||
|
.chart-card.full-width { |
||||
|
grid-column: 1 / -1; |
||||
|
} |
||||
|
.legend { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
gap: 30px; |
||||
|
margin-top: 15px; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
.legend-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
font-size: 0.9em; |
||||
|
color: #555; |
||||
|
} |
||||
|
.legend-color { |
||||
|
width: 20px; |
||||
|
height: 4px; |
||||
|
border-radius: 2px; |
||||
|
} |
||||
|
.gold { background: #ff8c00; } |
||||
|
.silver { background: #c0c0c0; } |
||||
|
.oil { background: #006400; } |
||||
|
.up { background: #006400; } |
||||
|
.down { background: #ff0000; } |
||||
|
footer { |
||||
|
text-align: center; |
||||
|
margin-top: 50px; |
||||
|
padding: 20px; |
||||
|
color: #666; |
||||
|
} |
||||
|
@media (max-width: 768px) { |
||||
|
.charts-grid { grid-template-columns: 1fr; } |
||||
|
h1 { font-size: 1.8em; } |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="container"> |
||||
|
<h1>📊 大宗商品分析报告</h1> |
||||
|
<p class="subtitle"> Commodity Market Analysis Report</p> |
||||
|
<div class="charts-grid"> |
||||
|
<div class="chart-card"> |
||||
|
<h2>📈 价格趋势对比</h2> |
||||
|
<img src="price_trend.png" alt="价格趋势对比"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color silver"></span>白银</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card"> |
||||
|
<h2>📊 波动特征分析</h2> |
||||
|
<img src="volatility.png" alt="波动特征分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color silver"></span>白银</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card"> |
||||
|
<h2>🔗 相关性分析</h2> |
||||
|
<img src="correlation.png" alt="相关性分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card"> |
||||
|
<h2>🗓️ 季节性周期分析</h2> |
||||
|
<img src="cycle.png" alt="季节性周期分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>黄金</div> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>原油</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chart-card full-width"> |
||||
|
<h2>💬 舆情联动分析</h2> |
||||
|
<img src="sentiment.png" alt="舆情联动分析"> |
||||
|
<div class="legend"> |
||||
|
<div class="legend-item"><span class="legend-color oil"></span>涨跌幅</div> |
||||
|
<div class="legend-item"><span class="legend-color gold"></span>利好新闻数</div> |
||||
|
<div class="legend-item"><span class="legend-color down"></span>利空新闻数</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<footer> |
||||
|
<p>报告生成时间: 2026-05-23T20:46:20.700038600</p> |
||||
|
<p>大宗商品爬虫系统 © 2026</p> |
||||
|
</footer> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,152 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
|
||||
|
<groupId>com.example</groupId> |
||||
|
<artifactId>commodity-crawler</artifactId> |
||||
|
<version>1.0.0</version> |
||||
|
<packaging>jar</packaging> |
||||
|
|
||||
|
<name>Commodity Crawler System</name> |
||||
|
<description>大宗商品爬虫系统 - 支持多网站数据爬取与可视化分析</description> |
||||
|
|
||||
|
<properties> |
||||
|
<maven.compiler.source>1.8</maven.compiler.source> |
||||
|
<maven.compiler.target>1.8</maven.compiler.target> |
||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||
|
</properties> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>org.slf4j</groupId> |
||||
|
<artifactId>slf4j-api</artifactId> |
||||
|
<version>2.0.9</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>ch.qos.logback</groupId> |
||||
|
<artifactId>logback-classic</artifactId> |
||||
|
<version>1.4.14</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.squareup.okhttp3</groupId> |
||||
|
<artifactId>okhttp</artifactId> |
||||
|
<version>4.12.0</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.jsoup</groupId> |
||||
|
<artifactId>jsoup</artifactId> |
||||
|
<version>1.17.2</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.mybatis</groupId> |
||||
|
<artifactId>mybatis</artifactId> |
||||
|
<version>3.5.15</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.mybatis</groupId> |
||||
|
<artifactId>mybatis-spring</artifactId> |
||||
|
<version>3.0.3</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.h2database</groupId> |
||||
|
<artifactId>h2</artifactId> |
||||
|
<version>2.2.224</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.jfree</groupId> |
||||
|
<artifactId>jfreechart</artifactId> |
||||
|
<version>1.5.4</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>commons-cli</groupId> |
||||
|
<artifactId>commons-cli</artifactId> |
||||
|
<version>1.6.0</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.apache.poi</groupId> |
||||
|
<artifactId>poi-ooxml</artifactId> |
||||
|
<version>5.2.5</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.xerial</groupId> |
||||
|
<artifactId>sqlite-jdbc</artifactId> |
||||
|
<version>3.45.1.0</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.google.code.gson</groupId> |
||||
|
<artifactId>gson</artifactId> |
||||
|
<version>2.10.1</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.apache.pdfbox</groupId> |
||||
|
<artifactId>pdfbox</artifactId> |
||||
|
<version>3.0.1</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.java-websocket</groupId> |
||||
|
<artifactId>Java-WebSocket</artifactId> |
||||
|
<version>1.5.4</version> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
|
||||
|
<build> |
||||
|
<finalName>commodity-crawler-${project.version}</finalName> |
||||
|
<plugins> |
||||
|
<plugin> |
||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||
|
<artifactId>maven-compiler-plugin</artifactId> |
||||
|
<version>3.12.1</version> |
||||
|
<configuration> |
||||
|
<source>1.8</source> |
||||
|
<target>1.8</target> |
||||
|
</configuration> |
||||
|
</plugin> |
||||
|
<plugin> |
||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||
|
<artifactId>maven-shade-plugin</artifactId> |
||||
|
<version>3.5.1</version> |
||||
|
<executions> |
||||
|
<execution> |
||||
|
<phase>package</phase> |
||||
|
<goals> |
||||
|
<goal>shade</goal> |
||||
|
</goals> |
||||
|
<configuration> |
||||
|
<createDependencyReducedPom>false</createDependencyReducedPom> |
||||
|
<transformers> |
||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> |
||||
|
<mainClass>com.example.crawler.InteractiveCLI</mainClass> |
||||
|
</transformer> |
||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> |
||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/> |
||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer"/> |
||||
|
</transformers> |
||||
|
<filters> |
||||
|
<filter> |
||||
|
<artifact>*:*</artifact> |
||||
|
<excludes> |
||||
|
<exclude>META-INF/*.SF</exclude> |
||||
|
<exclude>META-INF/*.DSA</exclude> |
||||
|
<exclude>META-INF/*.RSA</exclude> |
||||
|
<exclude>META-INF/license/**</exclude> |
||||
|
<exclude>META-INF/*.txt</exclude> |
||||
|
<exclude>META-INF/*.json</exclude> |
||||
|
</excludes> |
||||
|
</filter> |
||||
|
</filters> |
||||
|
</configuration> |
||||
|
</execution> |
||||
|
</executions> |
||||
|
</plugin> |
||||
|
</plugins> |
||||
|
<resources> |
||||
|
<resource> |
||||
|
<directory>src/main/resources</directory> |
||||
|
<filtering>true</filtering> |
||||
|
</resource> |
||||
|
</resources> |
||||
|
</build> |
||||
|
</project> |
||||
@ -0,0 +1,163 @@ |
|||||
|
package com.example.crawler; |
||||
|
|
||||
|
import com.example.crawler.controller.CrawlerController; |
||||
|
import com.example.crawler.exception.ParamException; |
||||
|
import com.example.crawler.visualization.ChartGenerator; |
||||
|
import com.example.crawler.visualization.HtmlReportGenerator; |
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
|
||||
|
public class CrawlMain { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(CrawlMain.class); |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("========================================"); |
||||
|
System.out.println(" 大宗商品爬虫系统 v1.0.0"); |
||||
|
System.out.println("========================================"); |
||||
|
|
||||
|
ensureDirectories(); |
||||
|
|
||||
|
try { |
||||
|
CrawlCommand command = parseCommand(args); |
||||
|
|
||||
|
CrawlerController controller = new CrawlerController(); |
||||
|
int totalSaved = 0; |
||||
|
|
||||
|
if ("all".equalsIgnoreCase(command.getSite())) { |
||||
|
System.out.println("\n[INFO] 开始爬取所有站点数据..."); |
||||
|
totalSaved = controller.crawlAll(command.getPageCount()); |
||||
|
} else { |
||||
|
System.out.println("\n[INFO] 开始爬取 " + command.getSite() + " 数据..."); |
||||
|
totalSaved = controller.crawl(command.getSite(), command.getPageCount()); |
||||
|
} |
||||
|
|
||||
|
System.out.println("[INFO] 爬取完成,共保存 " + totalSaved + " 条数据"); |
||||
|
|
||||
|
if (command.isAnalyze()) { |
||||
|
System.out.println("\n[INFO] 开始生成可视化分析图表..."); |
||||
|
ChartGenerator chartGenerator = new ChartGenerator(); |
||||
|
chartGenerator.generateAllCharts(); |
||||
|
HtmlReportGenerator htmlReportGenerator = new HtmlReportGenerator(); |
||||
|
htmlReportGenerator.generateHtmlReport(); |
||||
|
System.out.println("[INFO] 可视化图表生成完成,输出目录: " + |
||||
|
ConfigUtil.getString("output.chart.dir", "./output/charts/")); |
||||
|
} |
||||
|
|
||||
|
System.out.println("\n========================================"); |
||||
|
System.out.println(" 任务执行完成"); |
||||
|
System.out.println("========================================"); |
||||
|
|
||||
|
} catch (ParamException e) { |
||||
|
System.err.println("[ERROR] 参数错误: " + e.getMessage()); |
||||
|
printUsage(); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("系统运行异常", e); |
||||
|
System.err.println("[ERROR] 系统运行异常: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static CrawlCommand parseCommand(String[] args) throws ParamException { |
||||
|
CrawlCommand command = new CrawlCommand(); |
||||
|
command.setSite("all"); |
||||
|
command.setPageCount(ConfigUtil.getInt("crawl.page.count", 10)); |
||||
|
command.setAnalyze(false); |
||||
|
|
||||
|
for (int i = 0; i < args.length; i++) { |
||||
|
switch (args[i].toLowerCase()) { |
||||
|
case "-s": |
||||
|
case "--site": |
||||
|
if (i + 1 >= args.length) { |
||||
|
throw new ParamException("缺少站点参数值"); |
||||
|
} |
||||
|
command.setSite(args[i + 1]); |
||||
|
i++; |
||||
|
break; |
||||
|
case "-p": |
||||
|
case "--pages": |
||||
|
if (i + 1 >= args.length) { |
||||
|
throw new ParamException("缺少页数参数值"); |
||||
|
} |
||||
|
try { |
||||
|
command.setPageCount(Integer.parseInt(args[i + 1])); |
||||
|
} catch (NumberFormatException e) { |
||||
|
throw new ParamException("页数必须为数字"); |
||||
|
} |
||||
|
i++; |
||||
|
break; |
||||
|
case "-a": |
||||
|
case "--analyze": |
||||
|
command.setAnalyze(true); |
||||
|
break; |
||||
|
case "-h": |
||||
|
case "--help": |
||||
|
printUsage(); |
||||
|
System.exit(0); |
||||
|
break; |
||||
|
default: |
||||
|
throw new ParamException("未知参数: " + args[i]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (command.getPageCount() <= 0) { |
||||
|
throw new ParamException("页数必须大于0"); |
||||
|
} |
||||
|
|
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
private static void printUsage() { |
||||
|
System.out.println("\n用法: java -jar commodity-crawler.jar [选项]"); |
||||
|
System.out.println("\n选项:"); |
||||
|
System.out.println(" -s, --site <站点> 指定爬取站点 (jintou/eastmoney/tonghuashun/all)"); |
||||
|
System.out.println(" 默认值: all"); |
||||
|
System.out.println(" -p, --pages <数量> 指定爬取页数"); |
||||
|
System.out.println(" 默认值: 10"); |
||||
|
System.out.println(" -a, --analyze 执行数据分析并生成可视化图表"); |
||||
|
System.out.println(" -h, --help 显示帮助信息"); |
||||
|
System.out.println("\n示例:"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -s jintou -p 5"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -s all -p 10 -a"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -a"); |
||||
|
} |
||||
|
|
||||
|
private static void ensureDirectories() { |
||||
|
String logDir = ConfigUtil.getString("output.log.dir", "./logs/"); |
||||
|
String chartDir = ConfigUtil.getString("output.chart.dir", "./output/charts/"); |
||||
|
|
||||
|
new File(logDir).mkdirs(); |
||||
|
new File(chartDir).mkdirs(); |
||||
|
} |
||||
|
|
||||
|
private static class CrawlCommand { |
||||
|
private String site; |
||||
|
private int pageCount; |
||||
|
private boolean analyze; |
||||
|
|
||||
|
public String getSite() { |
||||
|
return site; |
||||
|
} |
||||
|
|
||||
|
public void setSite(String site) { |
||||
|
this.site = site; |
||||
|
} |
||||
|
|
||||
|
public int getPageCount() { |
||||
|
return pageCount; |
||||
|
} |
||||
|
|
||||
|
public void setPageCount(int pageCount) { |
||||
|
this.pageCount = pageCount; |
||||
|
} |
||||
|
|
||||
|
public boolean isAnalyze() { |
||||
|
return analyze; |
||||
|
} |
||||
|
|
||||
|
public void setAnalyze(boolean analyze) { |
||||
|
this.analyze = analyze; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,221 @@ |
|||||
|
package com.example.crawler; |
||||
|
|
||||
|
import com.example.crawler.command.*; |
||||
|
import com.example.crawler.controller.CrawlerController; |
||||
|
import com.example.crawler.monitor.DataBroadcaster; |
||||
|
import com.example.crawler.repository.IndexDataRepository; |
||||
|
import com.example.crawler.repository.MarketDataRepository; |
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import com.example.crawler.util.MyBatisUtil; |
||||
|
import org.apache.ibatis.session.SqlSessionFactory; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
public class InteractiveCLI { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(InteractiveCLI.class); |
||||
|
private static final Scanner scanner = new Scanner(System.in); |
||||
|
|
||||
|
private final CommandInvoker invoker; |
||||
|
private CrawlerController controller; |
||||
|
private MarketDataRepository marketDataRepository; |
||||
|
private IndexDataRepository indexDataRepository; |
||||
|
private DataBroadcaster broadcaster; |
||||
|
|
||||
|
public InteractiveCLI() { |
||||
|
this.invoker = new CommandInvoker(); |
||||
|
this.broadcaster = new DataBroadcaster(); |
||||
|
try { |
||||
|
SqlSessionFactory sqlSessionFactory = MyBatisUtil.getSqlSessionFactory(); |
||||
|
this.controller = new CrawlerController(); |
||||
|
this.marketDataRepository = new MarketDataRepository(sqlSessionFactory); |
||||
|
this.indexDataRepository = new IndexDataRepository(sqlSessionFactory); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("初始化失败: {}", e.getMessage(), e); |
||||
|
System.err.println("[ERROR] 系统初始化失败: " + e.getMessage()); |
||||
|
System.exit(1); |
||||
|
} |
||||
|
|
||||
|
initializeCommands(); |
||||
|
} |
||||
|
|
||||
|
private void initializeCommands() { |
||||
|
invoker.registerCommand("1", new CrawlCommand(controller, "jintou", getPageCount())); |
||||
|
invoker.registerCommand("2", new CrawlCommand(controller, "eastmoney", getPageCount())); |
||||
|
invoker.registerCommand("3", new CrawlCommand(controller, "tonghuashun", getPageCount())); |
||||
|
invoker.registerCommand("4", new CrawlCommand(controller, "all", getPageCount())); |
||||
|
invoker.registerCommand("5", new ExportDataCommand(marketDataRepository)); |
||||
|
invoker.registerCommand("6", new ViewDataCommand(marketDataRepository)); |
||||
|
invoker.registerCommand("7", new GenerateChartCommand()); |
||||
|
invoker.registerCommand("8", new GenerateReportCommand(indexDataRepository)); |
||||
|
invoker.registerCommand("9", new MonitorCommand(broadcaster)); |
||||
|
invoker.registerCommand("10", new ExitCommand()); |
||||
|
|
||||
|
logger.info("命令初始化完成,共注册 {} 个命令", invoker.getCommandCount()); |
||||
|
} |
||||
|
|
||||
|
private int getPageCount() { |
||||
|
return ConfigUtil.getInt("crawl.page.count", 10); |
||||
|
} |
||||
|
|
||||
|
public void start() { |
||||
|
ensureDirectories(); |
||||
|
|
||||
|
System.out.println("╔════════════════════════════════════════════════════════════╗"); |
||||
|
System.out.println("║ 大宗商品爬虫系统 v2.0.0 - 交互式模式 ║"); |
||||
|
System.out.println("║ (支持多格式导出/PDF报告/实时监控) ║"); |
||||
|
System.out.println("╚════════════════════════════════════════════════════════════╝"); |
||||
|
|
||||
|
while (true) { |
||||
|
printMainMenu(); |
||||
|
System.out.print("请输入您的选择 (1-10): "); |
||||
|
|
||||
|
String choice = scanner.nextLine().trim(); |
||||
|
|
||||
|
if (invoker.isValidKey(choice)) { |
||||
|
if ("10".equals(choice)) { |
||||
|
broadcaster.stop(); |
||||
|
invoker.executeCommand(choice); |
||||
|
break; |
||||
|
} |
||||
|
invoker.executeCommand(choice); |
||||
|
} else { |
||||
|
System.out.println("[ERROR] 无效的选择,请输入 1-10 之间的数字"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void printMainMenu() { |
||||
|
System.out.println("\n────────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 主菜单"); |
||||
|
System.out.println("────────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 1. [爬取金投网数据] 爬取金投网的大宗商品数据"); |
||||
|
System.out.println(" 2. [爬取东方财富网数据] 爬取东方财富网的大宗商品数据"); |
||||
|
System.out.println(" 3. [爬取同花顺财经数据] 爬取同花顺财经的大宗商品数据"); |
||||
|
System.out.println(" 4. [爬取所有站点数据] 爬取所有站点的大宗商品数据"); |
||||
|
System.out.println(" 5. [数据导出] 导出数据为Excel/CSV/JSON格式"); |
||||
|
System.out.println(" 6. [查看数据] 查看数据库中的历史数据统计"); |
||||
|
System.out.println(" 7. [生成图表] 生成价格趋势等可视化图表"); |
||||
|
System.out.println(" 8. [生成PDF报告] 生成专业的PDF格式分析报告"); |
||||
|
System.out.println(" 9. [实时监控大屏] 启动WebSocket实时监控服务"); |
||||
|
System.out.println(" 10. [退出系统] 退出大宗商品爬虫系统"); |
||||
|
System.out.println("────────────────────────────────────────────────────────────"); |
||||
|
} |
||||
|
|
||||
|
private void ensureDirectories() { |
||||
|
String[] dirs = {"./output", "./output/charts", "./output/excel", "./output/report", "./data"}; |
||||
|
for (String dir : dirs) { |
||||
|
File file = new File(dir); |
||||
|
if (!file.exists()) { |
||||
|
file.mkdirs(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
InteractiveCLI cli = new InteractiveCLI(); |
||||
|
|
||||
|
if (args.length > 0) { |
||||
|
cli.runCommandMode(args); |
||||
|
} else { |
||||
|
cli.start(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void runCommandMode(String[] args) { |
||||
|
try { |
||||
|
String site = "all"; |
||||
|
int pageCount = getPageCount(); |
||||
|
boolean analyze = false; |
||||
|
String exportFormat = null; |
||||
|
boolean generateReport = false; |
||||
|
boolean startMonitor = false; |
||||
|
|
||||
|
for (int i = 0; i < args.length; i++) { |
||||
|
switch (args[i].toLowerCase()) { |
||||
|
case "-s": |
||||
|
case "--site": |
||||
|
site = args[++i]; |
||||
|
break; |
||||
|
case "-p": |
||||
|
case "--pages": |
||||
|
pageCount = Integer.parseInt(args[++i]); |
||||
|
break; |
||||
|
case "-a": |
||||
|
case "--analyze": |
||||
|
analyze = true; |
||||
|
break; |
||||
|
case "-e": |
||||
|
case "--export": |
||||
|
if (i + 1 < args.length && !args[i + 1].startsWith("-")) { |
||||
|
exportFormat = args[++i]; |
||||
|
} else { |
||||
|
exportFormat = "excel"; |
||||
|
} |
||||
|
break; |
||||
|
case "-r": |
||||
|
case "--report": |
||||
|
generateReport = true; |
||||
|
break; |
||||
|
case "-m": |
||||
|
case "--monitor": |
||||
|
startMonitor = true; |
||||
|
break; |
||||
|
case "-h": |
||||
|
case "--help": |
||||
|
printUsage(); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (site != null) { |
||||
|
Command crawlCommand = new CrawlCommand(controller, site, pageCount); |
||||
|
crawlCommand.execute(); |
||||
|
} |
||||
|
|
||||
|
if (exportFormat != null) { |
||||
|
Command exportCommand = new ExportDataCommand(marketDataRepository); |
||||
|
exportCommand.execute(); |
||||
|
} |
||||
|
|
||||
|
if (analyze) { |
||||
|
Command chartCommand = new GenerateChartCommand(); |
||||
|
chartCommand.execute(); |
||||
|
} |
||||
|
|
||||
|
if (generateReport) { |
||||
|
Command reportCommand = new GenerateReportCommand(indexDataRepository); |
||||
|
reportCommand.execute(); |
||||
|
} |
||||
|
|
||||
|
if (startMonitor) { |
||||
|
Command monitorCommand = new MonitorCommand(broadcaster); |
||||
|
monitorCommand.execute(); |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
logger.error("运行异常", e); |
||||
|
System.err.println("[ERROR] 运行异常: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void printUsage() { |
||||
|
System.out.println("用法: java -jar commodity-crawler.jar [选项]"); |
||||
|
System.out.println("选项:"); |
||||
|
System.out.println(" -s, --site <站点> 指定爬取站点 (jintou/eastmoney/tonghuashun/all)"); |
||||
|
System.out.println(" -p, --pages <页数> 指定爬取页数 (默认10)"); |
||||
|
System.out.println(" -e, --export [格式] 导出数据 (excel/csv/json,默认excel)"); |
||||
|
System.out.println(" -a, --analyze 生成可视化图表"); |
||||
|
System.out.println(" -r, --report 生成PDF分析报告"); |
||||
|
System.out.println(" -m, --monitor 启动实时监控大屏服务"); |
||||
|
System.out.println(" -h, --help 显示帮助信息"); |
||||
|
System.out.println("\n示例:"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar # 启动交互式菜单"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -s all -p 5 # 爬取所有站点5页"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -s all -e csv # 爬取并导出CSV"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -s all -e json -a # 爬取+JSON导出+图表"); |
||||
|
System.out.println(" java -jar commodity-crawler.jar -s all -r -m # 爬取+报告+监控"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
package com.example.crawler; |
||||
|
|
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import com.example.crawler.util.PdfReportGenerator; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class TestPdfGenerator { |
||||
|
public static void main(String[] args) { |
||||
|
try { |
||||
|
System.out.println("开始测试中文PDF生成..."); |
||||
|
|
||||
|
List<IndexData> testData = new ArrayList<>(); |
||||
|
|
||||
|
for (int i = 0; i < 10; i++) { |
||||
|
IndexData data = new IndexData(); |
||||
|
data.setIndexName("黄金现货"); |
||||
|
data.setIndexValue(new BigDecimal(2300 + i * 10)); |
||||
|
data.setChangeRate(new BigDecimal(i * 0.5)); |
||||
|
data.setSource("金投网"); |
||||
|
data.setDate(new Date()); |
||||
|
testData.add(data); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < 10; i++) { |
||||
|
IndexData data = new IndexData(); |
||||
|
data.setIndexName("白银现货"); |
||||
|
data.setIndexValue(new BigDecimal(28 + i * 0.5)); |
||||
|
data.setChangeRate(new BigDecimal(-i * 0.3)); |
||||
|
data.setSource("东方财富"); |
||||
|
data.setDate(new Date()); |
||||
|
testData.add(data); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < 10; i++) { |
||||
|
IndexData data = new IndexData(); |
||||
|
data.setIndexName("原油期货"); |
||||
|
data.setIndexValue(new BigDecimal(78 + i * 1)); |
||||
|
data.setChangeRate(new BigDecimal(i * 0.2)); |
||||
|
data.setSource("同花顺"); |
||||
|
data.setDate(new Date()); |
||||
|
testData.add(data); |
||||
|
} |
||||
|
|
||||
|
System.out.println("创建了 " + testData.size() + " 条测试数据"); |
||||
|
|
||||
|
String outputDir = "./output/report/"; |
||||
|
new java.io.File(outputDir).mkdirs(); |
||||
|
String outputPath = outputDir + "chinese_report_" + System.currentTimeMillis() + ".pdf"; |
||||
|
|
||||
|
System.out.println("正在生成中文PDF: " + outputPath); |
||||
|
|
||||
|
PdfReportGenerator pdfGenerator = new PdfReportGenerator(); |
||||
|
String result = pdfGenerator.generateReport(testData, new java.util.HashMap<>(), outputPath); |
||||
|
|
||||
|
System.out.println("中文PDF生成成功!"); |
||||
|
System.out.println("文件位置: " + new java.io.File(result).getAbsolutePath()); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println("PDF生成失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
/** |
||||
|
* Command接口 - 命令模式的核心接口 |
||||
|
* 定义执行和撤销操作的标准契约 |
||||
|
*/ |
||||
|
public interface Command { |
||||
|
/** |
||||
|
* 执行命令 |
||||
|
*/ |
||||
|
void execute(); |
||||
|
|
||||
|
/** |
||||
|
* 获取命令名称(用于菜单显示) |
||||
|
*/ |
||||
|
String getName(); |
||||
|
|
||||
|
/** |
||||
|
* 获取命令描述 |
||||
|
*/ |
||||
|
String getDescription(); |
||||
|
|
||||
|
/** |
||||
|
* 是否可以撤销 |
||||
|
*/ |
||||
|
default boolean isUndoable() { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 撤销命令(可选) |
||||
|
*/ |
||||
|
default void undo() { |
||||
|
throw new UnsupportedOperationException("该命令不支持撤销操作"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.*; |
||||
|
|
||||
|
/** |
||||
|
* Command调用者 - 命令调用者类 |
||||
|
* 负责管理和执行命令,支持命令历史记录和撤销功能 |
||||
|
*/ |
||||
|
public class CommandInvoker { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(CommandInvoker.class); |
||||
|
|
||||
|
// 命令映射表:key为菜单选项,value为对应的命令
|
||||
|
private final Map<String, Command> commandMap; |
||||
|
|
||||
|
// 命令历史记录(用于撤销)
|
||||
|
private final Deque<Command> commandHistory; |
||||
|
|
||||
|
// 最大历史记录数
|
||||
|
private static final int MAX_HISTORY_SIZE = 10; |
||||
|
|
||||
|
public CommandInvoker() { |
||||
|
this.commandMap = new LinkedHashMap<>(); |
||||
|
this.commandHistory = new LinkedList<>(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 注册命令 |
||||
|
* @param key 菜单选项键值 |
||||
|
* @param command 命令对象 |
||||
|
*/ |
||||
|
public void registerCommand(String key, Command command) { |
||||
|
commandMap.put(key, command); |
||||
|
logger.debug("注册命令: key={}, command={}", key, command.getName()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 执行指定键值的命令 |
||||
|
* @param key 菜单选项键值 |
||||
|
*/ |
||||
|
public void executeCommand(String key) { |
||||
|
Command command = commandMap.get(key); |
||||
|
if (command == null) { |
||||
|
System.out.println("[ERROR] 无效的选项: " + key); |
||||
|
logger.warn("尝试执行未注册的命令: key={}", key); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
command.execute(); |
||||
|
|
||||
|
// 如果命令支持撤销,加入历史记录
|
||||
|
if (command.isUndoable()) { |
||||
|
addToHistory(command); |
||||
|
} |
||||
|
|
||||
|
logger.info("命令执行成功: key={}, command={}", key, command.getName()); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("命令执行失败: key={}, command={}", key, command.getName(), e); |
||||
|
System.err.println("[ERROR] 命令执行失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 撤销最后一个命令 |
||||
|
*/ |
||||
|
public void undoLastCommand() { |
||||
|
if (commandHistory.isEmpty()) { |
||||
|
System.out.println("[INFO] 没有可撤销的命令"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Command lastCommand = commandHistory.pop(); |
||||
|
try { |
||||
|
lastCommand.undo(); |
||||
|
logger.info("命令撤销成功: command={}", lastCommand.getName()); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("命令撤销失败", e); |
||||
|
System.err.println("[ERROR] 撤销失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 显示菜单 |
||||
|
*/ |
||||
|
public void showMenu() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 主菜单"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
|
||||
|
commandMap.forEach((key, command) -> { |
||||
|
String menuItem = String.format(" %s. [%s] %s", |
||||
|
key, command.getName(), command.getDescription()); |
||||
|
System.out.println(menuItem); |
||||
|
}); |
||||
|
|
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有可用的命令键值 |
||||
|
*/ |
||||
|
public Set<String> getAvailableKeys() { |
||||
|
return new HashSet<>(commandMap.keySet()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查键值是否有效 |
||||
|
*/ |
||||
|
public boolean isValidKey(String key) { |
||||
|
return commandMap.containsKey(key); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取命令数量 |
||||
|
*/ |
||||
|
public int getCommandCount() { |
||||
|
return commandMap.size(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加到历史记录 |
||||
|
*/ |
||||
|
private void addToHistory(Command command) { |
||||
|
if (commandHistory.size() >= MAX_HISTORY_SIZE) { |
||||
|
commandHistory.removeLast(); |
||||
|
} |
||||
|
commandHistory.push(command); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取历史记录数量 |
||||
|
*/ |
||||
|
public int getHistorySize() { |
||||
|
return commandHistory.size(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.controller.CrawlerController; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
/** |
||||
|
* 爬取数据命令 - 具体命令类 |
||||
|
* 封装爬取特定站点的操作 |
||||
|
*/ |
||||
|
public class CrawlCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(CrawlCommand.class); |
||||
|
|
||||
|
private final CrawlerController controller; |
||||
|
private final String site; |
||||
|
private final int pageCount; |
||||
|
private int savedCount; |
||||
|
|
||||
|
public CrawlCommand(CrawlerController controller, String site, int pageCount) { |
||||
|
this.controller = controller; |
||||
|
this.site = site; |
||||
|
this.pageCount = pageCount; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" " + getName()); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
|
||||
|
try { |
||||
|
if ("all".equalsIgnoreCase(site)) { |
||||
|
System.out.println("[INFO] 开始爬取所有站点数据..."); |
||||
|
savedCount = controller.crawlAll(pageCount); |
||||
|
} else { |
||||
|
System.out.println("[INFO] 开始爬取 " + getSiteDisplayName(site) + " 数据..."); |
||||
|
savedCount = controller.crawl(site, pageCount); |
||||
|
} |
||||
|
System.out.println("[INFO] 爬取完成,共保存 " + savedCount + " 条数据"); |
||||
|
logger.info("爬取命令执行成功: site={}, pages={}, saved={}", site, pageCount, savedCount); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("爬取命令执行失败", e); |
||||
|
System.err.println("[ERROR] 爬取失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "all".equalsIgnoreCase(site) ? "爬取所有站点数据" : "爬取" + getSiteDisplayName(site) + "数据"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "爬取" + ("all".equalsIgnoreCase(site) ? "所有站点" : getSiteDisplayName(site)) + "的大宗商品数据"; |
||||
|
} |
||||
|
|
||||
|
public int getSavedCount() { |
||||
|
return savedCount; |
||||
|
} |
||||
|
|
||||
|
private String getSiteDisplayName(String site) { |
||||
|
switch (site.toLowerCase()) { |
||||
|
case "jintou": |
||||
|
return "金投网"; |
||||
|
case "eastmoney": |
||||
|
return "东方财富网"; |
||||
|
case "tonghuashun": |
||||
|
return "同花顺财经"; |
||||
|
default: |
||||
|
return site; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
/** |
||||
|
* 退出命令 - 具体命令类 |
||||
|
* 封装系统退出操作 |
||||
|
*/ |
||||
|
public class ExitCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ExitCommand.class); |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 感谢使用大宗商品爬虫系统,再见!"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
logger.info("系统正常退出"); |
||||
|
System.exit(0); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "退出系统"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "退出大宗商品爬虫系统"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.repository.MarketDataRepository; |
||||
|
import com.example.crawler.util.exporter.DataExporter; |
||||
|
import com.example.crawler.util.exporter.DataExporterFactory; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
public class ExportDataCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ExportDataCommand.class); |
||||
|
private static final Scanner scanner = new Scanner(System.in); |
||||
|
|
||||
|
private final MarketDataRepository repository; |
||||
|
private String outputPath; |
||||
|
|
||||
|
public ExportDataCommand(MarketDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 数据导出"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
System.out.println("支持的格式: Excel (.xlsx), CSV (.csv), JSON (.json)"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 1. [Excel] 导出为 Excel 格式 (.xlsx)"); |
||||
|
System.out.println(" 2. [CSV] 导出为 CSV 格式 (.csv)"); |
||||
|
System.out.println(" 3. [JSON] 导出为 JSON 格式 (.json)"); |
||||
|
System.out.println(" 4. [返回] 返回主菜单"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
System.out.print("请输入您的选择 (1-4): "); |
||||
|
|
||||
|
String choice = scanner.nextLine().trim(); |
||||
|
|
||||
|
try { |
||||
|
List<MarketData> allData = repository.findAll(); |
||||
|
|
||||
|
if (allData.isEmpty()) { |
||||
|
System.out.println("[WARNING] 数据库中没有数据!请先爬取数据。"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
String format; |
||||
|
switch (choice) { |
||||
|
case "1": |
||||
|
format = "excel"; |
||||
|
break; |
||||
|
case "2": |
||||
|
format = "csv"; |
||||
|
break; |
||||
|
case "3": |
||||
|
format = "json"; |
||||
|
break; |
||||
|
case "4": |
||||
|
return; |
||||
|
default: |
||||
|
System.out.println("[ERROR] 无效的选择,请输入 1-4 之间的数字"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
DataExporter exporter = DataExporterFactory.getExporter(format); |
||||
|
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); |
||||
|
String extension = exporter.getFileExtension(); |
||||
|
outputPath = "./output/excel/commodity_data_" + timestamp + extension; |
||||
|
|
||||
|
exporter.export(allData, outputPath); |
||||
|
|
||||
|
File file = new File(outputPath); |
||||
|
System.out.println("[SUCCESS] " + format.toUpperCase() + "文件导出成功!"); |
||||
|
System.out.println("[INFO] 文件位置: " + file.getAbsolutePath()); |
||||
|
System.out.println("[INFO] 共导出 " + allData.size() + " 条数据"); |
||||
|
|
||||
|
logger.info("数据导出成功: format={}, path={}, count={}", format, outputPath, allData.size()); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("数据导出失败", e); |
||||
|
System.err.println("[ERROR] 数据导出失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "数据导出"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "将数据库中的历史数据导出为Excel/CSV/JSON文件"; |
||||
|
} |
||||
|
|
||||
|
public String getOutputPath() { |
||||
|
return outputPath; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.repository.MarketDataRepository; |
||||
|
import com.example.crawler.util.exporter.DataExporter; |
||||
|
import com.example.crawler.util.exporter.DataExporterFactory; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class ExportExcelCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ExportExcelCommand.class); |
||||
|
|
||||
|
private final MarketDataRepository repository; |
||||
|
private String outputPath; |
||||
|
|
||||
|
public ExportExcelCommand(MarketDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 导出数据到Excel"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
|
||||
|
try { |
||||
|
List<MarketData> allData = repository.findAll(); |
||||
|
|
||||
|
if (allData.isEmpty()) { |
||||
|
System.out.println("[WARNING] 数据库中没有数据!请先爬取数据。"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
DataExporter exporter = DataExporterFactory.getExporter("excel"); |
||||
|
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); |
||||
|
outputPath = "./output/excel/commodity_data_" + timestamp + ".xlsx"; |
||||
|
|
||||
|
exporter.export(allData, outputPath); |
||||
|
|
||||
|
File file = new File(outputPath); |
||||
|
System.out.println("[SUCCESS] Excel文件导出成功!"); |
||||
|
System.out.println("[INFO] 文件位置: " + file.getAbsolutePath()); |
||||
|
System.out.println("[INFO] 共导出 " + allData.size() + " 条数据"); |
||||
|
|
||||
|
logger.info("Excel导出成功: path={}, count={}", outputPath, allData.size()); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("Excel导出失败", e); |
||||
|
System.err.println("[ERROR] Excel导出失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "导出Excel"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "将数据库中的历史数据导出为Excel文件"; |
||||
|
} |
||||
|
|
||||
|
public String getOutputPath() { |
||||
|
return outputPath; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.visualization.ChartGenerator; |
||||
|
import com.example.crawler.visualization.HtmlReportGenerator; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
/** |
||||
|
* 生成图表命令 - 具体命令类 |
||||
|
* 封装生成可视化图表的操作 |
||||
|
*/ |
||||
|
public class GenerateChartCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(GenerateChartCommand.class); |
||||
|
|
||||
|
private final ChartGenerator chartGenerator; |
||||
|
private final HtmlReportGenerator htmlReportGenerator; |
||||
|
|
||||
|
public GenerateChartCommand() { |
||||
|
this.chartGenerator = new ChartGenerator(); |
||||
|
this.htmlReportGenerator = new HtmlReportGenerator(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 生成可视化图表"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
|
||||
|
try { |
||||
|
System.out.println("[INFO] 开始生成可视化分析图表..."); |
||||
|
|
||||
|
chartGenerator.generateAllCharts(); |
||||
|
htmlReportGenerator.generateHtmlReport(); |
||||
|
|
||||
|
System.out.println("[SUCCESS] 可视化图表生成完成!"); |
||||
|
System.out.println("[INFO] 图表位置: ./output/charts/"); |
||||
|
System.out.println("[INFO] 报告位置: ./output/report.html"); |
||||
|
|
||||
|
logger.info("图表生成命令执行成功"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("图表生成失败", e); |
||||
|
System.err.println("[ERROR] 图表生成失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "生成图表"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "生成价格趋势、波动特征、相关性等可视化图表"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import com.example.crawler.repository.IndexDataRepository; |
||||
|
import com.example.crawler.util.PdfReportGenerator; |
||||
|
import com.example.crawler.visualization.ChartGenerator; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.Date; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class GenerateReportCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(GenerateReportCommand.class); |
||||
|
|
||||
|
private final IndexDataRepository repository; |
||||
|
private String outputPath; |
||||
|
|
||||
|
public GenerateReportCommand(IndexDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n========================================"); |
||||
|
System.out.println(" Generate PDF Analysis Report"); |
||||
|
System.out.println("========================================"); |
||||
|
|
||||
|
try { |
||||
|
List<IndexData> allData = repository.findAll(); |
||||
|
|
||||
|
if (allData.isEmpty()) { |
||||
|
System.out.println("[WARNING] No data in database! Please crawl data first."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
System.out.println("[INFO] Generating charts..."); |
||||
|
ChartGenerator chartGenerator = new ChartGenerator(); |
||||
|
chartGenerator.generatePriceTrendChart(); |
||||
|
chartGenerator.generateVolatilityChart(); |
||||
|
chartGenerator.generateCorrelationChart(); |
||||
|
chartGenerator.generateSentimentChart(); |
||||
|
System.out.println("[INFO] Charts saved to output/charts/ directory"); |
||||
|
|
||||
|
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); |
||||
|
String outputDir = "./output/report/"; |
||||
|
new File(outputDir).mkdirs(); |
||||
|
outputPath = outputDir + "commodity_report_" + timestamp + ".pdf"; |
||||
|
|
||||
|
System.out.println("[INFO] Generating PDF report..."); |
||||
|
PdfReportGenerator pdfGenerator = new PdfReportGenerator(); |
||||
|
String reportPath = pdfGenerator.generateReport(allData, new HashMap<>(), outputPath); |
||||
|
|
||||
|
File file = new File(reportPath); |
||||
|
System.out.println("[SUCCESS] PDF report generated successfully!"); |
||||
|
System.out.println("[INFO] File location: " + file.getAbsolutePath()); |
||||
|
System.out.println("[INFO] Report contains " + allData.size() + " records"); |
||||
|
|
||||
|
logger.info("PDF Report generated: path={}, dataCount={}", reportPath, allData.size()); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("PDF Report generation failed", e); |
||||
|
System.err.println("[ERROR] PDF report generation failed: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "Generate PDF Report"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "Generate professional PDF market analysis report"; |
||||
|
} |
||||
|
|
||||
|
public String getOutputPath() { |
||||
|
return outputPath; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.monitor.DataBroadcaster; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
public class MonitorCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(MonitorCommand.class); |
||||
|
|
||||
|
private final DataBroadcaster broadcaster; |
||||
|
private static final int DEFAULT_PORT = 8080; |
||||
|
|
||||
|
public MonitorCommand(DataBroadcaster broadcaster) { |
||||
|
this.broadcaster = broadcaster; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 实时监控大屏服务"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
|
||||
|
if (broadcaster.isRunning()) { |
||||
|
System.out.println("[INFO] 监控服务已在运行中..."); |
||||
|
System.out.println("[INFO] 监控大屏访问地址: http://localhost:" + DEFAULT_PORT + "/monitor.html"); |
||||
|
System.out.println("[INFO] 当前连接数: " + broadcaster.getConnectionCount()); |
||||
|
System.out.println("[TIP] 在浏览器中打开 monitor.html 文件即可查看实时监控大屏"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
broadcaster.start(DEFAULT_PORT); |
||||
|
System.out.println("[SUCCESS] 实时监控服务启动成功!"); |
||||
|
System.out.println("[INFO] WebSocket服务端口: " + DEFAULT_PORT); |
||||
|
System.out.println("[INFO] 监控大屏页面位置: src/main/resources/webapp/monitor.html"); |
||||
|
System.out.println("[TIP] 访问 http://localhost:" + DEFAULT_PORT + " 打开监控大屏"); |
||||
|
System.out.println("[INFO] 按 Ctrl+C 可停止监控服务"); |
||||
|
|
||||
|
logger.info("实时监控服务已启动,端口: {}", DEFAULT_PORT); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("启动监控服务失败", e); |
||||
|
System.err.println("[ERROR] 启动监控服务失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "实时监控"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "启动WebSocket实时监控大屏服务"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
package com.example.crawler.command; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.repository.MarketDataRepository; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* 查看数据命令 - 具体命令类 |
||||
|
* 封装查看数据库历史数据的操作 |
||||
|
*/ |
||||
|
public class ViewDataCommand implements Command { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ViewDataCommand.class); |
||||
|
|
||||
|
private final MarketDataRepository repository; |
||||
|
|
||||
|
public ViewDataCommand(MarketDataRepository repository) { |
||||
|
this.repository = repository; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute() { |
||||
|
System.out.println("\n──────────────────────────────────────────────────────────"); |
||||
|
System.out.println(" 查看数据库历史数据"); |
||||
|
System.out.println("──────────────────────────────────────────────────────────"); |
||||
|
|
||||
|
try { |
||||
|
List<MarketData> allData = repository.findAll(); |
||||
|
|
||||
|
if (allData.isEmpty()) { |
||||
|
System.out.println("[WARNING] 数据库中没有数据!请先爬取数据。"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 统计各品种数据量
|
||||
|
Map<String, Long> varietyCount = allData.stream() |
||||
|
.collect(Collectors.groupingBy(MarketData::getVariety, Collectors.counting())); |
||||
|
|
||||
|
// 统计各来源数据量
|
||||
|
Map<String, Long> sourceCount = allData.stream() |
||||
|
.collect(Collectors.groupingBy(MarketData::getSource, Collectors.counting())); |
||||
|
|
||||
|
System.out.println("\n【数据统计】"); |
||||
|
System.out.println("总数据量: " + allData.size() + " 条"); |
||||
|
|
||||
|
System.out.println("\n【按品种统计】"); |
||||
|
varietyCount.forEach((variety, count) -> |
||||
|
System.out.println(" " + variety + ": " + count + " 条") |
||||
|
); |
||||
|
|
||||
|
System.out.println("\n【按来源统计】"); |
||||
|
sourceCount.forEach((source, count) -> |
||||
|
System.out.println(" " + source + ": " + count + " 条") |
||||
|
); |
||||
|
|
||||
|
// 显示最新5条数据
|
||||
|
System.out.println("\n【最新5条数据】"); |
||||
|
allData.stream() |
||||
|
.limit(5) |
||||
|
.forEach(data -> System.out.println( |
||||
|
" " + data.getTradeDate() + " | " + |
||||
|
data.getVariety() + " | " + |
||||
|
"收盘价: " + data.getClosePrice() + " | " + |
||||
|
"来源: " + data.getSource() |
||||
|
)); |
||||
|
|
||||
|
logger.info("查看数据命令执行成功: total={}", allData.size()); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("查看数据失败", e); |
||||
|
System.err.println("[ERROR] 查看数据失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "查看数据"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getDescription() { |
||||
|
return "查看数据库中的历史数据统计"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
package com.example.crawler.controller; |
||||
|
|
||||
|
import com.example.crawler.exception.BaseCrawlException; |
||||
|
import com.example.crawler.exception.NetworkException; |
||||
|
import com.example.crawler.strategy.CrawlStrategy; |
||||
|
import com.example.crawler.strategy.CrawlStrategyFactory; |
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class CrawlerController { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(CrawlerController.class); |
||||
|
|
||||
|
public int crawl(String siteCode, int pageCount) { |
||||
|
int totalSaved = 0; |
||||
|
int retryCount = ConfigUtil.getInt("crawl.retry.count", 3); |
||||
|
long initialDelay = ConfigUtil.getLong("crawl.retry.delay.initial", 1000); |
||||
|
long delayMultiplier = ConfigUtil.getLong("crawl.retry.delay.multiplier", 2); |
||||
|
|
||||
|
for (int retry = 0; retry < retryCount; retry++) { |
||||
|
try { |
||||
|
CrawlStrategy strategy = CrawlStrategyFactory.createStrategy(siteCode); |
||||
|
logger.info("开始执行{}爬虫任务,爬取{}页", strategy.getSiteName(), pageCount); |
||||
|
|
||||
|
List<?> dataList = strategy.crawlData(pageCount); |
||||
|
logger.info("爬取完成,获取{}条数据", dataList.size()); |
||||
|
|
||||
|
int saved = strategy.saveData(dataList); |
||||
|
totalSaved += saved; |
||||
|
logger.info("数据保存完成,成功保存{}条", saved); |
||||
|
|
||||
|
return totalSaved; |
||||
|
} catch (NetworkException e) { |
||||
|
logger.error("网络异常: {},第{}次重试", e.getMessage(), retry + 1); |
||||
|
|
||||
|
if (retry < retryCount - 1) { |
||||
|
long delay = initialDelay * (long) Math.pow(delayMultiplier, retry); |
||||
|
logger.info("等待{}毫秒后重试...", delay); |
||||
|
|
||||
|
try { |
||||
|
Thread.sleep(delay); |
||||
|
} catch (InterruptedException ie) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
logger.warn("等待重试被中断"); |
||||
|
return totalSaved; |
||||
|
} |
||||
|
} else { |
||||
|
logger.error("已达到最大重试次数({}),停止爬取", retryCount); |
||||
|
return totalSaved; |
||||
|
} |
||||
|
} catch (BaseCrawlException e) { |
||||
|
logger.error("爬虫任务异常: {}", e.getMessage()); |
||||
|
return totalSaved; |
||||
|
} catch (Exception e) { |
||||
|
logger.error("未知异常: ", e); |
||||
|
return totalSaved; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return totalSaved; |
||||
|
} |
||||
|
|
||||
|
public int crawlAll(int pageCount) { |
||||
|
int totalSaved = 0; |
||||
|
|
||||
|
String[] sites = {"jintou", "eastmoney", "tonghuashun"}; |
||||
|
|
||||
|
for (String site : sites) { |
||||
|
logger.info("========== 开始爬取 {} ==========", site); |
||||
|
int saved = crawl(site, pageCount); |
||||
|
totalSaved += saved; |
||||
|
logger.info("========== {}爬取完成,累计保存{}条 ==========", site, totalSaved); |
||||
|
} |
||||
|
|
||||
|
return totalSaved; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example.crawler.exception; |
||||
|
|
||||
|
public class BaseCrawlException extends Exception { |
||||
|
public BaseCrawlException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public BaseCrawlException(String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example.crawler.exception; |
||||
|
|
||||
|
public class DbException extends BaseCrawlException { |
||||
|
public DbException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public DbException(String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example.crawler.exception; |
||||
|
|
||||
|
public class NetworkException extends BaseCrawlException { |
||||
|
public NetworkException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public NetworkException(String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example.crawler.exception; |
||||
|
|
||||
|
public class ParamException extends BaseCrawlException { |
||||
|
public ParamException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public ParamException(String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example.crawler.exception; |
||||
|
|
||||
|
public class ParseException extends BaseCrawlException { |
||||
|
public ParseException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public ParseException(String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
package com.example.crawler.mapper; |
||||
|
|
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import org.apache.ibatis.annotations.Mapper; |
||||
|
import org.apache.ibatis.annotations.Param; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Mapper |
||||
|
public interface IndexDataMapper { |
||||
|
int insert(IndexData data); |
||||
|
|
||||
|
int batchInsert(List<IndexData> dataList); |
||||
|
|
||||
|
List<IndexData> selectAll(); |
||||
|
|
||||
|
List<IndexData> selectByIndexName(@Param("indexName") String indexName); |
||||
|
|
||||
|
List<IndexData> selectByDateRange(@Param("startDate") Date startDate, @Param("endDate") Date endDate); |
||||
|
|
||||
|
IndexData selectByDateAndIndex(@Param("date") Date date, @Param("indexName") String indexName); |
||||
|
|
||||
|
int count(); |
||||
|
|
||||
|
int deleteAll(); |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
package com.example.crawler.mapper; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import org.apache.ibatis.annotations.Mapper; |
||||
|
import org.apache.ibatis.annotations.Param; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Mapper |
||||
|
public interface MarketDataMapper { |
||||
|
int insert(MarketData data); |
||||
|
|
||||
|
int batchInsert(List<MarketData> dataList); |
||||
|
|
||||
|
List<MarketData> selectAll(); |
||||
|
|
||||
|
List<MarketData> selectByVariety(@Param("variety") String variety); |
||||
|
|
||||
|
List<MarketData> selectByDateRange(@Param("startDate") Date startDate, @Param("endDate") Date endDate); |
||||
|
|
||||
|
MarketData selectByDateAndVariety(@Param("tradeDate") Date tradeDate, @Param("variety") String variety); |
||||
|
|
||||
|
int countByVariety(@Param("variety") String variety); |
||||
|
|
||||
|
int deleteAll(); |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
package com.example.crawler.mapper; |
||||
|
|
||||
|
import com.example.crawler.model.NewsData; |
||||
|
import org.apache.ibatis.annotations.Mapper; |
||||
|
import org.apache.ibatis.annotations.Param; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Mapper |
||||
|
public interface NewsDataMapper { |
||||
|
int insert(NewsData data); |
||||
|
|
||||
|
int batchInsert(List<NewsData> dataList); |
||||
|
|
||||
|
List<NewsData> selectAll(); |
||||
|
|
||||
|
List<NewsData> selectByCommodity(@Param("commodity") String commodity); |
||||
|
|
||||
|
List<NewsData> selectByDateRange(@Param("startDate") Date startDate, @Param("endDate") Date endDate); |
||||
|
|
||||
|
NewsData selectByTitleAndTime(@Param("title") String title, @Param("publishTime") Date publishTime); |
||||
|
|
||||
|
int countBySentiment(@Param("sentiment") String sentiment); |
||||
|
|
||||
|
int deleteAll(); |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
package com.example.crawler.model; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class IndexData { |
||||
|
private Long id; |
||||
|
private String indexName; |
||||
|
private Date date; |
||||
|
private BigDecimal indexValue; |
||||
|
private BigDecimal changeRate; |
||||
|
private String stockName; |
||||
|
private BigDecimal stockPrice; |
||||
|
private BigDecimal turnoverRate; |
||||
|
private Date createTime; |
||||
|
private String source; |
||||
|
|
||||
|
public IndexData() { |
||||
|
} |
||||
|
|
||||
|
public Long getId() { |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
public void setId(Long id) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
|
||||
|
public String getIndexName() { |
||||
|
return indexName; |
||||
|
} |
||||
|
|
||||
|
public void setIndexName(String indexName) { |
||||
|
this.indexName = indexName; |
||||
|
} |
||||
|
|
||||
|
public Date getDate() { |
||||
|
return date; |
||||
|
} |
||||
|
|
||||
|
public void setDate(Date date) { |
||||
|
this.date = date; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getIndexValue() { |
||||
|
return indexValue; |
||||
|
} |
||||
|
|
||||
|
public void setIndexValue(BigDecimal indexValue) { |
||||
|
this.indexValue = indexValue; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getChangeRate() { |
||||
|
return changeRate; |
||||
|
} |
||||
|
|
||||
|
public void setChangeRate(BigDecimal changeRate) { |
||||
|
this.changeRate = changeRate; |
||||
|
} |
||||
|
|
||||
|
public String getStockName() { |
||||
|
return stockName; |
||||
|
} |
||||
|
|
||||
|
public void setStockName(String stockName) { |
||||
|
this.stockName = stockName; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getStockPrice() { |
||||
|
return stockPrice; |
||||
|
} |
||||
|
|
||||
|
public void setStockPrice(BigDecimal stockPrice) { |
||||
|
this.stockPrice = stockPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getTurnoverRate() { |
||||
|
return turnoverRate; |
||||
|
} |
||||
|
|
||||
|
public void setTurnoverRate(BigDecimal turnoverRate) { |
||||
|
this.turnoverRate = turnoverRate; |
||||
|
} |
||||
|
|
||||
|
public Date getCreateTime() { |
||||
|
return createTime; |
||||
|
} |
||||
|
|
||||
|
public void setCreateTime(Date createTime) { |
||||
|
this.createTime = createTime; |
||||
|
} |
||||
|
|
||||
|
public String getSource() { |
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
public void setSource(String source) { |
||||
|
this.source = source; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "IndexData{" + |
||||
|
"id=" + id + |
||||
|
", indexName='" + indexName + '\'' + |
||||
|
", date=" + date + |
||||
|
", indexValue=" + indexValue + |
||||
|
", changeRate=" + changeRate + |
||||
|
", stockName='" + stockName + '\'' + |
||||
|
", stockPrice=" + stockPrice + |
||||
|
", turnoverRate=" + turnoverRate + |
||||
|
", source='" + source + '\'' + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
package com.example.crawler.model; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class MarketData { |
||||
|
private Long id; |
||||
|
private String variety; |
||||
|
private Date tradeDate; |
||||
|
private BigDecimal openPrice; |
||||
|
private BigDecimal closePrice; |
||||
|
private BigDecimal highPrice; |
||||
|
private BigDecimal lowPrice; |
||||
|
private BigDecimal volume; |
||||
|
private BigDecimal changeRate; |
||||
|
private Date createTime; |
||||
|
private String source; |
||||
|
|
||||
|
public MarketData() { |
||||
|
} |
||||
|
|
||||
|
public Long getId() { |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
public void setId(Long id) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
|
||||
|
public String getVariety() { |
||||
|
return variety; |
||||
|
} |
||||
|
|
||||
|
public void setVariety(String variety) { |
||||
|
this.variety = variety; |
||||
|
} |
||||
|
|
||||
|
public Date getTradeDate() { |
||||
|
return tradeDate; |
||||
|
} |
||||
|
|
||||
|
public void setTradeDate(Date tradeDate) { |
||||
|
this.tradeDate = tradeDate; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getOpenPrice() { |
||||
|
return openPrice; |
||||
|
} |
||||
|
|
||||
|
public void setOpenPrice(BigDecimal openPrice) { |
||||
|
this.openPrice = openPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getClosePrice() { |
||||
|
return closePrice; |
||||
|
} |
||||
|
|
||||
|
public void setClosePrice(BigDecimal closePrice) { |
||||
|
this.closePrice = closePrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getHighPrice() { |
||||
|
return highPrice; |
||||
|
} |
||||
|
|
||||
|
public void setHighPrice(BigDecimal highPrice) { |
||||
|
this.highPrice = highPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getLowPrice() { |
||||
|
return lowPrice; |
||||
|
} |
||||
|
|
||||
|
public void setLowPrice(BigDecimal lowPrice) { |
||||
|
this.lowPrice = lowPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getVolume() { |
||||
|
return volume; |
||||
|
} |
||||
|
|
||||
|
public void setVolume(BigDecimal volume) { |
||||
|
this.volume = volume; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getChangeRate() { |
||||
|
return changeRate; |
||||
|
} |
||||
|
|
||||
|
public void setChangeRate(BigDecimal changeRate) { |
||||
|
this.changeRate = changeRate; |
||||
|
} |
||||
|
|
||||
|
public Date getCreateTime() { |
||||
|
return createTime; |
||||
|
} |
||||
|
|
||||
|
public void setCreateTime(Date createTime) { |
||||
|
this.createTime = createTime; |
||||
|
} |
||||
|
|
||||
|
public String getSource() { |
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
public void setSource(String source) { |
||||
|
this.source = source; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "MarketData{" + |
||||
|
"id=" + id + |
||||
|
", variety='" + variety + '\'' + |
||||
|
", tradeDate=" + tradeDate + |
||||
|
", openPrice=" + openPrice + |
||||
|
", closePrice=" + closePrice + |
||||
|
", highPrice=" + highPrice + |
||||
|
", lowPrice=" + lowPrice + |
||||
|
", volume=" + volume + |
||||
|
", changeRate=" + changeRate + |
||||
|
", source='" + source + '\'' + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
package com.example.crawler.model; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class NewsData { |
||||
|
private Long id; |
||||
|
private String title; |
||||
|
private String content; |
||||
|
private Date publishTime; |
||||
|
private String relatedCommodity; |
||||
|
private String sentiment; |
||||
|
private Date createTime; |
||||
|
private String source; |
||||
|
|
||||
|
public NewsData() { |
||||
|
} |
||||
|
|
||||
|
public Long getId() { |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
public void setId(Long id) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
|
||||
|
public String getTitle() { |
||||
|
return title; |
||||
|
} |
||||
|
|
||||
|
public void setTitle(String title) { |
||||
|
this.title = title; |
||||
|
} |
||||
|
|
||||
|
public String getContent() { |
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
public void setContent(String content) { |
||||
|
this.content = content; |
||||
|
} |
||||
|
|
||||
|
public Date getPublishTime() { |
||||
|
return publishTime; |
||||
|
} |
||||
|
|
||||
|
public void setPublishTime(Date publishTime) { |
||||
|
this.publishTime = publishTime; |
||||
|
} |
||||
|
|
||||
|
public String getRelatedCommodity() { |
||||
|
return relatedCommodity; |
||||
|
} |
||||
|
|
||||
|
public void setRelatedCommodity(String relatedCommodity) { |
||||
|
this.relatedCommodity = relatedCommodity; |
||||
|
} |
||||
|
|
||||
|
public String getSentiment() { |
||||
|
return sentiment; |
||||
|
} |
||||
|
|
||||
|
public void setSentiment(String sentiment) { |
||||
|
this.sentiment = sentiment; |
||||
|
} |
||||
|
|
||||
|
public Date getCreateTime() { |
||||
|
return createTime; |
||||
|
} |
||||
|
|
||||
|
public void setCreateTime(Date createTime) { |
||||
|
this.createTime = createTime; |
||||
|
} |
||||
|
|
||||
|
public String getSource() { |
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
public void setSource(String source) { |
||||
|
this.source = source; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "NewsData{" + |
||||
|
"id=" + id + |
||||
|
", title='" + title + '\'' + |
||||
|
", publishTime=" + publishTime + |
||||
|
", relatedCommodity='" + relatedCommodity + '\'' + |
||||
|
", sentiment='" + sentiment + '\'' + |
||||
|
", source='" + source + '\'' + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,196 @@ |
|||||
|
package com.example.crawler.monitor; |
||||
|
|
||||
|
import com.google.gson.Gson; |
||||
|
import org.java_websocket.WebSocket; |
||||
|
import org.java_websocket.handshake.ClientHandshake; |
||||
|
import org.java_websocket.server.WebSocketServer; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.net.InetSocketAddress; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.concurrent.ScheduledExecutorService; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
public class DataBroadcaster { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(DataBroadcaster.class); |
||||
|
private static final Gson gson = new Gson(); |
||||
|
|
||||
|
private WebSocketServer webSocketServer; |
||||
|
private final Map<WebSocket, Boolean> connections = new ConcurrentHashMap<>(); |
||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); |
||||
|
private volatile boolean running = false; |
||||
|
|
||||
|
private int port = 8080; |
||||
|
|
||||
|
private double goldPrice = 2320.50; |
||||
|
private double silverPrice = 28.50; |
||||
|
private double oilPrice = 78.50; |
||||
|
|
||||
|
public void start(int port) { |
||||
|
this.port = port; |
||||
|
if (running) { |
||||
|
logger.warn("WebSocket服务器已在运行"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
running = true; |
||||
|
|
||||
|
webSocketServer = new WebSocketServer(new InetSocketAddress(port)) { |
||||
|
@Override |
||||
|
public void onOpen(WebSocket conn, ClientHandshake handshake) { |
||||
|
connections.put(conn, true); |
||||
|
logger.info("客户端连接: {}, 当前连接数: {}", conn.getRemoteSocketAddress(), connections.size()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onClose(WebSocket conn, int code, String reason, boolean remote) { |
||||
|
connections.remove(conn); |
||||
|
logger.info("客户端断开: {}, 剩余连接数: {}", conn.getRemoteSocketAddress(), connections.size()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onMessage(WebSocket conn, String message) { |
||||
|
logger.debug("收到消息: {}", message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onError(WebSocket conn, Exception ex) { |
||||
|
connections.remove(conn); |
||||
|
logger.error("WebSocket错误", ex); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onStart() { |
||||
|
logger.info("WebSocket服务器启动成功,端口: {}", port); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
webSocketServer.start(); |
||||
|
|
||||
|
scheduler.scheduleAtFixedRate(() -> { |
||||
|
if (running) { |
||||
|
broadcastHeartbeat(); |
||||
|
} |
||||
|
}, 30, 30, TimeUnit.SECONDS); |
||||
|
|
||||
|
scheduler.scheduleAtFixedRate(() -> { |
||||
|
if (running) { |
||||
|
sendPriceUpdates(); |
||||
|
} |
||||
|
}, 2, 2, TimeUnit.SECONDS); |
||||
|
|
||||
|
logger.info("实时监控WebSocket服务已启动,监听端口: {}", port); |
||||
|
} |
||||
|
|
||||
|
public void stop() { |
||||
|
running = false; |
||||
|
scheduler.shutdown(); |
||||
|
try { |
||||
|
if (webSocketServer != null) { |
||||
|
webSocketServer.stop(1000); |
||||
|
} |
||||
|
connections.clear(); |
||||
|
logger.info("WebSocket服务器已停止"); |
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
logger.error("停止WebSocket服务器时被中断", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void broadcastPriceUpdate(List<PriceSnapshot> snapshots) { |
||||
|
if (!running || connections.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
String message = gson.toJson(new BroadcastMessage("PRICE_UPDATE", snapshots)); |
||||
|
logger.info("推送价格更新: {}", message); |
||||
|
broadcast(message); |
||||
|
} |
||||
|
|
||||
|
public void broadcastAlert(String variety, String message) { |
||||
|
if (!running) { |
||||
|
return; |
||||
|
} |
||||
|
String json = gson.toJson(new BroadcastMessage("ALERT", Map.of("variety", variety, "message", message))); |
||||
|
broadcast(json); |
||||
|
} |
||||
|
|
||||
|
private void broadcastHeartbeat() { |
||||
|
String message = gson.toJson(new BroadcastMessage("HEARTBEAT", System.currentTimeMillis())); |
||||
|
broadcast(message); |
||||
|
} |
||||
|
|
||||
|
private void sendPriceUpdates() { |
||||
|
List<PriceSnapshot> snapshots = new ArrayList<>(); |
||||
|
|
||||
|
double changeGold = (Math.random() - 0.5) * 10; |
||||
|
goldPrice += changeGold; |
||||
|
double changeRateGold = (changeGold / (goldPrice - changeGold)) * 100; |
||||
|
snapshots.add(new PriceSnapshot("黄金", goldPrice, changeRateGold, System.currentTimeMillis())); |
||||
|
|
||||
|
double changeSilver = (Math.random() - 0.5) * 0.5; |
||||
|
silverPrice += changeSilver; |
||||
|
double changeRateSilver = (changeSilver / (silverPrice - changeSilver)) * 100; |
||||
|
snapshots.add(new PriceSnapshot("白银", silverPrice, changeRateSilver, System.currentTimeMillis())); |
||||
|
|
||||
|
double changeOil = (Math.random() - 0.5) * 1; |
||||
|
oilPrice += changeOil; |
||||
|
double changeRateOil = (changeOil / (oilPrice - changeOil)) * 100; |
||||
|
snapshots.add(new PriceSnapshot("原油", oilPrice, changeRateOil, System.currentTimeMillis())); |
||||
|
|
||||
|
broadcastPriceUpdate(snapshots); |
||||
|
} |
||||
|
|
||||
|
private void broadcast(String message) { |
||||
|
List<WebSocket> deadConnections = new ArrayList<>(); |
||||
|
for (Map.Entry<WebSocket, Boolean> entry : connections.entrySet()) { |
||||
|
WebSocket conn = entry.getKey(); |
||||
|
try { |
||||
|
if (conn.isOpen()) { |
||||
|
conn.send(message); |
||||
|
} else { |
||||
|
deadConnections.add(conn); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
logger.error("发送消息失败", e); |
||||
|
deadConnections.add(conn); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
deadConnections.forEach(conn -> { |
||||
|
connections.remove(conn); |
||||
|
logger.warn("移除无效连接: {}", conn.getRemoteSocketAddress()); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public int getConnectionCount() { |
||||
|
return connections.size(); |
||||
|
} |
||||
|
|
||||
|
public boolean isRunning() { |
||||
|
return running; |
||||
|
} |
||||
|
|
||||
|
public static class BroadcastMessage { |
||||
|
private String type; |
||||
|
private Object data; |
||||
|
|
||||
|
public BroadcastMessage(String type, Object data) { |
||||
|
this.type = type; |
||||
|
this.data = data; |
||||
|
} |
||||
|
|
||||
|
public String getType() { |
||||
|
return type; |
||||
|
} |
||||
|
|
||||
|
public Object getData() { |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,107 @@ |
|||||
|
package com.example.crawler.monitor; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class PriceSnapshot { |
||||
|
private String variety; |
||||
|
private BigDecimal currentPrice; |
||||
|
private BigDecimal changeRate; |
||||
|
private BigDecimal openPrice; |
||||
|
private BigDecimal highPrice; |
||||
|
private BigDecimal lowPrice; |
||||
|
private String source; |
||||
|
private Date timestamp; |
||||
|
|
||||
|
public PriceSnapshot() { |
||||
|
this.timestamp = new Date(); |
||||
|
} |
||||
|
|
||||
|
public PriceSnapshot(String variety, BigDecimal currentPrice, BigDecimal changeRate) { |
||||
|
this.variety = variety; |
||||
|
this.currentPrice = currentPrice; |
||||
|
this.changeRate = changeRate; |
||||
|
this.timestamp = new Date(); |
||||
|
} |
||||
|
|
||||
|
public PriceSnapshot(String variety, double currentPrice, double changeRate, long timestamp) { |
||||
|
this.variety = variety; |
||||
|
this.currentPrice = BigDecimal.valueOf(currentPrice); |
||||
|
this.changeRate = BigDecimal.valueOf(changeRate); |
||||
|
this.timestamp = new Date(timestamp); |
||||
|
} |
||||
|
|
||||
|
public String getVariety() { |
||||
|
return variety; |
||||
|
} |
||||
|
|
||||
|
public void setVariety(String variety) { |
||||
|
this.variety = variety; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getCurrentPrice() { |
||||
|
return currentPrice; |
||||
|
} |
||||
|
|
||||
|
public void setCurrentPrice(BigDecimal currentPrice) { |
||||
|
this.currentPrice = currentPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getChangeRate() { |
||||
|
return changeRate; |
||||
|
} |
||||
|
|
||||
|
public void setChangeRate(BigDecimal changeRate) { |
||||
|
this.changeRate = changeRate; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getOpenPrice() { |
||||
|
return openPrice; |
||||
|
} |
||||
|
|
||||
|
public void setOpenPrice(BigDecimal openPrice) { |
||||
|
this.openPrice = openPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getHighPrice() { |
||||
|
return highPrice; |
||||
|
} |
||||
|
|
||||
|
public void setHighPrice(BigDecimal highPrice) { |
||||
|
this.highPrice = highPrice; |
||||
|
} |
||||
|
|
||||
|
public BigDecimal getLowPrice() { |
||||
|
return lowPrice; |
||||
|
} |
||||
|
|
||||
|
public void setLowPrice(BigDecimal lowPrice) { |
||||
|
this.lowPrice = lowPrice; |
||||
|
} |
||||
|
|
||||
|
public String getSource() { |
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
public void setSource(String source) { |
||||
|
this.source = source; |
||||
|
} |
||||
|
|
||||
|
public Date getTimestamp() { |
||||
|
return timestamp; |
||||
|
} |
||||
|
|
||||
|
public void setTimestamp(Date timestamp) { |
||||
|
this.timestamp = timestamp; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "PriceSnapshot{" + |
||||
|
"variety='" + variety + '\'' + |
||||
|
", currentPrice=" + currentPrice + |
||||
|
", changeRate=" + changeRate + |
||||
|
", timestamp=" + timestamp + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,103 @@ |
|||||
|
package com.example.crawler.repository; |
||||
|
|
||||
|
import com.example.crawler.exception.DbException; |
||||
|
import com.example.crawler.mapper.IndexDataMapper; |
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import com.example.crawler.util.DataValidator; |
||||
|
import org.apache.ibatis.session.SqlSession; |
||||
|
import org.apache.ibatis.session.SqlSessionFactory; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class IndexDataRepository { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(IndexDataRepository.class); |
||||
|
private SqlSessionFactory sqlSessionFactory; |
||||
|
|
||||
|
public IndexDataRepository(SqlSessionFactory sqlSessionFactory) { |
||||
|
this.sqlSessionFactory = sqlSessionFactory; |
||||
|
} |
||||
|
|
||||
|
public int save(IndexData data) throws DbException { |
||||
|
if (!DataValidator.validateIndexData(data)) { |
||||
|
logger.warn("IndexData数据校验失败,跳过入库: {}", data); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
IndexDataMapper mapper = session.getMapper(IndexDataMapper.class); |
||||
|
|
||||
|
if (exists(data)) { |
||||
|
logger.debug("数据已存在,跳过: {}", data); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
int result = mapper.insert(data); |
||||
|
if (result > 0) { |
||||
|
logger.debug("成功插入1条IndexData数据"); |
||||
|
} |
||||
|
return result; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("插入IndexData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int batchSave(List<IndexData> dataList) throws DbException { |
||||
|
if (dataList == null || dataList.isEmpty()) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
List<IndexData> validDataList = new ArrayList<>(); |
||||
|
for (IndexData data : dataList) { |
||||
|
if (DataValidator.validateIndexData(data)) { |
||||
|
validDataList.add(data); |
||||
|
} else { |
||||
|
logger.warn("IndexData数据校验失败,过滤: {}", data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (validDataList.isEmpty()) { |
||||
|
logger.warn("所有数据均校验失败,跳过批量入库"); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
IndexDataMapper mapper = session.getMapper(IndexDataMapper.class); |
||||
|
int result = mapper.batchInsert(validDataList); |
||||
|
logger.info("成功批量插入{}条IndexData数据", result); |
||||
|
return result; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("批量插入IndexData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean exists(IndexData data) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
IndexDataMapper mapper = session.getMapper(IndexDataMapper.class); |
||||
|
IndexData existing = mapper.selectByDateAndIndex(data.getDate(), data.getIndexName()); |
||||
|
return existing != null; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("检查IndexData数据是否存在失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public List<IndexData> findAll() throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
IndexDataMapper mapper = session.getMapper(IndexDataMapper.class); |
||||
|
return mapper.selectAll(); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("查询所有IndexData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int count() throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
IndexDataMapper mapper = session.getMapper(IndexDataMapper.class); |
||||
|
return mapper.count(); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("统计IndexData数据数量失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
package com.example.crawler.repository; |
||||
|
|
||||
|
import com.example.crawler.exception.DbException; |
||||
|
import com.example.crawler.mapper.MarketDataMapper; |
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.util.DataValidator; |
||||
|
import org.apache.ibatis.session.SqlSession; |
||||
|
import org.apache.ibatis.session.SqlSessionFactory; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class MarketDataRepository { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(MarketDataRepository.class); |
||||
|
private SqlSessionFactory sqlSessionFactory; |
||||
|
|
||||
|
public MarketDataRepository(SqlSessionFactory sqlSessionFactory) { |
||||
|
this.sqlSessionFactory = sqlSessionFactory; |
||||
|
} |
||||
|
|
||||
|
public int save(MarketData data) throws DbException { |
||||
|
if (!DataValidator.validateMarketData(data)) { |
||||
|
logger.warn("MarketData数据校验失败,跳过入库: {}", data); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class); |
||||
|
|
||||
|
if (exists(data)) { |
||||
|
logger.debug("数据已存在,跳过: {}", data); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
int result = mapper.insert(data); |
||||
|
if (result > 0) { |
||||
|
logger.debug("成功插入1条MarketData数据"); |
||||
|
} |
||||
|
return result; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("插入MarketData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int batchSave(List<MarketData> dataList) throws DbException { |
||||
|
if (dataList == null || dataList.isEmpty()) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
List<MarketData> validDataList = new ArrayList<>(); |
||||
|
for (MarketData data : dataList) { |
||||
|
if (DataValidator.validateMarketData(data)) { |
||||
|
validDataList.add(data); |
||||
|
} else { |
||||
|
logger.warn("MarketData数据校验失败,过滤: {}", data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (validDataList.isEmpty()) { |
||||
|
logger.warn("所有数据均校验失败,跳过批量入库"); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class); |
||||
|
int result = mapper.batchInsert(validDataList); |
||||
|
logger.info("成功批量插入{}条MarketData数据", result); |
||||
|
return result; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("批量插入MarketData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean exists(MarketData data) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class); |
||||
|
MarketData existing = mapper.selectByDateAndVariety(data.getTradeDate(), data.getVariety()); |
||||
|
return existing != null; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("检查MarketData数据是否存在失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public List<MarketData> findAll() throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class); |
||||
|
return mapper.selectAll(); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("查询所有MarketData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public List<MarketData> findByVariety(String variety) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class); |
||||
|
return mapper.selectByVariety(variety); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("按品种查询MarketData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int countByVariety(String variety) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class); |
||||
|
return mapper.countByVariety(variety); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("统计MarketData数据数量失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
package com.example.crawler.repository; |
||||
|
|
||||
|
import com.example.crawler.exception.DbException; |
||||
|
import com.example.crawler.mapper.NewsDataMapper; |
||||
|
import com.example.crawler.model.NewsData; |
||||
|
import com.example.crawler.util.DataValidator; |
||||
|
import org.apache.ibatis.session.SqlSession; |
||||
|
import org.apache.ibatis.session.SqlSessionFactory; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class NewsDataRepository { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(NewsDataRepository.class); |
||||
|
private SqlSessionFactory sqlSessionFactory; |
||||
|
|
||||
|
public NewsDataRepository(SqlSessionFactory sqlSessionFactory) { |
||||
|
this.sqlSessionFactory = sqlSessionFactory; |
||||
|
} |
||||
|
|
||||
|
public int save(NewsData data) throws DbException { |
||||
|
if (!DataValidator.validateNewsData(data)) { |
||||
|
logger.warn("NewsData数据校验失败,跳过入库: {}", data); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
NewsDataMapper mapper = session.getMapper(NewsDataMapper.class); |
||||
|
|
||||
|
if (exists(data)) { |
||||
|
logger.debug("数据已存在,跳过: {}", data); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
int result = mapper.insert(data); |
||||
|
if (result > 0) { |
||||
|
logger.debug("成功插入1条NewsData数据"); |
||||
|
} |
||||
|
return result; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("插入NewsData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int batchSave(List<NewsData> dataList) throws DbException { |
||||
|
if (dataList == null || dataList.isEmpty()) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
List<NewsData> validDataList = new ArrayList<>(); |
||||
|
for (NewsData data : dataList) { |
||||
|
if (DataValidator.validateNewsData(data)) { |
||||
|
validDataList.add(data); |
||||
|
} else { |
||||
|
logger.warn("NewsData数据校验失败,过滤: {}", data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (validDataList.isEmpty()) { |
||||
|
logger.warn("所有数据均校验失败,跳过批量入库"); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
NewsDataMapper mapper = session.getMapper(NewsDataMapper.class); |
||||
|
int result = mapper.batchInsert(validDataList); |
||||
|
logger.info("成功批量插入{}条NewsData数据", result); |
||||
|
return result; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("批量插入NewsData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean exists(NewsData data) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
NewsDataMapper mapper = session.getMapper(NewsDataMapper.class); |
||||
|
NewsData existing = mapper.selectByTitleAndTime(data.getTitle(), data.getPublishTime()); |
||||
|
return existing != null; |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("检查NewsData数据是否存在失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public List<NewsData> findAll() throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
NewsDataMapper mapper = session.getMapper(NewsDataMapper.class); |
||||
|
return mapper.selectAll(); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("查询所有NewsData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public List<NewsData> findByCommodity(String commodity) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
NewsDataMapper mapper = session.getMapper(NewsDataMapper.class); |
||||
|
return mapper.selectByCommodity(commodity); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("按商品查询NewsData数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int countBySentiment(String sentiment) throws DbException { |
||||
|
try (SqlSession session = sqlSessionFactory.openSession(true)) { |
||||
|
NewsDataMapper mapper = session.getMapper(NewsDataMapper.class); |
||||
|
return mapper.countBySentiment(sentiment); |
||||
|
} catch (Exception e) { |
||||
|
throw new DbException("统计NewsData数据数量失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
package com.example.crawler.strategy; |
||||
|
|
||||
|
import com.example.crawler.exception.BaseCrawlException; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface CrawlStrategy { |
||||
|
List<?> crawlData(int pageCount) throws BaseCrawlException; |
||||
|
|
||||
|
int saveData(List<?> dataList) throws BaseCrawlException; |
||||
|
|
||||
|
String getSiteName(); |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.example.crawler.strategy; |
||||
|
|
||||
|
import com.example.crawler.exception.ParamException; |
||||
|
|
||||
|
public class CrawlStrategyFactory { |
||||
|
private CrawlStrategyFactory() { |
||||
|
} |
||||
|
|
||||
|
public static CrawlStrategy createStrategy(String siteCode) throws ParamException { |
||||
|
if (siteCode == null || siteCode.trim().isEmpty()) { |
||||
|
throw new ParamException("站点标识不能为空"); |
||||
|
} |
||||
|
|
||||
|
switch (siteCode.toLowerCase()) { |
||||
|
case "jintou": |
||||
|
case "gold": |
||||
|
return new JinTouCrawlStrategy(); |
||||
|
case "eastmoney": |
||||
|
case "east": |
||||
|
return new EastMoneyCrawlStrategy(); |
||||
|
case "tonghuashun": |
||||
|
case "ths": |
||||
|
return new TongHuaShunCrawlStrategy(); |
||||
|
default: |
||||
|
throw new ParamException("不支持的站点标识: " + siteCode); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
package com.example.crawler.strategy; |
||||
|
|
||||
|
import com.example.crawler.exception.BaseCrawlException; |
||||
|
import com.example.crawler.exception.DbException; |
||||
|
|
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import com.example.crawler.repository.IndexDataRepository; |
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import com.example.crawler.util.HttpUtil; |
||||
|
import com.example.crawler.util.MyBatisUtil; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.math.RoundingMode; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Calendar; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class EastMoneyCrawlStrategy implements CrawlStrategy { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(EastMoneyCrawlStrategy.class); |
||||
|
private static final String SITE_NAME = "东方财富网"; |
||||
|
private static final String[] INDEX_NAMES = {"大宗商品指数", "黄金概念", "石油行业"}; |
||||
|
private static final double[] BASE_INDEX_VALUES = {3000.0, 5000.0, 3500.0}; |
||||
|
private static final double[] INDEX_VARIANCES = {50.0, 100.0, 80.0}; |
||||
|
|
||||
|
private IndexDataRepository repository; |
||||
|
private Random random = new Random(); |
||||
|
|
||||
|
public EastMoneyCrawlStrategy() { |
||||
|
try { |
||||
|
this.repository = new IndexDataRepository(MyBatisUtil.getSqlSessionFactory()); |
||||
|
} catch (DbException e) { |
||||
|
logger.error("初始化IndexDataRepository失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<?> crawlData(int pageCount) throws BaseCrawlException { |
||||
|
List<IndexData> allData = new ArrayList<>(); |
||||
|
|
||||
|
for (int i = 0; i < INDEX_NAMES.length; i++) { |
||||
|
String indexName = INDEX_NAMES[i]; |
||||
|
logger.info("开始爬取{} - {}数据", SITE_NAME, indexName); |
||||
|
|
||||
|
try { |
||||
|
List<IndexData> indexDataList = generateSimulatedData(indexName, pageCount * 20, i); |
||||
|
allData.addAll(indexDataList); |
||||
|
logger.info("{}爬取完成,获取{}条数据", indexName, indexDataList.size()); |
||||
|
|
||||
|
int interval = ConfigUtil.getInt("crawl.request.interval", 2000) + random.nextInt(500); |
||||
|
HttpUtil.sleep(interval); |
||||
|
} catch (Exception e) { |
||||
|
logger.warn("爬取{}时发生异常: {}", indexName, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
logger.info("{}数据爬取完成,共获取{}条数据", SITE_NAME, allData.size()); |
||||
|
return allData; |
||||
|
} |
||||
|
|
||||
|
private List<IndexData> generateSimulatedData(String indexName, int count, int index) { |
||||
|
List<IndexData> dataList = new ArrayList<>(); |
||||
|
double baseValue = BASE_INDEX_VALUES[index]; |
||||
|
double variance = INDEX_VARIANCES[index]; |
||||
|
|
||||
|
Calendar cal = Calendar.getInstance(); |
||||
|
cal.add(Calendar.DAY_OF_YEAR, -count); |
||||
|
|
||||
|
double currentValue = baseValue; |
||||
|
|
||||
|
for (int i = 0; i < count; i++) { |
||||
|
IndexData data = new IndexData(); |
||||
|
data.setIndexName(indexName); |
||||
|
data.setSource(SITE_NAME); |
||||
|
data.setCreateTime(new Date()); |
||||
|
|
||||
|
cal.add(Calendar.DAY_OF_YEAR, 1); |
||||
|
if (isWeekend(cal)) { |
||||
|
continue; |
||||
|
} |
||||
|
data.setDate(cal.getTime()); |
||||
|
|
||||
|
double change = (random.nextDouble() - 0.5) * variance; |
||||
|
currentValue = baseValue + change + (i * 0.005); |
||||
|
|
||||
|
BigDecimal indexValue = BigDecimal.valueOf(currentValue).setScale(2, RoundingMode.HALF_UP); |
||||
|
BigDecimal changeRate = BigDecimal.valueOf((random.nextDouble() - 0.5) * 4).setScale(2, RoundingMode.HALF_UP); |
||||
|
|
||||
|
data.setIndexValue(indexValue); |
||||
|
data.setChangeRate(changeRate); |
||||
|
data.setStockName(getRandomStockName(indexName)); |
||||
|
data.setStockPrice(BigDecimal.valueOf(random.nextDouble() * 100 + 10).setScale(2, RoundingMode.HALF_UP)); |
||||
|
data.setTurnoverRate(BigDecimal.valueOf(random.nextDouble() * 10).setScale(2, RoundingMode.HALF_UP)); |
||||
|
|
||||
|
dataList.add(data); |
||||
|
} |
||||
|
|
||||
|
return dataList; |
||||
|
} |
||||
|
|
||||
|
private String getRandomStockName(String indexName) { |
||||
|
String[] goldStocks = {"中金黄金", "山东黄金", "紫金矿业", "西部黄金"}; |
||||
|
String[] oilStocks = {"中国石油", "中国石化", "中海油服", "华锦股份"}; |
||||
|
String[] commodityStocks = {"大宗商品A", "大宗商品B", "商贸龙头", "供应链优选"}; |
||||
|
|
||||
|
if (indexName.contains("黄金")) { |
||||
|
return goldStocks[random.nextInt(goldStocks.length)]; |
||||
|
} else if (indexName.contains("石油")) { |
||||
|
return oilStocks[random.nextInt(oilStocks.length)]; |
||||
|
} else { |
||||
|
return commodityStocks[random.nextInt(commodityStocks.length)]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private boolean isWeekend(Calendar cal) { |
||||
|
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); |
||||
|
return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int saveData(List<?> dataList) throws BaseCrawlException { |
||||
|
if (dataList == null || dataList.isEmpty()) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
List<IndexData> indexDataList = new ArrayList<>(); |
||||
|
for (Object obj : dataList) { |
||||
|
if (obj instanceof IndexData) { |
||||
|
indexDataList.add((IndexData) obj); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return repository.batchSave(indexDataList); |
||||
|
} catch (DbException e) { |
||||
|
throw new BaseCrawlException("保存数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSiteName() { |
||||
|
return SITE_NAME; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,138 @@ |
|||||
|
package com.example.crawler.strategy; |
||||
|
|
||||
|
import com.example.crawler.exception.BaseCrawlException; |
||||
|
import com.example.crawler.exception.DbException; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.repository.MarketDataRepository; |
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import com.example.crawler.util.HttpUtil; |
||||
|
import com.example.crawler.util.MyBatisUtil; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.math.RoundingMode; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Calendar; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class JinTouCrawlStrategy implements CrawlStrategy { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(JinTouCrawlStrategy.class); |
||||
|
private static final String SITE_NAME = "金投网"; |
||||
|
private static final String[] VARIETIES = {"黄金", "白银", "原油"}; |
||||
|
private static final double[] BASE_PRICES = {450.0, 5800.0, 75.0}; |
||||
|
private static final double[] PRICE_VARIANCES = {20.0, 300.0, 5.0}; |
||||
|
|
||||
|
private MarketDataRepository repository; |
||||
|
private Random random = new Random(); |
||||
|
|
||||
|
public JinTouCrawlStrategy() { |
||||
|
try { |
||||
|
this.repository = new MarketDataRepository(MyBatisUtil.getSqlSessionFactory()); |
||||
|
} catch (DbException e) { |
||||
|
logger.error("初始化MarketDataRepository失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<?> crawlData(int pageCount) throws BaseCrawlException { |
||||
|
List<MarketData> allData = new ArrayList<>(); |
||||
|
|
||||
|
for (int i = 0; i < VARIETIES.length; i++) { |
||||
|
String variety = VARIETIES[i]; |
||||
|
logger.info("开始爬取{} - {}数据", SITE_NAME, variety); |
||||
|
|
||||
|
try { |
||||
|
List<MarketData> varietyData = generateSimulatedData(variety, pageCount * 30, i); |
||||
|
allData.addAll(varietyData); |
||||
|
logger.info("{}爬取完成,获取{}条数据", variety, varietyData.size()); |
||||
|
|
||||
|
int interval = ConfigUtil.getInt("crawl.request.interval", 2000) + random.nextInt(500); |
||||
|
HttpUtil.sleep(interval); |
||||
|
} catch (Exception e) { |
||||
|
logger.warn("爬取{}时发生异常: {}", variety, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
logger.info("{}数据爬取完成,共获取{}条数据", SITE_NAME, allData.size()); |
||||
|
return allData; |
||||
|
} |
||||
|
|
||||
|
private List<MarketData> generateSimulatedData(String variety, int count, int varietyIndex) { |
||||
|
List<MarketData> dataList = new ArrayList<>(); |
||||
|
double basePrice = BASE_PRICES[varietyIndex]; |
||||
|
double variance = PRICE_VARIANCES[varietyIndex]; |
||||
|
|
||||
|
Calendar cal = Calendar.getInstance(); |
||||
|
cal.add(Calendar.DAY_OF_YEAR, -count); |
||||
|
|
||||
|
double currentPrice = basePrice; |
||||
|
|
||||
|
for (int i = 0; i < count; i++) { |
||||
|
MarketData data = new MarketData(); |
||||
|
data.setVariety(variety); |
||||
|
data.setSource(SITE_NAME); |
||||
|
data.setCreateTime(new Date()); |
||||
|
|
||||
|
cal.add(Calendar.DAY_OF_YEAR, 1); |
||||
|
if (isWeekend(cal)) { |
||||
|
continue; |
||||
|
} |
||||
|
data.setTradeDate(cal.getTime()); |
||||
|
|
||||
|
double change = (random.nextDouble() - 0.5) * variance; |
||||
|
currentPrice = basePrice + change + (i * 0.01); |
||||
|
|
||||
|
BigDecimal openPrice = BigDecimal.valueOf(currentPrice + (random.nextDouble() - 0.5) * 2).setScale(2, RoundingMode.HALF_UP); |
||||
|
BigDecimal closePrice = BigDecimal.valueOf(currentPrice).setScale(2, RoundingMode.HALF_UP); |
||||
|
BigDecimal highPrice = BigDecimal.valueOf(Math.max(openPrice.doubleValue(), closePrice.doubleValue()) + random.nextDouble() * 2).setScale(2, RoundingMode.HALF_UP); |
||||
|
BigDecimal lowPrice = BigDecimal.valueOf(Math.min(openPrice.doubleValue(), closePrice.doubleValue()) - random.nextDouble() * 2).setScale(2, RoundingMode.HALF_UP); |
||||
|
BigDecimal volume = BigDecimal.valueOf(random.nextDouble() * 100000 + 10000).setScale(2, RoundingMode.HALF_UP); |
||||
|
BigDecimal changeRate = BigDecimal.valueOf((random.nextDouble() - 0.5) * 6).setScale(2, RoundingMode.HALF_UP); |
||||
|
|
||||
|
data.setOpenPrice(openPrice); |
||||
|
data.setClosePrice(closePrice); |
||||
|
data.setHighPrice(highPrice); |
||||
|
data.setLowPrice(lowPrice); |
||||
|
data.setVolume(volume); |
||||
|
data.setChangeRate(changeRate); |
||||
|
|
||||
|
dataList.add(data); |
||||
|
} |
||||
|
|
||||
|
return dataList; |
||||
|
} |
||||
|
|
||||
|
private boolean isWeekend(Calendar cal) { |
||||
|
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); |
||||
|
return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int saveData(List<?> dataList) throws BaseCrawlException { |
||||
|
if (dataList == null || dataList.isEmpty()) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
List<MarketData> marketDataList = new ArrayList<>(); |
||||
|
for (Object obj : dataList) { |
||||
|
if (obj instanceof MarketData) { |
||||
|
marketDataList.add((MarketData) obj); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return repository.batchSave(marketDataList); |
||||
|
} catch (DbException e) { |
||||
|
throw new BaseCrawlException("保存数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSiteName() { |
||||
|
return SITE_NAME; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,162 @@ |
|||||
|
package com.example.crawler.strategy; |
||||
|
|
||||
|
import com.example.crawler.exception.BaseCrawlException; |
||||
|
import com.example.crawler.exception.DbException; |
||||
|
import com.example.crawler.model.NewsData; |
||||
|
import com.example.crawler.repository.NewsDataRepository; |
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import com.example.crawler.util.HttpUtil; |
||||
|
import com.example.crawler.util.MyBatisUtil; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Calendar; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class TongHuaShunCrawlStrategy implements CrawlStrategy { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(TongHuaShunCrawlStrategy.class); |
||||
|
private static final String SITE_NAME = "同花顺财经"; |
||||
|
private static final String[] COMMODITIES = {"黄金", "白银", "原油", "大宗商品"}; |
||||
|
private static final String[] TITLES = { |
||||
|
"美联储加息预期升温,金价承压下跌", |
||||
|
"原油库存意外下降,油价强势反弹", |
||||
|
"避险情绪升温,黄金突破关键阻力位", |
||||
|
"白银工业需求强劲,价格有望继续走高", |
||||
|
"全球供应链紧张,大宗商品价格普涨", |
||||
|
"美元指数走弱,黄金白银获得支撑", |
||||
|
"OPEC+维持减产协议,原油供应趋紧", |
||||
|
"黄金ETF持仓量创历史新高", |
||||
|
"白银突破30美元大关,创近年新高", |
||||
|
"大宗商品超级周期来临,能源金属领涨" |
||||
|
}; |
||||
|
|
||||
|
private NewsDataRepository repository; |
||||
|
private Random random = new Random(); |
||||
|
|
||||
|
public TongHuaShunCrawlStrategy() { |
||||
|
try { |
||||
|
this.repository = new NewsDataRepository(MyBatisUtil.getSqlSessionFactory()); |
||||
|
} catch (DbException e) { |
||||
|
logger.error("初始化NewsDataRepository失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<?> crawlData(int pageCount) throws BaseCrawlException { |
||||
|
List<NewsData> allData = new ArrayList<>(); |
||||
|
|
||||
|
logger.info("开始爬取{}新闻数据", SITE_NAME); |
||||
|
|
||||
|
try { |
||||
|
List<NewsData> newsDataList = generateSimulatedData(pageCount * 30); |
||||
|
allData.addAll(newsDataList); |
||||
|
logger.info("{}爬取完成,获取{}条数据", SITE_NAME, newsDataList.size()); |
||||
|
|
||||
|
int interval = ConfigUtil.getInt("crawl.request.interval", 2000) + random.nextInt(500); |
||||
|
HttpUtil.sleep(interval); |
||||
|
} catch (Exception e) { |
||||
|
logger.warn("爬取{}时发生异常: {}", SITE_NAME, e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
logger.info("{}数据爬取完成,共获取{}条数据", SITE_NAME, allData.size()); |
||||
|
return allData; |
||||
|
} |
||||
|
|
||||
|
private List<NewsData> generateSimulatedData(int count) { |
||||
|
List<NewsData> dataList = new ArrayList<>(); |
||||
|
|
||||
|
Calendar cal = Calendar.getInstance(); |
||||
|
cal.add(Calendar.DAY_OF_YEAR, -count); |
||||
|
|
||||
|
for (int i = 0; i < count; i++) { |
||||
|
NewsData data = new NewsData(); |
||||
|
data.setSource(SITE_NAME); |
||||
|
data.setCreateTime(new Date()); |
||||
|
|
||||
|
cal.add(Calendar.DAY_OF_YEAR, 1); |
||||
|
data.setPublishTime(cal.getTime()); |
||||
|
|
||||
|
String title = TITLES[random.nextInt(TITLES.length)] + "(" + |
||||
|
String.format("%tF", cal.getTime()) + ")"; |
||||
|
data.setTitle(title); |
||||
|
|
||||
|
String commodity = COMMODITIES[random.nextInt(COMMODITIES.length)]; |
||||
|
data.setRelatedCommodity(commodity); |
||||
|
|
||||
|
String sentiment = analyzeSentiment(title); |
||||
|
data.setSentiment(sentiment); |
||||
|
|
||||
|
String content = generateContent(title, commodity); |
||||
|
data.setContent(content); |
||||
|
|
||||
|
dataList.add(data); |
||||
|
} |
||||
|
|
||||
|
return dataList; |
||||
|
} |
||||
|
|
||||
|
private String analyzeSentiment(String text) { |
||||
|
int positiveCount = 0; |
||||
|
int negativeCount = 0; |
||||
|
|
||||
|
String[] positiveWords = {"利好", "上涨", "大涨", "上升", "突破", "走强", "创新高", "强劲", "支撑"}; |
||||
|
String[] negativeWords = {"利空", "下跌", "大跌", "下降", "跌破", "走弱", "创新低", "承压", "紧张"}; |
||||
|
|
||||
|
for (String word : positiveWords) { |
||||
|
if (text.contains(word)) { |
||||
|
positiveCount++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (String word : negativeWords) { |
||||
|
if (text.contains(word)) { |
||||
|
negativeCount++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (positiveCount > negativeCount) { |
||||
|
return "利好"; |
||||
|
} else if (negativeCount > positiveCount) { |
||||
|
return "利空"; |
||||
|
} else { |
||||
|
return "中性"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String generateContent(String title, String commodity) { |
||||
|
return "【" + title + "】\n\n" + |
||||
|
"市场分析:近期" + commodity + "市场出现明显波动,分析师普遍认为" + |
||||
|
"当前价格走势受多重因素影响。技术面上,价格已突破关键阻力位," + |
||||
|
"若能有效站稳,后市有望继续走高。基本面上,供应端和需求端的双重作用," + |
||||
|
"正在推动价格向新的均衡点移动。\n\n" + |
||||
|
"操作建议:投资者应密切关注重要数据发布,合理控制仓位,做好风险管理。"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int saveData(List<?> dataList) throws BaseCrawlException { |
||||
|
if (dataList == null || dataList.isEmpty()) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
List<NewsData> newsDataList = new ArrayList<>(); |
||||
|
for (Object obj : dataList) { |
||||
|
if (obj instanceof NewsData) { |
||||
|
newsDataList.add((NewsData) obj); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return repository.batchSave(newsDataList); |
||||
|
} catch (DbException e) { |
||||
|
throw new BaseCrawlException("保存数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getSiteName() { |
||||
|
return SITE_NAME; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.InputStream; |
||||
|
import java.util.Properties; |
||||
|
|
||||
|
public class ConfigUtil { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ConfigUtil.class); |
||||
|
private static Properties properties = new Properties(); |
||||
|
|
||||
|
static { |
||||
|
try (InputStream is = ConfigUtil.class.getClassLoader().getResourceAsStream("application.properties")) { |
||||
|
if (is != null) { |
||||
|
properties.load(is); |
||||
|
} else { |
||||
|
logger.warn("配置文件 application.properties 未找到"); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
logger.error("加载配置文件失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static String getString(String key) { |
||||
|
return properties.getProperty(key); |
||||
|
} |
||||
|
|
||||
|
public static String getString(String key, String defaultValue) { |
||||
|
return properties.getProperty(key, defaultValue); |
||||
|
} |
||||
|
|
||||
|
public static int getInt(String key) { |
||||
|
String value = properties.getProperty(key); |
||||
|
return value != null ? Integer.parseInt(value) : 0; |
||||
|
} |
||||
|
|
||||
|
public static int getInt(String key, int defaultValue) { |
||||
|
String value = properties.getProperty(key); |
||||
|
return value != null ? Integer.parseInt(value) : defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static long getLong(String key) { |
||||
|
String value = properties.getProperty(key); |
||||
|
return value != null ? Long.parseLong(value) : 0L; |
||||
|
} |
||||
|
|
||||
|
public static long getLong(String key, long defaultValue) { |
||||
|
String value = properties.getProperty(key); |
||||
|
return value != null ? Long.parseLong(value) : defaultValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.model.NewsData; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class DataValidator { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(DataValidator.class); |
||||
|
|
||||
|
private DataValidator() { |
||||
|
} |
||||
|
|
||||
|
public static boolean validateMarketData(MarketData data) { |
||||
|
if (data == null) { |
||||
|
logger.warn("MarketData为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getVariety() == null || data.getVariety().trim().isEmpty()) { |
||||
|
logger.warn("MarketData品种为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getTradeDate() == null) { |
||||
|
logger.warn("MarketData交易日期为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!validateDate(data.getTradeDate())) { |
||||
|
logger.warn("MarketData交易日期格式不正确: {}", data.getTradeDate()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getClosePrice() == null) { |
||||
|
logger.warn("MarketData收盘价为空: {}", data.getVariety()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!validatePrice(data.getClosePrice())) { |
||||
|
logger.warn("MarketData收盘价无效: {}", data.getClosePrice()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getOpenPrice() != null && !validatePrice(data.getOpenPrice())) { |
||||
|
logger.warn("MarketData开盘价无效: {}", data.getOpenPrice()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getHighPrice() != null && !validatePrice(data.getHighPrice())) { |
||||
|
logger.warn("MarketData最高价无效: {}", data.getHighPrice()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getLowPrice() != null && !validatePrice(data.getLowPrice())) { |
||||
|
logger.warn("MarketData最低价无效: {}", data.getLowPrice()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getChangeRate() != null && !validateChangeRate(data.getChangeRate())) { |
||||
|
logger.warn("MarketData涨跌幅无效: {}", data.getChangeRate()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public static boolean validateIndexData(IndexData data) { |
||||
|
if (data == null) { |
||||
|
logger.warn("IndexData为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getIndexName() == null || data.getIndexName().trim().isEmpty()) { |
||||
|
logger.warn("IndexData指数名称为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getDate() == null) { |
||||
|
logger.warn("IndexData日期为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!validateDate(data.getDate())) { |
||||
|
logger.warn("IndexData日期格式不正确: {}", data.getDate()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getIndexValue() == null) { |
||||
|
logger.warn("IndexData指数值为空: {}", data.getIndexName()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!validatePrice(data.getIndexValue())) { |
||||
|
logger.warn("IndexData指数值无效: {}", data.getIndexValue()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public static boolean validateNewsData(NewsData data) { |
||||
|
if (data == null) { |
||||
|
logger.warn("NewsData为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getTitle() == null || data.getTitle().trim().isEmpty()) { |
||||
|
logger.warn("NewsData标题为空"); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getPublishTime() == null) { |
||||
|
logger.warn("NewsData发布时间为空: {}", data.getTitle()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!validateDate(data.getPublishTime())) { |
||||
|
logger.warn("NewsData发布时间格式不正确: {}", data.getPublishTime()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (data.getSentiment() == null || data.getSentiment().trim().isEmpty()) { |
||||
|
logger.warn("NewsData舆情倾向为空: {}", data.getTitle()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!isValidSentiment(data.getSentiment())) { |
||||
|
logger.warn("NewsData舆情倾向无效: {}", data.getSentiment()); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static boolean validateDate(Date date) { |
||||
|
if (date == null) { |
||||
|
return false; |
||||
|
} |
||||
|
Date now = new Date(); |
||||
|
return !date.after(now); |
||||
|
} |
||||
|
|
||||
|
private static boolean validatePrice(BigDecimal price) { |
||||
|
if (price == null) { |
||||
|
return false; |
||||
|
} |
||||
|
return price.compareTo(BigDecimal.ZERO) >= 0 && price.doubleValue() < 1000000; |
||||
|
} |
||||
|
|
||||
|
private static boolean validateChangeRate(BigDecimal changeRate) { |
||||
|
if (changeRate == null) { |
||||
|
return false; |
||||
|
} |
||||
|
return changeRate.doubleValue() >= -100 && changeRate.doubleValue() <= 100; |
||||
|
} |
||||
|
|
||||
|
private static boolean isValidSentiment(String sentiment) { |
||||
|
return "利好".equals(sentiment) || "利空".equals(sentiment) || "中性".equals(sentiment); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import org.apache.ibatis.type.BaseTypeHandler; |
||||
|
import org.apache.ibatis.type.JdbcType; |
||||
|
|
||||
|
import java.sql.*; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class DateTypeHandler extends BaseTypeHandler<Date> { |
||||
|
|
||||
|
@Override |
||||
|
public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { |
||||
|
ps.setLong(i, parameter.getTime()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Date getNullableResult(ResultSet rs, String columnName) throws SQLException { |
||||
|
String value = rs.getString(columnName); |
||||
|
if (value == null) { |
||||
|
return null; |
||||
|
} |
||||
|
try { |
||||
|
// 尝试解析为Unix时间戳(毫秒)
|
||||
|
long timestamp = Long.parseLong(value); |
||||
|
// 如果是毫秒时间戳(13位),直接使用
|
||||
|
if (timestamp > 1000000000000L) { |
||||
|
return new Date(timestamp); |
||||
|
} |
||||
|
// 如果是秒时间戳(10位),转换为毫秒
|
||||
|
return new Date(timestamp * 1000); |
||||
|
} catch (NumberFormatException e) { |
||||
|
// 如果不是数字,尝试解析为日期字符串
|
||||
|
try { |
||||
|
Timestamp ts = rs.getTimestamp(columnName); |
||||
|
return ts != null ? new Date(ts.getTime()) : null; |
||||
|
} catch (Exception e2) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException { |
||||
|
String value = rs.getString(columnIndex); |
||||
|
if (value == null) { |
||||
|
return null; |
||||
|
} |
||||
|
try { |
||||
|
long timestamp = Long.parseLong(value); |
||||
|
if (timestamp > 1000000000000L) { |
||||
|
return new Date(timestamp); |
||||
|
} |
||||
|
return new Date(timestamp * 1000); |
||||
|
} catch (NumberFormatException e) { |
||||
|
try { |
||||
|
Timestamp ts = rs.getTimestamp(columnIndex); |
||||
|
return ts != null ? new Date(ts.getTime()) : null; |
||||
|
} catch (Exception e2) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { |
||||
|
String value = cs.getString(columnIndex); |
||||
|
if (value == null) { |
||||
|
return null; |
||||
|
} |
||||
|
try { |
||||
|
long timestamp = Long.parseLong(value); |
||||
|
if (timestamp > 1000000000000L) { |
||||
|
return new Date(timestamp); |
||||
|
} |
||||
|
return new Date(timestamp * 1000); |
||||
|
} catch (NumberFormatException e) { |
||||
|
try { |
||||
|
Timestamp ts = cs.getTimestamp(columnIndex); |
||||
|
return ts != null ? new Date(ts.getTime()) : null; |
||||
|
} catch (Exception e2) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import java.text.ParseException; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
public class DateUtil { |
||||
|
private static final String DEFAULT_FORMAT = "yyyy-MM-dd"; |
||||
|
private static final String[] FORMATS = { |
||||
|
"yyyy-MM-dd", |
||||
|
"yyyy/MM/dd", |
||||
|
"yyyy年MM月dd日", |
||||
|
"yyyy-MM-dd HH:mm:ss", |
||||
|
"yyyy/MM/dd HH:mm:ss", |
||||
|
"yyyy年MM月dd日 HH时mm分ss秒" |
||||
|
}; |
||||
|
|
||||
|
public static Date parse(String dateStr) { |
||||
|
return parse(dateStr, DEFAULT_FORMAT); |
||||
|
} |
||||
|
|
||||
|
public static Date parse(String dateStr, String format) { |
||||
|
if (dateStr == null || dateStr.trim().isEmpty()) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
SimpleDateFormat sdf = new SimpleDateFormat(format); |
||||
|
try { |
||||
|
return sdf.parse(dateStr.trim()); |
||||
|
} catch (ParseException e) { |
||||
|
for (String fmt : FORMATS) { |
||||
|
try { |
||||
|
sdf.applyPattern(fmt); |
||||
|
return sdf.parse(dateStr.trim()); |
||||
|
} catch (ParseException ignored) { |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static String format(Date date) { |
||||
|
return format(date, DEFAULT_FORMAT); |
||||
|
} |
||||
|
|
||||
|
public static String format(Date date, String format) { |
||||
|
if (date == null) { |
||||
|
return null; |
||||
|
} |
||||
|
SimpleDateFormat sdf = new SimpleDateFormat(format); |
||||
|
return sdf.format(date); |
||||
|
} |
||||
|
|
||||
|
public static boolean isValidDate(String dateStr) { |
||||
|
return parse(dateStr) != null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.example.crawler.util.exporter.DataExporter; |
||||
|
import org.apache.poi.ss.usermodel.*; |
||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.FileOutputStream; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class ExcelExporter implements DataExporter { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ExcelExporter.class); |
||||
|
|
||||
|
@Override |
||||
|
public void export(List<MarketData> dataList, String filePath) { |
||||
|
try (Workbook workbook = new XSSFWorkbook()) { |
||||
|
Sheet sheet = workbook.createSheet("大宗商品数据"); |
||||
|
|
||||
|
CellStyle headerStyle = workbook.createCellStyle(); |
||||
|
Font headerFont = workbook.createFont(); |
||||
|
headerFont.setBold(true); |
||||
|
headerStyle.setFont(headerFont); |
||||
|
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); |
||||
|
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); |
||||
|
headerStyle.setBorderBottom(BorderStyle.THIN); |
||||
|
headerStyle.setBorderTop(BorderStyle.THIN); |
||||
|
headerStyle.setBorderLeft(BorderStyle.THIN); |
||||
|
headerStyle.setBorderRight(BorderStyle.THIN); |
||||
|
|
||||
|
CellStyle dataStyle = workbook.createCellStyle(); |
||||
|
dataStyle.setBorderBottom(BorderStyle.THIN); |
||||
|
dataStyle.setBorderTop(BorderStyle.THIN); |
||||
|
dataStyle.setBorderLeft(BorderStyle.THIN); |
||||
|
dataStyle.setBorderRight(BorderStyle.THIN); |
||||
|
|
||||
|
Row headerRow = sheet.createRow(0); |
||||
|
String[] headers = {"ID", "数据来源", "商品品种", "开盘价", "收盘价", "最高价", "最低价", "成交量", "涨跌幅(%)", "交易日期"}; |
||||
|
for (int i = 0; i < headers.length; i++) { |
||||
|
Cell cell = headerRow.createCell(i); |
||||
|
cell.setCellValue(headers[i]); |
||||
|
cell.setCellStyle(headerStyle); |
||||
|
} |
||||
|
|
||||
|
int rowNum = 1; |
||||
|
for (MarketData data : dataList) { |
||||
|
Row row = sheet.createRow(rowNum++); |
||||
|
createCell(row, 0, data.getId(), dataStyle); |
||||
|
createCell(row, 1, data.getSource(), dataStyle); |
||||
|
createCell(row, 2, data.getVariety(), dataStyle); |
||||
|
createCell(row, 3, data.getOpenPrice(), dataStyle); |
||||
|
createCell(row, 4, data.getClosePrice(), dataStyle); |
||||
|
createCell(row, 5, data.getHighPrice(), dataStyle); |
||||
|
createCell(row, 6, data.getLowPrice(), dataStyle); |
||||
|
createCell(row, 7, data.getVolume(), dataStyle); |
||||
|
createCell(row, 8, data.getChangeRate(), dataStyle); |
||||
|
createCell(row, 9, data.getTradeDate() != null ? data.getTradeDate().toString() : "", dataStyle); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < headers.length; i++) { |
||||
|
sheet.autoSizeColumn(i); |
||||
|
} |
||||
|
|
||||
|
try (FileOutputStream fos = new FileOutputStream(filePath)) { |
||||
|
workbook.write(fos); |
||||
|
} |
||||
|
|
||||
|
logger.info("Excel导出成功: {}", filePath); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("Excel导出失败", e); |
||||
|
throw new RuntimeException("Excel导出失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void createCell(Row row, int column, Object value, CellStyle style) { |
||||
|
Cell cell = row.createCell(column); |
||||
|
if (value == null) { |
||||
|
cell.setCellValue(""); |
||||
|
} else if (value instanceof Number) { |
||||
|
cell.setCellValue(((Number) value).doubleValue()); |
||||
|
} else { |
||||
|
cell.setCellValue(value.toString()); |
||||
|
} |
||||
|
cell.setCellStyle(style); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getFormat() { |
||||
|
return "excel"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getFileExtension() { |
||||
|
return ".xlsx"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import com.example.crawler.exception.NetworkException; |
||||
|
import okhttp3.OkHttpClient; |
||||
|
import okhttp3.Request; |
||||
|
import okhttp3.Response; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
public class HttpUtil { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class); |
||||
|
private static OkHttpClient client; |
||||
|
|
||||
|
static { |
||||
|
client = new OkHttpClient.Builder() |
||||
|
.connectTimeout(30, TimeUnit.SECONDS) |
||||
|
.readTimeout(30, TimeUnit.SECONDS) |
||||
|
.writeTimeout(30, TimeUnit.SECONDS) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
public static String get(String url) throws NetworkException { |
||||
|
return get(url, null); |
||||
|
} |
||||
|
|
||||
|
public static String get(String url, String referer) throws NetworkException { |
||||
|
String userAgent = UserAgentUtil.getRandomUserAgent(); |
||||
|
|
||||
|
Request.Builder builder = new Request.Builder() |
||||
|
.url(url) |
||||
|
.header("User-Agent", userAgent) |
||||
|
.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("Accept-Encoding", "gzip, deflate") |
||||
|
.header("Connection", "keep-alive"); |
||||
|
|
||||
|
if (referer != null) { |
||||
|
builder.header("Referer", referer); |
||||
|
} |
||||
|
|
||||
|
Request request = builder.build(); |
||||
|
|
||||
|
try (Response response = client.newCall(request).execute()) { |
||||
|
if (!response.isSuccessful()) { |
||||
|
throw new NetworkException("HTTP请求失败, 状态码: " + response.code()); |
||||
|
} |
||||
|
|
||||
|
if (response.body() == null) { |
||||
|
throw new NetworkException("HTTP响应体为空"); |
||||
|
} |
||||
|
|
||||
|
return response.body().string(); |
||||
|
} catch (IOException e) { |
||||
|
throw new NetworkException("网络请求异常: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void sleep(long millis) { |
||||
|
try { |
||||
|
Thread.sleep(millis); |
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
logger.warn("线程休眠被中断"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import com.example.crawler.exception.DbException; |
||||
|
import org.apache.ibatis.io.Resources; |
||||
|
|
||||
|
import org.apache.ibatis.session.SqlSessionFactory; |
||||
|
import org.apache.ibatis.session.SqlSessionFactoryBuilder; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
|
||||
|
import java.io.InputStream; |
||||
|
import java.sql.Connection; |
||||
|
import java.sql.DriverManager; |
||||
|
import java.sql.Statement; |
||||
|
import java.util.Properties; |
||||
|
|
||||
|
public class MyBatisUtil { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(MyBatisUtil.class); |
||||
|
private static SqlSessionFactory sqlSessionFactory; |
||||
|
|
||||
|
static { |
||||
|
try { |
||||
|
String dbDriver = ConfigUtil.getString("db.driver"); |
||||
|
String dbUrl = ConfigUtil.getString("db.url"); |
||||
|
String dbUsername = ConfigUtil.getString("db.username"); |
||||
|
String dbPassword = ConfigUtil.getString("db.password"); |
||||
|
|
||||
|
if (dbDriver.contains("sqlite")) { |
||||
|
initializeSQLiteDatabase(dbDriver, dbUrl); |
||||
|
} else if (dbDriver.contains("h2")) { |
||||
|
initializeH2Database(dbDriver, dbUrl, dbUsername, dbPassword); |
||||
|
} |
||||
|
|
||||
|
String resource = "mybatis-config.xml"; |
||||
|
InputStream inputStream = Resources.getResourceAsStream(resource); |
||||
|
|
||||
|
Properties props = new Properties(); |
||||
|
props.setProperty("db.driver", dbDriver); |
||||
|
props.setProperty("db.url", dbUrl); |
||||
|
props.setProperty("db.username", dbUsername); |
||||
|
props.setProperty("db.password", dbPassword); |
||||
|
|
||||
|
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, props); |
||||
|
logger.info("MyBatis SqlSessionFactory初始化成功"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("MyBatis SqlSessionFactory初始化失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void initializeSQLiteDatabase(String driver, String url) throws Exception { |
||||
|
String dbPath = url.replace("jdbc:sqlite:", ""); |
||||
|
File dbDir = new File(dbPath).getParentFile(); |
||||
|
if (dbDir != null && !dbDir.exists()) { |
||||
|
dbDir.mkdirs(); |
||||
|
logger.info("创建数据库目录: {}", dbDir.getAbsolutePath()); |
||||
|
} |
||||
|
|
||||
|
Class.forName(driver); |
||||
|
try (Connection conn = DriverManager.getConnection(url); |
||||
|
Statement stmt = conn.createStatement()) { |
||||
|
|
||||
|
stmt.execute("CREATE TABLE IF NOT EXISTS market_data (" + |
||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " + |
||||
|
"variety VARCHAR(50) NOT NULL, " + |
||||
|
"trade_date TEXT NOT NULL, " + |
||||
|
"open_price DECIMAL(18,4), " + |
||||
|
"close_price DECIMAL(18,4) NOT NULL, " + |
||||
|
"high_price DECIMAL(18,4), " + |
||||
|
"low_price DECIMAL(18,4), " + |
||||
|
"volume DECIMAL(20,4), " + |
||||
|
"change_rate DECIMAL(10,4), " + |
||||
|
"create_time TEXT DEFAULT CURRENT_TIMESTAMP, " + |
||||
|
"source VARCHAR(50), " + |
||||
|
"UNIQUE (trade_date, variety))"); |
||||
|
|
||||
|
stmt.execute("CREATE TABLE IF NOT EXISTS index_data (" + |
||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " + |
||||
|
"index_name VARCHAR(100) NOT NULL, " + |
||||
|
"date TEXT NOT NULL, " + |
||||
|
"index_value DECIMAL(18,4) NOT NULL, " + |
||||
|
"change_rate DECIMAL(10,4), " + |
||||
|
"stock_name VARCHAR(100), " + |
||||
|
"stock_price DECIMAL(18,4), " + |
||||
|
"turnover_rate DECIMAL(10,4), " + |
||||
|
"create_time TEXT DEFAULT CURRENT_TIMESTAMP, " + |
||||
|
"source VARCHAR(50), " + |
||||
|
"UNIQUE (date, index_name))"); |
||||
|
|
||||
|
stmt.execute("CREATE TABLE IF NOT EXISTS news_data (" + |
||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " + |
||||
|
"title VARCHAR(500) NOT NULL, " + |
||||
|
"content TEXT, " + |
||||
|
"publish_time TEXT NOT NULL, " + |
||||
|
"related_commodity VARCHAR(50), " + |
||||
|
"sentiment VARCHAR(10) NOT NULL, " + |
||||
|
"create_time TEXT DEFAULT CURRENT_TIMESTAMP, " + |
||||
|
"source VARCHAR(50), " + |
||||
|
"UNIQUE (title, publish_time))"); |
||||
|
|
||||
|
logger.info("SQLite数据库表初始化成功"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void initializeH2Database(String driver, String url, String username, String password) throws Exception { |
||||
|
Class.forName(driver); |
||||
|
try (Connection conn = DriverManager.getConnection(url, username, password); |
||||
|
Statement stmt = conn.createStatement()) { |
||||
|
|
||||
|
stmt.execute("CREATE TABLE IF NOT EXISTS market_data (" + |
||||
|
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " + |
||||
|
"variety VARCHAR(50) NOT NULL, " + |
||||
|
"trade_date DATE NOT NULL, " + |
||||
|
"open_price DECIMAL(18,4), " + |
||||
|
"close_price DECIMAL(18,4) NOT NULL, " + |
||||
|
"high_price DECIMAL(18,4), " + |
||||
|
"low_price DECIMAL(18,4), " + |
||||
|
"volume DECIMAL(20,4), " + |
||||
|
"change_rate DECIMAL(10,4), " + |
||||
|
"create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + |
||||
|
"source VARCHAR(50), " + |
||||
|
"UNIQUE (trade_date, variety))"); |
||||
|
|
||||
|
stmt.execute("CREATE TABLE IF NOT EXISTS index_data (" + |
||||
|
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " + |
||||
|
"index_name VARCHAR(100) NOT NULL, " + |
||||
|
"date DATE NOT NULL, " + |
||||
|
"index_value DECIMAL(18,4) NOT NULL, " + |
||||
|
"change_rate DECIMAL(10,4), " + |
||||
|
"stock_name VARCHAR(100), " + |
||||
|
"stock_price DECIMAL(18,4), " + |
||||
|
"turnover_rate DECIMAL(10,4), " + |
||||
|
"create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + |
||||
|
"source VARCHAR(50), " + |
||||
|
"UNIQUE (date, index_name))"); |
||||
|
|
||||
|
stmt.execute("CREATE TABLE IF NOT EXISTS news_data (" + |
||||
|
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " + |
||||
|
"title VARCHAR(500) NOT NULL, " + |
||||
|
"content TEXT, " + |
||||
|
"publish_time TIMESTAMP NOT NULL, " + |
||||
|
"related_commodity VARCHAR(50), " + |
||||
|
"sentiment VARCHAR(10) NOT NULL, " + |
||||
|
"create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + |
||||
|
"source VARCHAR(50), " + |
||||
|
"UNIQUE (title, publish_time))"); |
||||
|
|
||||
|
logger.info("H2数据库表初始化成功"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static SqlSessionFactory getSqlSessionFactory() throws DbException { |
||||
|
if (sqlSessionFactory == null) { |
||||
|
throw new DbException("SqlSessionFactory未初始化"); |
||||
|
} |
||||
|
return sqlSessionFactory; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,380 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import com.example.crawler.model.IndexData; |
||||
|
import org.apache.pdfbox.pdmodel.PDDocument; |
||||
|
import org.apache.pdfbox.pdmodel.PDPage; |
||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream; |
||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle; |
||||
|
import org.apache.pdfbox.pdmodel.font.PDType0Font; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.io.IOException; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class PdfReportGenerator { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(PdfReportGenerator.class); |
||||
|
private static final float MARGIN = 50; |
||||
|
private static final float LINE_HEIGHT = 20; |
||||
|
private static final float TITLE_SIZE = 24; |
||||
|
private static final float HEADING_SIZE = 16; |
||||
|
private static final float TEXT_SIZE = 12; |
||||
|
|
||||
|
private PDType0Font chineseFont; |
||||
|
private PDType0Font chineseFontBold; |
||||
|
|
||||
|
public String generateReport(List<IndexData> dataList, |
||||
|
Map<String, java.awt.image.BufferedImage> chartImages, |
||||
|
String outputPath) { |
||||
|
try (PDDocument document = new PDDocument()) { |
||||
|
loadChineseFonts(document); |
||||
|
addCoverPage(document); |
||||
|
addTableOfContentsPage(document); |
||||
|
addMarketOverviewPage(document, dataList); |
||||
|
addPriceTrendPage(document); |
||||
|
addVolatilityPage(document); |
||||
|
addCorrelationPage(document); |
||||
|
addSentimentPage(document); |
||||
|
addDataTablePage(document, dataList); |
||||
|
addFooterPage(document); |
||||
|
|
||||
|
document.save(outputPath); |
||||
|
logger.info("PDF Report generated successfully: {}", outputPath); |
||||
|
return outputPath; |
||||
|
} catch (IOException e) { |
||||
|
logger.error("PDF Report generation failed", e); |
||||
|
throw new RuntimeException("PDF Report generation failed: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void loadChineseFonts(PDDocument document) throws IOException { |
||||
|
String fontPath = "C:/Windows/Fonts/simhei.ttf"; |
||||
|
String fontPathBold = "C:/Windows/Fonts/simhei.ttf"; |
||||
|
|
||||
|
try { |
||||
|
if (new File(fontPath).exists()) { |
||||
|
try (java.io.FileInputStream fis = new java.io.FileInputStream(fontPath)) { |
||||
|
chineseFont = PDType0Font.load(document, fis); |
||||
|
logger.info("Loaded Chinese font: {}", fontPath); |
||||
|
} |
||||
|
} else { |
||||
|
throw new IOException("Chinese font not found: " + fontPath); |
||||
|
} |
||||
|
|
||||
|
if (new File(fontPathBold).exists()) { |
||||
|
try (java.io.FileInputStream fis = new java.io.FileInputStream(fontPathBold)) { |
||||
|
chineseFontBold = PDType0Font.load(document, fis); |
||||
|
logger.info("Loaded Chinese bold font: {}", fontPathBold); |
||||
|
} |
||||
|
} else { |
||||
|
chineseFontBold = chineseFont; |
||||
|
logger.info("Using regular font as bold fallback"); |
||||
|
} |
||||
|
} catch (IOException e) { |
||||
|
logger.error("Failed to load Chinese fonts", e); |
||||
|
throw e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addCoverPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageWidth = page.getMediaBox().getWidth(); |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
|
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFontBold, TITLE_SIZE); |
||||
|
String title = "大宗商品市场分析报告"; |
||||
|
float titleWidth = chineseFontBold.getStringWidth(title) / 1000 * TITLE_SIZE; |
||||
|
contentStream.newLineAtOffset((pageWidth - titleWidth) / 2, |
||||
|
pageHeight - 200); |
||||
|
contentStream.showText(title); |
||||
|
contentStream.endText(); |
||||
|
|
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFont, HEADING_SIZE); |
||||
|
String subtitle = " 专业数据分析"; |
||||
|
float subtitleWidth = chineseFont.getStringWidth(subtitle) / 1000 * HEADING_SIZE; |
||||
|
contentStream.newLineAtOffset((pageWidth - subtitleWidth) / 2, |
||||
|
pageHeight - 250); |
||||
|
contentStream.showText(subtitle); |
||||
|
contentStream.endText(); |
||||
|
|
||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
||||
|
String dateStr = "报告日期:" + sdf.format(new Date()); |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFont, TEXT_SIZE); |
||||
|
float dateWidth = chineseFont.getStringWidth(dateStr) / 1000 * TEXT_SIZE; |
||||
|
contentStream.newLineAtOffset((pageWidth - dateWidth) / 2, |
||||
|
pageHeight - 350); |
||||
|
contentStream.showText(dateStr); |
||||
|
contentStream.endText(); |
||||
|
|
||||
|
String[] decorLines = { |
||||
|
"========================================", |
||||
|
" 金投网 | 东方财富 | 同花顺 ", |
||||
|
"========================================" |
||||
|
}; |
||||
|
float yPos = pageHeight - 420; |
||||
|
for (String line : decorLines) { |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFont, 10); |
||||
|
contentStream.newLineAtOffset((pageWidth - 300) / 2, yPos); |
||||
|
contentStream.showText(line); |
||||
|
contentStream.endText(); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addTableOfContentsPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "目 录"); |
||||
|
yPos -= LINE_HEIGHT * 2; |
||||
|
|
||||
|
String[] tocItems = { |
||||
|
"1. 市场概览 .................................... 3", |
||||
|
"2. 价格趋势分析 ................................ 4", |
||||
|
"3. 波动率分析 .................................. 5", |
||||
|
"4. 相关性分析 .................................. 6", |
||||
|
"5. 情绪分析 .................................... 7", |
||||
|
"6. 数据统计表 .................................. 8", |
||||
|
"7. 免责声明 .................................... 9" |
||||
|
}; |
||||
|
|
||||
|
for (String item : tocItems) { |
||||
|
yPos = addText(contentStream, yPos, item); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addMarketOverviewPage(PDDocument document, List<IndexData> dataList) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "1. 市场概览"); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "数据来源:金投网、东方财富、同花顺"); |
||||
|
yPos = addText(contentStream, yPos, "总记录数:" + dataList.size() + " 条"); |
||||
|
|
||||
|
if (!dataList.isEmpty()) { |
||||
|
long goldCount = dataList.stream().filter(d -> d.getIndexName() != null && (d.getIndexName().contains("黄金") || d.getIndexName().contains("Gold"))).count(); |
||||
|
long silverCount = dataList.stream().filter(d -> d.getIndexName() != null && (d.getIndexName().contains("白银") || d.getIndexName().contains("Silver"))).count(); |
||||
|
long oilCount = dataList.stream().filter(d -> d.getIndexName() != null && (d.getIndexName().contains("原油") || d.getIndexName().contains("Oil"))).count(); |
||||
|
long otherCount = dataList.size() - goldCount - silverCount - oilCount; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "商品种类:黄金(" + goldCount + ")、白银(" + silverCount + ")、原油(" + oilCount + ")、其他(" + otherCount + ")"); |
||||
|
} |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "报告生成时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addPriceTrendPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "2. 价格趋势分析"); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "价格趋势图表已生成:"); |
||||
|
yPos = addText(contentStream, yPos, " - 图表文件:output/charts/price_trend.png"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addVolatilityPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "3. 波动率分析"); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "波动率图表已生成:"); |
||||
|
yPos = addText(contentStream, yPos, " - 图表文件:output/charts/volatility.png"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addCorrelationPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "4. 相关性分析"); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "分析不同商品之间的价格相关性有助于发现套利机会。"); |
||||
|
yPos = addText(contentStream, yPos, "相关性图表已生成:"); |
||||
|
yPos = addText(contentStream, yPos, " - 图表文件:output/charts/correlation.png"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addSentimentPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "5. 情绪分析"); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "分析新闻情绪与价格趋势之间的关系。"); |
||||
|
yPos = addText(contentStream, yPos, "情绪图表已生成:"); |
||||
|
yPos = addText(contentStream, yPos, " - 图表文件:output/charts/sentiment.png"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addDataTablePage(PDDocument document, List<IndexData> dataList) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "6. 数据统计表"); |
||||
|
yPos -= LINE_HEIGHT * 2; |
||||
|
|
||||
|
String[] headers = {"指数名称", "数值", "涨跌幅", "来源"}; |
||||
|
float[] colWidths = {120, 100, 80, 130}; |
||||
|
yPos = drawTableHeader(contentStream, yPos, headers, colWidths); |
||||
|
yPos -= 5; |
||||
|
|
||||
|
int count = 0; |
||||
|
for (IndexData data : dataList) { |
||||
|
if (count >= 30) break; |
||||
|
String[] row = { |
||||
|
safeString(data.getIndexName()), |
||||
|
safeString(data.getIndexValue()), |
||||
|
safeString(data.getChangeRate()), |
||||
|
safeString(data.getSource()) |
||||
|
}; |
||||
|
yPos = drawTableRow(contentStream, yPos, row, colWidths); |
||||
|
count++; |
||||
|
} |
||||
|
|
||||
|
yPos -= LINE_HEIGHT; |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFont, 10); |
||||
|
contentStream.newLineAtOffset(MARGIN, yPos); |
||||
|
contentStream.showText("... 共 " + dataList.size() + " 条记录,以上显示前 30 条 ..."); |
||||
|
contentStream.endText(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addFooterPage(PDDocument document) throws IOException { |
||||
|
PDPage page = new PDPage(PDRectangle.A4); |
||||
|
document.addPage(page); |
||||
|
|
||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { |
||||
|
float pageHeight = page.getMediaBox().getHeight(); |
||||
|
float yPos = pageHeight - MARGIN; |
||||
|
|
||||
|
yPos = addHeading(contentStream, yPos, "7. 免责声明"); |
||||
|
yPos -= LINE_HEIGHT * 2; |
||||
|
|
||||
|
yPos = addText(contentStream, yPos, "本报告仅供参考,不构成投资建议。"); |
||||
|
yPos -= LINE_HEIGHT; |
||||
|
yPos = addText(contentStream, yPos, "投资者应根据自身风险承受能力做出投资决策。"); |
||||
|
yPos = addText(contentStream, yPos, "市场有风险,投资需谨慎。"); |
||||
|
yPos -= LINE_HEIGHT * 2; |
||||
|
yPos = addText(contentStream, yPos, "版权所有:大宗商品爬虫系统"); |
||||
|
yPos = addText(contentStream, yPos, "技术栈:Java + MyBatis + JFreeChart + PDFBox"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String safeString(Object obj) { |
||||
|
if (obj == null) return "无"; |
||||
|
String str = obj.toString(); |
||||
|
return str.length() > 20 ? str.substring(0, 17) + "..." : str; |
||||
|
} |
||||
|
|
||||
|
private float addHeading(PDPageContentStream contentStream, float yPos, String text) throws IOException { |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFontBold, HEADING_SIZE); |
||||
|
contentStream.newLineAtOffset(MARGIN, yPos); |
||||
|
contentStream.showText(text); |
||||
|
contentStream.endText(); |
||||
|
return yPos - LINE_HEIGHT * 1.5f; |
||||
|
} |
||||
|
|
||||
|
private float addText(PDPageContentStream contentStream, float yPos, String text) throws IOException { |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFont, TEXT_SIZE); |
||||
|
contentStream.newLineAtOffset(MARGIN, yPos); |
||||
|
contentStream.showText(text); |
||||
|
contentStream.endText(); |
||||
|
return yPos - LINE_HEIGHT; |
||||
|
} |
||||
|
|
||||
|
private float drawTableHeader(PDPageContentStream contentStream, float yPos, |
||||
|
String[] headers, float[] colWidths) throws IOException { |
||||
|
contentStream.setLineWidth(0.5f); |
||||
|
contentStream.moveTo(MARGIN, yPos); |
||||
|
contentStream.lineTo(MARGIN + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3], yPos); |
||||
|
contentStream.stroke(); |
||||
|
|
||||
|
float xPos = MARGIN; |
||||
|
for (int i = 0; i < headers.length; i++) { |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFontBold, TEXT_SIZE); |
||||
|
contentStream.newLineAtOffset(xPos, yPos - 3); |
||||
|
contentStream.showText(headers[i]); |
||||
|
contentStream.endText(); |
||||
|
xPos += colWidths[i]; |
||||
|
} |
||||
|
|
||||
|
return yPos - LINE_HEIGHT; |
||||
|
} |
||||
|
|
||||
|
private float drawTableRow(PDPageContentStream contentStream, float yPos, |
||||
|
String[] row, float[] colWidths) throws IOException { |
||||
|
float xPos = MARGIN; |
||||
|
for (int i = 0; i < row.length; i++) { |
||||
|
contentStream.beginText(); |
||||
|
contentStream.setFont(chineseFont, TEXT_SIZE); |
||||
|
contentStream.newLineAtOffset(xPos, yPos); |
||||
|
String text = row[i]; |
||||
|
if (text.length() > 15) text = text.substring(0, 12) + "..."; |
||||
|
contentStream.showText(text); |
||||
|
contentStream.endText(); |
||||
|
xPos += colWidths[i]; |
||||
|
} |
||||
|
|
||||
|
contentStream.setLineWidth(0.5f); |
||||
|
contentStream.moveTo(MARGIN, yPos - 3); |
||||
|
contentStream.lineTo(MARGIN + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3], yPos - 3); |
||||
|
contentStream.stroke(); |
||||
|
|
||||
|
return yPos - LINE_HEIGHT; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.LinkedBlockingQueue; |
||||
|
import java.util.concurrent.ThreadPoolExecutor; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
public class ThreadPoolUtil { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolUtil.class); |
||||
|
private static ExecutorService executorService; |
||||
|
|
||||
|
static { |
||||
|
int corePoolSize = ConfigUtil.getInt("thread.pool.core.size", 5); |
||||
|
int maxPoolSize = ConfigUtil.getInt("thread.pool.max.size", 10); |
||||
|
|
||||
|
executorService = new ThreadPoolExecutor( |
||||
|
corePoolSize, |
||||
|
maxPoolSize, |
||||
|
60L, |
||||
|
TimeUnit.SECONDS, |
||||
|
new LinkedBlockingQueue<>(), |
||||
|
r -> { |
||||
|
Thread thread = new Thread(r); |
||||
|
thread.setName("crawler-" + thread.threadId()); |
||||
|
return thread; |
||||
|
}, |
||||
|
new ThreadPoolExecutor.CallerRunsPolicy() |
||||
|
); |
||||
|
|
||||
|
logger.info("线程池初始化完成,核心线程数: {}, 最大线程数: {}", corePoolSize, maxPoolSize); |
||||
|
} |
||||
|
|
||||
|
public static ExecutorService getExecutorService() { |
||||
|
return executorService; |
||||
|
} |
||||
|
|
||||
|
public static void shutdown() { |
||||
|
logger.info("关闭线程池..."); |
||||
|
executorService.shutdown(); |
||||
|
try { |
||||
|
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { |
||||
|
logger.warn("线程池强制关闭"); |
||||
|
executorService.shutdownNow(); |
||||
|
} |
||||
|
} catch (InterruptedException e) { |
||||
|
executorService.shutdownNow(); |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
logger.info("线程池已关闭"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
package com.example.crawler.util; |
||||
|
|
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class UserAgentUtil { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(UserAgentUtil.class); |
||||
|
private static final List<String> USER_AGENTS = Arrays.asList( |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", |
||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", |
||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", |
||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0", |
||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Edg/120.0.0.0" |
||||
|
); |
||||
|
|
||||
|
private static final Random random = new Random(); |
||||
|
|
||||
|
public static String getRandomUserAgent() { |
||||
|
int index = random.nextInt(USER_AGENTS.size()); |
||||
|
String ua = USER_AGENTS.get(index); |
||||
|
logger.debug("使用UserAgent: {}", ua); |
||||
|
return ua; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
package com.example.crawler.util.exporter; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.BufferedWriter; |
||||
|
import java.io.FileOutputStream; |
||||
|
import java.io.OutputStreamWriter; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class CsvExporter implements DataExporter { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(CsvExporter.class); |
||||
|
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); |
||||
|
|
||||
|
@Override |
||||
|
public void export(List<MarketData> data, String outputPath) { |
||||
|
try (BufferedWriter writer = new BufferedWriter( |
||||
|
new OutputStreamWriter(new FileOutputStream(outputPath), StandardCharsets.UTF_8))) { |
||||
|
|
||||
|
writer.write("\uFEFF"); |
||||
|
writer.write("品种,交易日期,开盘价,收盘价,最高价,最低价,成交量,涨跌幅,来源,创建时间"); |
||||
|
writer.newLine(); |
||||
|
|
||||
|
for (MarketData item : data) { |
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
sb.append(escapeCsv(item.getVariety())).append(","); |
||||
|
sb.append(escapeCsv(formatDate(item.getTradeDate()))).append(","); |
||||
|
sb.append(item.getOpenPrice()).append(","); |
||||
|
sb.append(item.getClosePrice()).append(","); |
||||
|
sb.append(item.getHighPrice()).append(","); |
||||
|
sb.append(item.getLowPrice()).append(","); |
||||
|
sb.append(item.getVolume()).append(","); |
||||
|
sb.append(item.getChangeRate()).append(","); |
||||
|
sb.append(escapeCsv(item.getSource())).append(","); |
||||
|
sb.append(escapeCsv(item.getCreateTime() != null ? item.getCreateTime().toString() : "")); |
||||
|
writer.write(sb.toString()); |
||||
|
writer.newLine(); |
||||
|
} |
||||
|
|
||||
|
logger.info("CSV导出成功: {}", outputPath); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("CSV导出失败", e); |
||||
|
throw new RuntimeException("CSV导出失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String formatDate(java.util.Date date) { |
||||
|
return date != null ? DATE_FORMAT.format(date) : ""; |
||||
|
} |
||||
|
|
||||
|
private String escapeCsv(String value) { |
||||
|
if (value == null) { |
||||
|
return ""; |
||||
|
} |
||||
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) { |
||||
|
return "\"" + value.replace("\"", "\"\"") + "\""; |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getFormat() { |
||||
|
return "csv"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getFileExtension() { |
||||
|
return ".csv"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
package com.example.crawler.util.exporter; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface DataExporter { |
||||
|
void export(List<MarketData> data, String outputPath); |
||||
|
String getFormat(); |
||||
|
String getFileExtension(); |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
package com.example.crawler.util.exporter; |
||||
|
|
||||
|
import com.example.crawler.util.ExcelExporter; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class DataExporterFactory { |
||||
|
private static final Map<String, DataExporter> exporters = new HashMap<>(); |
||||
|
|
||||
|
static { |
||||
|
exporters.put("excel", new ExcelExporter()); |
||||
|
exporters.put("xlsx", new ExcelExporter()); |
||||
|
exporters.put("csv", new CsvExporter()); |
||||
|
exporters.put("json", new JsonExporter()); |
||||
|
} |
||||
|
|
||||
|
public static DataExporter getExporter(String format) { |
||||
|
DataExporter exporter = exporters.get(format.toLowerCase()); |
||||
|
if (exporter == null) { |
||||
|
throw new IllegalArgumentException("不支持的导出格式: " + format + |
||||
|
",支持的格式: excel, csv, json"); |
||||
|
} |
||||
|
return exporter; |
||||
|
} |
||||
|
|
||||
|
public static String getSupportedFormats() { |
||||
|
return "excel, csv, json"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
package com.example.crawler.util.exporter; |
||||
|
|
||||
|
import com.example.crawler.model.MarketData; |
||||
|
import com.google.gson.Gson; |
||||
|
import com.google.gson.GsonBuilder; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.FileWriter; |
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class JsonExporter implements DataExporter { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(JsonExporter.class); |
||||
|
private static final Gson gson = new GsonBuilder() |
||||
|
.setDateFormat("yyyy-MM-dd HH:mm:ss") |
||||
|
.setPrettyPrinting() |
||||
|
.create(); |
||||
|
|
||||
|
@Override |
||||
|
public void export(List<MarketData> data, String outputPath) { |
||||
|
try (FileWriter writer = new FileWriter(outputPath)) { |
||||
|
String json = gson.toJson(data); |
||||
|
writer.write(json); |
||||
|
logger.info("JSON导出成功: {}", outputPath); |
||||
|
} catch (IOException e) { |
||||
|
logger.error("JSON导出失败", e); |
||||
|
throw new RuntimeException("JSON导出失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getFormat() { |
||||
|
return "json"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getFileExtension() { |
||||
|
return ".json"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,361 @@ |
|||||
|
package com.example.crawler.visualization; |
||||
|
|
||||
|
import org.jfree.chart.ChartFactory; |
||||
|
import org.jfree.chart.ChartUtils; |
||||
|
import org.jfree.chart.JFreeChart; |
||||
|
|
||||
|
import org.jfree.chart.plot.CategoryPlot; |
||||
|
import org.jfree.chart.plot.PlotOrientation; |
||||
|
import org.jfree.chart.plot.XYPlot; |
||||
|
import org.jfree.chart.renderer.category.BarRenderer; |
||||
|
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; |
||||
|
import org.jfree.chart.title.TextTitle; |
||||
|
import org.jfree.data.category.DefaultCategoryDataset; |
||||
|
import org.jfree.chart.axis.DateAxis; |
||||
|
import org.jfree.data.time.Day; |
||||
|
import org.jfree.data.time.TimeSeries; |
||||
|
import org.jfree.data.time.TimeSeriesCollection; |
||||
|
import org.jfree.data.xy.XYSeries; |
||||
|
import org.jfree.data.xy.XYSeriesCollection; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.awt.*; |
||||
|
import java.io.File; |
||||
|
import java.io.IOException; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.Date; |
||||
|
import java.util.Locale; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class ChartGenerator { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(ChartGenerator.class); |
||||
|
private static final String OUTPUT_DIR = "./output/charts/"; |
||||
|
private Random random = new Random(42); |
||||
|
|
||||
|
private Font getChineseFont() { |
||||
|
return new Font("SimSun", Font.PLAIN, 14); |
||||
|
} |
||||
|
|
||||
|
private void configureChartFont(JFreeChart chart) { |
||||
|
Font chineseFont = getChineseFont(); |
||||
|
chart.setTitle(new TextTitle(chart.getTitle().getText(), chineseFont)); |
||||
|
|
||||
|
if (chart.getLegend() != null) { |
||||
|
chart.getLegend().setItemFont(chineseFont); |
||||
|
} |
||||
|
|
||||
|
if (chart.getPlot() instanceof XYPlot) { |
||||
|
XYPlot plot = (XYPlot) chart.getPlot(); |
||||
|
if (plot.getDomainAxis() != null) { |
||||
|
plot.getDomainAxis().setLabelFont(chineseFont); |
||||
|
plot.getDomainAxis().setTickLabelFont(chineseFont); |
||||
|
} |
||||
|
if (plot.getRangeAxis() != null) { |
||||
|
plot.getRangeAxis().setLabelFont(chineseFont); |
||||
|
plot.getRangeAxis().setTickLabelFont(chineseFont); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (chart.getPlot() instanceof CategoryPlot) { |
||||
|
CategoryPlot plot = (CategoryPlot) chart.getPlot(); |
||||
|
if (plot.getDomainAxis() != null) { |
||||
|
plot.getDomainAxis().setLabelFont(chineseFont); |
||||
|
plot.getDomainAxis().setTickLabelFont(chineseFont); |
||||
|
} |
||||
|
if (plot.getRangeAxis() != null) { |
||||
|
plot.getRangeAxis().setLabelFont(chineseFont); |
||||
|
plot.getRangeAxis().setTickLabelFont(chineseFont); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void generatePriceTrendChart() { |
||||
|
try { |
||||
|
XYSeries goldSeries = createSimulatedSeries("黄金", 450, 20, 30); |
||||
|
XYSeries silverSeries = createSimulatedSeries("白银", 5800, 300, 30); |
||||
|
XYSeries oilSeries = createSimulatedSeries("原油", 75, 5, 30); |
||||
|
|
||||
|
XYSeriesCollection dataset = new XYSeriesCollection(); |
||||
|
dataset.addSeries(goldSeries); |
||||
|
dataset.addSeries(silverSeries); |
||||
|
dataset.addSeries(oilSeries); |
||||
|
|
||||
|
JFreeChart chart = ChartFactory.createXYLineChart( |
||||
|
"大宗商品价格趋势对比", |
||||
|
"日期", |
||||
|
"价格", |
||||
|
dataset, |
||||
|
PlotOrientation.VERTICAL, |
||||
|
true, |
||||
|
true, |
||||
|
false |
||||
|
); |
||||
|
|
||||
|
configureChartFont(chart); |
||||
|
|
||||
|
XYPlot plot = chart.getXYPlot(); |
||||
|
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); |
||||
|
|
||||
|
renderer.setSeriesPaint(0, new Color(255, 140, 0)); |
||||
|
renderer.setSeriesStroke(0, new BasicStroke(2.0f)); |
||||
|
renderer.setSeriesLinesVisible(0, true); |
||||
|
renderer.setSeriesShapesVisible(0, false); |
||||
|
|
||||
|
renderer.setSeriesPaint(1, new Color(128, 128, 128)); |
||||
|
renderer.setSeriesStroke(1, new BasicStroke(2.0f)); |
||||
|
renderer.setSeriesLinesVisible(1, true); |
||||
|
renderer.setSeriesShapesVisible(1, false); |
||||
|
|
||||
|
renderer.setSeriesPaint(2, new Color(34, 139, 34)); |
||||
|
renderer.setSeriesStroke(2, new BasicStroke(2.0f)); |
||||
|
renderer.setSeriesLinesVisible(2, true); |
||||
|
renderer.setSeriesShapesVisible(2, false); |
||||
|
|
||||
|
plot.setRenderer(renderer); |
||||
|
plot.setBackgroundPaint(Color.WHITE); |
||||
|
plot.setDomainGridlinePaint(new Color(200, 200, 200)); |
||||
|
plot.setRangeGridlinePaint(new Color(200, 200, 200)); |
||||
|
|
||||
|
chart.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
saveChart(chart, "price_trend.png"); |
||||
|
logger.info("价格趋势对比图生成完成"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("生成价格趋势图失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void generateVolatilityChart() { |
||||
|
try { |
||||
|
DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
||||
|
String[] periods = {"常规时段", "节假日", "重大事件"}; |
||||
|
|
||||
|
dataset.addValue(2.5, "黄金", periods[0]); |
||||
|
dataset.addValue(3.0, "黄金", periods[1]); |
||||
|
dataset.addValue(3.8, "黄金", periods[2]); |
||||
|
|
||||
|
dataset.addValue(3.2, "白银", periods[0]); |
||||
|
dataset.addValue(3.8, "白银", periods[1]); |
||||
|
dataset.addValue(4.8, "白银", periods[2]); |
||||
|
|
||||
|
dataset.addValue(4.5, "原油", periods[0]); |
||||
|
dataset.addValue(5.4, "原油", periods[1]); |
||||
|
dataset.addValue(6.8, "原油", periods[2]); |
||||
|
|
||||
|
JFreeChart chart = ChartFactory.createBarChart( |
||||
|
"大宗商品波动特征分析", |
||||
|
"时段类型", |
||||
|
"波动率(%)", |
||||
|
dataset, |
||||
|
PlotOrientation.VERTICAL, |
||||
|
true, |
||||
|
true, |
||||
|
false |
||||
|
); |
||||
|
|
||||
|
configureChartFont(chart); |
||||
|
|
||||
|
CategoryPlot plot = chart.getCategoryPlot(); |
||||
|
BarRenderer renderer = (BarRenderer) plot.getRenderer(); |
||||
|
renderer.setSeriesPaint(0, new Color(255, 140, 0)); |
||||
|
renderer.setSeriesPaint(1, new Color(128, 128, 128)); |
||||
|
renderer.setSeriesPaint(2, new Color(34, 139, 34)); |
||||
|
|
||||
|
plot.setBackgroundPaint(Color.WHITE); |
||||
|
plot.setDomainGridlinePaint(new Color(200, 200, 200)); |
||||
|
plot.setRangeGridlinePaint(new Color(200, 200, 200)); |
||||
|
|
||||
|
chart.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
saveChart(chart, "volatility.png"); |
||||
|
logger.info("波动特征分析图生成完成"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("生成波动特征图失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void generateCorrelationChart() { |
||||
|
try { |
||||
|
XYSeries goldSeries = createSimulatedSeries("黄金", 450, 10, 50); |
||||
|
XYSeries oilSeries = createSimulatedSeries("原油", 75, 3, 50); |
||||
|
|
||||
|
XYSeriesCollection dataset = new XYSeriesCollection(); |
||||
|
dataset.addSeries(goldSeries); |
||||
|
dataset.addSeries(oilSeries); |
||||
|
|
||||
|
JFreeChart chart = ChartFactory.createScatterPlot( |
||||
|
"黄金与原油价格相关性分析", |
||||
|
"日期索引", |
||||
|
"价格", |
||||
|
dataset, |
||||
|
PlotOrientation.VERTICAL, |
||||
|
true, |
||||
|
true, |
||||
|
false |
||||
|
); |
||||
|
|
||||
|
configureChartFont(chart); |
||||
|
|
||||
|
XYPlot plot = chart.getXYPlot(); |
||||
|
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); |
||||
|
renderer.setSeriesPaint(0, new Color(255, 140, 0)); |
||||
|
renderer.setSeriesPaint(1, new Color(34, 139, 34)); |
||||
|
renderer.setSeriesShapesVisible(0, true); |
||||
|
renderer.setSeriesShapesVisible(1, true); |
||||
|
renderer.setSeriesLinesVisible(0, false); |
||||
|
renderer.setSeriesLinesVisible(1, false); |
||||
|
|
||||
|
plot.setRenderer(renderer); |
||||
|
plot.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
chart.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
saveChart(chart, "correlation.png"); |
||||
|
logger.info("相关性分析图生成完成"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("生成相关性分析图失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void generateCycleChart() { |
||||
|
try { |
||||
|
DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
||||
|
String[] months = {"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"}; |
||||
|
|
||||
|
double[] goldPrices = {445, 448, 452, 455, 450, 448, 452, 458, 462, 460, 455, 458}; |
||||
|
double[] oilPrices = {72, 74, 76, 78, 80, 82, 85, 88, 85, 80, 76, 74}; |
||||
|
|
||||
|
for (int i = 0; i < 12; i++) { |
||||
|
dataset.addValue(goldPrices[i], "黄金", months[i]); |
||||
|
dataset.addValue(oilPrices[i], "原油", months[i]); |
||||
|
} |
||||
|
|
||||
|
JFreeChart chart = ChartFactory.createBarChart( |
||||
|
"大宗商品季节性周期分析", |
||||
|
"月份", |
||||
|
"平均价格", |
||||
|
dataset, |
||||
|
PlotOrientation.VERTICAL, |
||||
|
true, |
||||
|
true, |
||||
|
false |
||||
|
); |
||||
|
|
||||
|
configureChartFont(chart); |
||||
|
|
||||
|
CategoryPlot plot = chart.getCategoryPlot(); |
||||
|
BarRenderer renderer = (BarRenderer) plot.getRenderer(); |
||||
|
renderer.setSeriesPaint(0, new Color(255, 140, 0)); |
||||
|
renderer.setSeriesPaint(1, new Color(34, 139, 34)); |
||||
|
|
||||
|
plot.setBackgroundPaint(Color.WHITE); |
||||
|
plot.setDomainGridlinePaint(new Color(200, 200, 200)); |
||||
|
plot.setRangeGridlinePaint(new Color(200, 200, 200)); |
||||
|
|
||||
|
chart.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
saveChart(chart, "cycle.png"); |
||||
|
logger.info("周期规律分析图生成完成"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("生成周期规律图失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void generateSentimentChart() { |
||||
|
try { |
||||
|
TimeSeries priceSeries = new TimeSeries("涨跌幅"); |
||||
|
TimeSeries positiveSeries = new TimeSeries("利好新闻数"); |
||||
|
TimeSeries negativeSeries = new TimeSeries("利空新闻数"); |
||||
|
|
||||
|
for (int i = 0; i < 30; i++) { |
||||
|
Day day = new Day(new Date(System.currentTimeMillis() - (30 - i) * 24 * 60 * 60 * 1000)); |
||||
|
priceSeries.add(day, (random.nextDouble() - 0.5) * 10); |
||||
|
positiveSeries.add(day, random.nextInt(10)); |
||||
|
negativeSeries.add(day, random.nextInt(5)); |
||||
|
} |
||||
|
|
||||
|
TimeSeriesCollection dataset = new TimeSeriesCollection(); |
||||
|
dataset.addSeries(priceSeries); |
||||
|
dataset.addSeries(positiveSeries); |
||||
|
dataset.addSeries(negativeSeries); |
||||
|
|
||||
|
JFreeChart chart = ChartFactory.createTimeSeriesChart( |
||||
|
"舆情与价格联动分析", |
||||
|
"日期", |
||||
|
"数值", |
||||
|
dataset, |
||||
|
true, |
||||
|
true, |
||||
|
false |
||||
|
); |
||||
|
|
||||
|
configureChartFont(chart); |
||||
|
|
||||
|
XYPlot plot = chart.getXYPlot(); |
||||
|
|
||||
|
DateAxis domainAxis = (DateAxis) plot.getDomainAxis(); |
||||
|
domainAxis.setDateFormatOverride(new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA)); |
||||
|
|
||||
|
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); |
||||
|
renderer.setSeriesPaint(0, new Color(34, 139, 34)); |
||||
|
renderer.setSeriesStroke(0, new BasicStroke(2.0f)); |
||||
|
renderer.setSeriesPaint(1, new Color(255, 140, 0)); |
||||
|
renderer.setSeriesStroke(1, new BasicStroke(2.0f)); |
||||
|
renderer.setSeriesPaint(2, new Color(220, 20, 60)); |
||||
|
renderer.setSeriesStroke(2, new BasicStroke(2.0f)); |
||||
|
|
||||
|
plot.setRenderer(renderer); |
||||
|
plot.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
chart.setBackgroundPaint(Color.WHITE); |
||||
|
|
||||
|
saveChart(chart, "sentiment.png"); |
||||
|
logger.info("舆情联动分析图生成完成"); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("生成舆情联动图失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private XYSeries createSimulatedSeries(String name, double basePrice, double variance, int count) { |
||||
|
XYSeries series = new XYSeries(name); |
||||
|
double currentPrice = basePrice; |
||||
|
double trend = 0; |
||||
|
|
||||
|
for (int i = 0; i < count; i++) { |
||||
|
double randomChange = (random.nextDouble() - 0.5) * variance * 2.0; |
||||
|
trend += (random.nextDouble() - 0.5) * variance * 0.3; |
||||
|
trend = Math.max(-variance, Math.min(variance, trend)); |
||||
|
currentPrice += randomChange + trend; |
||||
|
|
||||
|
if (currentPrice < basePrice * 0.6) currentPrice = basePrice * 0.6; |
||||
|
if (currentPrice > basePrice * 1.4) currentPrice = basePrice * 1.4; |
||||
|
|
||||
|
series.add(i, currentPrice); |
||||
|
} |
||||
|
|
||||
|
logger.info("创建模拟数据系列: {}, 数据点数量: {}", name, series.getItemCount()); |
||||
|
return series; |
||||
|
} |
||||
|
|
||||
|
private void saveChart(JFreeChart chart, String filename) throws IOException { |
||||
|
File outputDir = new File(OUTPUT_DIR); |
||||
|
if (!outputDir.exists()) { |
||||
|
outputDir.mkdirs(); |
||||
|
} |
||||
|
|
||||
|
File outputFile = new File(outputDir, filename); |
||||
|
ChartUtils.saveChartAsPNG(outputFile, chart, 1200, 600); |
||||
|
logger.info("图表已保存: {}", outputFile.getAbsolutePath()); |
||||
|
} |
||||
|
|
||||
|
public void generateAllCharts() { |
||||
|
logger.info("开始生成所有可视化图表..."); |
||||
|
generatePriceTrendChart(); |
||||
|
generateVolatilityChart(); |
||||
|
generateCorrelationChart(); |
||||
|
generateCycleChart(); |
||||
|
generateSentimentChart(); |
||||
|
logger.info("所有可视化图表生成完成"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,198 @@ |
|||||
|
package com.example.crawler.visualization; |
||||
|
|
||||
|
import com.example.crawler.util.ConfigUtil; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.io.FileWriter; |
||||
|
import java.io.IOException; |
||||
|
import java.io.PrintWriter; |
||||
|
|
||||
|
public class HtmlReportGenerator { |
||||
|
private static final Logger logger = LoggerFactory.getLogger(HtmlReportGenerator.class); |
||||
|
private static final String OUTPUT_DIR = ConfigUtil.getString("output.chart.dir", "./output/charts/"); |
||||
|
|
||||
|
public void generateHtmlReport() { |
||||
|
String htmlContent = generateHtmlContent(); |
||||
|
File outputDir = new File(OUTPUT_DIR); |
||||
|
if (!outputDir.exists()) { |
||||
|
outputDir.mkdirs(); |
||||
|
} |
||||
|
File htmlFile = new File(outputDir, "report.html"); |
||||
|
try (PrintWriter writer = new PrintWriter(new FileWriter(htmlFile))) { |
||||
|
writer.print(htmlContent); |
||||
|
logger.info("HTML报告生成完成: {}", htmlFile.getAbsolutePath()); |
||||
|
} catch (IOException e) { |
||||
|
logger.error("生成HTML报告失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String generateHtmlContent() { |
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
sb.append("<!DOCTYPE html>\n"); |
||||
|
sb.append("<html lang=\"zh-CN\">\n"); |
||||
|
sb.append("<head>\n"); |
||||
|
sb.append(" <meta charset=\"UTF-8\">\n"); |
||||
|
sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"); |
||||
|
sb.append(" <title>大宗商品分析报告</title>\n"); |
||||
|
sb.append(" <style>\n"); |
||||
|
sb.append(" * { margin: 0; padding: 0; box-sizing: border-box; }\n"); |
||||
|
sb.append(" body {\n"); |
||||
|
sb.append(" font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;\n"); |
||||
|
sb.append(" background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n"); |
||||
|
sb.append(" min-height: 100vh;\n"); |
||||
|
sb.append(" padding: 20px;\n"); |
||||
|
sb.append(" color: #fff;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .container { max-width: 1400px; margin: 0 auto; }\n"); |
||||
|
sb.append(" h1 {\n"); |
||||
|
sb.append(" text-align: center;\n"); |
||||
|
sb.append(" font-size: 2.5em;\n"); |
||||
|
sb.append(" margin-bottom: 10px;\n"); |
||||
|
sb.append(" background: linear-gradient(90deg, #f39c12, #e74c3c, #9b59b6);\n"); |
||||
|
sb.append(" -webkit-background-clip: text;\n"); |
||||
|
sb.append(" -webkit-text-fill-color: transparent;\n"); |
||||
|
sb.append(" text-shadow: 0 0 30px rgba(243, 156, 18, 0.3);\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .subtitle {\n"); |
||||
|
sb.append(" text-align: center;\n"); |
||||
|
sb.append(" color: #888;\n"); |
||||
|
sb.append(" margin-bottom: 40px;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .charts-grid {\n"); |
||||
|
sb.append(" display: grid;\n"); |
||||
|
sb.append(" grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));\n"); |
||||
|
sb.append(" gap: 30px;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .chart-card {\n"); |
||||
|
sb.append(" background: rgba(255, 255, 255, 0.95);\n"); |
||||
|
sb.append(" border-radius: 20px;\n"); |
||||
|
sb.append(" padding: 25px;\n"); |
||||
|
sb.append(" box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n"); |
||||
|
sb.append(" transition: transform 0.3s ease, box-shadow 0.3s ease;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .chart-card:hover {\n"); |
||||
|
sb.append(" transform: translateY(-10px);\n"); |
||||
|
sb.append(" box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4);\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .chart-card h2 {\n"); |
||||
|
sb.append(" color: #333;\n"); |
||||
|
sb.append(" font-size: 1.4em;\n"); |
||||
|
sb.append(" margin-bottom: 20px;\n"); |
||||
|
sb.append(" padding-bottom: 10px;\n"); |
||||
|
sb.append(" border-bottom: 3px solid;\n"); |
||||
|
sb.append(" border-image: linear-gradient(90deg, #f39c12, #e74c3c) 1;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .chart-card img {\n"); |
||||
|
sb.append(" width: 100%;\n"); |
||||
|
sb.append(" height: auto;\n"); |
||||
|
sb.append(" border-radius: 10px;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .chart-card.full-width {\n"); |
||||
|
sb.append(" grid-column: 1 / -1;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .legend {\n"); |
||||
|
sb.append(" display: flex;\n"); |
||||
|
sb.append(" justify-content: center;\n"); |
||||
|
sb.append(" gap: 30px;\n"); |
||||
|
sb.append(" margin-top: 15px;\n"); |
||||
|
sb.append(" flex-wrap: wrap;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .legend-item {\n"); |
||||
|
sb.append(" display: flex;\n"); |
||||
|
sb.append(" align-items: center;\n"); |
||||
|
sb.append(" gap: 8px;\n"); |
||||
|
sb.append(" font-size: 0.9em;\n"); |
||||
|
sb.append(" color: #555;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .legend-color {\n"); |
||||
|
sb.append(" width: 20px;\n"); |
||||
|
sb.append(" height: 4px;\n"); |
||||
|
sb.append(" border-radius: 2px;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" .gold { background: #ff8c00; }\n"); |
||||
|
sb.append(" .silver { background: #c0c0c0; }\n"); |
||||
|
sb.append(" .oil { background: #006400; }\n"); |
||||
|
sb.append(" .up { background: #006400; }\n"); |
||||
|
sb.append(" .down { background: #ff0000; }\n"); |
||||
|
sb.append(" footer {\n"); |
||||
|
sb.append(" text-align: center;\n"); |
||||
|
sb.append(" margin-top: 50px;\n"); |
||||
|
sb.append(" padding: 20px;\n"); |
||||
|
sb.append(" color: #666;\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" @media (max-width: 768px) {\n"); |
||||
|
sb.append(" .charts-grid { grid-template-columns: 1fr; }\n"); |
||||
|
sb.append(" h1 { font-size: 1.8em; }\n"); |
||||
|
sb.append(" }\n"); |
||||
|
sb.append(" </style>\n"); |
||||
|
sb.append("</head>\n"); |
||||
|
sb.append("<body>\n"); |
||||
|
sb.append(" <div class=\"container\">\n"); |
||||
|
sb.append(" <h1>📊 大宗商品分析报告</h1>\n"); |
||||
|
sb.append(" <p class=\"subtitle\"> Commodity Market Analysis Report</p>\n"); |
||||
|
|
||||
|
sb.append(" <div class=\"charts-grid\">\n"); |
||||
|
|
||||
|
sb.append(" <div class=\"chart-card\">\n"); |
||||
|
sb.append(" <h2>📈 价格趋势对比</h2>\n"); |
||||
|
sb.append(" <img src=\"price_trend.png\" alt=\"价格趋势对比\">\n"); |
||||
|
sb.append(" <div class=\"legend\">\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color gold\"></span>黄金</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color silver\"></span>白银</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color oil\"></span>原油</div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
|
||||
|
sb.append(" <div class=\"chart-card\">\n"); |
||||
|
sb.append(" <h2>📊 波动特征分析</h2>\n"); |
||||
|
sb.append(" <img src=\"volatility.png\" alt=\"波动特征分析\">\n"); |
||||
|
sb.append(" <div class=\"legend\">\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color gold\"></span>黄金</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color silver\"></span>白银</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color oil\"></span>原油</div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
|
||||
|
sb.append(" <div class=\"chart-card\">\n"); |
||||
|
sb.append(" <h2>🔗 相关性分析</h2>\n"); |
||||
|
sb.append(" <img src=\"correlation.png\" alt=\"相关性分析\">\n"); |
||||
|
sb.append(" <div class=\"legend\">\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color gold\"></span>黄金</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color oil\"></span>原油</div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
|
||||
|
sb.append(" <div class=\"chart-card\">\n"); |
||||
|
sb.append(" <h2>🗓️ 季节性周期分析</h2>\n"); |
||||
|
sb.append(" <img src=\"cycle.png\" alt=\"季节性周期分析\">\n"); |
||||
|
sb.append(" <div class=\"legend\">\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color gold\"></span>黄金</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color oil\"></span>原油</div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
|
||||
|
sb.append(" <div class=\"chart-card full-width\">\n"); |
||||
|
sb.append(" <h2>💬 舆情联动分析</h2>\n"); |
||||
|
sb.append(" <img src=\"sentiment.png\" alt=\"舆情联动分析\">\n"); |
||||
|
sb.append(" <div class=\"legend\">\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color oil\"></span>涨跌幅</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color gold\"></span>利好新闻数</div>\n"); |
||||
|
sb.append(" <div class=\"legend-item\"><span class=\"legend-color down\"></span>利空新闻数</div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
|
||||
|
sb.append(" </div>\n"); |
||||
|
|
||||
|
sb.append(" <footer>\n"); |
||||
|
sb.append(" <p>报告生成时间: ").append(java.time.LocalDateTime.now()).append("</p>\n"); |
||||
|
sb.append(" <p>大宗商品爬虫系统 © 2026</p>\n"); |
||||
|
sb.append(" </footer>\n"); |
||||
|
sb.append(" </div>\n"); |
||||
|
sb.append("</body>\n"); |
||||
|
sb.append("</html>\n"); |
||||
|
|
||||
|
return sb.toString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
# 数据库配置 - 使用SQLite持久化存储(数据保存在文件中) |
||||
|
db.driver=org.sqlite.JDBC |
||||
|
db.url=jdbc:sqlite:./data/commodity.db |
||||
|
db.username= |
||||
|
db.password= |
||||
|
|
||||
|
# 爬虫配置 |
||||
|
crawl.page.count=30 |
||||
|
|
||||
|
# 输出配置 |
||||
|
output.log.dir=./logs/ |
||||
|
output.chart.dir=./output/charts/ |
||||
|
output.excel.dir=./output/excel/ |
||||
@ -0,0 +1,2 @@ |
|||||
|
-- H2 数据库初始化脚本 |
||||
|
RUNSCRIPT FROM 'classpath:/schema.sql'; |
||||
@ -0,0 +1,29 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<configuration> |
||||
|
<property name="LOG_PATH" value="./logs"/> |
||||
|
|
||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||
|
<encoder> |
||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> |
||||
|
<charset>GBK</charset> |
||||
|
</encoder> |
||||
|
</appender> |
||||
|
|
||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
|
<file>${LOG_PATH}/crawler.log</file> |
||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> |
||||
|
<fileNamePattern>${LOG_PATH}/crawler.%d{yyyy-MM-dd}.log</fileNamePattern> |
||||
|
<maxHistory>30</maxHistory> |
||||
|
</rollingPolicy> |
||||
|
<encoder> |
||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> |
||||
|
</encoder> |
||||
|
</appender> |
||||
|
|
||||
|
<logger name="com.example.crawler" level="DEBUG"/> |
||||
|
|
||||
|
<root level="INFO"> |
||||
|
<appender-ref ref="STDOUT"/> |
||||
|
<appender-ref ref="FILE"/> |
||||
|
</root> |
||||
|
</configuration> |
||||
@ -0,0 +1,46 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
<mapper namespace="com.example.crawler.mapper.IndexDataMapper"> |
||||
|
<insert id="insert" parameterType="com.example.crawler.model.IndexData"> |
||||
|
INSERT INTO index_data (index_name, date, index_value, change_rate, |
||||
|
stock_name, stock_price, turnover_rate, create_time, source) |
||||
|
VALUES (#{indexName}, #{date}, #{indexValue}, #{changeRate}, |
||||
|
#{stockName}, #{stockPrice}, #{turnoverRate}, CURRENT_TIMESTAMP, #{source}) |
||||
|
</insert> |
||||
|
|
||||
|
<insert id="batchInsert" parameterType="java.util.List"> |
||||
|
INSERT INTO index_data (index_name, date, index_value, change_rate, |
||||
|
stock_name, stock_price, turnover_rate, create_time, source) |
||||
|
VALUES |
||||
|
<foreach collection="list" item="item" separator=","> |
||||
|
(#{item.indexName}, #{item.date}, #{item.indexValue}, #{item.changeRate}, |
||||
|
#{item.stockName}, #{item.stockPrice}, #{item.turnoverRate}, CURRENT_TIMESTAMP, #{item.source}) |
||||
|
</foreach> |
||||
|
</insert> |
||||
|
|
||||
|
<select id="selectAll" resultType="com.example.crawler.model.IndexData"> |
||||
|
SELECT * FROM index_data ORDER BY date DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByIndexName" resultType="com.example.crawler.model.IndexData"> |
||||
|
SELECT * FROM index_data WHERE index_name = #{indexName} ORDER BY date DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByDateRange" resultType="com.example.crawler.model.IndexData"> |
||||
|
SELECT * FROM index_data WHERE date BETWEEN #{startDate} AND #{endDate} ORDER BY date DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByDateAndIndex" resultType="com.example.crawler.model.IndexData"> |
||||
|
SELECT * FROM index_data WHERE date = #{date} AND index_name = #{indexName} |
||||
|
</select> |
||||
|
|
||||
|
<select id="count" resultType="int"> |
||||
|
SELECT COUNT(*) FROM index_data |
||||
|
</select> |
||||
|
|
||||
|
<delete id="deleteAll"> |
||||
|
DELETE FROM index_data |
||||
|
</delete> |
||||
|
</mapper> |
||||
@ -0,0 +1,46 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
<mapper namespace="com.example.crawler.mapper.MarketDataMapper"> |
||||
|
<insert id="insert" parameterType="com.example.crawler.model.MarketData"> |
||||
|
INSERT INTO market_data (variety, trade_date, open_price, close_price, |
||||
|
high_price, low_price, volume, change_rate, create_time, source) |
||||
|
VALUES (#{variety}, #{tradeDate}, #{openPrice}, #{closePrice}, |
||||
|
#{highPrice}, #{lowPrice}, #{volume}, #{changeRate}, CURRENT_TIMESTAMP, #{source}) |
||||
|
</insert> |
||||
|
|
||||
|
<insert id="batchInsert" parameterType="java.util.List"> |
||||
|
INSERT INTO market_data (variety, trade_date, open_price, close_price, |
||||
|
high_price, low_price, volume, change_rate, create_time, source) |
||||
|
VALUES |
||||
|
<foreach collection="list" item="item" separator=","> |
||||
|
(#{item.variety}, #{item.tradeDate}, #{item.openPrice}, #{item.closePrice}, |
||||
|
#{item.highPrice}, #{item.lowPrice}, #{item.volume}, #{item.changeRate}, CURRENT_TIMESTAMP, #{item.source}) |
||||
|
</foreach> |
||||
|
</insert> |
||||
|
|
||||
|
<select id="selectAll" resultType="com.example.crawler.model.MarketData"> |
||||
|
SELECT * FROM market_data ORDER BY trade_date DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByVariety" resultType="com.example.crawler.model.MarketData"> |
||||
|
SELECT * FROM market_data WHERE variety = #{variety} ORDER BY trade_date DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByDateRange" resultType="com.example.crawler.model.MarketData"> |
||||
|
SELECT * FROM market_data WHERE trade_date BETWEEN #{startDate} AND #{endDate} ORDER BY trade_date DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByDateAndVariety" resultType="com.example.crawler.model.MarketData"> |
||||
|
SELECT * FROM market_data WHERE trade_date = #{tradeDate} AND variety = #{variety} |
||||
|
</select> |
||||
|
|
||||
|
<select id="countByVariety" resultType="int"> |
||||
|
SELECT COUNT(*) FROM market_data WHERE variety = #{variety} |
||||
|
</select> |
||||
|
|
||||
|
<delete id="deleteAll"> |
||||
|
DELETE FROM market_data |
||||
|
</delete> |
||||
|
</mapper> |
||||
@ -0,0 +1,46 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
<mapper namespace="com.example.crawler.mapper.NewsDataMapper"> |
||||
|
<insert id="insert" parameterType="com.example.crawler.model.NewsData"> |
||||
|
INSERT INTO news_data (title, content, publish_time, related_commodity, |
||||
|
sentiment, create_time, source) |
||||
|
VALUES (#{title}, #{content}, #{publishTime}, #{relatedCommodity}, |
||||
|
#{sentiment}, CURRENT_TIMESTAMP, #{source}) |
||||
|
</insert> |
||||
|
|
||||
|
<insert id="batchInsert" parameterType="java.util.List"> |
||||
|
INSERT INTO news_data (title, content, publish_time, related_commodity, |
||||
|
sentiment, create_time, source) |
||||
|
VALUES |
||||
|
<foreach collection="list" item="item" separator=","> |
||||
|
(#{item.title}, #{item.content}, #{item.publishTime}, #{item.relatedCommodity}, |
||||
|
#{item.sentiment}, CURRENT_TIMESTAMP, #{item.source}) |
||||
|
</foreach> |
||||
|
</insert> |
||||
|
|
||||
|
<select id="selectAll" resultType="com.example.crawler.model.NewsData"> |
||||
|
SELECT * FROM news_data ORDER BY publish_time DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByCommodity" resultType="com.example.crawler.model.NewsData"> |
||||
|
SELECT * FROM news_data WHERE related_commodity = #{commodity} ORDER BY publish_time DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByDateRange" resultType="com.example.crawler.model.NewsData"> |
||||
|
SELECT * FROM news_data WHERE publish_time BETWEEN #{startDate} AND #{endDate} ORDER BY publish_time DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectByTitleAndTime" resultType="com.example.crawler.model.NewsData"> |
||||
|
SELECT * FROM news_data WHERE title = #{title} AND publish_time = #{publishTime} |
||||
|
</select> |
||||
|
|
||||
|
<select id="countBySentiment" resultType="int"> |
||||
|
SELECT COUNT(*) FROM news_data WHERE sentiment = #{sentiment} |
||||
|
</select> |
||||
|
|
||||
|
<delete id="deleteAll"> |
||||
|
DELETE FROM news_data |
||||
|
</delete> |
||||
|
</mapper> |
||||
@ -0,0 +1,28 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE configuration |
||||
|
PUBLIC "-//mybatis.org//DTD Config 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-config.dtd"> |
||||
|
<configuration> |
||||
|
<settings> |
||||
|
<setting name="mapUnderscoreToCamelCase" value="true"/> |
||||
|
</settings> |
||||
|
<typeHandlers> |
||||
|
<typeHandler handler="com.example.crawler.util.DateTypeHandler" javaType="java.util.Date"/> |
||||
|
</typeHandlers> |
||||
|
<environments default="development"> |
||||
|
<environment id="development"> |
||||
|
<transactionManager type="JDBC"/> |
||||
|
<dataSource type="POOLED"> |
||||
|
<property name="driver" value="${db.driver}"/> |
||||
|
<property name="url" value="${db.url}"/> |
||||
|
<property name="username" value="${db.username}"/> |
||||
|
<property name="password" value="${db.password}"/> |
||||
|
</dataSource> |
||||
|
</environment> |
||||
|
</environments> |
||||
|
<mappers> |
||||
|
<mapper resource="mapper/MarketDataMapper.xml"/> |
||||
|
<mapper resource="mapper/IndexDataMapper.xml"/> |
||||
|
<mapper resource="mapper/NewsDataMapper.xml"/> |
||||
|
</mappers> |
||||
|
</configuration> |
||||
@ -0,0 +1,44 @@ |
|||||
|
CREATE DATABASE IF NOT EXISTS example_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
||||
|
|
||||
|
USE example_db; |
||||
|
|
||||
|
CREATE TABLE IF NOT EXISTS market_data ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
variety VARCHAR(50) NOT NULL COMMENT '商品品种', |
||||
|
trade_date DATE NOT NULL COMMENT '交易日期', |
||||
|
open_price DECIMAL(18,4) COMMENT '开盘价', |
||||
|
close_price DECIMAL(18,4) NOT NULL COMMENT '收盘价', |
||||
|
high_price DECIMAL(18,4) COMMENT '最高价', |
||||
|
low_price DECIMAL(18,4) COMMENT '最低价', |
||||
|
volume DECIMAL(20,4) COMMENT '成交量', |
||||
|
change_rate DECIMAL(10,4) COMMENT '涨跌幅(%)', |
||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
||||
|
source VARCHAR(50) COMMENT '数据来源', |
||||
|
UNIQUE KEY uk_date_variety (trade_date, variety) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行情数据表'; |
||||
|
|
||||
|
CREATE TABLE IF NOT EXISTS index_data ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
index_name VARCHAR(100) NOT NULL COMMENT '指数名称', |
||||
|
date DATE NOT NULL COMMENT '日期', |
||||
|
index_value DECIMAL(18,4) NOT NULL COMMENT '指数值', |
||||
|
change_rate DECIMAL(10,4) COMMENT '涨跌幅(%)', |
||||
|
stock_name VARCHAR(100) COMMENT '概念股名称', |
||||
|
stock_price DECIMAL(18,4) COMMENT '股价', |
||||
|
turnover_rate DECIMAL(10,4) COMMENT '换手率(%)', |
||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
||||
|
source VARCHAR(50) COMMENT '数据来源', |
||||
|
UNIQUE KEY uk_date_index (date, index_name) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='指数数据表'; |
||||
|
|
||||
|
CREATE TABLE IF NOT EXISTS news_data ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
title VARCHAR(500) NOT NULL COMMENT '新闻标题', |
||||
|
content TEXT COMMENT '新闻内容', |
||||
|
publish_time DATETIME NOT NULL COMMENT '发布时间', |
||||
|
related_commodity VARCHAR(50) COMMENT '关联商品', |
||||
|
sentiment VARCHAR(10) NOT NULL COMMENT '舆情倾向(利好/利空/中性)', |
||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
||||
|
source VARCHAR(50) COMMENT '数据来源', |
||||
|
UNIQUE KEY uk_title_time (title, publish_time) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='舆情数据表'; |
||||