ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python生产级舆情分析系统:多源采集、MySQL存储与情感分析落地实践

Python生产级舆情分析系统:多源采集、MySQL存储与情感分析落地实践

简介:本资源是一个基于Python开发的网络舆情分析系统,面向高校毕业设计学生、舆情监控管理人员及Web全栈初学者,旨在帮助用户对微博、微信公众号、贴吧等平台的公开言论进行情感倾向识别与可视化统计,解决负面舆情早期发现与多角色协同管理难题。压缩包共292个文件,含42个核心Python源码(含数据采集、NLP预处理、情感分析模块)、34个JavaScript前端交互脚本、28个JPG/PNG图表素材、15个CSS样式文件(含LayUI、Admin等主流框架样式),以及SQL数据库脚本和日志、配置等辅助文件,整体115.16MB,结构完整覆盖前后端与部署说明。已有150人学习下载,提供可直接运行的完整项目工程,包含用户权限分级(普通用户+唯一管理员)、饼状图舆情统计、个人信息维护、用户增删改查后台等功能,代码注释清晰,适合作为课程设计参考、毕设原型或舆情分析入门实践范例。

1. 为什么你写的舆情爬虫跑三天就崩了?——一个能真正在业务里扛住数据洪流的 Python 网络舆情分析系统,不是 demo,是带日志、可重试、能存进 MySQL 的生产级脚手架

你肯定见过这类标题:“Python 舆情分析入门”“50 行代码搞定微博热点抓取”。但现实是:你刚把热搜词填进 requests.get(),运行两小时后发现微博反爬升级了;你用正则硬抠评论区,第三天发现 HTML 结构全变了;你把数据 print 出来觉得“有结果就行”,等要导出报表时才发现时间格式乱码、情感极性没归一化、同一事件在不同平台被拆成 7 个关键词……这不是代码写得不够炫,而是缺了一套带数据生命周期管理的最小可行系统。这个基于python的网络舆情分析系统.zip不是教学玩具,它是一线团队在政务舆情监测、品牌声量日报、竞品动态追踪等真实场景中反复打磨出的落地骨架:支持多源(微博、知乎、新闻 RSS、主流论坛)并行采集,内置 UA 池与请求退避策略,用 SQLAlchemy 统一抽象存储层,情感分析模块可插拔(TextBlob / SnowNLP / 自训练 BERT 分类器),所有环节带结构化日志与失败重试标记。适合需要快速上线、后续可扩展、且不能总半夜被报警电话叫醒的工程师和数据分析师——它不教你 Python 基础,但会告诉你,当 2000 条微博在 3 秒内涌进来时,queue.Queue和concurrent.futures.ThreadPoolExecutor怎么配才不丢数据。


2. 从零搭起舆情系统的四层地基:环境隔离、数据管道、存储契约、分析入口

2.1 用 conda 创建纯净 Python 环境,彻底告别 “pip install 后 import 报错”

很多舆情脚本本地跑通、部署就跪,根源在依赖冲突。微博 SDK、MySQL 驱动、NLP 库对 NumPy 版本极其敏感。我坚持用 conda 而非 pip+venv,因为 conda 能锁死 C 扩展依赖(如 PyMySQL 的 SSL 层、jieba 的分词引擎)。以下是经 3 个生产项目验证的最小安全环境配置:

# 创建专用环境,Python 3.9 是当前 NLP 生态最稳版本(避开了 3.11 的 asyncio 兼容问题) conda create -n舆情分析 python=3.9 # 激活环境 conda activate 舆情分析 # 用 conda-forge 渠道安装核心包(比 pypi 更早适配新 OS) conda install -c conda-forge pymysql sqlalchemy beautifulsoup4 lxml requests # 再用 pip 安装生态活跃但 conda 滞后的包(注意顺序!先装基础再装上层) pip install jieba snownlp textblob transformers torch scikit-learn pandas matplotlib

提示:不要跳过conda-forge。实测在 CentOS 7 上,直接pip install pymysql会因 OpenSSL 版本不匹配导致连接 MySQL 时抛SSL error: [SSL: WRONG_VERSION_NUMBER];而conda install -c conda-forge pymysql自动绑定系统 OpenSSL 1.0.2k,零报错。

