ARTICLE DETAIL

建站实战干货

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

基于Python的财经舆情情感分析与自动化报告系统

2026/9/12 23:42:37 拓冰建站 浏览量
基于Python的财经舆情情感分析与自动化报告系统 简介这一项目将东财股吧与新浪财经的爬虫采集与情感分析整合为自动化的舆情报告生成工具适合计算机相关专业学生用于课程设计、毕业设计也可供爬虫与NLP初学者进阶参考。压缩包共31个文件约8.01MB其中11个Python脚本覆盖爬虫、情感分析、数据存储、报告生成与邮件发送等模块文档类文件如docx报告、md说明、txt词典与停用词等辅助理解使用另含SQL脚本、配置文件和结构示意图便于快速部署。目前已有64人学习下载。资源提供完整可运行代码附带详细项目说明文档情感分析部分不仅内置SnowNLP、BosonNLP等多套词典还支持自定义评分数值并自动生成舆情分析报告既可直接用于相关课题演示也可在此基础上扩展功能。整体目录结构清晰适合拿到后按模块逐步学习和二次开发。1. 舆情监控的真实需求从股吧评论到可读报告当你需要盯某只股票的舆情打开东方财富股吧几十页帖子“明天涨停”“主力跑了”夹杂着各种噪声人工扫一遍至少一小时新浪财经的新闻列表更是每几分钟刷新一次。这个项目做的事情就是用爬虫把这两个数据源抓下来分别做词典情感分析和snownlp分析折算成分数写入MySQL最后用python-docx生成一份带情感分布、热度排名和结论的DOCX报告再由smtplib发到邮箱。适合用Python做毕设/课设的人也适合想在自己监控脚本里加“情绪指标”的从业者。整个闭环是requests数据采集 → 文本清洗 → 双路情感打分 → 分数入库 → 报表生成 → 邮件分发。2. 爬虫层架构UA池、配置文件和两个数据源2.1 数据源与字段设计东方财富股吧 vs 新浪财经这个项目选的两个源正好补位东方财富股吧给的是散户讨论文本新浪财经给的是新闻标题和摘要。一个代表“市场情绪”一个代表“事件驱动”。抓取字段也有意做了区分股吧存发帖标题、阅读数、评论数新浪财经存新闻标题、发布时间、来源。两个源的页面结构和分页方式差别很大所以爬虫模块拆成两个文件但公共的请求头、超时、重试逻辑抽到tools层复用。数据源入口URL页面类型抓取字段分页方式东方财富股吧guba.eastmoney.com/list,{code}.html列表页详情页标题、链接、阅读量、评论数、发布日期页码参数 {code}_{page}.html新浪财经finance.sina.com.cn/stock/新闻列表标题、URL、发布时间、来源翻页/滚动加载从代码目录能看出spider/sina_finance.py和spider/eastmoney_guba.py分别处理两个源都复用了tools/ua.py里的UA获取函数。这样设计的理由很简单之后如果加雪球或财经头条只需要新建一个spider模块继承公共请求方法而不是把每个页面都写成重逻辑。字段结构保持一致后续情感分析和入库模块就不用改。2.2 config.py与UA池让请求先活下来抓这类页面最先遇到的不是解析而是403或“访问频繁”。项目里把数据库连接、请求延时、目标列表全部放到config.py方便在不动业务代码的情况下调参。简化后的config.py大概是这样的# config.py DB_CONFIG { host: 127.0.0.1, user: root, password: your_password, database: finance_edu, charset: utf8mb4, } CRAWL_INTERVAL (0.5, 2.0) # 两次请求之间的随机秒数 REQUEST_TIMEOUT 10 # 单位秒 RETRY_TIMES 3 # 请求失败重试次数 GUBA_STOCK_CODE zssh000001 # 上证指数股吧 SINA_FINANCE_LIST https://finance.sina.com.cn/stock/ REPORT_SAVE_PATH ./output/舆情分析报告.docxDB_CONFIG用于score_store.py和email_send.py的持久化与记录CRAWL_INTERVAL故意设为元组配合random.uniform做随机延时。如果目标是高频抓取可以把区间缩到(0.2, 0.8)但如果对方有频率限制建议保持1秒以上。UA池存在ua_pool.sql里每行一条User-Agent启动时一次性读入内存-- ua_pool.sql 结构 CREATE TABLE ua_pool ( id INT PRIMARY KEY AUTO_INCREMENT, user_agent VARCHAR(255) NOT NULL, platform VARCHAR(32) DEFAULT pc, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO ua_pool (user_agent, platform) VALUES (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36, pc), (Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.2 Safari/605.1.15, mac);在tools/ua.py里随机从表里取一条UA并拼上Accept-Language和Referer。有一个细节股吧的Referer如果为空部分页面会返回“访问过于频繁”的提示所以爬虫代码里Referer要与目标域名保持一致。如果UA池数据量不够抓取一段时间后可能被识别出轮换规律建议至少存50条不同操作系统和浏览器版本的组合。2.3 爬虫模块的拆分思路对外只暴露“下一页”两个爬虫模块在结构上非常接近构造URL → 带headers请求 → 解析HTML → 返回结构化列表。我一般会把“翻页”逻辑封装在类内部对外只暴露fetch_page(page)方法这样清洗和分析层不关心URL怎么拼。下面这段是东财股吧模块的简化版。# spider/eastmoney_guba.py import random import time import requests from lxml import etree from tools.ua import get_ua class EastMoneyGubaSpider: def __init__(self, stock_codezssh000001): self.stock_code stock_code self.session requests.Session() def _build_url(self, page: int) - str: return fhttps://guba.eastmoney.com/list,{self.stock_code}_{page}.html def fetch_page(self, page: int): url self._build_url(page) headers { User-Agent: get_ua(), Referer: https://guba.eastmoney.com/, Accept-Language: zh-CN,zh;q0.9, } resp self.session.get(url, headersheaders, timeout10) resp.encoding utf-8 tree etree.HTML(resp.text) items [] # 常见结构列表页帖子标题在 article_title 下的 a 标签 for node in tree.xpath(//span[contains(class,article_title)]/a): title node.xpath(string(.)).strip() href node.xpath(./href)[0] if node.xpath(./href) else items.append({title: title, url: href, source: guba}) time.sleep(random.uniform(*CRAWL_INTERVAL)) return items这里用requests.Session而不是直接requests.get是为了复用TCP连接减少握手开销。xpath里的contains匹配能容忍class名带动态后缀但如果在实际抓取中遇到“返回列表为空”优先检查页面里class名是否被改成了article_title_202x之类的固定后缀改回完整class名即可。新浪财经模块的差异在于发布时间在新闻标题旁边需要额外提取一个time标签。它们的共同接口都是返回dict列表后续情感分析不关心数据来自哪个源。3. 情感分析词典打分与snownlp如何配合3.1 两种方法的分工逻辑从资源目录看sentiment目录下同时存在dict_analysis.py和snownlp_analysis.py这不是冗余。snownlp内部是朴素贝叶斯分类器对通用短文本效果好但遇到“机构出货”“业绩暴雷”这类金融黑话时常把负面判断成中性。词典法则直接命中“减持”“跌停”“清仓”等词对特定领域更敏感但组合短语容易误判。项目里的做法是两个分数都算再做加权融合snownlp分数负责整体语气倾向词典分数负责金融关键词的能量大小。融合之后既不会漏掉通用负面词也能抓住金融领域的黑话。3.2 金融情感词库与否定词/程度副词处理dict目录下有几个关键文件BosonNLP_sentiment_score.txt存的是带情感分数的词financial_sentiment_score.txt是金融场景补充词表程度副词.txt用于调整强度否定词.txt用于反转。加载和打分逻辑抽取出来看就是这个样子。# sentiment/dict_analysis.py def load_sentiment_dict(path): score_map {} with open(path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: score_map[parts[0]] float(parts[1]) return score_map def analyze_by_dict(text, seg_words, sentiment_dict, negative_words, degree_words, stopwords): total_score 0.0 count 0 degree 1.0 for w in seg_words: if w in stopwords: continue if w in degree_words: degree degree_words[w] # 例如 非常1.8较1.2 elif w in negative_words: degree * -1 # 出现否定词就把当前方向反转一次 elif w in sentiment_dict: total_score sentiment_dict[w] * degree count 1 degree 1.0 # 处理完一个情感词后重置程度 return total_score / count if count 0 else 0.0这段代码的关键在degree变量先命中“非常”把程度系数放大到1.8再遇到“不”就把系数变成负的于是“非常不看好”会被计算成正向程度的负值。实际金融文本里“不看好”“不及预期”恰恰靠这种连乘逻辑才能被识别出来。如果你的测试集里“不跌反涨”这类转折从句多建议把否定词作用范围限制在同一个分句而不是整句累计。遇到“跌停”这种词在BosonNLP里通常自带负分数不需要依赖否定词去救。3.3 snownlp_analysis.py的封装与融合方程snownlp的用法简单但直接调用会有两个坑一是返回的sentiments是0到1的概率越接近1越正向二是对股票标题这种没主语的短文本概率会被压到0.5附近。项目里封装时做了平滑处理把0.5映射为中性分0。# sentiment/snownlp_analysis.py from snownlp import SnowNLP def snownlp_prob(text: str) - float: return SnowNLP(text).sentiments # range: [0, 1] def snownlp_score(text: str) - float: prob snownlp_prob(text) return (prob - 0.5) * 2 # 映射到 [-1, 1]融合时如果文本被词典分析识别出金融关键词就提高词典法的权重否则以snownlp为主。这个规则可以写成一个简单的分支def final_emotion_score(dict_score, snow_score, has_negative_words): if has_negative_words: return 0.4 * snow_score 0.6 * dict_score return 0.7 * snow_score 0.3 * dict_score理由很简单出现金融敏感词时词典法的信号更可靠没有敏感词时snownlp的通用判断更稳定。这里的权重不一定要固定你可以拿历史复盘数据做一次网格搜索找到准确率最高的组合。资源里的BosonNLP_sentiment_score.txt本身带了约一万个词基本覆盖常见金融词表但如果你的目标是医药板块建议手动补充“集采”“临床失败”等词否则词典分会被中性化。4. 数据加工与舆情报告生成4.1 score_store.py把情感分数落到MySQL爬下来的标题不能每次都重新算分score_store.py负责把“文本各模型分数”写进MySQL。这里先看建表语句。CREATE TABLE IF NOT EXISTS sentiment_daily ( id BIGINT AUTO_INCREMENT PRIMARY KEY, source VARCHAR(20) NOT NULL, content_hash CHAR(64) NOT NULL UNIQUE, content TEXT NOT NULL, snownlp_score FLOAT DEFAULT 0, dict_score FLOAT DEFAULT 0, final_score FLOAT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_source_time (source, created_at) );content_hash是为了去重用hashlib.sha256对标题做摘要重复抓取时INSERT IGNORE即可。score_store.py中的核心插入语句用executemany批量提交避免每条连接一次数据库。# data_process/score_store.py import hashlib import pymysql def store_scores(items, conn): records [] for it in items: content it[title] if not content: continue digest hashlib.sha256(content.encode(utf-8)).hexdigest() records.append(( it[source], digest, content, it.get(snownlp_score, 0), it.get(dict_score, 0), it.get(final_score, 0) )) sql INSERT IGNORE INTO sentiment_daily (source, content_hash, content, snownlp_score, dict_score, final_score) VALUES (%s, %s, %s, %s, %s, %s) with conn.cursor() as cursor: cursor.executemany(sql, records) conn.commit()这个模块被拆出来很有必要只要数据库结构不变爬虫和情感分析都可以单独替换。content_hash的粒度是“标题”但同一标题在不同时间重复出现也会被去重所以如果你要分析“热度趋势”建议再加一列posted_date把同一标题在不同日期的数据区分开而不是只靠哈希。4.2 report_generate.py生成带表格的docx报告日报的核心是让看报告的人一眼知道今天情绪偏多还是偏空。report_generate.py使用python-docx生成报告结构依次是标题、统计摘要、情感分布表、Top5热帖。下面是一个可运行的简化版。# data_process/report_generate.py from docx import Document from docx.shared import Pt def generate_report(df, output_path): doc Document() doc.add_heading(舆情分析报告, 0) doc.add_paragraph(f统计日期{df[created_at].max()}) table doc.add_table(rows1, cols3) table.style Light Grid Accent 1 hdr table.rows[0].cells hdr[0].text 数据源 hdr[1].text 评论数 hdr[2].text 平均情感分 for source, group in df.groupby(source): row table.add_row().cells row[0].text source row[1].text str(len(group)) row[2].text f{group[final_score].mean():.2f} doc.add_heading(情感倾向Top5, level1) for _, r in df.nlargest(5, final_score).iterrows(): p doc.add_paragraph() p.add_run(r[content]).bold True p.add_run(f 得分{r[final_score]:.2f}) doc.save(output_path) return output_path平均情感分如果大于0.05可以给出“偏多”结论小于-0.05给“偏空”中间则提示“震荡”。这个阈值写死在report_generate.py里实际使用建议换成从数据库的历史分布算分位数避免行情单边时天天都报“偏多”。报告中还可以加入一段“高风险个股”名单把跌幅榜和强烈负面词同时出现的标题聚合起来。如果你觉得docx不够直观也可以在同一段流程里用matplotlib画一张情感分布直方图插入到表格下方但注意要先在Linux环境装中文字体否则图片里的中文会变成方块。4.3 whole_process.py串起全链路的主控脚本整个项目最终由data_process/whole_process.py调用它的工作顺序是初始化数据库连接 → 依次运行两个爬虫 → 对每条文本做清洗和双路情感分析 → 调用score_store存储 → 调用report_generate生成docx → 交给email_send发送。主控代码不长。# data_process/whole_process.py from spider.eastmoney_guba import EastMoneyGubaSpider from spider.sina_finance import SinaFinanceSpider from sentiment.dict_analysis import analyze_by_dict from sentiment.snownlp_analysis import snownlp_score from data_process.score_store import store_scores from data_process.report_generate import generate_report def run_pipeline(): items [] items EastMoneyGubaSpider(zssh000001).fetch_page(page1) items SinaFinanceSpider().fetch_news() for it in items: it[dict_score] analyze_by_dict(it[title], ...) it[snownlp_score] snownlp_score(it[title]) it[final_score] final_emotion_score(it[dict_score], it[snownlp_score], ...) store_scores(items, db_conn) generate_report(load_df(), 舆情分析报告.docx)注意这里所有模块都不直接print而是把状态写入日志文件。原因很简单任务挂在定时器里没人盯着终端日志是排错的唯一入口。5. 部署与自动化定时任务、邮件发送和排错5.1 email_send.py把报告作为附件发出去生成docx之后如果不推送给使用者价值和手动跑没区别。email_send.py用smtplib发送先读config.py里的邮箱配置再用MIMEMultipart把报告作为附件加进去。# data_process/email_send.py import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication def send_report(subject, body, attach_path, smtp_cfg): msg MIMEMultipart() msg[From] smtp_cfg[from_addr] msg[To] smtp_cfg[to_addr] msg[Subject] subject msg.attach(MIMEText(body, plain, utf-8)) with open(attach_path, rb) as f: part MIMEApplication(f.read()) part.add_header(Content-Disposition, attachment, filename(utf-8, , attach_path.split(/)[-1])) msg.attach(part) with smtplib.SMTP_SSL(smtp_cfg[smtp_server], smtp_cfg[smtp_port]) as server: server.login(smtp_cfg[from_addr], smtp_cfg[auth_code]) server.sendmail(smtp_cfg[from_addr], smtp_cfg[to_addr], msg.as_string())注意使用授权码而不是登录密码smtp_port用465走SSL很多免费邮箱的普通25端口在云服务器上默认被封。收件人、发件人、授权码统一放config.py不要写死在代码中。如果你的邮箱服务商支持TLS而不是SSL就把SMTP_SSL改成SMTP并显式调用starttls否则会报连接被重置。5.2 定时调度Linux crontab与Windows计划任务爬虫适合在交易时段后跑一次收盘舆情或者在早盘前生成前一日报告。Linux下直接加一行crontab。# 每个工作日 18:30 生成当日舆情报告 30 18 * * 1-5 cd /opt/finance_analysis_edu /usr/bin/python3 main.py /var/log/guba_report.log 21这里用main.py而不是whole_process.py是因为项目根目录的main.py对外提供了命令行入口可以传一个循环次数参数。main.py的片段是这样的。# main.py 命令行入口片段 import argparse def main(): parser argparse.ArgumentParser(description舆情分析流水线) parser.add_argument(--max-pages, typeint, default1, help股吧抓取最大页码) parser.add_argument(--no-send, actionstore_true, help只生成报告不发送邮件) args parser.parse_args() # 根据参数调整爬取页数和发送行为建议在crontab里把--max-pages调成3只抓前3页因为股吧第1页往往是热门帖前3页已经能代表当天情绪多抓并没有额外信息量反而增加被限流概率。Windows环境则用“任务计划程序”触发器设为每天18:30操作里填python.exe和main.py的完整路径。5.3 常见失败点与排查方法几个项目跑起来最容易出的问题整理如下。现象可能原因排查方法爬虫返回空列表页面结构更新或class名变化用浏览器DevTools重新定位xpath情感分全部接近0分词后的词不在词典里检查停用词表是否误杀了“涨停”等词数据库插入失败content_hash重复或字段为NULL看whole_process.log中的具体SQL报错邮件发送超时SMTP端口被运营商封锁换465端口或改发件服务器snownlp报UnicodeDecodeError文本来源编码不是utf-8resp.encoding改为gbk或gb18030排错时先用main.py里的单测模式抓一条数据不经过全流程能快速定位是解析层还是分析层的问题。如果情感分全部接近0优先看分词结果是否把数字和英文全部丢掉了因为“跌5%”“涨3个点”这类表达在简单分词里经常会被切成无关片段。6. 进阶把UA池做成动态流量控制6.1 伪随机UA与自适应请求间隔ua_pool.sql里存UA只是第一步进阶做法是按访问成功率动态调整UA池权重。维护一张request_log表记录每次请求的HTTP状态码连续失败超过3次的UA权重减半。请求间隔也不要用死区间而是根据最近一次响应时间调整如果响应耗时大于5秒把下一次延时上限提高50%。def adaptive_sleep(history): avg sum(history[-10:]) / len(history[-10:]) base random.uniform(0.8, 1.5) if avg 5: base * 1.5 time.sleep(base)这个函数传入response_time的历史列表每次请求后追加一个值。它解决的是“固定延时也不安全”的问题对方如果做滑动窗口限流固定1秒的请求必然在每分钟60次处全部命中。基于响应时间的自适应间隔能让请求频率跟随对方负载变化服务器压力大时自动放慢恢复后自动提速。6.2 文本相似度去重同一篇新闻可能同时出现在新浪财经和东方财富股吧直接算分会让它被数两次。用simhash或简单特征hash做去重标题和正文的前30字拼接后取sha256片段即可。但要注意“贵州茅台股东大会召开”和“贵州茅台股东大会通过分红方案”这种标题前缀相同、内容不同的情况。更好的做法是计算MinHash相似度阈值设为0.85只在插入数据库前过滤。score_store.py里的content_hash只解决完全重复解决不了近似重复所以这个步骤需要放在情感分析之前否则两条只有几个字差异的新闻会被重复计入平均分。6.3 情感分数的时间序列指标把final_score按日期聚合后可以得到一个日均情感分序列。用SQL窗口函数能直接看近7日变化。SELECT DATE(created_at) AS day, AVG(final_score) AS avg_score, COUNT(*) AS total, SUM(CASE WHEN final_score 0.05 THEN 1 ELSE 0 END) / COUNT(*) AS pos_ratio FROM sentiment_daily WHERE created_at DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(created_at) ORDER BY day;这个查询结果既可以画成折线图也可以作为下一版报告的第一页。当pos_ratio连续三天下降且avg_score转负时系统可以自动把报告标题改成“情绪降温预警”给盯盘的人一个明确的异常信号。本文还有配套的精品资源点击获取