我们组选题的时候导师给了三个方向一个前后端分离的商城系统、一个后台权限管理框架还有一个就是这次的题目——基于Spring Boot爬虫和网页数据抓取技术的在线新闻聚合平台。我选了最后这个。说实话当时吸引我的不是“大数据”这三个字而是这个题目里有一条完整的工程链路数据从哪来、怎么抓、怎么洗、怎么存、怎么展示、怎么定时更新每一步都是实打实的代码不是那种“你懂的”空架子。这个题目适合两类人参考一是正在做毕设、需要一套能讲清楚原理又能跑通的项目的学生二是想快速搭建一个“内容自动聚合展示”原型的开发者。它的核心价值在于用一套技术栈同时覆盖了爬虫采集、数据清洗、任务调度、Web展示、检索推荐五个模块既能体现工程能力又能体现算法思维。接下来我把整个项目从选题到答辩的关键决策、核心代码思路、以及那些文档里不会写的坑一次性说清楚。1. 这个毕设题目到底在做什么先看清系统的完整面貌1.1 新闻聚合平台的常规需求边界所谓新闻聚合平台说白了就是“不自己生产新闻只做新闻的搬运工和整理员”。系统定时从多个公开网站抓取新闻内容经过清洗、去重、分类之后统一展示在自己的页面上并提供搜索、浏览和简单推荐功能。但你千万别把“抓取展示”想得过于简单。一个能过答辩、能写进简历的新闻聚合平台至少需要拆成以下几个子模块数据采集模块负责抓取新闻列表页和详情页提取标题、正文、发布时间、来源、封面图等信息。数据清洗模块去除HTML标签、脚本片段、广告噪声统一时间格式剔除无效字符。去重模块在同一新闻被多个站点转载的情况下只保留一条记录。分类与标签模块对新闻进行自动分类或者打上关键词标签。存储模块设计合理的数据库表结构满足按时间、分类、关键词的查询需求。聚合展示模块面向用户的首页、分类页、搜索页、详情页。定时调度模块按固定频率增量抓取新闻不能重复抓、不能漏抓。我在实际开发时把需求优先级排成了两档第一档是“能跑通闭环”即采集→清洗→入库→展示→搜索第二档是“加分项”即定时更新、相似度推荐、数据可视化看板。毕业设计阶段先保证第一档100%稳定再去加第二档的亮点。1.2 为什么这个题目能在“工程性”和“算法性”之间取得平衡很多毕设题目要么偏纯业务开发增删改查要么偏纯算法模型调参而这个题目恰好两头都占。用户能看到网页效果、能操作搜索筛选属于直观的成果展示而爬虫调度策略、正文提取规则、关键词权重计算又属于可以深挖的技术点。尤其值得说的是“去重”这个环节。纯靠标题字符串相等去重实战中会发现大量问题有加标题前缀的《重磅xxx》、有反序排列的、有 URL 相同但标题不一致的。我最后采用的是“MD5签名”方案对标题清洗后做归一化再拼接来源域名进行MD5哈希作为唯一键。具体思路在第三章会展开。这一块在答辩时很容易成为老师追问的焦点因为人人都知道新闻会被转载但很少有人真正处理过跨站转载的重复数据。2. 技术选型为什么核心框架用Spring Boot爬虫部分又能怎么搭2.1 主框架的取舍逻辑Spring Boot是国内中小型Web项目的绝对主流既然题目直接点名了Spring Boot主框架就没有悬念了。Spring Boot的价值不只是“约定优于配置”和“内置Tomcat”更重要的是它把所有中间件整合的门槛降到最低引入一个Starter依赖、配置一行连接串Redis、Elasticsearch、消息队列都能快速接入并且社区资料极多遇到问题随便一搜就有解决方案。对毕设来说选Spring Boot还有一个隐性好处便于后续找工作面试时迁移到真实项目场景。很多公司内部的后台服务就是Spring Boot架构你在毕设里积累的经验——比如用拦截器处理请求、用Scheduled写定时任务、用MyBatis-Plus做分页查询——这些都能直接复用。有一点我要提醒不要为了“显得高级”盲目升级Spring Boot版本到最新建议用2.7.x或3.0.x稳定版。太高版本有时会带来Java版本兼容问题我遇到过Spring Boot 3.x要求JDK 17但实验室机器还是JDK 8最后被迫降级重写部分依赖的尴尬情况。2.2 爬虫工具链Java原生抓取还是Python协作这是个关键决策很多人看到“爬虫”两个字第一反应是Python。但在这个项目里爬虫模块属于整个Spring Boot系统的一部分我更推荐用Java原生体系实现。理由有三点技术栈统一不用维护两套代码。纯Java方案可以用HttpClient发送请求、Jsoup解析HTML、Jackson处理JSON整套流程都在Spring Boot内不需要额外拉起Python服务。部署简单。如果引入Python爬虫意味着服务器上要装Python解释器还要用subprocess或HTTP接口通信与Java交互答辩演示时多了一个故障点。对“并发抓取”的支持更好。Java的线程池和CompletableFuture在控制抓取速率、并发数量上非常顺手。唯一的例外是目标站点属于复杂的JavaScript渲染页面用普通HTTP请求无法拿到真实正文这时可以用PythonSelenium作为备选方案但建议把Selenium部分独立包装成一个服务不强依赖。毕设项目里尽量避免高频使用Selenium因为它在公网环境的登录态、验证码、慢加载问题上非常折腾。通常我们能选到的适合爬虫练习的站点大多数是服务端渲染J soup 足以应付。2.3 存储与检索MySQL为底、Redis为辅、搜索用量不能浪费数据存储我用的是MySQL表结构围绕新闻实体设计新闻表、分类表、抓取来源表、关键词表关联关系尽量简单。MySQL对这种“百万级以下数据量常规查询”的场景完全是杀鸡用牛刀但对毕设来说它的生态和可视化工具Navicat、DataGrip最友好导出SQL建表脚本、交给导师检查都方便。Redis在这里的作用有两点一是缓存前端查询结果比如首页新闻列表设置60秒过期能明显减少对MySQL的重复查询二是记录抓取URL的去重Set用SADD命令在抓取前快速判断一个URL是否已抓过。注意Redis的去重只是第一层持久化到MySQL之后还要用MD5签名去重双保险。如果你想在论文里加一点“大数据”的味道可以考虑引入Elasticsearch做全文搜索。但我个人建议在毕设阶段要把投入产出比算清楚ES的学习成本不低并且需要额外的内存资源。更轻量的替代方案是用MySQL的LIKE %keyword%查询配合倒排索引概念在论文中做原理描述也完全够用。如果你确实想加亮点可以引入ES但只对标题和正文建索引而且数据同步用Logstash会比自己写同步代码省力。2.4 加分项技术HanLP分词、Minio存储、MyBatis-Plus分页这三个工具是我在项目中逐步加进来的性价比都很高HanLP分词用于新闻关键词提取。抓取完正文后调用HanLP的extractKeyword接口可以自动抽出3-5个关键词存入标签表前端展示“相关标签”和“相似新闻推荐”全靠它。Minio如果你要处理封面图和详情页图片建议用Minio做对象存储而不是把图片以Base64形式塞进MySQL。Minio兼容S3接口部署一个单机实例非常轻量。MyBatis-Plus分页查询直接用PageNews page new Page(current, size); newsMapper.selectPage(page, wrapper);配合LambdaQueryWrapper写条件查询代码量比裸MyBatis减少一半以上。需要强调加分项不要一开始就引入。先把基础功能跑通再选其中一两个作为论文的“创新点”这样老师的追问范围完全在你掌控之中。3. 新闻采集链路拆解从URL调度到去重入库的完整实现3.1 数据源与URL管理策略先建索引再抓内容新闻聚合平台的数据源选择是最容易被忽略、却直接影响整个项目稳定性的环节。我的建议是不要随便找那些强反爬、登录墙、验证码满天飞的站点。优先选择提供RSS订阅源或者结构规整、信息公开展示的网站。在项目初期先用3-5个数据源每个数据源写对应的解析规则跑通之后再加。我把数据源管理抽象成一张source_config表字段包括source_name来源名称如“新浪科技”“36氪”“开源中国”等泛指示例。entry_url入口列表页地址。list_selector列表项的CSS选择器。title_selector标题的CSS选择器。link_selector链接的CSS选择器。content_selector正文的CSS选择器。这样做的价值在于数据源的可配置化新增一个站点时不需要改Java代码只需要往表里插一条记录解析器通过反射机制读取选择器规则。这在答辩演示时是很大的加分点因为它体现了“可扩展性设计”。URL管理我用了一个简单的队列思路启动时从source_config加载所有入口URL放入LinkedBlockingQueue由多个线程消费。每个线程抓完列表页后把解析出的详情页URL推入另一个“待抓取队列”同时将其写入Redis Set。下次遇到相同URL时通过SISMEMBER判断是否已抓过。3.2 请求发送与页面解析的常规套路基础的HTTP请求代码并不复杂但有几个细节决定成败请求头必须伪装成浏览器至少带上User-Agent、Accept、Accept-Language。很多站点对默认的Java HttpClient UA直接返回403。两次请求之间必须间隔随机时间我一般设置Thread.sleep(2000 random.nextInt(3000))。这是最朴素、也最有效的反爬规避策略。解析HTML用Jsoup获取元素时注意选择器的兼容性。比如列表页的标题可能同时存在于h2和a标签中优先抓a标签的文本和href属性。编码问题部分站点是GBK编码使用Jsoup.parse(inputStream, GBK, url)来指定字符集避免中文乱码。老生常谈但必须说的一句话爬虫需要尊重数据来源的公开访问规则本项目定位为学习与技术验证用途抓取频率和数据量都需克制。毕设项目尤其如此我们只做功能验证不做大规模商业采集。页面解析的核心代码逻辑大致是Document listDoc Jsoup.connect(entryUrl) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64)) .timeout(10000) .get(); Elements links listDoc.select(listSelector); for (Element link : links) { String title link.select(titleSelector).text(); String url link.absUrl(href); if (StringUtils.isEmpty(title) || StringUtils.isEmpty(url)) { continue; } // 送入去重队列和待抓取队列 }这里有一个非常实用的小技巧link.absUrl(href)会自动把相对路径转成绝对路径省去了手动拼接域名的工作。很多初学者在转换相对URL时容易漏掉当前站点的context-path比如/news/123.html和https://site.com.cn/news/123.html的关系用absUrl就不会踩这个坑。3.3 去重、清洗与入库防止脏数据毁掉整个项目去重分为三个层面缺一不可第一层是URL去重用Redis Set解决。第二层是标题归一化MD5签名去重用数据库唯一索引兜底。第三层是“正文相似度去重”这一层最容易忽略比如有的站点把一篇新闻换了标题重发你只看标题永远发现不了。我的做法是取正文前200字计算SimHash指纹再存入一张独立表每次入库前比较与已有记录的汉明距离小于阈值则视为重复。SimHash的实现代码不复杂论文里也可以作为算法亮点。清洗阶段要处理的噪声包括导航菜单、页脚版权信息、广告链接、JavaScript脚本、CSS样式。Jsoup的select()配合remove()可以精准删除不想要的节点Document detailDoc Jsoup.connect(newsUrl).get(); // 删除脚本和样式 detailDoc.select(script, style, iframe, .advert).remove(); // 提取正文 Elements content detailDoc.select(contentSelector); String cleanText content.text(); // 自动去掉内部HTML标签时间字段的清洗也值得重视。不同新闻站点的发布时间格式五花八门有可能是“2024-05-12 10:30:00”也有可能是“3小时前”“昨天 12:20”。我的处理方式是写一个DateParser工具类把各种相对时间先换算成时间戳再统一格式化为yyyy-MM-dd HH:mm:ss。入库时我采用批量插入策略每次攒够20条再批量写入配合ON DUPLICATE KEY UPDATE处理唯一键冲突。这里有个关键点如果你发现批量插入比逐条插入慢大概率是数据库连接串没有加rewriteBatchedStatementstrue这是一个MySQL JDBC驱动的隐藏参数网上很少被人提到。3.4 定时抓取任务与异常恢复毕设中最容易被老师追问的章节定时任务我用Spring自带的Scheduled注解配合一个“锁表”实现集群防重。生产环境中如果部署了多实例定时任务会在每个实例上同时执行可能造成重复抓取。毕设可以简化加一个task_lock表执行前先尝试插入一条带有任务名和时间的记录插入成功才执行任务。Component public class NewsCrawlTask { Scheduled(cron 0 0 */2 * * ?) // 每2小时执行一次 public void crawlTask() { if (!tryLock(news-crawl, 600)) { return; } crawlService.doCrawl(); } }异常恢复的做法是每次抓取都记录到crawl_log表包含数据源、抓取状态、失败原因。任务启动时先从日志表里找最近24小时内失败的URL按失败次数排优先级重试。同时设置一个最大连续失败次数阈值比如同一个数据源连续失败超过5次就暂停该源并在后台页面醒目标注。这一节的代码量不大但逻辑闭环非常重要。不少同学的毕设项目在演示时出现“抓取任务跑完一次后第二次就不动了”“有些新闻入库后时间变成1970-01-01”这种低级事故都是异常恢复和时间解析没做好。答辩时老师只要看一眼你有没有失败重试机制就能判断你的工程思维成不成熟。4. 新闻数据加工与聚合展示让抓下来的内容产生业务价值4.1 分类、标签与关键词提取让数据从“存储”变为“可利用”抓下来的新闻只是一堆结构化文本直接展示给用户会非常单调。想让项目有亮点就要增加数据加工层。我做了三个加工操作一是自动分类。用规则字典的方式做新闻分类比如配置几个关键词组命中“芯片、手机、AI大模型”视为科技类“基金、股票、利率”视为财经类再配合HanLP的文本分类接口辅助。分类规则的配置我放在category_rules表里前端后台可以动态增删关键词。二是标签提取。对正文进行HanLP分词和TF-IDF权重计算提取权重最高的3-5个词作为标签。这里的实现思路是构建TfIdfExtractor工具先分词、过滤停用词再统计词频和逆文档频率取综合得分Top N。三是热点值计算。我定义了一个简单公式hotScore 基础权重0.6 * 浏览量 0.3 * 评论数 0.1 * 时间衰减因子。时间衰减因子是一个指数函数让最近两小时的新闻hotScore更高这样首页“热门新闻”板块排序更合理。这套公式我可以明说不是最佳实践但它是你能在答辩时讲清楚的计算逻辑比泛泛而谈“算法排序”要实在得多。4.2 后端查询接口与分页设计注意一次别把表查穿聚合展示的REST接口我设计了几个核心端点GET /api/news/list?page1size10categoryIdxx分页查询新闻。GET /api/news/{id}新闻详情同时累加浏览量。GET /api/search?keywordxx按标题和标签搜索。GET /api/news/hot热门新闻列表。GET /api/stats/overview数据面板接口。分页用的是MyBatis-Plus代码非常简洁。但要注意一个细节列表页不要select *返回全字段而是要建一个NewsVO只查id、title、summary、coverUrl、publishTime、sourceName。新闻正文动辄几千字如果列表接口把正文也查出来不仅网络开销大前端渲染也会卡顿。public PageResultNewsVO getNewsPage(int page, int size, Integer categoryId, String keyword) { PageNews p new Page(page, size); LambdaQueryWrapperNews wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, News::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), News::getTitle, keyword) .orderByDesc(News::getPublishTime); PageNews result newsMapper.selectPage(p, wrapper); // 转VO、填充标签 }前端我用的方案是Vue 3 Element Plus与Spring Boot通过JSON交互。如果你不熟悉前端工程化也可以用Thymeleaf服务端渲染减少跨域和打包的麻烦。但如果你想让项目看起来更像“互联网产品”建议还是采用前后端分离因为答辩时老师常会问“前后端怎么通信”“跨域怎么解决”这些问题你能把Vue CLI的代理配置和CORS注解讲清楚本身就证明你已经理解了工程化开发的基本概念。4.3 首页信息流、分类导航与搜索让体验配得上数据首页主要包含四个区域顶部导航分类Tab、热门新闻轮播、最新新闻信息流、右侧标签云。信息流采用“滚动到底部自动加载更多”的方式通过前端监听滚动事件每次请求下一页接口追加渲染。交互细节上有一个常见痛点用户点击新闻后希望“返回列表还在原来的位置”如果每次刷新列表页都会回到顶部体验很差。我的处理方式是在列表页缓存当前滚动位置和已加载的页码返回时恢复。这个功能虽然小但在演示时给老师的体验印象会很直观。搜索页我用的是“关键词高亮分页结果”方案。前端拿到搜索结果后用v-html高亮匹配词但要注意XSS风险——后端返回的正文文本必须经过HtmlUtils.htmlEscape转义只允许搜索关键词部分做特殊处理后再渲染。我在项目里还加了一个全局过滤器拦截并清理请求和响应中的可疑HTML标签这个过滤器可以在网上找到通用写法但不要照搬要自己梳理一遍逻辑再集成。4.4 简单可行的“相似新闻推荐”给论文增加算法含量相似新闻推荐是我这个项目的算法亮点实现的核心是关键词向量与余弦相似度。每次新闻入库时我已经提取了关键词比如“苹果、iPhone、发布会、AI”把这些词映射为标签ID在news_tags表中建立关联。查询相似新闻时取出当前新闻的标签ID集合找出同时包含其中至少两个标签的其他新闻按匹配个数和时间排序SELECT n.id, n.title, n.publish_time, COUNT(nt.tag_id) AS match_count FROM news_tag nt JOIN news n ON n.id nt.news_id WHERE nt.tag_id IN (SELECT tag_id FROM news_tag WHERE news_id ?) AND n.id ! ? GROUP BY n.id, n.title, n.publish_time HAVING match_count 2 ORDER BY match_count DESC, n.publish_time DESC LIMIT 6;用SQL解决相似度问题的思路比硬上复杂的推荐算法更容易演示。你可以在论文里指出“基于标签共现的热度推荐模型的轻量实现”如果导师追问有没有考虑更复杂的协同过滤你可以回答在数据规模受限的场景下基于内容标签的推荐比协同过滤更及时且冷启动问题更小。这个回答已经是现成的答辩话术了。5. 从可运行代码到顺利答辩远程调试、部署演示和文档撰写的实操经验5.1 毕业设计项目的运行环境一致性最容易被版本坑死的环节很多同学以为代码写完就万事大吉实际上毕设演示翻车最集中的原因不是功能缺失而是环境不一致。你在自己电脑上能跑换到实验室电脑就各种报错这种情况几乎每个人都遇到过。我的建议非常朴素写一份详细的README把JDK版本、Maven版本、MySQL版本、Redis版本全部固定下来。项目内统一使用Maven的properties锁定依赖版本不要在别人机器上临时升级。另外数据库脚本不只要给create table还要给一份init_data.sql把数据源配置、分类规则、权限账号等基础数据都预置进去。我在给别人进行远程协助时第一步永远不是看代码而是让对面先运行一个简单的健康检查接口/api/ping确认服务起来了再说别的。远程调试时推荐先用内网穿透或云服务器部署一个测试环境让协作者能直接访问Web页面这样比反复截图沟通效率高很多。把application-prod.yml里的数据源配置放好使用云服务器时注意放开安全组的端口MySQL不要开放公网访问只允许本机连接。5.2 远程协助与定制修改的沟通细节少走弯路的几条经验毕设服务里经常被提的是“远程调试讲解定制”。我在这块踩过一些坑总结成了几条经验远程协助时不要直接上手改对方的代码。先让对方自己复述报错信息再引导他们查看日志和关键配置。一旦你直接改了代码对方理解不了后续只会越来越依赖你。定制修改通常集中在三类需求换皮肤配色、加一个图表页面、增加一个数据源。换皮肤是最简单的只要CSS变量抽得好加图表可以用ECharts官方示例直接套增加数据源最需要小心因为新的站点结构完全不一样解析规则要重新写。每次协助结束要求对方自己完整操作一遍“从启动到演示”的流程。如果他们能独立完成说明这次远程调试真正解决了问题。5.3 论文与文档怎么组织让工程价值在文字中显性化论文或设计文档的结构通常包括绪论、相关技术介绍、需求分析、系统设计、系统实现、系统测试、总结。但有一个普遍问题很多学生把“相关技术介绍”写成了百度百科式的大抄写页数不少但没有一点自己的思考。我的做法是技术介绍部分每个技术只写两页内前半段讲你“为什么用”它后半段讲你在项目中“怎么用”它配合一张架构图或一段核心代码片段。需求分析部分不要只写“系统需要实现新闻展示功能”而是写清楚功能需求和非功能需求——比如“系统的多线程抓取模块需支持10个并发线程且不会因单条数据异常导致整个任务终止”——这样的指标性描述才有说服力。测试部分除了常规的功能测试表格一定要放性能测试数据。我当时用JMeter对查询接口做了简单压测在100并发下接口平均响应时间26ms、错误率0%这个数据一贴出来老师的表情就不一样了。5.4 演示脚本与答辩问答准备把“讲得清楚”变成你的优势演示是整个毕设的临门一脚我的建议是准备一份时间轴脚本打开首页正常信息流展示10秒。点击一个分类Tab展示分类筛选效果10秒。搜索一个关键词展示结果高亮10秒。打开一篇新闻详情展示相关推荐10秒。进入管理后台或数据看板展示今日抓取量和分类占比图表15秒。手动触发一次增量抓取控制台实时打印日志展示新数据入库15秒。整个演示控制在2分钟以内节奏非常紧凑。每次演示前先清空Redis缓存和MySQL日志表确保演示时是“全新状态”。另外提前准备好两个绝活问题一是“你这套系统还有哪些不足”二是一旦联网失败、如何切换到本地演示模式。后者的方案是保留一份预置好的新闻数据快照即使抓取功能失效前台展示也不会停摆。6. 做完这个项目我建议你避开的六个坑一些基于个人经验的大实话这个项目从零到一加上后续的文档、演示、定制优化我前后走了不少弯路。最后把这几个最值得讲的坑一次性列出来。第一不要为了“大数据”三个字强行引入Hadoop和Spark。新闻聚合平台的数据量远达不到分布式计算的门槛老师问到“你的数据量多少”就会露馅。大数据这个点用“数据清洗流程Tf-Idf关键词提取热点值计算”来体现更扎实。第二爬虫的目标站点选择不能只看名气要看能不能稳定访问。我试过的一些大型门户一级页面需要复杂的cookie校验在校园网环境下时通时不通。选题初期就建立一个“数据源可用性检查表”用脚本批量验证别等到写代码时才发现页面结构一直在变。第三异常处理一定要分级。抓取任务里最忌讳的是“所有异常都catch住然后打印日志”。我后来改成网络超时算一级异常重试3次解析失败算二级异常记录日志并跳过数据库写入失败算三级异常立即终止当前批次避免部分失败导致的数据不一致。第四前端页面不要过度设计。花里胡哨的动画和组件库特技会消耗大量调试时间而项目落地最需要的其实是干净的信息布局和响应速度。当时我把首页从“卡片阴影飘浮动画”简化为“单列文本缩略图”反而觉得整体清爽了很多。第五数据可视化模块一定放在最后做但一定要做。一个简单的ECharts折线图展示近7天抓取量和一个饼图展示分类占比成本不超过两个小时却能给评委留下“这个项目有数据分析意识”的印象。第六所有的自定义规则尽量入库而不是写死在Java代码里。分类规则、抓取间隔、首页推荐条数这些参数如果都能在后台页面修改你的系统就在“可配置化”这个维度超出绝大多数毕设作品了。我个人在这套项目中收获最大的不是把Spring Boot用得多熟练而是理解了“一个数据工程项目从采集到展示的完整链路中真正的精力消耗点在哪里”。这些经验后来帮我解决了不少实际问题也希望这篇分享能让你在做类似题目的时候少走一点弯路。