2.2 构建可插拔的数据采集管道:用抽象基类约束所有数据源行为

系统里所有数据源(微博、知乎、新闻 RSS)必须实现统一接口,否则后续清洗、存储、分析会变成补丁大会。我们定义BaseDataSource抽象类,强制子类实现三个方法:

# src/data_source/base.py from abc import ABC, abstractmethod from typing import List, Dict, Optional class BaseDataSource(ABC): """所有数据源必须继承此基类,确保输入/输出契约一致""" @abstractmethod def fetch_raw_data(self, keyword: str, days_ago: int = 7) -> List[Dict]: """获取原始数据列表,每条记录至少含:title, content, publish_time, source_url, platform""" pass @abstractmethod def validate_data(self, item: Dict) -> bool: """校验单条数据合法性,如时间是否为 datetime 对象、content 是否非空""" pass @abstractmethod def get_platform_name(self) -> str: """返回平台标识符,用于后续路由到不同清洗规则""" pass

以微博为例,WeiboDataSource实现fetch_raw_data时,绝不直接拼接 URL 请求,而是封装成带重试、UA 轮换、频率控制的健壮方法:

# src/data_source/weibo.py import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from src.data_source.base import BaseDataSource class WeiboDataSource(BaseDataSource): def __init__(self): # 构建带重试策略的 session self.session = requests.Session() retry_strategy = Retry( total=3, backoff_factor=1, # 指数退避:1s, 2s, 4s status_forcelist=[429, 500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retry_strategy) self.session.mount("http://", adapter) self.session.mount("https://", adapter) # UA 池(从真实浏览器 UA 列表中随机选,避免被识别为爬虫) self.ua_pool = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ] def fetch_raw_data(self, keyword: str, days_ago: int = 7) -> List[Dict]: # 1. 计算时间范围(微博搜索只支持近 7 天,超期自动截断) end_time = datetime.now() start_time = end_time - timedelta(days=min(days_ago, 7)) # 2. 设置请求头,每次请求随机 UA headers = { "User-Agent": random.choice(self.ua_pool), "Accept": "application/json, text/plain, */*", "Referer": "https://s.weibo.com/" } # 3. 构造搜索 URL(关键:用微博官方搜索 API,非前端渲染页) # 注意:此处使用模拟登录后的 cookies(需提前用 Selenium 或手动登录获取) # 实际项目中,cookies 存于 config/secrets.yaml,由 dotenv 加载 search_url = f"https://s.weibo.com/ajax/search/suggest?q={keyword}&type=all" try: response = self.session.get(search_url, headers=headers, timeout=10) response.raise_for_status() # 解析 JSON 响应(微博 API 返回结构化数据,比解析 HTML 稳定 10 倍) data = response.json() return self._parse_weibo_response(data, keyword) except requests.exceptions.RequestException as e: # 记录结构化错误日志,便于后续告警 logger.error(f"Weibo fetch failed for '{keyword}': {str(e)}") return [] def _parse_weibo_response(self, api_data: dict, keyword: str) -> List[Dict]: """将微博 API 响应转为标准字典列表""" items = [] for item in api_data.get("result", []): # 微博 API 字段名与系统契约字段映射 items.append({ "title": item.get("title", ""), "content": item.get("content", ""), "publish_time": datetime.fromtimestamp(item.get("created_at", 0)), "source_url": f"https://weibo.com/{item.get('id', '')}", "platform": "weibo" }) return items

参数说明:

  • days_ago=7:微博搜索限制,硬编码为 7 是防止用户误输 30 导致请求失败;
  • backoff_factor=1:首次重试等待 1 秒,第二次 2 秒,第三次 4 秒,避免雪崩;
  • status_forcelist:明确指定哪些 HTTP 状态码触发重试,429(限流)必须包含;
  • timeout=10:单次请求超时设为 10 秒,超过则放弃,防止线程卡死。

