☰
基于Python的贴吧舆情监测系统:爬虫、数据分析与可视化实践
2026/10/12 3:09:34 网站建设 项目流程

简介:一份面向大数据开发、爬虫工程及舆情分析学习者的完整毕业设计文档。内容围绕Python舆情监测系统的设计展开,覆盖数据采集、文本分析、可视化与系统架构四个核心模块,具体讲解BeautifulSoup、Scrapy、正则表达式、DOM/XPath、XML/JSON解析、MongoDB存储、nltk与jieba分词、时间序列分析、Flask后端、Echarts和jQuery前端等关键技术,并给出微服务架构下的模块拆分和API通信方案。资源为1个docx文档,压缩包大小约762KB,文档包含摘要、关键词、目录、正文及参考文献,结构完整可直接作为毕业设计或课程设计的参考模板。当前已有361人学习,适合需要快速搭建舆情监测系统原型并撰写设计说明的读者,可从中获取系统架构思路、技术选型依据和模块实现细节,辅助完成从数据采集到可视化展示的完整链路设计。

1. 从贴吧数据到舆情看板:这套 Python 系统到底解决了什么

在百度贴吧扔进去一个关键词,两个小时后打开浏览器,就能看到这个词的发帖热度走势、留言时间分布和一张词云图——这就是一个基于 Python 的舆情监测系统跑起来之后的效果。它不是简单的“爬虫 + 图表”拼接,而是把数据采集、文本分析、可视化三条链路串成一条完整流水线:requests 伪装请求头拿网页,BS4 转树形结构定位数据,正则收口具体字段,MongoDB 存非结构化文档,jieba 分词提取高频词,最后用 Flask + ECharts 把结果渲染到浏览器。适合三类人:做舆情相关毕设的学生、想快速验证舆情产品原型的从业者,以及想把 Python 爬虫链路完整跑通的新手。这篇笔记会按实际开发顺序拆解整个落地方案,边复现边把坑点标出来。

2. 技术选型与系统架构:广度优先策略、请求头伪装与 MongoDB 的理由

先讲选型,是因为整个系统能不能稳定跑通,一半取决于采集层的数据质量和抓取效率。原项目技术栈是 Python 3.5 + requests + BeautifulSoup + 正则 + MongoDB + Flask + ECharts,这套组合放到今天依然是爬虫类舆情系统最常见的基础班底。原项目的开发环境是 Windows 10 + PyCharm,所以下面所有命令和路径都以 Windows 为准,Linux 上只有字体路径和 MongoDB 启动方式略有差异。

2.1 三个采集目标与系统分层

舆情监测系统拆成数据采集、数据分析、可视化三个模块,采集层是地基。以百度贴吧为数据源,需要抓两类结构化信息:一是贴吧首页各发言贴的 url、发帖人 ID、发帖标题、留言数;二是进入每条发言贴之后,留言内容、留言图片、留言人 ID、留言时间、留言设备型号。这两类数据粒度不同,所以原项目拆成两个爬虫,分别负责“列表页”和“详情页”。

模块输入输出核心手段
采集贴吧首页帖子索引(url / 标题 / 留言数)requests + BS4 + 正则
采集帖子详情页留言内容 / 时间 / 设备型号requests + 正则 + MongoDB
分析MongoDB 文本字段高频词 / 词云 / 热度序列jieba + Counter + wordcloud
可视化统计结果 JSON浏览器图表Flask + ECharts + JQuery

这个分层设计最大的好处是每个模块可以单独验证:爬虫跑完看 MongoDB 的条数是否增长,分析模块跑完看输出文件,可视化模块跑完看页面是否出图。出问题时不用从头到尾查,按层定位即可。

2.2 广度优先策略:爬贴吧为什么要逐层展开

原项目在采集算法上选了广度优先策略(BFS)。对应贴吧场景:从目标贴吧首页出发,先抓当前页所有帖子 url,再逐个进入详情页,按层推进。用 BFS 而不是 DFS 的原因很实际——贴吧的帖子之间通过“下一页”和“楼层跳转”互相链接,深度优先容易一头扎进某个帖子的上百层楼里出不来,广度优先则能保证首页覆盖面。

from collections import deque def bfs_crawler(start_url, max_depth=2): queue = deque([(start_url, 0)]) visited = set() while queue: url, depth = queue.popleft() if depth > max_depth or url in visited: continue visited.add(url) # extract_links 是对贴吧列表页解析函数的封装,返回页面内所有帖子url links = extract_links(url) for link in links: queue.append((link, depth + 1))

