ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Python股票分析系统:网络舆情数据流水线实战拆解

Python股票分析系统:网络舆情数据流水线实战拆解

简介:一份面向计算机相关专业学生及初学者的高分大作业项目,围绕网络舆情与股票市场的关联展开,综合利用Python爬虫、情感分析、数据可视化与预测建模等技术,实现从舆情数据采集到价格趋势预判的完整流程。资源共包含89个文件,其中49个Python源码文件构成系统主体,另配套CSV数据表、HTML展示页面、SQL数据库脚本、Word说明文档及环境配置文件,整体压缩包约2.87MB,模块划分清楚,便于按功能点学习与二次开发。已有88人学习下载。项目中不仅提供新闻/社交媒体舆情抓取、股票行情数据接口、情感倾向分析、可视化图表等核心模块,还附带项目立项文档、需求分析文档与软件计划说明,可帮助快速理解一个评分98分项目的设计思路与代码结构。所有源码经调试可本地运行,适合课程答辩、毕业设计参考及数据分析实战练习。

1. 基于网络舆情的 Python 股票分析系统:一套能跑通的数据流水线

你盯着一只股票的新闻刷了一上午,评论区情绪从“利好”刷到“完了”,可 K 线上还没动静。这时候你最需要的不是再读十条快讯,而是一套能把舆情量化成数字的流程。这个基于网络舆情的 Python 股票分析系统,就是一条从抓取新闻、情感打分、关联股价到给出预测的完整数据流水线。它不是只写写 PPT 的演示项目,而是带 MySQL 数据库、带 Web 界面、带爬虫模块、带 Python 源码和说明文档的高分大作业成品,适合计算机专业学生拿去消化课程设计,也适合想入门舆情分析与金融数据处理的从业者研究它的工程结构。

2. 系统架构与数据链路:三个核心模块怎么打通

拿到项目压缩包,先别急着跑,把文件结构看懂,后面排查问题会省一半时间。整个系统的骨架非常清晰:app.py作为应用入口,controller管路由,service管业务逻辑,model管数据库表映射,templates和static是 Web 页面层,news_data是爬虫落地的原始数据目录,外加environment.yml和两个 SQL 文件负责环境与数据初始化。这是课程设计里最标准的 MVC 变体,也方便你在此基础上改成自己的选题。

2.1 从文件结构反推技术选型

技术栈从文件名里基本能读出来:app.py加templates加static,典型的 Flask 项目结构;model目录里放着 ORM 映射,配合stocksystem0107.sql和database.sql,说明持久层用的是 MySQL;news_data目录说明爬虫不是只在内存里跑一圈,而是会把原始数据落盘。再加上environment.yml里锁定的数据分析、自然语言处理类依赖,整套系统就是“Flask 做壳、MySQL 做存储、爬虫做采集、NLP 做分析”的经典组合。

选 Flask 而不是 Django,对这个场景是合理的。项目核心是舆情分析和股票数据展示,不是内容管理,Flask 的轻量路由和自由目录结构能让每个模块独立成文件,调试时只需要盯着一个函数看。MySQL 则是课程设计的“安全牌”,导师评审时看到明确的建库脚本和初始数据,比 SQLite 显得完整得多。这套组合很适合做演示:页面能交互,数据库能查数据,爬虫能实时更新,每个环节都有可见的产物。

2.2 舆情数据采集链路:news_data 目录承担的角色

采集链路是这套系统里最容易被忽略、但最值得拆开看的部分。很多初学者写爬虫,习惯抓一条处理一条直接入库,这个项目却先把数据落到news_data目录,再做后续清洗。这个设计有两个实际好处:一是爬虫目标网站的页面结构经常调整,选择器失效是常事,原始数据落盘后可以反复调试解析逻辑,不用每次重新请求;二是导师检查时能看到真实的舆情样本,而不是只有数据库里加工后的结果。

具体流程是:爬虫按股票名称或行业关键词抓取新闻标题和正文,经过简单去重和清洗后写入本地文件,再由service层的分析模块读取。这样做还有个隐藏优点——如果当天采集失败,系统还能用历史落盘数据兜底跑通演示,不会被现场反爬搞得下不来台。

2.3 股价数据与舆情数据如何对齐:代码关联与时间对齐