2.3 设计存储层契约:用 SQLAlchemy ORM 定义舆情数据模型,让 MySQL 表结构随代码演进

舆情数据不是扔进 CSV 就完事。真实业务中,你需要按时间范围查某品牌声量趋势、按情感极性统计负面占比、按平台对比传播效率——这些都依赖规范的数据库设计。我们用 SQLAlchemy ORM 定义SentimentRecord模型,所有字段类型、索引、约束都在 Python 类里声明,后续用alembic迁移,杜绝手动改表:

# src/models.py from sqlalchemy import Column, Integer, String, Text, DateTime, Float, Boolean, Index from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime Base = declarative_base() class SentimentRecord(Base): __tablename__ = "sentiment_records" id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(500), nullable=False, index=True) # 标题加索引,加速关键词检索 content = Column(Text, nullable=False) # 正文用 TEXT,避免 VARCHAR 长度限制 publish_time = Column(DateTime, nullable=False, index=True) # 时间索引,支撑按天聚合 source_url = Column(String(1000), nullable=False, unique=True) # 唯一约束,防重复入库 platform = Column(String(50), nullable=False, index=True) # 平台索引,支撑多源对比 sentiment_score = Column(Float, nullable=True, default=0.0) # 情感分(-1~1),NULL 表示未分析 is_positive = Column(Boolean, nullable=True) # 二分类标签,加速 COUNT 查询 keyword = Column(String(100), nullable=False, index=True) # 关键词索引,支撑多主题监控 created_at = Column(DateTime, default=datetime.utcnow, index=True) # 记录入库时间,用于数据血缘 # 复合索引:高频查询组合(平台+时间+关键词) __table_args__ = ( Index('ix_platform_time_keyword', 'platform', 'publish_time', 'keyword'), )

为什么这样设计?

  • source_url UNIQUE:微博转发链中同一条原博会被多个账号转载,URL 去重是去噪第一关;
  • publish_time INDEX:舆情分析核心是时间序列,没有时间索引,SELECT COUNT(*) WHERE publish_time > '2024-01-01'会全表扫描;
  • ix_platform_time_keyword:当你查“小米手机在微博近 24 小时的负面声量”,这个复合索引让查询从秒级降到毫秒级;
  • sentiment_score DEFAULT 0.0:新入库数据默认中性,避免 NULL 参与计算导致结果异常(如 AVG(NULL, 0.5) = NULL)。

初始化数据库只需一行命令(已封装在manage.py中):

# 创建表(首次运行)或执行迁移(后续字段变更) python manage.py db upgrade

2.4 情感分析模块:TextBlob 快速启动 + SnowNLP 中文加固 + BERT 微调接口预留

舆情系统的核心价值是“读懂情绪”。新手常陷入“用哪个库最好”的争论,但真实路径是:先用轻量级工具跑通闭环,再按需升级。本系统提供三级分析能力:

层级工具适用场景响应速度准确率(中文)
L1 快速验证TextBlob英文舆情、简单英文评论< 10ms/条~65%(英文)
L2 中文主力SnowNLP微博、知乎、新闻正文~50ms/条~78%(经测试集验证)
L3 高精度定制HuggingFace Transformers品牌专属语料、金融/医疗垂直领域~300ms/条>85%(需微调)

系统通过策略模式注入分析器,config.py中一键切换:

# config.py SENTIMENT_ANALYZER = "snownlp" # 可选: "textblob", "snownlp", "bert" # src/analysis/sentiment.py from snownlp import SnowNLP from textblob import TextBlob def get_sentiment_analyzer(analyzer_name: str): if analyzer_name == "textblob": return lambda text: TextBlob(text).sentiment.polarity elif analyzer_name == "snownlp": return lambda text: SnowNLP(text).sentiments # 返回 0~1,转为 -1~1 elif analyzer_name == "bert": # 预留接口:加载本地微调好的 bert-base-chinese 模型 from transformers import pipeline classifier = pipeline("sentiment-analysis", model="./models/bert_chinese_finetuned", tokenizer="bert-base-chinese") return lambda text: classifier(text)[0]["score"] * (1 if classifier(text)[0]["label"]=="POSITIVE" else -1) else: raise ValueError(f"Unknown analyzer: {analyzer_name}")

