简介这是一套面向企业技术团队与舆情分析从业者的开源免费Java舆情监测系统专为本地化部署设计解决品牌声誉管理、网络风险预警与海量舆情数据深度挖掘等核心问题。资源包共2000个文件大小99.11MB涵盖1723个JavaScript前端交互逻辑文件、128个CSS/LESS/SCSS样式资源含bootstrap、jsgrid、图表主题等专业UI组件、84个HTML页面模板、26个XML配置与元数据文件以及Vue、JSON、Shell脚本等辅助模块结构完整、开箱即用。已有1091人学习下载适合具备Java Web开发基础的中高级工程师快速掌握舆情系统架构设计与二次开发要点。用户可直接部署运行获得从数据采集、交叉分析、可视化看板到预警响应的全链路能力显著提升企业品牌价值评估与风控响应效率。 做开源这种事儿最怕的就是自嗨完后没人用。这套基于Java的舆情监测网络监控系统最初是我给一个做品牌方的朋友临时赶出来的内部工具需求就一句话别等负面新闻上热搜了才知道。后来我把核心代码整理开源发现陆陆续续有人在GitHub上提issue问部署和二次开发还有几个公司直接拿去改成了竞品监测平台。今天干脆把整套系统的设计思路、源码结构、核心实现和踩坑记录完整摊开来讲希望对想自建舆情监控系统、或者正在做Java Web数据处理类项目的同学有一个直接的参考。这套系统本身解决的问题并不复杂定时采集指定网站、新闻源、论坛和社交平台公开页面的内容经过清洗、分词、去重后存入数据库再按关键词、情感倾向、传播趋势等维度做指标计算最终在Web管理后台展示并通过邮件或webhook触发告警。相比商业舆情服务动辄一年几十万的报价这套东西胜在免费、开源、代码完全可控尤其适合技术团队自用或者作为毕业设计、面试项目的底子。1. 项目从哪来为什么要自建一套Java舆情监测系统市面上不是没有舆情监测平台但绝大多数是SaaS订阅制按关键词数量、采集频次、数据量收钱一个基础套餐大几千一个月稍微上点规模就贵得离谱。商业平台还有一个很现实的问题数据不出内网但舆情数据必须上传到SaaS服务商的服务器这在不少企业内部过不了安全评审。所以自建的需求一直存在只是之前没有合适的开源方案——要么是Python写的脚本级工具缺乏任务调度、告警和可视化要么是阉割版只采集不分析等于白干。1.1 为什么是Java而不是Python很多人在技术选型的时候会本能地选Python因为爬虫库和数据分析库确实丰富scrapy、requests、pandas一套下来一天就能做个小demo。但如果目标是能长期运行、多人使用、能扩展的监控系统Python暴露出来的问题也不少进程长时间运行内存管理需要操心工程化配一个像样的后台管理和任务调度体系要额外搭不少东西团队招人维护也有门槛。Java这边的情况正好反过来。Spring Boot的生态太成熟了定时任务、Web管理后台、数据库访问、消息队列、缓存几乎都有开箱即用的组件写业务代码的时间被压缩到极致。再加上Java自身的强类型和JVM的稳定性跑一个常驻服务几个月不宕机很正常。另外这类监控系统后期的数据量往往不小Java和Elasticsearch、MySQL、Kafka这些存储组件的配合也更成熟。所以我最后选了Java做底座实测下来项目从零到可用版本大概花了三周这个速度在Python方案下未必能快多少但后续维护省心得多。1.2 系统架构与模块边界整体架构不复杂核心就是采集—清洗—存储—分析—展示五层每层负责一块独立的事层与层之间用数据库或队列解耦。采集层负责任务调度、页面抓取、数据入队。技术选型上用了Spring的Scheduled做定时调度HTTP客户端用OkHttp和Jsoup抓到的HTML先不解析原始内容直接放进队列。清洗层消费队列里的原始页面用Jsoup解析正文、标题、发布时间、来源做编码检测、HTML标签剔除、去重操作。存储层MySQL存结构化数据Elasticsearch存全文索引数据Redis做缓存和分布式锁。分析层对入库文本做中文分词、关键词匹配、情感倾向计算、热度排序产出舆情指标。展示层Spring Boot Thymeleaf搭的管理后台包含监控面板、关键词配置、告警记录、数据查询页面。模块边界清楚有一个好处任何一个环节想替换都不至于牵一发动全身。比如你不想用Elasticsearch存储层可以只保留MySQL你不想用自带的情感分析接口分析层也有预留接口给你对接算法团队的大模型。1.3 这套源码的技术栈全景如果你打算在这个基础上做二次开发先确认你对下面的技术栈是否熟悉。技术组件用途版本建议JDK运行环境8或11社区版建议8兼容性最好Spring Boot应用框架2.3.x 或 2.7.xMySQL结构化数据存储5.7或8.0Redis缓存、分布式锁、任务队列5.x及以上Elasticsearch全文检索与聚合7.x注意和Spring Data ES版本对应JsoupHTML解析采集1.14HanLP中文分词与情感分析1.8.xOkHttpHTTP请求3.14MyBatis-Plus数据库持久层3.5.x这套栈选型不是凑热门每一个组件都是因为确实需要一个能解决特定问题的角色才放进来。ES不是必须的但对于新闻列表页这类正文检索场景MySQL的LIKE查询在数据量过百万后性能会断崖式下跌ES走倒排索引明显更划算。2. 核心模块拆解采集、清洗、存储、分析是怎么串起来的很多人拿到别人源码第一反应是打开IDE编译跑通然后对着控制台日志一头雾水。所以我在讲源码之前先按模块把整个系统的数据流串一遍。你理解了整体流转之后再看代码会轻松很多。2.1 数据采集层从Robots协议到线程池采集层是舆情监测系统的起点也是最容易忽略规则的地方。直接粗暴抓取网站页面轻则被限制访问重则涉及不合规的数据采集所以采集器从设计之初就内置了三个基础约束第一尊重目标网站的Robots协议如果协议里明确禁止爬取不做强行抓取第二为每个采集任务配置合理的请求间隔和超时时间避免对目标服务器造成压力第三自定义User-Agent标识来源方便对方服务器管理方识别请求来源。模块内的核心类叫CrawlerTaskExecutor它做的核心工作包括从数据库读取任务配置包括目标URL、采集频率、解析规则、启用状态。通过Spring的Scheduled注解驱动定时触发按cron表达式调度。每个任务执行时先用Redis的SETNX命令抢一个分布式锁防止多个实例部署时同一个任务被重复执行。请求成功后把HTML原文包装成一个CrawlResult对象丢进内存队列。队列选择LinkedBlockingQueue默认容量5000。容量设置是有考虑的太小了消费不过来会丢失任务太大了任务堆积会造成内存紧张。5000这个值是实际压测出来的平衡点当然你源站采集量大也可以调但要注意配合堆内存配置。Component public class CrawlQueue { private final BlockingQueueCrawlResult queue new LinkedBlockingQueue(5000); public boolean push(CrawlResult result) { return queue.offer(result); } public CrawlResult poll(long timeout, TimeUnit unit) throws InterruptedException { return queue.poll(timeout, unit); } public int size() { return queue.size(); } }2.2 文本清洗与中文分词内容质量的第一道关口采集回来的原始页面不能直接用里面全是HTML标签、脚本代码、广告噪音和页面导航。清洗层的任务就是用Jsoup选择器把正文、标题、发布时间、来源这些关键字段抽出来再把不需要的标签删掉。这一层看起来没有技术含量但实际坑最多。比如有些网页的正文是JS动态渲染的Jsoup拿到的HTML里根本没有正文这时候需要在采集层多做一个判断必要时对接无头浏览器或者直接配置数据源的API接口。清洗之后进入分词环节。中文分词我用了HanLP一个开源的中文自然语言处理工具包支持标准分词、NLP分词、索引分词等多种模式还带一个基础的情感分析模型。之所以不用最基础的IK Analyzer是因为HanLP能输出更丰富的词性信息方便后续判断情感倾向和实体识别。ListTerm termList HanLP.segment(text); for (Term term : termList) { String word term.word; String nature term.nature.toString(); // 过滤停用词保留名词、动词、形容词 if (word.length() 2 !stopWords.contains(word) isMeaningfulNature(nature)) { wordCountMap.merge(word, 1, Integer::sum); } }注意一个容易踩的坑HanLP的默认模型是data字典首次加载时会比较慢大概几秒钟所以在服务启动时要做预热把分词组件的初始化放进ApplicationRunner里而不是等第一条消息进来才加载。2.3 指标计算热度分、情感分、告警判断是怎么算出来的数据入库后单纯搜索并不能叫舆情监测真正有价值的是算出来的指标。这套系统里最核心的指标有三个热度分、情感分、传播趋势。热度分的设计我参考了媒体热度的常见计算逻辑但不是简单数评论数而是把几个维度加权求和。计算规则是热度分 原发数量权重40% 转载数量权重30% 评论互动权重20% 来源权重10%每个单项都做了归一化处理保证不同量级的对比不会让单一项主导全局。这样算出来的好处是即使某个小网站出现一条爆炸性新闻也不至于直接被几千条转载的大站刷掉。情感分的实现比较朴素基于情感词典匹配加打分正向词加1分负向词减1分最后除以总词数归一化到-1到1之间。这个逻辑简单但胜在可解释性强出问题也好排查。如果你的数据量大可以替换成预训练的语言模型接口架构上已经预留了算法替换的位置。告警判断就更直接了两个条件触发热度分超过阈值或者情感分低于某个负向阈值。每判定一次就往告警记录表里写一条数据同时调告警发送服务发邮件和webhook。阈值建议做成数据库配置而不是写死在系统配置类里因为运营人员调整阈值非常频繁不想每次都改配置重启服务。3. 源码实操从零到能跑的最小可用版本代码光看没用得跑起来才算数。这部分我把从环境准备到最终部署的完整过程走一遍重点标注每个环节容易出问题的地方。3.1 环境准备与依赖清单先用IntelliJ IDEA新建一个Spring Boot项目JDK建议直接选8。虽然JDK 17出来很久了但这个项目的第三方依赖版本都是围绕JDK 8验证过的选8最省心。pom.xml里关键的依赖就这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.14.3/version /dependency dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.3/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-mail/artifactId /dependencyHanLP这里注意portable版本是包含基础词典的不需要额外下载data目录体积大概10MB左右。如果你要自定义领域词典比如把竞品词、行业词加进分词器就得下载完整版data目录并配置hanlp.properties里的root路径。3.2 采集入库全流程代码演示采集一个新闻列表页并解析出标题和正文链接是这套系统最基础的功能。下面这段代码演示了从抓取页面到解析列表再到请求正文并入库的全过程可以直接复制到项目里跑通。public ListNewsArticle crawlNewsList(String listUrl) { ListNewsArticle articles new ArrayList(); try { // 模拟浏览器UA提高兼容性 Connection conn Jsoup.connect(listUrl) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .timeout(10000); Document doc conn.get(); // 选择器按实际页面结构调整这里以常见的新闻列表结构为例 Elements items doc.select(div.news_list ul li h4 a); for (Element item : items) { String title item.text(); String url item.absUrl(href); // 正文详情页二次解析 Document detailDoc Jsoup.connect(url) .userAgent(Mozilla/5.0) .timeout(10000) .get(); String content detailDoc.select(div.article-content).first() null ? : detailDoc.select(div.article-content).first().text(); NewsArticle article new NewsArticle(); article.setTitle(title); article.setUrl(url); article.setContent(content); article.setSource(extractSiteName(listUrl)); article.setPublishTime(new Date()); articles.add(article); } } catch (IOException e) { log.error(抓取新闻列表失败: {}, listUrl, e); } return articles; }这段代码跑通后你就理解了从URL到HTML到结构化数据的完整链路。实际项目中我不会在循环里同步解析正文而是把列表页解析出的标题和链接放进队列由另一个线程池去消费并请求详情页。原因很简单详情页大概率分布在不同的服务器上网络IO耗时差异大同步方式会让整体采集速度被最慢的一个详情页拖死。入库用MyBatis-Plus直接写实体类调用saveBatch批量插入实测500条一组的批量插入效率不比手写SQL差代码还干净。3.3 舆情指标计算与告警触发示例热度计算模块的代码本身不复杂就是一个定时任务每隔5分钟扫描一次最近一小时入库的数据按主题词分组统计。Component public class HotScoreCalculator { Scheduled(cron 0 */5 * * * ?) public void calculate() { ListArticleStat stats articleMapper.selectRawCountSinceLastHour(); for (ArticleStat stat : stats) { double hotScore stat.getOriginCount() * 0.4 stat.getReprintCount() * 0.3 stat.getCommentCount() * 0.2 stat.getSourceWeight() * 0.1; hotScore hotScore / getMaxHistoricalScore() * 100; // 超过阈值触发告警 if (hotScore alertThreshold) { alertService.sendAlert(stat.getKeyword(), hotScore); } } } }要注意的是定时任务本身没有做分布式锁处理。如果你只部署一个实例问题不大一旦你做高可用部署两条告警就会被多个实例重复发送。我用的方案是Redis的SETNX命令做互斥key设计成任务名加时间窗口获取不到锁的实例直接跳过本次执行。3.4 Docker Compose快速部署考虑到最终要部署到服务器我直接把MySQL、Redis、Elasticsearch和应用服务编排成了Docker Compose文件。本地开发用这套配置服务器部署也用它省得维护两套环境。注意docker-compose.yml里服务启动顺序要处理应用容器需要等MySQL和Redis初始化完成最简单的方式是在应用启动命令里执行一个等待脚本。version: 3.8 services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: opinion_monitor ports: - 3306:3306 volumes: - ./sql:/docker-entrypoint-initdb.d command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci redis: image: redis:5.0 ports: - 6379:6379 app: build: . depends_on: - mysql - redis ports: - 8080:8080 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/opinion_monitor?useUnicodetruecharacterEncodingutf8 SPRING_REDIS_HOST: redisMySQL容器启动时挂载了sql目录第一次创建数据库时会自动执行建表脚本所以初始化sql文件务必在部署前准备好否则后面又要手动建表徒增工作量。4. 常见问题与排查技巧实录这部分内容是真实的踩坑记录不是网上复制的方案每一个问题都在这套系统里实际遇到过而且大部分是在生产环境跑了一周之后才陆续暴露的。4.1 采集质量不稳定的三种典型原因第一个高发问题列表页能抓到详情页请求失败。原因通常不是网络而是部分详情页有Referer校验直接打开URL会返回403。解决方案是请求时带上Referer为列表页地址或者把Cookie头也带上。第二个高发问题内容出现乱码。大部分现代网站都是UTF-8编码但偶尔会遇到GB2312或GBK编码的页面。Jsoup会自动检测meta里的charset但很多网站的HTTP头和HTML meta声明的编码不一致导致自动检测失效。我建议统一用InputStream的方式读取字节流然后用一个轻量级编码探测工具比如juniversalchardet先识别编码再交给Jsoup解析。InputStream in new URL(url).openStream(); byte[] bytes readAllBytes(in); String charset CharsetDetector.detect(bytes); Document doc Jsoup.parse(new String(bytes, charset), url);第三个高发问题采集任务执行到一半后面全部超时。这不是代码bug而是目标网站对连续请求做了频控。我曾经连续采集同一个网站的100个详情页到第50个的时候直接全部超时。解决办法是每个来源站点单独配置请求间隔热门来源建议500ms到1秒次热门来源可以放到2秒以上。虽然采集速度降低了但稳定性和合规性都提升了这笔账怎么算都划算。4.2 定时任务重复执行与数据重复表现是数据库里同一条新闻出现两条记录而且排查发现不是采集器重复抓取而是同一个详情链接被不同列表页引用或者同一个任务在集群部署时被多个节点执行了。第一种情况好处理库表里给详情页URL加唯一索引重复数据入库时直接由数据库拦截。第二种情况需要分布式锁。我用的实现是封装一个RedisLockUtil在使用Scheduled的任务执行前先获取锁锁的过期时间设置为略大于任务最大执行时间防止任务中途死掉导致锁一直不释放。boolean locked redisLockUtil.tryLock(crawl:task: taskId, 300, TimeUnit.SECONDS); if (!locked) { log.info(任务已被其他节点执行本次跳过); return; }这里有个小坑锁超时时间设置太短任务还没执行完锁就过期了另一个节点又会抢着执行还是会产生重复数据。所以超时时间一定要按照历史执行耗时的最大值来设定预留20%的余量。4.3 数据量上来之后查询和存储怎么优化系统上线初期数据量小MySQL的查询随便写都不慢。但跑到一个月左右舆情数据表轻松破百万行这时候按关键词的LIKE查询就开始出现毫秒级到秒级的退化。我的优化路径是这样的第一步确认是不是查询条件没走索引。常规关键词表加普通索引后等值查询没有问题但只要查询条件里带了模糊匹配或者时间范围函数索引就失效了。最典型的就是在WHERE条件里对时间字段做DATE_FORMAT这种写法必然全表扫描要改成时间范围between。第二步如果模糊查询确实避免不了就该把Elasticsearch接进来。采集入MySQL的同时往ES写一条文档查询展示直接查ES。百万级数据在ES里做分词检索基本是亚秒级响应。这套系统目前就是MySQL保底、ES提速的双写方案。第三步针对历史数据做冷热分离。三个月前的数据移到归档表只保留趋势聚合结果不在主表里全量存。这样主表的数据量能压在一个可控范围内查询性能和备份维护都更轻松。4.4 内存溢出与ShutdownHook处理舆情系统跑时间长了最典型的报错是OutOfMemoryError。排查过程我发现除队列堆积外还有一个隐蔽原因Jsoup的parse操作会生成大量中间字符串如果业务代码里无意中持有了Document引用GC根本回收不掉。排查手段是启动时加JVM参数明确指定堆大小和GC日志路径然后再通过dump文件分析具体占用。我推荐这套启动参数组合java -Xms512m -Xmx1024m \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/logs/heapdump.hprof \ -Xloggc:/data/logs/gc.log \ -jar opinion-monitor.jar另外在应用关闭时要主动停止线程池和队列消费不然会出现关闭后又继续处理一半的情况。Spring Boot的PreDestroy注解配合ExecutorService的shutdown方法就够了简单可靠。4.5 问题速查表现象排查思路解决办法采集页面返回403缺乏来源站必要的请求头增加Referer、Cookie随机UA池详情内容为空页面靠JS动态渲染改用Selenium/无头浏览器或找数据API源定时任务重复执行集群下未做任务互斥引入Redis分布式锁关键词搜不到最近数据ES索引未刷新调整刷新间隔或手动调用refresh接口告警短信/邮件发不出去邮件服务端口被封webhook地址错误检查网络连通性配置重试机制5. 开源协议与二次开发路线既然标题里有开源这部分就绕不开。开源不只是把代码丢到GitHub上那么简单协议选错会直接影响别人能不能合规地使用你的代码。5.1 开源协议怎么选如果你抄过别人的开源代码再发布要注意许可证传染性问题。这套系统我自己从头写的部分不多但用到的开源依赖都要遵守各自的协议。发布时我选择了Apache License 2.0。这是一个对使用者非常友好的协议允许商用、允许修改、允许再分发唯一要求是保留版权声明和修改说明同时不为你提供的代码作任何担保。如果你希望别人使用你代码时也必须开源衍生项目那就选GPL如果你希望更宽松别人用了你代码之后可以闭源商用那选MIT或Apache 2.0都行。我推荐Apache 2.0的原因在于很多公司对GPL协议有顾虑而MIT虽然最简单但少了专利授权相关的保护条款。如果项目后期打算商业化Apache 2.0是最稳妥的起点。在Gitee或GitHub上新建仓库时选协议的地方直接勾选Apache-2.0仓库里会自动生成LICENSE文件不需要自己手写法律文本。5.2 代码扩展点从舆情监测到竞品分析的改造路径系统骨架搭好之后扩展方向全看业务需求。我目前见过几个从这套源码衍生的方向接入更多数据源。现在的采集器是按网站为粒度做的如果你想采微博、知乎、小红书这类平台需要额外处理它们的登录态和加密参数。建议在采集层抽象一层Channel接口让每个平台一个实现类这样代码组织不会乱。对接AI大模型做自动摘要和分类。分析层的SentimentService已经设计成接口你可以实现一个LLMSentimentServiceImpl调用大模型API做语义级情感判别替换掉现在的词典匹配实现。预警渠道扩展。AlertSender目前只支持邮件和钉钉/企业微信webhook你要接飞书、短信、站内信只需要新增一个实现类并注册到Spring容器里。5.3 部署中的性能与容量规划源码跑通是一回事上线之后能扛住多少数据量是另一回事这两个话题需要分开讲。就这套系统的默认配置来说单机部署能支撑的采集量大概在每分钟500到1000条网页内容配合MySQL和Redis足够撑起一个中小型企业的日常舆情监测需求这个量级大概对应几十个采集源、每天几十万条新增数据。一旦你的数据源超过50个或者每天新增数据量到了百万级就需要考虑横向扩容了。扩容的第一瓶颈通常不在应用本身而在MySQL。这个时候建议把ES从可选组件变成必需组件所有检索和聚合查询全部切到ESMySQL只承担事务性写入和后台管理查询。第二瓶颈是任务调度多个应用实例同时跑定时任务时分布式锁是必须做好的否则采集并发度上去之后同一批数据会被几个实例重复处理既浪费资源又污染数据。我自己现在的生产配置是两台4C8G的云服务器一台跑应用采集加分析一台跑MySQL和RedisES单独用了云厂商的托管服务。这个配置成本可控日常运行也平稳短时间内不需要再动它。写在最后从最初想快速交付一个能用的工具到后来开源、整理文档、写默认配置整个过程踩了不少坑但也把舆情监测这个方向彻底摸了一遍。如果你打算基于这套源码做二次开发我建议从最小可用的版本跑起用自己熟悉的语料库去测试分词和采集效果观察一两周后再逐步增加数据源和分析维度。一次加太多外部依赖出了问题反而很难定位是哪一环引入的。最后分享一个小技巧舆情系统的数据质量七成取决于采集层的稳定性而不是分析层的算法多牛。先保证每个数据源都能稳定、合规、不重不漏地抓下来再考虑怎么把分析结果做得更漂亮。调了一两个月的告警阈值之后你就会发现真正靠谱的系统不是一堆高深算法的堆砌而是每个环节都能稳定复现的工程结果。本文还有配套的精品资源点击获取