ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python舆情监测系统实战:爬虫、分词与可视化全流程解析

2026/9/18 12:35:56 拓冰建站 浏览量
Python舆情监测系统实战:爬虫、分词与可视化全流程解析 简介一份基于Python的舆情监测系统设计文档面向Python学习者、数据分析爱好者及需要进行舆情分析项目开发的技术人员系统梳理了从网络数据采集、文本分析到可视化展示的完整技术链路也可为相关毕业设计提供选题参考。资源包仅含1个docx文件压缩包大小约762KB文档结构清晰包含摘要、关键词、目录及正文可直接作为课设或毕设参考资料。内容上详细讲解通过网络爬虫抓取社交数据时如何设置User-Agent、Referer与Cookie以规避反爬限制利用正则表达式和DOM树定位网页信息并介绍XML/JSON解析及MongoDB存储方案分析部分涵盖中文分词、高频词提取与基于时间序列的中长期舆情趋势研判可视化部分基于Flask搭建Web服务配合HTML、Echarts和JQuery实现动态图表展示。此外还涉及微服务架构与技术选型分析已有360人学习下载对想深入理解舆情监测系统设计的人来说具有较高参考价值。1. 舆情监测系统解决什么问题从贴吧数据采集到舆论倾向判断网络舆论不再是被动等待媒体报告而是可以从公开社交平台直接采集、量化、追踪。用 Python 写一个舆情监测系统本质上是把「数据采集 → 数据存储 → 文本分析 → 可视化」四段流程串成一条自动化管线。基于 Python 的舆情监测系统之所以适合个人或小团队复现是因为爬虫、分词、Web 展示这三个环节都有成熟的库支撑requests 负责抓取MongoDB 负责存非结构化文本jieba 做中文分词Flask 暴露接口ECharts 负责图表。这套方案解决的核心问题是如何从贴吧这类 UGC 平台持续拿到发帖和留言数据并把散乱文本变成可读的热度曲线和热词排行。适合熟悉 Python 基础、想完整走一遍爬虫分析与可视化链路的人也适合需要给团队做轻量舆情 Demo 的工程师。2. 数据采集模块请求头伪装、广度优先遍历与 MongoDB 落库2.1 采集策略为什么是广度优先而不是“见链就爬”舆情采集和通用爬虫的最大差异是目标页面结构相对固定。以百度贴吧为例列表页会展示一批主题帖每个主题帖内又有多条留言。采集策略采用广度优先先遍历列表页拿到当前层级的所有帖子 URL再逐条进入帖子详情页抓留言。这样做的好处是能控制深度避免顺着用户主页、外链等无限爬下去同时每个层级的页面数量是可以预期的方便做断点续采。爬虫的基础流程很简单发送 HTTP 请求获得 HTML 源码在源码里定位目标字段最后把结果写入数据库。真正的工程细节在请求头伪装和解析策略上这两项直接决定采集是否会被拦截、数据字段是否完整。下面分别处理。2.2 requests 模拟浏览器请求User-Agent、Referer 与 Cookie 的作用目标服务器识别客户端主要靠 HTTP 请求头。默认的Python-requests标识很容易被识别所以要在每次请求时带上完整的浏览器头。下面是这个系统里通用的请求函数import requests import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://tieba.baidu.com/, Accept-Language: zh-CN,zh;q0.9, Cookie: 你的百度Cookie # 需要替换成实际登录后的Cookie } def get_page(url, retry3): for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f[{i1}/{retry}] 请求失败: {url} {e}) time.sleep(2) return None逻辑说明requests.get发送 GET 请求timeout10防止某个慢页面拖死整个采集retry做简单重试resp.encoding resp.apparent_encoding是把页面声明的编码改成从内容里推测出的编码避免中文乱码。参数里最关键的是 User-Agent最好与当前主流浏览器版本保持一致Referer 表示来源页面某些验证逻辑会检查它是否来自站内Cookie 用于维持登录态采集需要登录后可见的板块时是必需的。2.3 解析策略正则表达式定位 URLBeautifulSoup 提取结构化字段拿到 HTML 源码后需要提取两类数据。列表页要提取的是帖子标题、帖子链接、发帖人 ID、回复数详情页要提取的是楼层内容、留言时间、留言图片、设备型号。原文里用“先 BeautifulSoup 转树形结构再转字符串用正则精确定位”的方式实际操作中我一般混合使用。from bs4 import BeautifulSoup import re def parse_tieba_list(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(li.j_thread_list): a li.select_one(a.j_th_tit) href a.get(href) if a else title a.get_text(stripTrue) if a else # 部分字段用正则从整个 li 块的文本里抓比反复找嵌套节点快 block_text li.get_text( , stripTrue) reply_match re.search(r(\d)\s*回复, block_text) items.append({ title: title, href: https://tieba.baidu.com href, reply_num: int(reply_match.group(1)) if reply_match else 0, author: li.select_one(span.frs-author-name).get_text(stripTrue) }) return items逻辑说明先用select定位每个帖子所在的li节点再用select_one精确取值。之所以保留正则是因为回复数这类短字段有时会被 HTML 标签打断直接剥字符串后丢给正则匹配比构造多层 DOM 选择器更省事。需要留意href是相对路径必须拼接站点根地址reply_num转成 int方便后续按热度排序。详情页的留言时间和图片可以用类似方式处理时间字段有时隐藏在>from pymongo import MongoClient, errors client MongoClient(mongodb://localhost:27017/, connectTimeoutMS3000) db client[opinion_monitor] posts db[tieba_posts] comments db[tieba_comments] posts.create_index(href, uniqueTrue) comments.create_index([(post_href, 1), (comment_time, 1)], uniqueTrue) def save_post(item): try: posts.insert_one(item) return True except errors.DuplicateKeyError: return False逻辑说明create_index在 MongoDB 里创建唯一索引第二个参数是排序方向。save_post里的DuplicateKeyError表示这条 URL 已经在库里直接跳过。这样爬虫重跑时不会将已经入库的帖子重复写入也能作为增量采集的判据。2.5 两段式爬虫列表爬虫与留言爬虫如何衔接上面两个集合由两个爬虫分别填充crawl_tieba_list负责从列表页批量拿帖子 URLcrawl_tieba_detail负责从tieba_posts里循环读取尚未抓详情页的链接。留言爬虫每次取一个 URL采集完成后更新对应帖子状态。我用一个crawled布尔字段标记哪些帖子已经处理过避免重启后重新抓一遍。def crawl_detail_from_posts(): while True: target posts.find_one({crawled: False}) if not target: break html get_page(target[href]) detail_list parse_tieba_detail(html) if detail_list: comments.insert_many(detail_list) posts.update_one({_id: target[_id]}, {$set: {crawled: True}}) time.sleep(1)这里的关键是find_one每次取一个未处理的帖子处理完立即更新状态进程中断后可以继续。time.sleep(1)是基础的请求间隔避免短时间高频请求。更稳妥的做法是让留言爬虫走队列或中间表数据量大时再用 Celery 一类分布式队列当前体量下用 MongoDB 自身当队列已经够用。3. 数据清洗与文本分析jieba 分词、高频词统计与热度曲线3.1 数据清洗不能省去重、去空、去噪声从页面直接采到的文本往往带着广告语、用户、超链接和表情符号。如果把这些噪声直接丢给分词器词云里容易出现“转发”“链接”这类无效词。清洗阶段我会做三步删除 content 为空的文档去掉重复内容用正则把 URL、邮箱、用户名替换成空格。import re def clean_text(text): if not text: return text re.sub(rhttps?://\S, , text) # 去掉链接 text re.sub(r\w, , text) # 去掉 用户名 text re.sub(r#.*?#, , text) # 去掉话题标签 text re.sub(r\s, , text) return text.strip()逻辑说明正则里\S匹配除空白外的连续字符用来吞掉整个 URL\w匹配常见的用户 ID话题标签用非贪婪.*?去掉#...#包裹的部分。替换成空格而不是直接删除是为了避免两个词被拼成一个新词影响分词结果。3.2 jieba 分词与停用词过滤中文分词直接使用 jieba它对网络文本的兼容性比基于空格分词的方式好很多。实际处理时要先加载自定义词典把贴吧语境下的固定词如“吧友”“楼主”“镇楼”强制识别成独立词再过滤停用词。import jieba from collections import Counter STOP_WORDS {的, 了, 是, 我, 你, 他, 也, 就, 都, 啊} def analyze_words(texts, top_n50): counter Counter() for text in texts: words jieba.cut(text) for w in words: w w.strip() if len(w) 2: continue if w in STOP_WORDS: continue counter[w] 1 return counter.most_common(top_n)逻辑说明jieba.cut返回一个可迭代的分词结果。len(w) 2过滤单字词但会牺牲“怼”“刚”这类高频单字如果业务场景需要保留可以去掉这个条件。Counter.most_common直接给出按频次倒序的词和次数作为后面热词榜的数据源。分词效果可以通过自定义词典提升。下面是一个对比输入语句默认分词加载自定义词典后吧友都在盖楼吧友 / 都 / 在 / 盖楼吧友 / 盖楼前排围观楼主前排 / 围观 / 楼主前排 / 围观 / 楼主加载词典的代码是jieba.load_userdict(dict.txt)每行一个词可以附带词频。3.3 时间维度热度统计从时间戳到小时级曲线单看词频只能知道大家聊什么要知道舆情走势还得统计消息量随时间的变化。每条留言都有comment_time采集时我建议统一转成毫秒时间戳存进 MongoDB聚合时按时间粒度分组。from datetime import datetime, timedelta pipeline [ {$match: {comment_time: {$gte: start_ts, $lt: end_ts}}}, {$group: {_id: {$dateToString: {format: %Y-%m-%d %H:00, date: {$toDate: $comment_time}}}, count: {$sum: 1}}}, {$sort: {_id: 1}} ] result list(comments.aggregate(pipeline))逻辑说明MongoDB 聚合的$group里先用$toDate把时间戳转成日期对象再用$dateToString格式化成小时级字符串最后$sort按时间排序。如果只关心天级趋势把 format 改成%Y-%m-%d即可。这个聚合结果直接返回给前端折线图几乎没有后端运算成本。对中长期分析关键是把同一小时的数据持续采集并合并。比如跑一周后2025-01-10 20:00这组数字已经代表了那一小时里全部留言量用两张折线图并排对比“昨天同一时段”和“上周同一时段”就能看出舆论是否异常上升。4. Flask ECharts 可视化路由、JSON 接口与图表映射4.1 Flask 后端只做数据接口不碰页面渲染可视化模块如果直接在后端拼 HTML前端图表很难维护。我习惯的做法是 Flask 只暴露 JSON 接口前端用 JQuery 请求接口拿到数据再交给 ECharts 渲染。这样一个接口可以被多个图表复用也能让前端和后端分别调试。from flask import Flask, jsonify from pymongo import MongoClient app Flask(__name__) client MongoClient(mongodb://localhost:27017/) db client[opinion_monitor] app.route(/api/hot_words) def hot_words(): words list(db[word_count].find({}, {_id: 0, word: 1, count: 1}).sort(count, -1).limit(50)) return jsonify(words)逻辑说明db[word_count]是提前跑完分词后写入的热词集合。路由返回的是[{word: 吧友, count: 120}, ...]这种纯 JSON 结构_id字段被排除前端拿到的数组可以直接用于 ECharts 的data。这里不需要模板渲染Flask 只充当数据网关。4.2 前端模板JQuery 发请求ECharts 做柱状图前端页面用templates/index.html放在 Flask 默认模板目录下核心代码是 JQuery 的$.ajax从接口拉数据再调用echarts.init初始化图表。ECharts 的 dataset 属性允许直接把数据数组喂给图表省去手工转换。!DOCTYPE html html head meta charsetutf-8 title舆情监测可视化/title script src/static/echarts.min.js/script script src/static/jquery.min.js/script /head body div idword_bar stylewidth: 900px; height: 500px;/div script $.ajax({ url: /api/hot_words, method: GET, dataType: json, success: function(data) { var chart echarts.init(document.getElementById(word_bar)); chart.setOption({ dataset: { dimensions: [word, count], source: data }, xAxis: { type: category }, yAxis: { type: value }, series: [{ type: bar, encode: { x: word, y: count } }] }); } }); /script /body /html逻辑说明dataset.source直接放接口返回的数组dimensions声明字段顺序。encode告诉 ECharts 哪个维度映射到 X 轴哪个映射到 Y 轴。这样后端只需要保证 JSON 字段名一致前端就能把图换掉类型而不改数据逻辑。echarts.min.js和jquery.min.js这里放在 Flask 的 static 目录避免依赖外链。除了柱状图这个系统的可视化模块通常还要有舆情总览、时间趋势、发帖设备占比等图表。可以按下面的接口和图表对应关系来组织数据需求接口路径图表类型数据来源热词榜/api/hot_words柱状图word_count 集合小时热度曲线/api/trend折线图comments 聚合结果设备分布/api/device饼图comments 的 device 字段帖子活跃度/api/top_posts表格tieba_posts 按回复数排序接口保持返回轻量 JSON图表渲染全交给前端。遇到跨域问题时如果 Flask 和静态页面不同源需要在 Flask 里配置flask-cors同源部署则无需处理。4.3 ECharts 在大数据量下的两个设置采集到几十万条留言后直接把全部点画在折线图里会卡。ECharts 提供了两个实用的处理方式sampling属性和dataZoom。sampling: lttb会让折线图在保留趋势的前提下抽掉部分点dataZoom配合缩放手势只显示当前窗口内的数据。series: [{ type: line, sampling: lttb, data: trendData }], dataZoom: [{ type: inside }]逻辑说明lttb采样算法适合时间序列比简单抽稀更保留波峰波谷。dataZoom的inside类型允许鼠标滚轮缩放无需额外加滑动条控件。这两个配置在大数据可视化里是性价比最高的优化。5. 爬虫与部署中的常见坑请求频率、索引冲突和接口性能5.1 请求频率别让重试变成压力测试爬虫最常见的翻车不是解析失败而是请求过快触发反爬。这个项目里我用了一个最简单的限速器在每个页面请求之间随机等待 1 到 3 秒并把请求日志写入文件。遇到 403 或 429 状态码时退避时间按照2^n递增。重试函数里不要一失败就立刻重试否则会把目标服务器的错误响应也当成高并发信号。import random import time def controlled_request(url, max_retry4): for attempt in range(max_retry): resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text if resp.status_code in (403, 429): time.sleep(2 ** attempt random.uniform(0.5, 1)) return None这里random.uniform让等待时间带一点随机性避免多个进程同时发起重试形成汇聚请求。如果业务允许更彻底的方式是给不同目标分配独立抓取账号并轮换但这不是个人 Demo 的优先事项。5.2 去重索引的实际问题虽然 MongoDB 唯一索引能挡重复插入但索引本身在字段过长时可能会达到单文档索引大小限制。href这类长字段建议先对它做哈希把原始 URL 和哈希值同时存下来用哈希字段建唯一索引。否则一个超长 URL 会直接触发索引错误而且重试时难以定位是哪条文档。import hashlib def build_post_doc(item): item[href_hash] hashlib.md5(item[href].encode(utf-8)).hexdigest() return item逻辑说明hashlib.md5把任意长度 URL 转成固定 32 位字符串索引体积大幅下降。后续判断重复时先查href_hash命中后再读原始 URL 做二次确认能够防哈希碰撞对正常数据的误杀。5.3 Flask 接口的性能瓶颈Flask 自带开发服务器是单线程的一个慢查询就可能阻塞整个界面。接入生产流量前至少用 waitress 或 gunicorn 起多 worker。启动命令很简单waitress-serve --port 8080 --threads 8 app:app逻辑说明--threads 8让接口可以同时处理 8 个请求配合 MongoDB 的聚合管道单机支撑几十人的内部看板没有问题。如果未来数据量到千万级可以把热词统计改成离线预计算任务周期更新word_count集合接口不再实时跑聚合。5.4 可调参数速查表下面是这套系统里最值得调试的参数调整前先确认瓶颈在网络、数据库还是前端渲染。模块参数影响常见配置爬虫请求间隔采集速度和反爬风险1-3 秒爬虫超时时间单页失败率10 秒数据库唯一索引字段去重准确性href_hash 或 post_href时间分词自定义词典热词质量按业务补充可视化sampling渲染性能lttb服务部署worker 数并发能力4-8 个线程实际验证时我会把请求间隔从 0.5 秒逐步增加到 3 秒对比同一批页面的采集成功率再把 worker 数从 4 调到 8看接口响应时间的下降幅度。通过这两组数据确定当前目标站的合理采集窗口和部署规模。本文还有配套的精品资源点击获取