关键细节:

  • SnowNLP 的sentiments返回 0~1,我们统一映射为 -1~1(score * 2 - 1),与 TextBlob 量纲对齐;
  • BERT 接口预留./models/bert_chinese_finetuned路径,实际项目中用transformers.Trainer在自有语料上微调,准确率提升显著;
  • 所有分析器包装为lambda,保证调用签名一致:analyzer(text) -> float。

3. 数据清洗与标准化:为什么你的“负面词库”越扩越不准?

3.1 清洗流水线:用 Pandas DataFrame 链式操作处理脏数据

原始数据充满噪声:微博标题带“【】”符号、知乎回答含大量 HTML 标签、新闻稿有版权声明水印。我们构建DataCleaner类,用 Pandas 链式操作(.pipe())实现可复用、可测试的清洗步骤:

# src/cleaner.py import re import pandas as pd from bs4 import BeautifulSoup class DataCleaner: def __init__(self): # 预编译正则,提升性能 self.re_html_tag = re.compile(r'<[^>]+>') self.re_special_char = re.compile(r'[^\w\s\u4e00-\u9fff]') # 保留中文、字母、数字、空格 self.re_extra_space = re.compile(r'\s+') def clean_text(self, text: str) -> str: """清洗单条文本:去 HTML、去特殊符号、规整空格""" if not isinstance(text, str): return "" # 1. 去 HTML 标签(知乎、新闻源常见) text = self.re_html_tag.sub('', text) # 2. 去除特殊符号(保留中文、英文字母、数字、空格) text = self.re_special_char.sub(' ', text) # 3. 合并多余空格 text = self.re_extra_space.sub(' ', text).strip() return text def clean_dataframe(self, df: pd.DataFrame) -> pd.DataFrame: """清洗整个 DataFrame,链式调用""" return (df .assign(title=lambda x: x['title'].apply(self.clean_text)) .assign(content=lambda x: x['content'].apply(self.clean_text)) .assign(keyword=lambda x: x['keyword'].str.strip()) # 关键词去首尾空格 .query("title != '' and content != ''") # 过滤空标题/空正文 .drop_duplicates(subset=['source_url'], keep='first') # URL 去重 ) # 使用示例 cleaner = DataCleaner() clean_df = cleaner.clean_dataframe(raw_df)

为什么不用正则一次性搞定?
因为链式操作.pipe()支持单元测试:你可以单独测试clean_text方法,也可以对clean_dataframe注入 mock 数据验证去重逻辑。而一长串re.sub()嵌套无法拆解验证。

3.2 中文分词与停用词:用 jieba + 自定义词典解决“苹果手机”被切成“苹果/手机”

中文分词是舆情分析的基石。jieba 默认词典会把“iPhone15”切为“iPhone/15”,把“特斯拉降价”切为“特斯拉/降价”,但我们需要“iPhone15”作为一个整体词参与 TF-IDF 计算。解决方案:加载自定义词典 + 关键词强制合并。

# src/cleaner.py import jieba class ChineseTokenizer: def __init__(self, custom_dict_path: str = "config/custom_dict.txt"): # 加载自定义词典(每行一个词,如:iPhone15 100 nz) jieba.load_userdict(custom_dict_path) # 预定义品牌词典(避免每次调用都读文件) self.brand_terms = {"苹果手机", "华为Mate60", "小米汽车", "特斯拉"} def tokenize(self, text: str) -> List[str]: words = list(jieba.cut(text)) # 步骤1:合并品牌词(如“苹果 手机” → “苹果手机”) merged_words = [] i = 0 while i < len(words): # 检查当前词+下一个词是否构成品牌词 if i < len(words) - 1: two_word = words[i] + words[i + 1] if two_word in self.brand_terms: merged_words.append(two_word) i += 2 continue merged_words.append(words[i]) i += 1 # 步骤2:过滤停用词(从 config/stopwords.txt 加载) stopwords = self._load_stopwords() return [w for w in merged_words if w not in stopwords and len(w) > 1] def _load_stopwords(self) -> set: with open("config/stopwords.txt", "r", encoding="utf-8") as f: return set(line.strip() for line in f if line.strip())