舆情数据和股票价格数据天然存在对齐难题。新闻里很少直接写股票代码,比如一条标题为“某公司发布业绩预增公告”的新闻,正文里出现的通常是公司简称而不是六位代码,所以采集时必须维护一张“公司简称 -> 股票代码”的映射表,按关键词匹配后手动绑定。另一个问题是时间对齐:当天的舆情影响的是当天还是后面几天的股价,没有定论,所以系统在做相关性分析时会按 T 日舆情、T+1 或 T+2 日价格的方式做滞后对比。

这个设计说明作者不只是在做展示,而是认真考虑了金融数据的时序特性。你在复现时可以把时间窗口参数单独拎出来调,后面第 6 章会讲具体怎么验证滞后效果。

3. 本地部署与运行:从 environment.yml 到页面出来的完整路径

这个项目能不能跑起来,九成卡在环境和数据库两步。源码本身不太可能出错——毕竟经过调试和评审,但每个人的本机环境不一样,最容易翻车的恰恰是版本、编码、端口这类“边角料”。下面按我实际复现的顺序走一遍。

3.1 用 environment.yml 重建虚拟环境

压缩包里的environment.yml是这个项目最友好的部分,它把 Python 版本和主要依赖一次性锁好。打开命令行切到项目根目录:

conda env create -f environment.yml conda activate stocksystem

第一条命令会按照 yml 文件里的依赖列表创建虚拟环境,第二条命令激活它。需要注意,如果 yml 里锁定的 Python 版本和你本机 conda 默认源不匹配,创建可能失败,报错信息通常指向某一个具体包下载不了。常见做法是先把 yml 里的 Python 版本号改成你本机已有的版本,比如 3.9 到 3.11 之间,再重新执行;如果 conda 装到一半中断,也不用重复跑整条命令,改用pip install把缺的包补上即可。

这个文件本身值得你多看一眼。它能告诉你这套系统的技术栈底线:明确写了flask、pandas、requests、pymysql,说明 Web 框架、数据处理、HTTP 请求、数据库驱动四个方向都有覆盖;如果里面还有自然语言处理相关的库,那对应的就是 4.2 节的情感分析环节。后续你想给它加功能,第一件事就是往这个文件里补依赖。

3.2 MySQL 初始化:建库脚本与业务数据导入

数据库初始化是最容易顺序搞反的一步。压缩包里给了两个 SQL 文件:database.sql和stocksystem0107.sql。我的习惯是先打开database.sql看开头有没有CREATE DATABASE语句,有就先执行它建库建表,再导入业务数据;如果database.sql只包含表结构,那stocksystem0107.sql里大概率自带建库语句。命令行操作如下:

mysql -u root -p < database.sql mysql -u root -p < stocksystem0107.sql

导入完成后,进 MySQL 检查一下表数量和关键表的数据行数,确认不是空库。这里有两个实用细节:一是 MySQL 版本最好在 5.7 以上,否则某些字段类型或字符集设置可能不兼容;二是连接字符集务必用utf8mb4,下面的 config 配置里会体现,否则舆情数据里的中文和表情符号入库后就是乱码。

这个项目的数据质量比一般大作业强的地方在于:业务库里预置了新闻样本和股票历史行情,导入完成后不用等爬虫跑完就能看到页面效果,这对答辩演示很重要。

3.3 config.py 连接配置与启动应用

config.py是所有配置的集中地。数据库账号密码、密钥、连接参数都在这里,不把它改对,app.py一启动就会报数据库连接错误。核心配置段一般长这样:

# config.py 核心配置段,按本机环境修改后即可用 import os class Config: # Flask 会话密钥,生产环境务必替换 SECRET_KEY = os.getenv("SECRET_KEY", "dev-only-key") # MySQL 连接参数 HOST = "127.0.0.1" PORT = 3306 USER = "root" PASSWORD = "改成你自己的密码" # 本地 MySQL 密码 DB = "stocksystem" # SQLAlchemy 连接串,charset=utf8mb4 保证中文不乱码 SQLALCHEMY_DATABASE_URI = ( f"mysql+pymysql://{USER}:{PASSWORD}@{HOST}:{PORT}/{DB}" "?charset=utf8mb4" ) SQLALCHEMY_TRACK_MODIFICATIONS = False

mysql+pymysql表示用 PyMySQL 驱动连接 MySQL,这是 Flask 项目里最常见的组合。charset=utf8mb4这一行必须保留,舆情新闻里经常出现特殊符号和中文标点,只用默认的utf8在部分 MySQL 版本上会报编码错误。SECRET_KEY是 Flask 的会话签名密钥,本地调试用默认值没问题,如果要部署到公网必须改成随机字符串。