visited 集合是防死循环的关键,同一个 url 只处理一次;max_depth 控制搜索层数,避免贴吧深链导致程序失控。参数上,start_url 通常是某贴吧首页的https://tieba.baidu.com/f?kw=关键词地址;max_depth 设为 2 已经能覆盖“首页 → 帖子 → 楼层”三层。实测过程中如果详情页分页超过 50 页,建议把翻页单独写成循环,不要叠在 BFS 深度里,否则去重集合会膨胀得很快,内存占用会明显上升。

2.3 请求头伪装:连续采集的前提

贴吧的反爬不算凶,但连续请求速度快了照样会 403。原项目明确提到要改写请求头里的 User-Agent、Referer 和 Cookie,这是模拟真实浏览器访问的关键一步。

import requests session = requests.Session() 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': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', } session.headers.update(headers) # 建议从浏览器复制当前登录态Cookie,部分吧内话题接口会校验登录 session.headers['Cookie'] = '你的实际Cookie'

User-Agent 写成完整浏览器版本号而不是默认的 python-requests,Referer 指向贴吧首页,会让服务器认为请求来自站内跳转;用 Session 保持连接能减少重复握手的时间开销。Cookie 的作用是权限——不登录也能抓大多数公开帖,但遇到“吧内话题”这类接口就会失效。还需要注意,请求头伪装不是万能的,控制抓取频率同样重要,我一般会在两次请求之间加 0.5~1 秒随机延时,这比堆 Header 更管用。

2.4 存储选 MongoDB 而不是 MySQL

爬虫抓下来的数据有两个特点:字段不固定(有的帖有图片链接,有的没有),嵌套结构多。用 MySQL 存要么建一堆空字段,要么拆多张表关联查询,都不划算。原项目选了 MongoDB,它的 document 直接对应一条留言,字段可以随时增加,采集到新属性时不需要改表结构。

对比项MySQLMongoDB
表结构需要预先建表、定字段文档结构,字段可增
嵌套数据需要拆表或拼 JSON 字段原生支持嵌套
爬虫写入拼接 SQL 或 ORMinsert_one 直接写字典
高并发读写常规性能更高,但看具体部署

数据库设计上,原项目拆成两个独立集合:一个存贴吧帖子列表信息,一个存留言详情信息,以 url 作为去重键。这样设计的好处是列表页和详情页爬虫可以独立运行,互不阻塞。后文的爬虫代码都按这个结构来写。

3. 贴吧爬虫与留言贴爬虫:从 HTML 剖析树到正则收口

框架定好之后,这一章直接落到代码。核心思路是先用 requests 拿 HTML 源码,再用 BS4 把 HTML 转成树形结构做粗定位,最后用正则表达式收口具体字段。原项目正是这样分两步走的——BS4 负责“知道数据大概在哪”,正则负责“把字段抠干净”。这种组合比单一手段更稳,因为 BS4 选择器面对页面改版时容易失效,正则可以对字符串做最后的兜底。

3.1 贴吧爬虫:帖子索引抓取与去重入库

贴吧列表页的帖子标题通常带固定的 class 属性,先用 soup.select 把候选元素筛出来,再逐个提取 href 拼成完整 url。抓回来的数据先查重再入库,避免重复抓取同一个帖子。

import requests from bs4 import BeautifulSoup from pymongo import MongoClient from datetime import datetime client = MongoClient('mongodb://localhost:27017/', serverSelectionTimeoutMS=3000) db = client['tieba_opinion'] post_col = db['post_list'] 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/', } def fetch_tieba_list(page_url): resp = requests.get(page_url, headers=HEADERS, timeout=10) resp.encoding = 'utf-8' soup = BeautifulSoup(resp.text, 'lxml') result = [] for a in soup.select('a.j_th_tit'): title = a.get_text(strip=True) href = a.get('href', '') if not href: continue full_url = href if href.startswith('http') else 'https://tieba.baidu.com' + href result.append({'title': title, 'url': full_url}) return result def save_posts(posts): for p in posts: if not post_col.find_one({'url': p['url']}): p['crawl_time'] = datetime.now().isoformat() post_col.insert_one(p)