config/custom_dict.txt示例:

iPhone15 100 nz 华为Mate60 100 nz 小米汽车 100 nz 特斯拉 100 nz

参数说明:

  • 100是词频,数值越大,jieba 越倾向将其作为一个整体切分;
  • nz是词性(名词-其他),帮助后续 POS 过滤;
  • len(w) > 1过滤单字词(如“的”“了”虽不在停用词表,但无意义)。

3.3 时间标准化:把“3小时前”“昨天”“2024-01-01”全转成 datetime

不同平台时间格式千奇百怪:

  • 微博:“3小时前”,“今天 14:23”,“2024-01-01 10:00”
  • 知乎:“2 天前”,“2024-01-01T10:00:00Z”
  • 新闻 RSS:“Wed, 01 Jan 2024 10:00:00 +0800”

硬写正则维护成本极高。我们采用dateparser 库 + 规则兜底的混合方案:

# src/utils/time_utils.py import dateparser from datetime import datetime, timedelta def parse_publish_time(time_str: str) -> datetime: """ 解析各种格式的时间字符串,返回标准 datetime """ if not time_str: return datetime.now() # 1. 尝试用 dateparser 解析(支持中文相对时间) parsed = dateparser.parse( time_str, settings={ 'RELATIVE_BASE': datetime.now(), # “3小时前”以此为基准 'TIMEZONE': 'Asia/Shanghai', 'RETURN_AS_TIMEZONE_AWARE': True } ) if parsed: return parsed.astimezone().replace(tzinfo=None) # 去时区,存入 MySQL DATETIME # 2. dateparser 失败时,用预定义正则兜底 # 匹配 “今天 14:23” today_match = re.match(r'今天\s+(\d{1,2}:\d{2})', time_str) if today_match: time_part = today_match.group(1) today = datetime.now().date() return datetime.combine(today, datetime.strptime(time_part, "%H:%M").time()) # 匹配 “3小时前” hour_match = re.match(r'(\d+)小时前', time_str) if hour_match: hours = int(hour_match.group(1)) return datetime.now() - timedelta(hours=hours) # 3. 兜底:返回当前时间(数据质量差时保底) logger.warning(f"Failed to parse time: {time_str}") return datetime.now()

为什么不用纯正则?
dateparser能处理“上个月15号”“下周三”等复杂相对时间,而正则只能覆盖有限模式。我们只在dateparser失败时启用正则兜底,平衡鲁棒性与开发效率。


4. 避坑指南:那些让舆情系统上线即崩溃的 4 个血泪经验

4.1 现象:MySQL 存储时报错Data too long for column 'content' at row 1

原因:微博正文可能含超长 URL 或 base64 图片编码,content字段设为VARCHAR(1000)不够用,而TEXT类型在 SQLAlchemy 中需显式指定长度(MySQL 5.7+ 要求Text(length=...))。
解决:在SentimentRecord.content字段声明中,必须指定Text(length=65535)(MySQL TEXT 最大长度),而非Text()默认值(某些方言下为 255):

content = Column(Text(length=65535), nullable=False) # ✅ 正确 # content = Column(Text, nullable=False) # ❌ 错误,MySQL 下可能截断

4.2 现象:多线程采集时,日志文件疯狂刷屏,磁盘 IO 扛不住

原因:每个线程独立打开日志文件写入,造成文件锁竞争和频繁 flush。
解决:全局单例日志器 + QueueHandler 异步写入。在src/logger.py中:

import logging from logging.handlers import QueueHandler, QueueListener import queue # 创建全局队列 log_queue = queue.Queue(-1) # 无上限队列 # 主日志器(主线程创建) root_logger = logging.getLogger("sentiment") root_logger.setLevel(logging.INFO) queue_handler = QueueHandler(log_queue) root_logger.addHandler(queue_handler) # 启动后台监听线程(只在主线程 run) listener = QueueListener(log_queue, logging.FileHandler("logs/app.log")) listener.start() # 所有模块通过 root_logger 获取 logger,自动走队列 logger = logging.getLogger("sentiment.weibo")