连接配置改完之后,启动应用:

python app.py

看到启动日志里出现Running on http://127.0.0.1:5000,浏览器打开这个地址就能进入系统界面。如果端口被占用,可以在启动命令里指定:

python app.py --host 0.0.0.0 --port 5001

0.0.0.0表示允许局域网内其他机器访问,方便演示时让评委用手机或另一台电脑打开页面。这一步能过,说明环境、数据库、代码三件事都通了。

4. 舆情采集与情感分析:把新闻变成情绪分数的实现细节

系统最核心的价值不在 Web 界面,而在“新闻怎么变成分数”。这个环节拆开看是三个步骤:爬虫把新闻抓回来,情感分析模块把文本转成分数,最后再把分数和股票代码绑定。每一步都有值得记录的工程细节。

4.1 爬虫模块:按关键词抓取新闻并落盘

news_data目录和采集模块承担的是数据入口。爬虫的设计思路是:按股票名称、公司简称和行业关键词构造搜索 URL,抓取标题和正文,清洗后写入本地。一个典型的采集函数如下:

# crawler.py —— 按关键词抓取新闻标题与正文,落到 news_data 目录 import requests import time from bs4 import BeautifulSoup from datetime import datetime HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } def fetch_news(keyword: str, pages: int = 3): results = [] for page in range(1, pages + 1): # 以关键词和页码构造检索地址,选择器按目标站实际结构调整 url = f"https://search.example.com/news?q={keyword}&p={page}" try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") # 常见的新闻列表结构:外层 .news-item,内部 .title 和 .content for item in soup.select(".news-item"): title = item.select_one(".title").get_text(strip=True) content = item.select_one(".content").get_text(strip=True) results.append({ "title": title, "content": content, "keyword": keyword, "crawled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) except requests.RequestException as e: print(f"第 {page} 页抓取失败: {e}") time.sleep(1) # 限速,避免高频请求触发反爬 return results

代码逻辑是循环翻页,每页解析新闻列表,把标题、正文、关键词和时间组装成字典。参数里值得注意的有三个:pages=3控制抓取深度,调成 5 会多抓两页但对时效性新闻差别不大;timeout=10是请求超时阈值,目标网站响应慢或连接被重置时不会卡死整个脚本;time.sleep(1)是限速,这个数字不能去掉,否则高频请求很快会触发反爬机制。

爬虫跑完之后,原始结果建议用 pandas 或 json 落盘到news_data目录,再走清洗入库流程。这里有一个实践经验:不要把“抓取”和“入库”写在同一个循环里,抓取时只做采集,入库前单独做去重和字段校验,这样哪一步出错都只影响局部。

4.2 情感分析:基于金融词典的情绪打分

文本情感分析是这个项目里最容易被导师追问的地方。常见的实现路线有两条:一是用开源库如 SnowNLP 直接读文本出情感分;二是自己做词典打分。这个项目更适合词典法,因为 SnowNLP 的默认语料来自电商评论,对金融文本的“利好、利空、增持、减持”这类词汇敏感度很差,硬用会得出“业绩暴雷”是正面情感的荒谬结果。词典法实现简单、可解释性强,答辩时也讲得清楚:

# sentiment.py —— 基于金融词典的情绪打分,返回 0~1 之间的分数 import jieba # 金融领域词典,按实际业务持续扩充 POSITIVE_WORDS = {"利好", "涨停", "增长", "盈利", "突破", "增持", "回购"} NEGATIVE_WORDS = {"利空", "跌停", "下滑", "亏损", "违规", "减持", "退市", "调查"} def score_text(text: str) -> float: # jieba 分词后统计正负向词出现次数 words = jieba.lcut(text) pos = sum(1 for w in words if w in POSITIVE_WORDS) neg = sum(1 for w in words if w in NEGATIVE_WORDS) total = pos + neg if total == 0: return 0.5 # 没有命中的情感词,判为中性 return round(pos / total, 4)

这里的核心逻辑是分词后统计正向词和负向词的出现次数,用正向占比作为情感分数。分数范围是 0 到 1,大于 0.6 偏积极,小于 0.4 偏消极,0.5 是中性。jieba.lcut(text)返回分词列表,这一步是词典法的基础;两个词典集合是这套系统的“灵魂”,你现在看到的正反向词条非常少,实际工程里要扩充到几百个词才能覆盖足够多的新闻场景。

