资讯动态

Java全栈实战:从数据抓取到网页呈现,构建新闻聚合平台

发布时间:2026/10/9 4:56:22 来源:尧图企业网站定制
简介本资源是面向高校学生与Java开发学习者的在线新闻聚合平台完整项目源码适用于毕业设计、课程作业及Web全栈练手场景。项目以SpringBoot构建响应式前端结合网络爬虫实现多源新闻的定时抓取、解析与聚合并围绕MySQL等数据库完成新闻标题、摘要、图片、链接的存储与检索同时涉及防SQL注入、XSS攻击等安全处理覆盖需求分析、系统设计、编码测试到部署上线的完整软件工程流程。压缩包共466个文件约13.77MB包含99个Java源文件、46个Vue组件、34个JavaScript脚本、9个Python爬虫脚本以及大量svg、jpg、png等前端资源与xml、yml、sql、properties等配置和数据库文件另附bat与cmd启动脚本目录结构清晰便于按模块阅读与二次开发。目前已有86人学习下载适合需要完整赛题方案、爬虫与后端实现参考及排错思路的读者。1. 在线新闻聚合平台从数据抓取到网页呈现Java 工程师的落地路径你每天打开某个新闻 App看到的是聚合好的标题、摘要、来源和时间线。这背后其实是一条完整的流水线抓取、清洗、存储、接口、渲染。用 Java 做这件事技术栈成熟、生态完整适合作为全栈练手项目也适合中小团队快速搭出可用的内容中台。这个标题讲的就是用 Java 网页开发加数据抓取把散落在多个站点的新闻聚合成一个可浏览、可检索的平台。它解决的是信息分散、人工搬运低效的问题适合有 Java 基础、想打通前后端与采集链路的开发者。下面按真实落地顺序拆开讲从抓取策略到页面渲染每一步都给出可复现的做法和参数。2. 抓取层设计用 Java 把新闻源变成结构化数据2.1 选型HttpClient Jsoup 还是 Selenium新闻站点分两类。一类是服务端渲染的静态页面HTML 里直接带标题和正文用 HttpClient 发请求、Jsoup 解析 DOM 就够了速度快、资源占用低。另一类是前端框架渲染的页面初始 HTML 里没有内容必须等 JavaScript 执行完才能拿到数据这时候要么找它的后端接口要么上 Selenium 或 Playwright 这类浏览器自动化工具。我一般优先找接口。打开浏览器开发者工具切到 Network 面板筛选 XHR 或 Fetch刷新页面看有没有返回 JSON 的请求。很多新闻站点的列表和详情都是通过接口拿数据的直接请求接口比解析 HTML 稳定得多字段也干净。找不到接口再考虑浏览器自动化因为它的资源开销大一个 Chrome 实例动辄几百 MB 内存并发上不去。依赖方面用 Maven 引入dependency groupIdorg.apache.httpcomponents.client5/groupId artifactIdhttpclient5/artifactId version5.3.1/version /dependency dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.17.2/version /dependencyHttpClient5 负责发请求Jsoup 负责解析。版本号按你项目实际锁定的来这里给的是写稿时的稳定版。注意 HttpClient5 的包名是org.apache.hc.client5和老的 HttpClient4 不兼容别混用。2.2 抓取策略列表页翻页与详情页抽取一个新闻源通常有列表页和详情页。列表页给出标题、链接、时间详情页给出正文和作者。抓取顺序是先抓列表页解析出详情页 URL再去抓详情页最后合并成一条完整记录。下面是一个最小可运行的抓取方法用 HttpClient5 发 GET 请求Jsoup 解析列表页import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public class NewsListCrawler { public static void main(String[] args) throws Exception { String listUrl https://example-news-site.com/list?page1; try (CloseableHttpClient client HttpClients.createDefault()) { HttpGet get new HttpGet(listUrl); // 设置 UA避免被简单拦截 get.setHeader(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); String html client.execute(get, response - EntityUtils.toString(response.getEntity(), UTF-8)); Document doc Jsoup.parse(html); // 假设列表项在 class 为 news-item 的 div 里 Elements items doc.select(div.news-item); for (Element item : items) { String title item.select(h3.title).text(); String link item.select(a).attr(href); String time item.select(span.time).text(); System.out.println(title | link | time); } } } }这段代码的逻辑是创建 HttpClient 实例构造 GET 请求设置 User-Agent 头执行请求拿到 HTML 字符串交给 Jsoup 解析成 Document再用 CSS 选择器提取列表项。doc.select(div.news-item)里的选择器要按目标站点的实际结构改用浏览器开发者工具右键元素、Copy selector 就能拿到。EntityUtils.toString的第二个参数指定编码中文站点常见 UTF-8 或 GBK编码错了会乱码。详情页抽取同理把列表页的 link 拼成完整 URL再发一次请求用选择器取正文容器。正文里往往有广告和无关推荐需要按 class 或标签过滤比如doc.select(div.article-content).select(p)只取段落。2.3 参数与边界并发、超时、重试、去重抓取不是发一次请求就完事。真实场景要控制并发、设置超时、处理失败重试、对 URL 去重。并发用线程池别用new Thread()裸开。核心线程数按目标站点的承受能力和本机资源定一般 5 到 10 个起步太高容易被封。超时设置分连接超时和响应超时HttpClient5 里通过 RequestConfig 配置import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.core5.util.Timeout; RequestConfig config RequestConfig.custom() .setConnectTimeout(Timeout.ofSeconds(5)) .setResponseTimeout(Timeout.ofSeconds(10)) .build(); HttpGet get new HttpGet(url); get.setConfig(config);连接超时 5 秒、响应超时 10 秒是常见起点。目标站点慢就调大但别无限等否则线程池会被拖死。重试用循环加退避。失败后等 1 秒、2 秒、4 秒再试最多三次。别立即重试容易触发限流。去重用 Set 存已抓过的 URL或者用 Redis 的 Set。单机跑用ConcurrentHashMap.newKeySet()就够分布式部署再上 Redis。注意抓取前确认目标站点的 robots.txt 和使用条款控制请求频率不要对目标站点造成压力。这是工程底线不是可选项。3. 存储与接口把抓到的数据变成可查询的服务3.1 表结构设计新闻主表与来源表抓到的数据要落库。用 MySQL 加 MyBatis-Plus 是 Java 生态里最常见的组合。两张核心表来源表存站点信息新闻表存具体条目。CREATE TABLE news_source ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 来源名称, base_url VARCHAR(255) NOT NULL COMMENT 站点根地址, list_pattern VARCHAR(255) COMMENT 列表页 URL 模板, enabled TINYINT DEFAULT 1 COMMENT 是否启用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE news_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, source_id BIGINT NOT NULL COMMENT 来源 ID, title VARCHAR(512) NOT NULL, url VARCHAR(1024) NOT NULL, author VARCHAR(128), publish_time DATETIME, content TEXT, summary VARCHAR(1024), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_url (url(255)), KEY idx_source_time (source_id, publish_time) );uk_url唯一索引保证同一条新闻不会重复插入抓取层去重是内存级数据库唯一索引是最后一道防线。idx_source_time支撑按来源和时间筛选这是列表页最常用的查询条件。content用 TEXT 存正文如果正文很长考虑拆到单独的详情表避免主表过大影响列表查询。MyBatis-Plus 的实体类直接映射这两张表用TableName和TableId注解。插入时用saveOrUpdate配合唯一索引重复 URL 会走更新而不是报错。3.2 接口层Spring Boot 提供分页与检索前端要的是分页列表和详情。用 Spring Boot 写两个接口就够GET /api/news?page1size20sourceId1返回分页列表GET /api/news/{id}返回详情。分页用 MyBatis-Plus 的 Page 对象GetMapping(/api/news) public PageNewsItem list( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int size, RequestParam(required false) Long sourceId) { PageNewsItem p new Page(page, size); LambdaQueryWrapperNewsItem qw new LambdaQueryWrapper(); if (sourceId ! null) { qw.eq(NewsItem::getSourceId, sourceId); } qw.orderByDesc(NewsItem::getPublishTime); return newsItemMapper.selectPage(p, qw); }page和size控制分页sourceId可选不传就查全部。排序按发布时间倒序这是新闻列表的默认行为。size要设上限比如最大 100防止前端传个 10000 把数据库拖垮。检索如果要做全文搜索MySQL 的 LIKE 在数据量大时性能差。常见做法是引入 Elasticsearch把新闻标题和正文同步过去搜索走 ES。项目初期数据量小LIKE 够用但要在接口层留好切换余地别把搜索逻辑写死在 SQL 里。3.3 定时抓取用 Spring Task 还是 Quartz抓取要定时跑。Spring Task 的Scheduled注解最简单适合单机、任务不多的场景Scheduled(cron 0 0/30 * * * ?) public void crawlAllSources() { ListNewsSource sources sourceMapper.selectList( new LambdaQueryWrapperNewsSource().eq(NewsSource::getEnabled, 1)); for (NewsSource source : sources) { try { crawlerService.crawl(source); } catch (Exception e) { log.error(抓取失败: {}, source.getName(), e); } } }cron表达式0 0/30 * * * ?表示每 30 分钟执行一次。每个来源单独 try-catch一个失败不影响其他。如果任务多、需要分布式调度和失败重试换 Quartz 或 XXL-JOB。单机项目 Spring Task 足够别过度设计。4. 网页呈现用 Thymeleaf 还是前后端分离4.1 模板渲染与接口渲染的取舍Java 网页开发有两条路。一条是服务端模板用 Thymeleaf 或 JSP后端直接把数据渲染成 HTML 返回。另一条是前后端分离后端只出 JSON前端用 Vue 或 React 渲染。Thymeleaf 的优势是简单一个 Spring Boot 项目搞定不用单独部署前端SEO 友好因为返回的是完整 HTML。缺点是交互复杂时页面会变重前端体验不如 SPA。前后端分离的优势是职责清晰前端独立迭代适合团队协作。缺点是部署多一层SEO 需要额外处理。我一般这样选如果平台以内容展示为主、交互简单用 Thymeleaf开发快、部署简单。如果要做复杂的筛选、无限滚动、用户交互用前后端分离。这个项目标题里是「网页开发」两种都算按团队情况定。4.2 Thymeleaf 列表页与详情页的最小实现用 Thymeleaf 渲染列表页控制器把 Page 对象放进 ModelGetMapping(/news) public String newsPage(RequestParam(defaultValue 1) int page, Model model) { PageNewsItem p newsService.page(new Page(page, 20)); model.addAttribute(newsPage, p); return news/list; }模板list.html里遍历!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org headmeta charsetUTF-8title新闻聚合/title/head body ul li th:eachitem : ${newsPage.records} a th:href{/news/ ${item.id}} th:text${item.title}标题/a span th:text${item.publishTime}时间/span /li /ul div a th:if${newsPage.current 1} th:href{/news?page ${newsPage.current - 1}}上一页/a a th:if${newsPage.current newsPage.pages} th:href{/news?page ${newsPage.current 1}}下一页/a /div /body /htmlth:each遍历记录th:href拼详情页链接分页用newsPage.current和newsPage.pages判断边界。Thymeleaf 的表达式在 HTML 属性里写浏览器直接打开也能看到静态占位内容调试方便。4.3 静态资源与列表性能列表页加载慢通常是图片和查询两个原因。图片用懒加载img loadinglazy一行搞定。查询方面列表只查需要的字段别SELECT *把正文也拉出来。MyBatis-Plus 可以用select()指定字段qw.select(NewsItem::getId, NewsItem::getTitle, NewsItem::getPublishTime, NewsItem::getSourceId);只取 id、标题、时间、来源正文不查。列表页不需要正文省下的 IO 很可观。详情页再按 id 查完整记录。5. 避坑与排查抓取和渲染里最容易翻车的地方5.1 抓取返回 403 或空内容现象请求返回 403或者返回的 HTML 里没有目标数据。原因目标站点识别出非浏览器请求或者页面是 JS 渲染的初始 HTML 里没有内容。解决先补 User-Agent、Referer、Accept 等请求头模拟浏览器。还不行就检查是不是 JS 渲染切到接口或浏览器自动化。别硬刚换策略比调参数快。5.2 中文乱码现象抓下来的标题和正文是乱码。原因目标页面编码不是 UTF-8或者 HttpClient 解析时用了默认编码。解决先看响应头Content-Type里的 charset没有就看 HTML 里的meta charset。EntityUtils.toString显式传编码别用默认。GBK 页面传GBKUTF-8 传UTF-8。5.3 数据库唯一索引冲突导致整批失败现象批量插入时一条重复 URL 让整个事务回滚。原因用了批量插入且没处理唯一键冲突。解决单条插入用saveOrUpdate或者用INSERT ... ON DUPLICATE KEY UPDATE。批量场景先查已存在的 URL过滤后再插。别让一条脏数据拖垮一批。5.4 定时任务重叠执行现象上一次抓取还没跑完下一次又开始了日志里同一来源抓了两遍。原因Scheduled默认不等待上一次完成任务执行时间超过间隔就会重叠。解决加分布式锁或本地锁或者把任务间隔调大。单机用ReentrantLock的tryLock拿不到锁就跳过本次。分布式用 Redis 锁。5.5 列表页分页参数被前端篡改现象前端传size10000数据库查询变慢甚至 OOM。原因接口没对分页参数做上限校验。解决在控制器里对size做Math.min(size, 100)page做最小值校验。参数校验是接口的基本功别省。6. 进阶技巧把抓取质量量化用指标驱动优化项目跑起来之后真正决定它好不好用的是数据质量不是功能多少。我习惯给抓取链路加几个指标用数据判断哪里该优化。第一个指标是抓取成功率。每次抓取记录成功和失败数按来源统计。成功率低于 90% 的来源优先排查是选择器失效还是被限流。选择器失效是常态目标站点改版一次你的 CSS 选择器就可能全废。把选择器配置化存到数据库或配置文件里改版时改配置不改代码。第二个指标是字段完整率。标题、时间、正文分别统计非空比例。正文完整率低说明详情页选择器没覆盖全或者有些新闻本身就没有正文。时间字段缺失多考虑用抓取时间兜底但要在页面上标注。第三个指标是重复率。用 URL 和标题双重去重统计重复比例。重复率高说明列表页翻页逻辑有问题或者同一新闻有多个 URL。下面是一个简单的统计埋点在抓取服务里记录public class CrawlMetrics { private final AtomicLong success new AtomicLong(); private final AtomicLong fail new AtomicLong(); private final AtomicLong emptyContent new AtomicLong(); public void recordSuccess(boolean hasContent) { success.incrementAndGet(); if (!hasContent) { emptyContent.incrementAndGet(); } } public void recordFail() { fail.incrementAndGet(); } public String report() { long total success.get() fail.get(); double rate total 0 ? 0 : success.get() * 100.0 / total; return String.format(成功率 %.1f%%正文缺失 %d 条, rate, emptyContent.get()); } }用AtomicLong保证并发安全report输出可读的统计。把这个报告接到日志或监控面板每天看一眼比盲目调参有效。还有一个技巧是抓取时间窗口。新闻有时效性只抓最近 24 小时或 48 小时的内容历史数据不重复抓。列表页按时间倒序遇到已抓过的 URL 就停止翻页这样能大幅减少无效请求。判断依据是数据库里该来源最新一条新闻的时间列表页翻到比它旧的就停。最后说一个我踩过的坑别在抓取线程里做耗时操作比如图片下载、正文分词。抓取只负责拿数据入库后续处理异步做。抓取线程被拖慢整个调度都会延迟。把职责拆开抓取、清洗、索引各管各的出问题也好定位。这套方案从抓取到渲染核心链路是通的参数和边界也给了。值不值得做取决于你要聚合的源有多少、更新频率多高。源少、更新慢单机 Spring Boot 加 MySQL 就够。源多、要求实时再考虑消息队列和搜索引擎。先从最小可用版本跑起来用指标说话别一上来就堆组件。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