4.3 现象:SnowNLP 分析中文时,sentiments方法返回 0.0,所有文本被判中性

原因:SnowNLP 训练语料为简体中文,若输入含繁体字、异体字或大量英文混排,概率模型失效。
解决:清洗阶段强制转简体 + 过滤低质量文本。引入opencc库:

pip install opencc-python-reimplemented
from opencc import OpenCC cc = OpenCC('t2s') # 繁体转简体 def preprocess_for_snownlp(text: str) -> str: text = cc.convert(text) # 先转简体 # 过滤英文占比 > 30% 的文本(SnowNLP 对纯英文无效) en_chars = len(re.findall(r'[a-zA-Z]', text)) if en_chars / len(text) > 0.3: return "" # 返回空,跳过分析 return text

4.4 现象:concurrent.futures.ThreadPoolExecutor提交 1000 个任务,内存暴涨后 OOM

原因:submit()返回Future对象,若不显式future.result()或future.cancel(),对象长期驻留内存,且ThreadPoolExecutor默认max_workers=None(等于 CPU 核心数),但 I/O 密集型任务应设为CPU_COUNT * 5。
解决:

  • 显式设置max_workers:ThreadPoolExecutor(max_workers=20)(I/O 密集推荐 10~50);
  • 提交后立即future.add_done_callback()处理结果,避免堆积:
def handle_future_result(future): try: result = future.result() # 获取结果,触发清理 save_to_db(result) # 保存 except Exception as e: logger.error(f"Task failed: {e}") with ThreadPoolExecutor(max_workers=20) as executor: futures = [executor.submit(fetch_one_source, kw) for kw in keywords] for future in futures: future.add_done_callback(handle_future_result) # ✅ 回调中清理

5. 让系统真正可用:构建可验证、可监控、可交付的舆情分析工作流

5.1 用 pytest 编写 3 类核心测试,拒绝“能跑就行”

一个舆情系统是否可靠,看它有没有这三类测试:

测试类型覆盖点示例代码片段为什么必须
单元测试单个函数逻辑(如parse_publish_time)assert parse_publish_time("3小时前") == datetime.now() - timedelta(hours=3)防止时间解析逻辑被意外修改
集成测试数据源 + 清洗 + 存储全流程test_weibo_to_mysql_flow():调用WeiboDataSource.fetch_raw_data→DataCleaner.clean_dataframe→save_to_db,断言 MySQL 表中记录数 > 0确保各模块胶水层正常
端到端测试CLI 命令完整执行test_cli_run_command():用subprocess.run(["python", "main.py", "--keyword", "AI"]),检查logs/app.log是否含"Success: saved 127 records"验证交付物可直接运行

tests/test_time_utils.py示例:

import pytest from src.utils.time_utils import parse_publish_time from datetime import datetime, timedelta def test_parse_3_hours_ago(): # Mock datetime.now() for deterministic test now = datetime(2024, 1, 1, 12, 0, 0) # Monkey patch datetime.now import datetime as dt original_now = dt.datetime.now dt.datetime.now = lambda: now result = parse_publish_time("3小时前") expected = now - timedelta(hours=3) assert result.year == expected.year assert result.month == expected.month assert result.day == expected.day assert result.hour == expected.hour # Restore dt.datetime.now = original_now

注意:时间相关测试必须 Mockdatetime.now(),否则测试结果随系统时间漂移,CI/CD 会间歇性失败。

5.2 监控关键指标:用 Prometheus + Grafana 看清系统健康度

舆情系统不是“跑起来就结束”,它需要持续可观测。我们在src/metrics.py中暴露 4 个核心指标:

