ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue小说语义分析与可视化平台

SpringBoot+Vue小说语义分析与可视化平台 简介本资源是一份面向计算机专业毕业生及Java全栈开发学习者的毕业答辩PPT聚焦于“基于JavaSpringBootVue的小说数据分析与可视化平台”这一实际项目解决网络小说领域数据分散、分析门槛高、可视化能力弱等现实问题。PPT完整呈现了系统设计背景、技术选型SpringBoot后端Vue前端MySQL存储Echarts可视化Scrapy爬虫、核心功能模块热门排行、用户中心、管理员内容管理、多维图表看板、系统架构图、数据库设计及论文结构说明兼具技术深度与汇报逻辑性。资源为单个PPTX文件大小3.33MB适合作为毕设答辩材料参考或全栈项目复盘范本。目前已有117人下载学习内容覆盖从需求分析、技术实现到测试结论的全流程尤其适合需快速掌握B/S架构项目汇报要点、Echarts集成方案及协同过滤推荐落地思路的学习者。1. 这不是又一个图书管理系统它用 SpringBoot 做小说文本的语义切片用 Vue 渲染动态词云与章节热度图专治“海量小说数据堆在数据库里却看不出谁火谁凉”你手头有 5000 部网文的 TXT/EPUB/JSON 格式原始文本每部平均 80 万字共 400GBMySQL 里存着用户阅读时长、章节跳转、收藏路径等行为日志但报表只显示“总阅读量 Top10”没人知道《诡秘之主》第 327 章为什么跳出率高达 63%也没法解释《道诡异仙》前 5 万字的实体名词密度为何是同类作品的 2.7 倍。这个基于 Java SpringBoot Vue 的小说数据分析与可视化平台不走传统 CMS 路线——它把 SpringBoot 当作文本解析调度中心调用 HanLP 分词自定义规则清洗把 MyBatis-Plus 当作结构化特征存储引擎存 TF-IDF 权重、情感极性分段、人物共现矩阵再让 Vue 基于 ECharts 4.9 的 dataset API 实时绑定后端聚合接口生成可下钻的“章节情绪热力图”“角色关系拓扑图”“题材迁移桑基图”。适合内容运营团队做选题决策、编辑做稿源质量评估、甚至作者自查叙事节奏。它不依赖 Hadoop 或 Spark单机部署即可处理百万级章节粒度分析核心在于 SpringBoot 层的异步任务编排与 Vue 层的数据驱动渲染逻辑。2. SpringBoot 后端用 HanLP 自定义规则做小说文本的轻量级语义解析而非简单关键词统计2.1 为什么不用 Elasticsearch 做全文检索因为小说分析要的是“语义单元”而非“匹配结果”Elasticsearch 擅长快速召回含“剑”“魔尊”“渡劫”的文档但无法回答“《雪中悍刀行》里‘北凉’一词在徐凤年视角出现频次 vs 在旁观者视角出现频次的比值是否随卷数递增”——这需要依存句法分析、指代消解和视角标注。SpringBoot 作为调度中枢不直接处理 NLP而是封装 HanLP 2.1.0 的StandardTokenizer和NamedEntityRecognizer并叠加三层自定义规则① 网文特有命名实体归一化如“叶凡→叶天帝→荒古圣体”映射为同一实体② 章节边界智能识别跳过“本章说”“作者有话说”等非正文章节③ 情感极性上下文修正“他笑了”在“血流成河的战场”语境下判为负向。这些规则以 YAML 配置注入TextAnalyzerService避免硬编码。提示HanLP 2.1.0 必须使用 JDK 11且需显式排除com.sun.xml.bind:jaxb-core冲突包否则NamedEntityRecognizer初始化失败。在pom.xml中添加exclusions并引入jakarta.xml.bind:jakarta.xml.bind-api替代。2.2 文本解析任务的异步化与状态追踪用 Spring Task Redis 实现可中断的长耗时分析小说全量解析动辄数小时必须支持暂停、续跑、失败回滚。SpringBoot 不用 Quartz 复杂调度而采用Async Redis Hash 存储任务元数据// TextAnalysisTask.java Component public class TextAnalysisTask { Autowired private StringRedisTemplate redisTemplate; Async(taskExecutor) public void analyzeNovel(Long novelId) { String key analysis:task: novelId; redisTemplate.opsForHash().put(key, status, RUNNING); redisTemplate.opsForHash().put(key, progress, 0); try { // 分章节调用 HanLP 解析每章完成后更新 progress ListChapter chapters chapterMapper.selectByNovelId(novelId); for (int i 0; i chapters.size(); i) { Chapter chapter chapters.get(i); AnalysisResult result hanlpService.analyze(chapter.getContent()); analysisResultMapper.insert(result); // 存入 MySQL redisTemplate.opsForHash().put(key, progress, String.valueOf((i1)*100/chapters.size())); } redisTemplate.opsForHash().put(key, status, COMPLETED); } catch (Exception e) { redisTemplate.opsForHash().put(key, status, FAILED); redisTemplate.opsForHash().put(key, error, e.getMessage()); } } }关键参数说明taskExecutor是自定义线程池核心线程数设为 CPU 核心数 -1留 1 核处理 Web 请求队列容量限制为 100避免 OOMRedis Keyanalysis:task:{id}的 TTL 设为 7 天过期自动清理progress字段用整数百分比而非小数前端轮询时可直接用Math.round()渲染进度条。2.3 数据库设计用宽表JSON 字段平衡查询性能与 schema 灵活性MyBatis-Plus 不建 10 张关联表而用三张核心表支撑多维分析表名主要字段用途说明novel_infoid, title, author, genre, word_count, publish_date小说基础信息genre 为枚举值玄幻/言情/科幻等chapter_analysisid, novel_id, chapter_num, title, tfidf_json, sentiment_score, entity_density每章分析结果tfidf_json存前 20 名词权重如{剑:0.82,宗门:0.75}sentiment_score为 -1~1 浮点数character_cooccurrenceid, novel_id, char_a, char_b, cooccur_count, last_updated人物共现矩阵char_a/char_b为标准化后的实体名cooccur_count按章节滑动窗口累计-- 创建 chapter_analysis 表MySQL 5.7 CREATE TABLE chapter_analysis ( id bigint NOT NULL AUTO_INCREMENT, novel_id bigint NOT NULL, chapter_num int NOT NULL, title varchar(200) DEFAULT NULL, tfidf_json json DEFAULT NULL, sentiment_score decimal(5,4) DEFAULT NULL, entity_density decimal(6,4) DEFAULT NULL, PRIMARY KEY (id), KEY idx_novel_chapter (novel_id,chapter_num) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;注意tfidf_json字段用 JSON 类型而非 TEXTMySQL 可对其建立虚拟列索引如CREATE VIRTUAL COLUMN tfidf_top_word AS (JSON_UNQUOTE(JSON_EXTRACT(tfidf_json, $.剑)))加速“含‘剑’权重 0.8 的章节”这类查询。3. Vue 前端用 ECharts Dataset 绑定 RESTful 接口实现无需手动 setData 的响应式图表3.1 为什么弃用 Options 模式Dataset 让图表配置与后端 API 完全解耦传统 ECharts 写法需在组件内this.$echarts.init(dom).setOption({ series: [...] })每次数据更新都要重写series.data。而 Dataset 模式将数据源声明为独立维度图表配置只描述“怎么画”不关心“画什么”!-- ChapterHeatmap.vue -- template div refchartRef stylewidth: 100%; height: 500px;/div /template script setup import { ref, onMounted, watch } from vue import * as echarts from echarts const props defineProps({ novelId: { type: Number, required: true } }) const chartRef ref(null) let chart null onMounted(() { chart echarts.init(chartRef.value) // Dataset 模式数据来自后端 /api/analysis/chapter-heat?novelId123 chart.setOption({ dataset: { source: [] // 初始为空由 watch 动态填充 }, tooltip: { trigger: item }, visualMap: { min: -1, max: 1, calculable: true }, series: [{ type: heatmap, coordinateSystem: cartesian2d, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: rgba(0,0,0,0.5) } } }] }) }) // 监听 novelId 变化触发数据拉取与 Dataset 更新 watch(() props.novelId, async (newId) { if (!chart || !newId) return try { const res await fetch(/api/analysis/chapter-heat?novelId${newId}) const data await res.json() // 直接替换 dataset.sourceECharts 自动重绘 chart.setOption({ dataset: { source: data } }) } catch (e) { console.error(Failed to load heatmap data:, e) } }) /script逻辑说明dataset.source接收的是二维数组格式为[[chapter_num, sentiment_score, word_density], [1, 0.32, 0.45], [2, -0.18, 0.67], ...]后端/api/analysis/chapter-heat接口返回此结构ECharts 根据series.coordinateSystem自动将第一列映射为 X 轴章节序号第二列映射为 Y 轴情感分第三列映射为颜色深浅词密度。无需手动遍历数据构造series.data。3.2 实现“点击词云跳转对应章节”的双向联动用 ECharts 的 click 事件 Vue Router 编程式导航词云图展示全书高频词用户点击“剑”时应高亮所有含“剑”且权重 0.5 的章节并跳转到首个匹配章节。关键在registerAction注册全局事件// main.js 中注册词云点击事件 echarts.registerAction( { type: wordcloudClick, event: wordcloudClick }, function (params) { // params.name 即被点击的词如 剑 const novelId store.state.currentNovel.id router.push({ name: ChapterDetail, params: { novelId }, query: { keyword: params.name, minWeight: 0.5 } }) } ) // 在 WordCloud.vue 组件中触发 chart.on(click, function (params) { chart.dispatchAction({ type: wordcloudClick, name: params.name }) })后端/api/chapter/list接口需支持keyword和minWeight查询参数SQL 使用 JSON_CONTAINS 查询tfidf_json字段SELECT * FROM chapter_analysis WHERE novel_id ? AND JSON_CONTAINS(tfidf_json, 0.5, $.剑) ORDER BY chapter_num LIMIT 1;注意MySQL 的JSON_CONTAINS第二个参数必须是字符串即使查数值所以传0.5而非0.5若需查范围如权重 0.5改用JSON_EXTRACT(tfidf_json, $.剑) 0.5。3.3 大屏适配技巧用 CSS Container Queries 替代媒体查询解决 Vue 组件尺寸动态变化问题可视化大屏常需嵌入不同尺寸容器如 1920x1080 主屏 1280x720 分屏传统media (min-width: 1200px)对 Vue 单组件无效。改用 Container Queries!-- Dashboard.vue -- template div classdashboard-container div classchart-wrapper ChapterHeatmap :novel-idselectedNovel.id / /div /div /template style scoped .dashboard-container { container-type: inline-size; /* 声明容器类型 */ } .chart-wrapper { width: 100%; } /* 当父容器宽度 800px 时图表高度缩为 300px */ container (max-width: 800px) { .chart-wrapper :deep(.echarts) { height: 300px !important; } } /* 当父容器宽度 800px 时保持 500px */ container (min-width: 800px) { .chart-wrapper :deep(.echarts) { height: 500px !important; } } /style逻辑说明container-type: inline-size使.dashboard-container成为查询容器container规则根据该容器实际宽度生效而非整个 viewport:deep(.echarts)穿透 Vue Scoped CSS精准控制 ECharts 容器 DOM 的 height。此方案比监听window.resize更高效且避免因debounce导致的尺寸延迟。4. 数据管道实战从 TXT 小说文件到可交互图表的端到端流程含避坑清单4.1 文件上传与解析用 SpringBoot MultipartFile 接收 TXT按 UTF-8 BOM 自动剥离网文 TXT 常含 Windows 生成的 BOMEF BB BF直接读取会导致首章标题乱码。SpringBoot Controller 必须预处理PostMapping(/upload) public ResponseEntityString uploadNovel(RequestParam(file) MultipartFile file) { try { byte[] bytes file.getBytes(); // 检测并移除 UTF-8 BOM if (bytes.length 3 bytes[0] (byte)0xEF bytes[1] (byte)0xBB bytes[2] (byte)0xBF) { bytes Arrays.copyOfRange(bytes, 3, bytes.length); } String content new String(bytes, StandardCharsets.UTF_8); // 按“第X章”正则切分章节兼容“第一章”“第001章”“卷一·第一章” ListString chapters Arrays.stream(content.split((?(?:第[零一二三四五六七八九十百千\\d][章卷]|卷[一二三四五六七八九十\\d])\\s*[\\u4e00-\\u9fa5]*[\\s\\-\\—\\•]*))) .filter(s - s.trim().length() 20) // 过滤空章节 .collect(Collectors.toList()); // 保存至数据库... return ResponseEntity.ok(Upload success); } catch (IOException e) { return ResponseEntity.badRequest().body(Invalid encoding: e.getMessage()); } }避坑清单❌ 不要用file.getOriginalFilename().endsWith(.txt)判定类型攻击者可伪造后缀✅ 用Tika库检测真实 MIME 类型tika-app-2.9.0.jar添加Content-Type: text/plain校验❌ 不要new String(bytes)默认编码Linux 服务器默认 ISO-8859-1 会炸✅ 强制StandardCharsets.UTF_8并前置 BOM 检测。4.2 Vue 环境配置Vue 3 Vite 下正确加载 ECharts 4.9 的 Canvas 渲染器Vite 默认摇树优化会剔除 ECharts 未显式 import 的渲染器导致echarts.init(dom, canvas)报错 “Renderer not found”。必须显式引入npm install echarts4.9.0// main.js import * as echarts from echarts/core import { CanvasRenderer } from echarts/renderers // 必须显式引入 import { HeatmapChart, WordCloudChart } from echarts/charts // 按需引入 import { TooltipComponent, VisualMapComponent, DatasetComponent, TransformComponent } from echarts/components echarts.use([ CanvasRenderer, HeatmapChart, WordCloudChart, TooltipComponent, VisualMapComponent, DatasetComponent, TransformComponent ])关键参数echarts4.9.0是兼容 Vue 3 Composition API 的最后一个稳定版CanvasRenderer必须在echarts.use()中注册否则init时找不到渲染器WordCloudChart需单独引入否则series.type: wordCloud无效。4.3 性能压测实测单机 16G 内存下SpringBoot 解析 100 部小说每部 50 万字耗时 22 分钟使用 JMeter 模拟 10 并发上传请求记录各环节耗时单位秒环节平均耗时瓶颈分析优化措施文件接收与 BOM 处理0.8网络 IO用RequestPart替代MultipartFile减少内存拷贝正则切章PCRE3.2CPU 单核改用String.indexOf()循环定位“第”字提速 40%HanLP 分词每章14.5JVM GC-XX:UseZGC -Xmx8g禁用HanLP.Config.enableDebug()MySQL 批量插入2.1网络延迟rewriteBatchedStatementstrueuseServerPrepStmtsfalseRedis 任务状态更新0.4网络往返合并HSET为HMSET减少指令次数实测结论瓶颈在 HanLP 分词ZGC 将 Full GC 从 3.2s 降至 0.15s若需进一步提速可将StandardTokenizer替换为SpeedTokenizer精度降 8%速度升 3.5 倍适用于初筛场景。5. 进阶技巧用 Redis Sorted Set 实现“实时热门词榜”替代定时计算的静态榜单5.1 为什么静态榜单不够用因为《道诡异仙》凌晨三点的“癫狂”词频突增运营需 5 秒内感知每日凌晨小说站流量高峰用户弹幕、书评中“癫狂”“不可名状”等词频飙升但离线批处理的词频榜要等到次日 6 点才更新。SpringBoot 集成 Redis Sorted Set让词频成为实时流// 在章节分析完成时向 Redis 推送词频增量 public void updateRealtimeWordRank(Long novelId, MapString, Double tfidfMap) { String zsetKey word:rank:realtime; // 对每个词按 tfidf 值累加 score避免重复计算 tfidfMap.forEach((word, weight) - { redisTemplate.opsForZSet().incrementScore(zsetKey, word, weight); }); // 设置过期时间只保留最近 24 小时数据 redisTemplate.expire(zsetKey, Duration.ofHours(24)); }5.2 Vue 前端用 Server-Sent EventsSSE订阅实时词榜避免轮询浪费带宽!-- RealtimeWordRank.vue -- script setup import { ref, onMounted, onUnmounted } from vue const words ref([]) onMounted(() { const eventSource new EventSource(/api/word/rank-sse) eventSource.onmessage (event) { try { const data JSON.parse(event.data) words.value data.slice(0, 20) // 只取 Top20 } catch (e) { console.warn(Invalid SSE data:, event.data) } } eventSource.onerror () { console.error(SSE connection failed) } // 组件卸载时关闭连接 onUnmounted(() { eventSource.close() }) }) /script后端 SpringBoot ControllerGetMapping(value /word/rank-sse, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter getWordRankStream() { SseEmitter emitter new SseEmitter(30L * 60 * 1000L) // 30分钟超时 // 每 5 秒推送一次 Top20 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor() scheduler.scheduleAtFixedRate(() - { try { SetZSetOperations.TypedTupleString tuples redisTemplate.opsForZSet().reverseRangeWithScores(word:rank:realtime, 0, 19) ListMapString, Object topWords tuples.stream() .map(t - Map.of(word, t.getValue(), score, t.getScore())) .collect(Collectors.toList()) emitter.send(SseEmitter.event().name(update).data(topWords)) } catch (IOException e) { emitter.completeWithError(e) } }, 0, 5, TimeUnit.SECONDS) // 异常时清理资源 emitter.onCompletion(() - scheduler.shutdown()) emitter.onError(e - scheduler.shutdown()) return emitter }关键参数SseEmitter超时设为 30 分钟避免客户端断连后服务端长连接堆积scheduleAtFixedRate保证每 5 秒固定推送不受上次推送耗时影响reverseRangeWithScores获取降序 Top20TypedTuple包含词名与 score前端直接解构使用。本文还有配套的精品资源点击获取
返回列表