find_one 按 url 查重,避免重复抓同一个帖子;crawl_time 是采集时间标记,后续做增量抓取时有用。timeout=10 防止单个页面卡死整个爬虫;serverSelectionTimeoutMS=3000 让 MongoDB 连接失败时快速抛错而不是无限等待。注意 select 里的类名 j_th_tit 是贴吧经典的帖子标题选择器,贴吧前端一旦改版这个类可能失效,到时候按第 5 章的排查思路重新定位。

3.2 留言贴爬虫:正文、时间、设备型号一次抓全

拿到帖子 url 之后进入详情页。详情页的楼层数据在 div.l_post 节点里,但 BS4 只能定位到“这一层是留言”,拿不到结构化的用户 ID、时间等字段。此时要结合节点自带的>import re def fetch_reply_detail(detail_url): resp = requests.get(detail_url, headers=HEADERS, timeout=10) resp.encoding = 'utf-8' soup = BeautifulSoup(resp.text, 'lxml') floors = soup.select('div.l_post') replies = [] for floor in floors: field = floor.get('data-field', '') name_match = re.search(r'"user_name":"(.*?)"', field) date_match = re.search(r'"date":"(.*?)"', field) content_node = floor.select_one('div.d_post_content') content = content_node.get_text(strip=True) if content_node else '' # 设备型号通常藏在楼层工具条的"来自xx客户端"文本里 device_match = re.search(r'来自(.*?)客户端', floor.get_text()) replies.append({ 'post_url': detail_url, 'user_name': name_match.group(1) if name_match else '', 'publish_time': date_match.group(1) if date_match else '', 'content': content, 'device': device_match.group(1) if device_match else '', }) return replies

>def clean_and_save(replies, content_col): seen = set() clean = [] for r in replies: if not r['content']: continue dedup_key = f"{r['post_url']}|{r['user_name']}|{r['publish_time']}" if dedup_key in seen: continue seen.add(dedup_key) # 把时间统一成 YYYY-MM-DD HH:MM:SS,方便后面按天聚合 r['publish_time'] = r['publish_time'].replace('T', ' ')[:19] clean.append(r) if clean: content_col.insert_many(clean)

insert_many 批量写入比逐条 insert_one 快很多,适合详情页这种一次返回几十条留言的场景。时间统一格式这一步很关键,第 4 章做热度统计时直接按字符串切片就能拿到日期,不需要再处理多种时间格式。这里用了 f-string,如果完全复刻原项目的 Python 3.5 环境,需要改成 format 写法,建议直接用 Python 3.8+,省掉这些兼容问题。

4. 数据分析模块:jieba 分词、词云生成与时间热度统计

采集层把数据存进 MongoDB 之后,分析层才有东西可挖。原项目的分析逻辑分两条线:文本线——分词、提取高频词、生成词云;时间线——按发帖时间聚合热度。两条线都围绕 MongoDB 里的 content 和 publish_time 字段展开。分析模块是整个系统里最像“黑匣子”的部分,输入是原始文本,输出是统计结果,中间每一步都可以单独打印出来验证。

4.1 jieba 分词与停用词过滤

中文文本没有空格分词,所以用 jieba。但裸分词会得到一堆“的”“了”“我”这样的停用词,必须过滤。做法是准备一份停用词表,每行一个词,遇到就跳过;同时过滤长度小于 2 的词,减少单字噪声。

import jieba from collections import Counter def load_stopwords(path='stopwords.txt'): with open(path, encoding='utf-8') as f: return set(f.read().split()) def extract_keywords(texts, stopwords, top_n=50): counter = Counter() for text in texts: words = jieba.cut(text) for w in words: w = w.strip() if w and w not in stopwords and len(w) > 1: counter[w] += 1 return counter.most_common(top_n)

这段返回的是 [(词, 次数), ...] 列表,可以直接丢给词云库使用。top_n 决定最终输出多少个关键词,调大信息更全但图表更杂乱。stopwords.txt 需要自己维护,常见的“的、了、是、在、我、你”都可以往里塞;网上有公开的中文停用词表,但建议整理一份贴吧语境专用的,否则“楼主”“帖子”这类词会霸榜,真实舆情关键词反而被挤掉。

4.2 词云生成:从高频词到图片

wordcloud 库接收字典格式的词频数据,generate_from_frequencies 是推荐入口。中文词云必须指定中文字体,否则渲染出来全是方框,这是新手最容易忽略的一步。