from prometheus_client import Counter, Histogram, Gauge # 1. 采集成功率(按平台) fetch_success_total = Counter( 'sentiment_fetch_success_total', 'Total number of successful fetches', ['platform'] ) # 2. 数据入库量(按关键词) records_saved_total = Counter( 'sentiment_records_saved_total', 'Total number of records saved to DB', ['keyword'] ) # 3. 情感分析耗时(直方图) sentiment_analysis_duration = Histogram( 'sentiment_analysis_duration_seconds', 'Time spent on sentiment analysis', buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 5.0] ) # 4. 当前待处理任务数(Gauge) pending_tasks = Gauge( 'sentiment_pending_tasks', 'Number of pending tasks in queue' )

在main.py中暴露/metrics端点:

from prometheus_client import make_wsgi_app from werkzeug.serving import make_server # 启动 metrics server(独立端口,避免干扰主服务) metrics_app = make_wsgi_app() server = make_server('0.0.0.0', 8000, metrics_app) server.serve_forever() # 后台线程运行

Grafana 面板配置建议:

  • 采集成功率看板:rate(sentiment_fetch_success_total[1h])按platform分组,阈值 < 95% 触发告警;
  • 数据延迟看板:time() - max(sentiment_records_created_at),超过 15 分钟标红;
  • 分析耗时看板:直方图sentiment_analysis_duration_seconds_bucket,观察 99 分位是否 < 1s。

5.3 交付物清单:一个 zip 包里必须有的 7 个文件

基于python的网络舆情分析系统.zip不是代码堆砌,而是可交付的工程制品。解压后目录结构必须如下,缺失任一文件,系统即不可用:

sentiment-system/ ├── README.md # 含快速启动命令、配置说明、常见问题 ├── requirements.txt # conda/pip 依赖清单(含版本号,如 pandas==2.0.3) ├── config/ # 配置目录 │ ├── config.yaml # 数据库连接、API Key、采集频率等 │ ├── custom_dict.txt # 品牌词典(供 jieba 加载) │ └── stopwords.txt # 中文停用词表 ├── src/ # 源码目录(按功能分包) │ ├── __init__.py │ ├── data_source/ │ ├── models.py # SQLAlchemy 模型 │ ├── cleaner.py # 数据清洗 │ └── ... ├── logs/ # 日志目录(空,运行时生成) ├── models/ # 模型目录(空,BERT 模型放此处) ├── tests/ # 测试目录 └── main.py # 主入口:python main.py --keyword "新能源汽车"

关键交付细节:

  • requirements.txt必须锁定版本:pandas==2.0.3而非pandas>=2.0.0,避免 CI 环境因新版本 bug 失败;
  • config.yaml模板中,数据库密码字段必须为DB_PASSWORD: ${DB_PASSWORD},通过环境变量注入,绝不硬编码;
  • README.md第一行必须是# 基于 Python 的网络舆情分析系统,第二行## 快速启动,第三行1. conda env create -f environment.yml—— 让新人 3 分钟内看到第一条数据。

5.4 我的运维习惯:每天凌晨 2 点自动执行的 3 个检查脚本

系统上线后,我坚持每天凌晨 2 点用 cron 执行三个检查,它们像哨兵一样守着系统:

脚本 1:检查昨日数据完整性(check_daily_data.py)

# 查询昨日各平台数据量,低于阈值则邮件告警 yesterday = (datetime.now() - timedelta(days=1)).date() for platform in ["weibo", "zhihu", "news"]: count = session.query(SentimentRecord).filter( SentimentRecord.platform == platform, func.date(SentimentRecord.publish_time) == yesterday ).count() if count < 50: # 阈值按业务调整 send_alert(f"{platform} 昨日数据仅 {count} 条,可能采集异常")

脚本 2:检查情感分析覆盖率(check_sentiment_coverage.py)

# 计算未分析数据占比,> 5% 则告警(说明 SnowNLP/BERT 服务宕机) total = session.query(SentimentRecord).filter( SentimentRecord.publish_time >= datetime.now() - timedelta(days=1) ).count() unanalyzed = session.query(SentimentRecord).filter( SentimentRecord.sentiment_score == 0.0, SentimentRecord.publish_time >= datetime.now() - timedelta(days=1) ).count() if unanalyzed / total > 0.05 <p> <a href="https://download.csdn.net/download/huaairen/89230786" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进