这个实现有个明显的好处:每一条新闻的打分依据都可以溯源,导师问“为什么这条新闻是 0.8 分”,你可以直接说是命中了“增长”和“盈利”两个正向词。换成深度学习模型反而很难解释。代价就是阈值敏感,词典覆盖不全时中性判断会偏多,所以第 5 章会专门讲怎么调这块。

4.3 情感分数与股票代码的绑定策略

情感分数算出来了,还要和股票关联上。前面 2.3 节说过,新闻里通常只有公司简称,所以系统维护了一张映射关系,把“公司简称、行业关键词”映射到股票代码。绑定流程是:先对新闻正文做关键词匹配,命中简称就绑定代码,再按新闻发布时间归入对应的交易日。当天所有新闻的分数做加权平均,得到这只股票当日的“舆情情绪指数”。

这个指数的计算通常在service层完成,核心逻辑就是按代码和日期分组聚合:

# service 层聚合逻辑示意:按股票代码和日期求情感均值 import pandas as pd def daily_sentiment(news_df: pd.DataFrame) -> pd.DataFrame: # 先按代码分组,再按日期分组,取情感分数的均值作为当日舆情指数 grouped = ( news_df.groupby(["stock_code", "date"])["sentiment_score"] .mean() .reset_index() ) return grouped.rename(columns={"sentiment_score": "sentiment_index"})

groupby(["stock_code", "date"])是先按股票代码聚合,再按日期聚合,两层维度缺一不可——只按日期会混淆多只股票,只按代码会把不同日期的舆情混在一起。mean()是聚合方式,也可以换成带权重的加权平均,比如标题情感权重高、正文情感权重低。实际操作中我发现标题的权重应该比正文大,因为标题是编辑提炼过的核心信息。

5. 常见问题与避坑:环境、采集、分析三个环节的翻车现场

这个项目我前后复现了两轮,第一次卡在环境,第二次卡在数据为空。下面五条是踩过最深的坑,按“现象 -> 原因 -> 处理”的顺序写,你可以直接对照排查。

5.1 conda 环境创建中断与 Python 版本错位

现象是执行conda env create -f environment.yml时,依赖装到一半提示找不到某个包,或者 Python 版本解析失败,整个环境建到一半就停了。

原因是environment.yml里锁的 Python 版本可能在本机 conda 源里没有对应安装包,尤其是跨小版本的组合,比如锁了 3.8 但本机默认源只更新到 3.11,解析时就断了。处理方法是打开environment.yml,把 python 那行的版本号改成你本机已有的版本,再重新执行创建命令;如果只是个别包失败,更快的做法是先把环境建出来,再用pip install手动补缺失的包,不用重跑整个 yml。从那以后我每次拿到新项目,第一件事是先看 yml 里锁的 Python 版本,而不是直接执行命令。

5.2 SQL 导入后中文乱码或表结构缺失

现象是 SQL 文件导入成功,但页面查询出来的新闻标题全是“???”,或者直接报“Table doesn't exist”。

原因是字符集不匹配,或者两个 SQL 文件的导入顺序反了。database.sql如果包含建库语句,必须先执行它;如果先导入了stocksystem0107.sql,里面引用的表还没建出来,自然报表不存在。乱码问题则几乎都是连接字符集不是utf8mb4造成的,检查 config.py 里的连接串末尾有没有?charset=utf8mb4。处理方法是:先确认两个 SQL 文件的导入顺序,再用命令行登录 MySQL 查看SHOW CREATE TABLE的建表语句,确认表定义里的DEFAULT CHARSET也是utf8mb4,必要时手动ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4修复存量数据。

5.3 爬虫抓回来的结果是空列表

现象是爬虫脚本跑完没有任何报错,但results列表长度是 0,news_data目录里也没有新文件。

原因是目标网站的页面结构变了,代码里写的选择器select(".news-item")匹配不到任何元素;或者网站返回了验证码页面,resp.status_code是 200 但内容里没有新闻。处理方法是:先打印resp.text[:500]看返回内容是不是正常 HTML,如果是一段包含“验证”字样的脚本,就是反爬拦截了;如果页面结构变了,用浏览器开发者工具重新定位新闻列表的 class 名,改掉选择器。另外,不要忽略time.sleep(1)这行限速,去掉之后高频请求会在几分钟内被封。演示前如果实在抓不到实时数据,就用news_data里的历史数据跑通流程,这个项目设计时就留了这条后路。