from wordcloud import WordCloud import matplotlib.pyplot as plt def make_wordcloud(freq_list, output='wordcloud.png'): wc = WordCloud( font_path='C:/Windows/Fonts/simhei.ttf', width=1000, height=700, background_color='white', max_words=100 ) wc.generate_from_frequencies(dict(freq_list)) plt.figure(figsize=(10, 7)) plt.imshow(wc, interpolation='bilinear') plt.axis('off') plt.savefig(output, dpi=150, bbox_inches='tight')

font_path 在 Windows 上指向 simhei.ttf,Linux 上要换成系统字体路径,比如/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc。max_words=100 控制词数上限,和 extract_keywords 的 top_n 配合使用,避免低频词混进图里。生成后建议先看一次输出图片,确认没有乱码再接入 Flask。

4.3 按时间聚合的舆论热度统计

舆论热度最直接的指标是发帖量随时间的变化曲线。从 publish_time 字段截取日期部分,按天计数,就能画出折线图。想看小时粒度,把切片长度从 10 改成 13 即可。

def daily_trend(records): trend = {} for r in records: day = r.get('publish_time', '')[:10] if day: trend[day] = trend.get(day, 0) + 1 return sorted([{'date': k, 'count': v} for k, v in trend.items()], key=lambda x: x['date']) # 用法:从 MongoDB 取数据再聚合 records = content_col.find({}, {'publish_time': 1, '_id': 0}) trend_data = daily_trend(records)

sorted 按日期升序,保证前端折线图的 x 轴从左到右是时间正序。注意 publish_time 在第 3 章清洗阶段已经被统一成YYYY-MM-DD HH:MM:SS,这里切片才不会错位。如果发现某天数据异常高,大概率是爬虫重复抓取了同一批数据,回头检查去重逻辑,不要急着怀疑算法。

5. 可视化与常见问题排查:Flask 路由、ECharts 对接和五个翻车点

数据算出来不算完,舆情监测系统最后要给人看。原项目用 Flask 搭建 Web 服务器,前端用 HTML + ECharts + JQuery 渲染图表,后端提供 JSON 接口。整个可视化链路只有三步:Flask 暴露 API,JQuery 拉数据,ECharts 画图。链路不长,但坑不少,尤其是中文编码和接口字段对齐这两个问题,几乎每次都会遇到。

5.1 Flask 后端:把统计结果暴露成 JSON 接口

后端只做两件事:读 MongoDB,返回 JSON。路由设计上,/api/trend返回热度序列,/api/wordcloud_data返回词频列表,前端按需调用。

from flask import Flask, jsonify, render_template from pymongo import MongoClient app = Flask(__name__) app.config['JSON_AS_ASCII'] = False # 关键:否则中文会变成 \uXXXX client = MongoClient('mongodb://localhost:27017/') db = client['tieba_opinion'] @app.route('/api/trend') def api_trend(): records = db['reply_info'].find({}, {'publish_time': 1, '_id': 0}) return jsonify(daily_trend(records)) @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)

JSON_AS_ASCII=False 是中文接口的必选项,不设置的话浏览器里看到的是\uXXXX转义串,虽然也能解析,但调试时没法直接读。debug=True 只适合开发环境,如果系统要 24 小时挂机监测,应该关掉 debug 并用 gunicorn 或 waitress 启动,否则代码一改就自动重启,日志也会刷屏。

5.2 ECharts 与 JQuery 的数据对接

前端页面可以直接引用 ECharts 官方 CDN,离线环境把 echarts.min.js 下载到 static 目录即可。核心逻辑是 JQuery 的$.get拿到 JSON,再把它塞进 ECharts 的 setOption。

$(document).ready(function () { $.get('/api/trend', function (data) { var dates = data.map(function (item) { return item.date; }); var counts = data.map(function (item) { return item.count; }); var chart = echarts.init(document.getElementById('trendChart')); chart.setOption({ title: { text: '发帖热度走势' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dates }, yAxis: { type: 'value' }, series: [{ name: '发帖量', type: 'line', data: counts }] }); }); });

data.map 把后端返回的对象数组拆成两个平行数组,ECharts 的 xAxis.data 和 series.data 必须长度一致,否则图表右侧会出现空白。这里用了 jQuery 的 CDN 版本,实际部署时建议下载到本地,避免内网环境加载失败。接口返回的字段名要和这里对应,如果后端改了键名,前端图表会静默失败,控制台也不报错,排查起来很费时间。

