1. 项目背景与核心目标
在当今内容消费爆炸式增长的时代,网络文学平台积累了海量用户行为数据。这些数据背后隐藏着读者偏好、内容趋势和商业价值。我最近完成了一个结合SpringBoot和Vue技术栈的小说数据采集与分析系统,能够自动抓取番茄小说平台的关键数据并进行可视化展示。
这个项目的核心价值在于:
- 实现了从数据采集到分析展示的完整链路
- 采用前后端分离架构保证系统扩展性
- 通过可视化手段直观呈现小说市场趋势
- 为内容运营决策提供数据支持
提示:在实际开发中需要注意网络爬虫的合规性问题,建议控制请求频率,仅用于学习研究目的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体架构方案
系统采用经典的三层架构设计:
code复制前端展示层(Vue) ←HTTP→ 业务逻辑层(SpringBoot) ←HTTP→ 数据存储层(MySQL+Redis)
前端使用Vue 3组合式API开发,主要依赖:
- ECharts 5.0 用于数据可视化
- Element Plus 提供UI组件
- Axios 处理HTTP请求
- Vue Router 实现前端路由
后端技术栈配置:
- SpringBoot 2.7.5
- MyBatis-Plus 3.5.1
- Jsoup 1.15.3 (HTML解析)
- HttpClient 4.5.13
- Redis 6.2 (缓存热点数据)
2.2 爬虫模块设计要点
爬虫模块需要解决几个关键问题:
- 反爬机制绕过:通过随机User-Agent、代理IP池和请求间隔控制
- 数据解析准确性:使用XPath和CSS选择器双重定位
- 增量爬取策略:基于最后更新时间过滤已采集数据
核心爬取流程:
java复制public void crawlNovelData() {
// 1. 获取小说列表页
String listHtml = httpClient.get(listUrl);
// 2. 解析列表获取详情页链接
List<String> detailUrls = parseListPage(listHtml);
// 3. 并发处理详情页
detailUrls.parallelStream().forEach(url -> {
String detailHtml = httpClient.get(url);
Novel novel = parseDetailPage(detailHtml);
novelService.saveOrUpdate(novel);
});
}
3. 核心功能实现细节
3.1 数据采集模块
3.1.1 网页解析策略
针对番茄小说的页面结构,我们设计了多级解析方案:
- 列表页解析:
xpath复制//div[@class="book-list"]/div[@class="book-item"]/a/@href
- 详情页关键字段提取:
java复制public Novel parseDetailPage(String html) {
Document doc = Jsoup.parse(html);
Novel novel = new Novel();
novel.setTitle(doc.selectFirst("h1.book-title").text());
novel.setAuthor(doc.select("div.author > span.name").text());
novel.setWordCount(parseWordCount(doc.select("div.book-info > span.count").text()));
novel.setUpdateTime(parseDate(doc.select("div.update-time").text()));
// 评分数据需要特殊处理
String ratingText = doc.select("div.rating > span.score").text();
novel.setRating(parseRating(ratingText));
return novel;
}
3.1.2 反反爬策略实践
在实际测试中,我们发现番茄小说有以下防护措施:
- IP频率限制:单个IP每分钟超过30次请求会被临时封禁
- 行为验证:异常访问模式会触发验证码
- 请求头校验:缺少Referer等字段会被拦截
我们的解决方案:
java复制// 在HttpClient配置中
RequestConfig config = RequestConfig.custom()
.setProxy(proxy) // 使用代理IP池
.setConnectTimeout(5000)
.setSocketTimeout(5000)
.build();
HttpClientBuilder.create()
.setDefaultRequestConfig(config)
.setUserAgent(getRandomUserAgent()) // 轮换UserAgent
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
.build();
3.2 数据存储设计
3.2.1 数据库表结构
主要实体关系设计:
sql复制CREATE TABLE `t_novel` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`author` varchar(50) NOT NULL,
`category` varchar(20) DEFAULT NULL,
`word_count` int DEFAULT '0',
`rating` decimal(3,1) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`status` tinyint DEFAULT '0' COMMENT '0连载 1完结',
PRIMARY KEY (`id`),
KEY `idx_category` (`category`),
KEY `idx_author` (`author`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2.2 缓存策略实现
使用Redis实现两级缓存:
- 热点数据缓存:小说基本信息TTL 1小时
- 排行榜缓存:每日榜单数据TTL 24小时
Spring Cache配置示例:
java复制@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(singletonMap(
"rankings",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
)).build();
}
}
4. 可视化系统实现
4.1 前端架构设计
采用Vue 3 + TypeScript技术栈,模块划分:
code复制src/
├── api/ # 接口定义
├── components/ # 通用组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── utils/ # 工具函数
└── views/ # 页面组件
4.2 核心可视化图表
4.2.1 小说字数分布热力图
使用ECharts的热力图展示不同字数区间的小说数量分布:
vue复制<script setup>
import { use } from 'echarts/core';
import { HeatmapChart } from 'echarts/charts';
use([HeatmapChart]);
const option = {
tooltip: {},
grid: { height: '80%' },
xAxis: { type: 'category', data: ['0-10万','10-30万','30-50万','50-100万','100万+'] },
yAxis: { type: 'category', data: ['玄幻','都市','科幻','历史','言情'] },
visualMap: { min: 0, max: 100, calculable: true },
series: [{
type: 'heatmap',
data: heatmapData,
label: { show: true },
emphasis: { itemStyle: { shadowBlur: 10 } }
}]
};
</script>
4.2.2 作者作品数量关系图
使用桑基图展示作者与作品类型的关系:
javascript复制function renderSankey(data) {
return {
series: [{
type: 'sankey',
data: data.nodes,
links: data.links,
emphasis: { focus: 'adjacency' },
levels: [{
depth: 0,
itemStyle: { color: '#fbb4ae' }
}, {
depth: 1,
itemStyle: { color: '#b3cde3' }
}],
lineStyle: { color: 'source', curveness: 0.5 }
}]
};
}
5. 部署与性能优化
5.1 系统部署方案
采用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
5.2 性能优化实践
- 爬虫性能优化:
- 使用连接池管理HTTP连接
- 实现异步非阻塞IO模型
- 采用分段批量提交策略
- 前端性能优化:
- 图表数据懒加载
- 虚拟滚动长列表
- 按需引入ECharts组件
- 缓存策略优化:
java复制@Cacheable(value = "novels", key = "#id", unless = "#result == null")
public Novel getById(Long id) {
return novelMapper.selectById(id);
}
@Caching(evict = {
@CacheEvict(value = "novels", key = "#novel.id"),
@CacheEvict(value = "rankings", allEntries = true)
})
public void updateNovel(Novel novel) {
novelMapper.updateById(novel);
}
6. 项目经验总结
在实际开发过程中,有几个关键点值得特别注意:
-
动态页面处理:番茄小说部分内容通过Ajax加载,需要使用Selenium等工具模拟浏览器行为。我们最终采用了分析接口的方式直接请求JSON数据,效率提升5倍以上。
-
数据一致性保障:建立唯一索引防止重复数据,同时实现断点续爬功能:
java复制@Transactional
public void saveCrawlProgress(String taskId, String lastUrl) {
// 使用乐观锁控制并发更新
CrawlProgress progress = progressMapper.selectForUpdate(taskId);
if (progress == null) {
progress = new CrawlProgress(taskId, lastUrl);
progressMapper.insert(progress);
} else {
progress.setLastUrl(lastUrl);
progressMapper.updateById(progress);
}
}
- 可视化交互优化:针对大数据量场景,我们实现了以下优化策略:
- 数据采样:超过1万条数据时自动降采样
- 渐进式渲染:分帧加载复杂图表
- Web Worker:将数据处理移出主线程
这个项目让我深刻体会到,一个完整的数据分析系统需要前后端技术的深度融合。特别是在处理动态网页爬取和大数据可视化时,需要不断调整技术方案来平衡性能、准确性和用户体验。