5.4 情感分析结果像“玄学”:正面新闻打出低分

现象是一条标题为“某公司签署重大合同”的新闻,情感分析给出的分数只有 0.3,明显与语义相反。

原因是词典法只认词表里的词,“签署”“重大”“合同”都不在正向词典里,而正文里如果出现“风险提示”或“违规”,负向词先命中了,分数就被拉低。这本质上是词典覆盖不足和权重设计过于简单的问题。处理方法是扩充词典:把“签署”“中标”“合同”“战略合作”“超预期”加进正向词表,把“问询”“监管”“冻结”加进负向词表;同时给标题里的情感词加 1.5 倍权重,因为标题比正文更能代表新闻基调。做这一步时我一般会把历史新闻跑一遍,把分数在 0.4 到 0.6 之间的样本抽出来人工看一眼,找出漏掉的词条。

5.5 页面能打开但图表数据为空

现象是最难排查的一种:Flask 服务正常启动,页面渲染出来,但图表区域是空的,控制台也没有报错。

原因是查询条件不匹配:页面默认查的是“当天的舆情数据”,但数据库里预置的数据日期不是今天,或者默认股票代码和库里预置的不一致,SQL 查询结果为空,前端图表自然画不出东西。处理方法是:打开数据库直接查SELECT DISTINCT stock_code, date FROM sentiment_daily,看库里实际有哪些代码和日期范围,然后在页面上选择库里真实存在的数据;如果前端传参格式跟后端不匹配,比如日期传成了2025-01-07而后端期望的是20250107,也会查不到结果。这个坑的教训是:页面空白时不要先怀疑前端代码,先看后端接口返回的 JSON 是不是空数组。

6. 进阶验证与延伸:把情感指数和 K 线对齐

到这里系统已经能跑通了,但如果你只想让它停留在“能演示”的程度,那还不够。导师追问频率最高的一个问题会是:“你怎么证明舆情分析和股价有关系?”这一章就把这件事做实——用相关性分析验证情感指数与股价是否存在统计意义上的关联。

6.1 计算情感指数与收盘价的相关系数

系统里已经有了每日情感指数和股票历史价格,接下来要做的是把两张表按日期对齐,算相关系数和滞后相关性:

# validate.py —— 验证舆情情感指数与股价的相关性 import pandas as pd # 情感指数表:date, stock_code, sentiment_index sentiment = pd.read_csv("sentiment_daily.csv", parse_dates=["date"]) # 股价表:date, stock_code, close price = pd.read_csv("price_daily.csv", parse_dates=["date"]) merged = pd.merge( sentiment, price, on=["date", "stock_code"], how="inner" ) # 当日相关性 corr_t0 = merged["sentiment_index"].corr(merged["close"]) print(f"当日相关系数: {corr_t0:.4f}") # 滞后 1~5 天的相关性:T 日舆情对 T+N 日收盘价的影响 for lag in range(1, 6): merged["sentiment_lag"] = merged["sentiment_index"].shift(lag) corr_lag = merged["sentiment_lag"].corr(merged["close"]) print(f"滞后 {lag} 天: {corr_lag:.4f}")

pd.merge是按日期和股票代码把两张表对齐,how="inner"表示只保留两边都有的日期,避免空值影响相关系数。shift(lag)是滞后操作,把情感指数整体往后移 N 行,模拟“今天的情感影响 N 天后的股价”。实际跑出来的结果里,当日相关系数如果偏低是正常的,金融市场的反应本来就存在滞后;滞后 1 到 3 天的相关性如果明显高于当日,说明这套系统抓到了舆情扩散的节奏,这就是你项目里的核心亮点。

6.2 把验证脚本变成日常习惯

相关系数算完之后,我把这个验证脚本固化成了一个习惯动作:每次修改情感词典或爬虫策略,第一件事就是跑一遍当天数据的相关性,看分数是变好还是变差。如果滞后 1 天的相关性明显提升,说明新词典有效;如果分数反而下降,就得回退改动。从那以后,我每次拿到这类舆情分析项目,都强制自己先走一遍“采集 -> 打分 -> 对齐股价 -> 算相关性”的完整闭环,而不是只看页面能不能打开——数据流水线的价值在验证里,不在界面上。希望这些拆解能帮到你,祝顺利跑通。

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

RELATED READING

延伸阅读

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