5.3 五个高频翻车点与排查方法

以下五个问题是我在复现这套系统时真实遇到的,按“现象 → 原因 → 解决”的顺序写清楚,遇到类似情况可以直接对照。

翻车点一:抓取几页后返回 403。

现象:requests 请求正常,但抓十几页后服务器返回 403 Forbidden。

原因:请求频率太高,且 User-Agent 是默认的 python-requests,服务器直接把请求判定为爬虫。

解决:每次请求前随机切换 User-Agent,请求之间加 0.5~1 秒延时;不要用同一个固定 Header 打满全场。

翻车点二:soup.select 返回空列表。

现象:页面能正常打开,但选择器一个节点都拿不到。

原因:贴吧页面改版,class 名变动,或者数据走了 Ajax 异步加载,HTML 里根本没有对应节点。

解决:浏览器 F12 重新定位节点,把旧 class 换成新 class;如果数据在 XHR 接口里,改用直接请求接口的方式。正则表达式解析不了 Ajax 渲染的内容,但可以直接抓 JSON 接口的数据。

翻车点三:MongoDB 里的中文全部乱码。

现象:数据库里存的是\xe4\xb8\xad这类字节串。

原因:requests 未指定编码,贴吧页面有时按 utf-8 返回,有时混入 gbk 片段,默认解码方式不对。

解决:拿到响应后立刻设置resp.encoding = 'utf-8'或'gbk',并打印前 200 个字符确认,再写解析逻辑。

翻车点四:Flask 返回的 JSON 全是 \uXXXX。

现象:浏览器能看到数据,但全是转义后的 Unicode 字符,可读性极差。

原因:Flask 默认对非 ASCII 字符做 ASCII 转义。

解决:在创建 app 后加一行app.config['JSON_AS_ASCII'] = False,问题立刻消失。

翻车点五:jieba 分词结果全是单字。

现象:高频词列表里全是“的”“是”“了”。

原因:没加载停用词表,也没有过滤单字词。

解决:加载自定义停用词表,过滤len(w) <= 1的词,再根据舆情主题加载用户词典,比如jieba.load_userdict('userdict.txt'),把“广州”“芯片”这类专有名词提前塞进去。

6. 验证与进阶:从最小可跑通到多数据源扩展

整套系统第一次复现时,不要贪多。选一个流量小的贴吧,比如某个冷门兴趣吧,把列表页翻页限制在 1~2 页,详情页只抓前 3 个帖子,跑完检查三个节点:MongoDB 的 post_list 和 reply_info 集合是否各有多条记录;分词输出的高频词是否符合直觉,比如游戏吧应该出现角色名而不是一堆停用词;浏览器打开 Flask 页面,折线图和词云是否正常渲染。三个节点都通过,再扩大到全量抓取。

这套系统真正能复用到其他项目的地方,在采集层和分析层的解耦。分析层和可视化层基本与数据源无关,想扩展微博评论或新闻站点,只需要把fetch_tieba_list和fetch_reply_detail两个函数换成对应站点的解析实现,保持返回字段不变即可。

class BaseParser: def parse_list(self, html): raise NotImplementedError def parse_detail(self, html): raise NotImplementedError class TiebaParser(BaseParser): def parse_list(self, html): # 复用 3.1 节的 BS4 + 正则逻辑 pass def parse_detail(self, html): # 复用 3.2 节的逻辑 pass

BaseParser 定义了解析器接口,新增数据源时只实现两个方法,存储和分析模块完全不用动。这是我后来给这套系统做扩展时的核心思路——把“数据长什么样”和“数据怎么用”彻底分开。如果还要做情感倾向分析,可以在 extract_keywords 之后接一个情感词典匹配,把每条留言打成正面或负面标签,再按时间维度统计正负占比,这样舆情看板会更有决策价值。

这套系统我完整复现过一遍,第一次跑崩就崩在编码上——列表页是 utf-8,详情页某几个楼层混了 gbk,数据入库之后做词云,满屏乱码。从那以后我每次抓新站点之前,都强制先打印一页源码的前 200 个字符确认编码,再写解析逻辑;每次改完选择器,先跑单页再放全量,绝不在没验证的情况下直接挂机抓数据。这个习惯帮我避开了后面很多次返工。希望这篇笔记里踩过的坑,能帮你少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询