☰
金融新闻分类预测实战:从爬虫到逻辑回归的NLP毕设指南
2026/10/1 5:38:45 网站建设 项目流程

简介:基于Python实现的上市公司新闻文本分析与分类预测完整代码与报告,主要面向计算机毕业设计、金融文本挖掘和自然语言处理学习者,帮助解决从海量新闻中提取股票事件并判断利好利空这类完整课题。压缩包共21个文件、仅63KB,以17个Python脚本为主体,覆盖爬虫、数据处理、文本分析、主流程等环节,另含停用词与金融词典两个txt、README说明及license,轻量且结构清晰。已有1702人学习下载。读者可获得新浪财经、每经网、金融界、中国证券网等财经网站的新闻爬虫,Tushare沪深日线数据获取脚本,以及完整的“去停用词、加载新词、分词、抽取相关股票、按N日涨跌贴利好利空标签、文本分类预测”流水线实现。附有报告文档框架,适合快速复现实验、扩展研究方向或作为毕业设计参考资料。

1. 上市公司新闻文本分析与分类预测:毕设做这个到底值不值

拿到这个题目时,很多人第一反应是"又要分词、又要深度学习,是不是得装显卡驱动跑BERT"。实际上,这是一个把 python 爬虫、文本清洗、特征工程、分类模型和数据分析可视化串成一条完整链路的选题,它的核心矛盾很简单:每天财经网站产生的公司新闻上千条,人工根本盯不过来,分类预测要做的是让机器自动判断一条新闻对上市公司是利好、利空还是中性,再用行情数据验证判断有没有道理。对计算机毕设来说,这个方向技术栈完整、数据真实可获取、结果能画成图表在答辩现场展示,比单纯做一个CRUD管理系统有价值得多。新手用它入门NLP,熟手可以用它做对比实验,门槛低但上限不低。

2. 从爬虫到语料库:先把数据链打通,再谈模型精度

2.1 数据从哪来:抓取策略与反爬边界

上市公司新闻没有一个现成的公开CSV文件,绝大多数毕设是自己动手爬财经网站。我一般用 requests + BeautifulSoup 做轻量爬虫,目标数据量在三千到一万条之间,这个规模足够训练一个像样的分类模型,又不至于把实验室IP搞进黑名单。

import requests from bs4 import BeautifulSoup import time import random UA_POOL = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", ] def fetch_list_page(url): headers = {"User-Agent": random.choice(UA_POOL)} resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" return BeautifulSoup(resp.text, "html.parser") def parse_news_links(soup, max_items=20): news_items = [] for a in soup.select("a[href*='roll'], a[href*='content']"): title = a.get_text(strip=True) href = a.get("href") if title and href and href.startswith("http"): news_items.append({"title": title, "url": href}) if len(news_items) >= max_items: break return news_items

这段代码从一个新闻列表页里提取标题和详情链接。UA_POOL的作用是让每次请求使用不同浏览器标识,避免固定指纹被服务端标记;a[href*='roll']是常见财经站点列表页链接的特征,实际使用时用浏览器的开发者工具检查你选定的页面,把选择器换成页面真实存在的结构。爬取详情页时同样要做编码设置和超时控制,resp.encoding = "utf-8"这一步不能省,否则新闻正文会出现大量乱码。

强调一点:爬虫只用于课程设计与学习研究,必须控制请求频率。列表页之间加随机延时两秒到五秒已经足够,不需要并发。

def fetch_detail(url): headers = {"User-Agent": random.choice(UA_POOL)} resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") content_div = soup.select_one("div.article-content") or soup.select_one("div#content") if content_div: return content_div.get_text(" ", strip=True) return ""

详情页的选择器因网站差异很大,我在代码里给了两个常见的候选,实际跑的时候可以把页面源码打印出来,在浏览器里直接复制对应的CSS路径,比靠猜稳妥得多。抓下来的字段建议存成stock_code, stock_name, title, content, news_date, url六列表格,后续切分和联动的原始依据都靠这些字段,宁多勿少。

2.2 清洗与分词:模型精度的隐藏决定因素

爬下来的新闻正文里混着HTML标签残余、URL、空格和大量数字,这些噪声不处理,TF-IDF特征里会冒出一堆无意义的"高维垃圾"。清洗和分词看起来是体力活,实际上它对最终F1的影响比换模型骨架还大。

import re import jieba STOP_WORDS = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: STOP_WORDS.add(line.strip()) def clean_text(text): text = re.sub(r"<[^>]*>", "", text) # HTML标签 text = re.sub(r"http\S+", "", text) # URL text = re.sub(r"\d+(\.\d+)?%?", " <NUM> ", text) # 数字和百分比归一化 text = re.sub(r"[^\u4e00-\u9fa5A-Za-z0-9]", " ", text) # 特殊符号转空格 return text def tokenize(text): words = jieba.lcut(clean_text(text)) return [w for w in words if w.strip() and w not in STOP_WORDS and len(w) > 1] jieba.load_userdict("finance_dict.txt")

这里最值得留意的是百分比没有被删除,而是被替换成<NUM>占位符。"同比增长120%"和"同比下降12%"原本是两个不同字符串,归一成增长 <NUM>和下降 <NUM>后,模型才能把数字变化这种共性特征学出来。如果直接删掉数字,模型就分不清"预增"和"预减"了。

finance_dict.txt是金融术语词典,每行一个词:

业绩预增 业绩预减 股东减持 股权质押 问询函 涨停 跌停

jieba 对"问询函""股权质押"这类专有名词经常切错成"问询/函""股权/质押",手动加入词典后分词结果会明显改善。验证方法很简单,对"公司收到交易所问询函,控股股东拟减持不超过2%"这一句打印分词结果,肉眼检查一遍是否有断裂。

2.3 标签定义:人工标注与弱标签的取舍

分类预测要预测什么,取决于标签怎么定。三种常见方案各有代价,我按容易出现的情况整理成表格。

标注方案做法优点明显代价
人工标注读新闻内容标利好/利空/中性标签质量高,语义准确耗时,3000条要标两三周
行情弱标签按新闻发布后首个交易日涨幅 >1% 算利好,< -1% 算利空,其余中性零人工成本,数据量大个股受大盘影响大,噪声很高
混合方案用弱标签全量标注,人工抽检1000条计算准确率,必要时修正兼顾规模与可信度需要额外写抽检脚本

毕设里最容易被答辩老师追问的是"标签是怎么来的",我建议直接说明采用了混合方案:先用行情弱标签生成全量标签,再人工复核抽检,并报告抽检准确率。计算抽检准确率时还要提一个指标——标注一致性,如果两个同学各自标100条只有60%相同,说明标注规范本身太模糊,标签质量不会好。把这一步写进报告,会让数据部分显得扎实。

3. 特征工程与分类模型:从TF-IDF到逻辑回归,稳扎稳打

3.1 为什么从TF-IDF开始:答辩与调参都更稳

新闻文本要变成模型能吃的数值,最常见路线有三条:TF-IDF 向量、Word2Vec 词向量平均、BERT 句向量。BERT 效果最好,但它是典型的黑匣子,需要GPU、需要调参、推理慢,一旦答辩被问"为什么这个新闻被判利好",很难解释清楚。Word2Vec 需要先在语料上训练词向量或下载预训练词向量,再对句子做平均池化,这里又牵扯到词向量质量。

我最终的方案逻辑是:主线用 TF-IDF + 逻辑回归,这是基线也是完整方案。TF-IDF 每个特征都对应一个真实词汇,"业绩预增"这个特征权重高,就能在答辩时直接导出特征排序表讲话。逻辑回归的系数又天然带正负方向,利好词正系数大,利空词负系数绝对值大,解释性碾压神经网络。

3.2 TF-IDF向量化的参数:min_df、max_df、ngram_range的边界

sklearn 的TfidfVectorizer一行代码就能把分词列表转成稀疏矩阵,但参数设不对,特征空间全是垃圾。

from sklearn.feature_extraction.text import TfidfVectorizer corpus = [" ".join(tokens) for tokens in all_token_list] vectorizer = TfidfVectorizer( max_features=10000, min_df=5, max_df=0.8, ngram_range=(1, 2), sublinear_tf=True, ) X = vectorizer.fit_transform(corpus) print("特征维度:", X.shape[1])

corpus里的每个元素是一整条新闻分词后用空格拼接的字符串,不要传 list of list,sklearn 的文本向量化器接收的是字符串序列。四个关键参数经验值如下:

参数取值作用调大/调小的后果
max_features8000~10000只保留词频最高的前N个词太小丢长尾特征,太大内存涨
min_df3~5去掉只在极少数新闻出现的词过大会丢"质押""问询"等重要低频词
max_df0.7~0.9去掉出现在大多数新闻里的通用词太小会误删"增长""公司"
ngram_range(1, 2)词组特征,如"业绩预增"只做(1,1)会丢短语语义

注意fit_transform只在训练集上调用一次,测试集和推理时必须用vectorizer.transform(),否则会把测试集自己的词频统计混进来,造成特征分布漂移。很多新手在这里翻车,模型在测试集上的分数虚高,答辩演示时一预测真实新闻就崩。

3.3 分类模型:逻辑回归打底,再留一个对比实验的口子

特征矩阵准备好之后,分类模型的选择不用太纠结。逻辑回归在稀疏高维文本特征上表现稳定,训练速度快,线性决策边界对这个任务足够。

from sklearn.linear_model import LogisticRegression model = LogisticRegression( max_iter=2000, C=1.0, class_weight="balanced", solver="liblinear", random_state=42 ) model.fit(X_train, y_train)

solver="liblinear"适合中小规模稀疏矩阵,比默认的lbfgs收敛更快;class_weight="balanced"自动按类别频率调整权重,缓解中性样本过多的问题;C是正则化强度的倒数,C越小正则越强,可以在 [0.001, 0.01, 0.1, 1, 10] 上做网格搜索。我把random_state固定为42,保证每次跑结果一致,写报告时数据可复现。

如果你的毕设要求必须有深度学习对比,建议在逻辑回归之外加一个 TextCNN,而不是直接上 BERT。TextCNN 的嵌入层维度设为100,卷积核尺寸取2、3、4,各配128个滤波器,CPU上训练两万条新闻也只需要几十分钟。代码可以用 Keras 三四十行写完,但不要让它替代逻辑回归,把它写成"对比实验"章节,结论写成"线性模型在中小规模数据上以更低成本取得相近效果",这样既满足深度学习的创新点要求,又不会把自己拖进调参泥潭。

4. 训练、评估与落盘:让结果能复现、能展示、能交差

4.1 切分数据:别用默认的随机切分

金融文本有一条铁律:时间顺序不可乱。如果用train_test_split(test_size=0.2)默认的随机切分,同一事件的两篇报道可能一条进训练集、一条进测试集,模型等于提前看到了"答案",测试分数虚高。这在财经新闻场景里尤其常见,因为同一家公司的同一公告会被多个媒体改写转载。

正确做法是按时间切分或者按股票分组切分。

train_df = df[df["news_date"] < "2023-10-01"] test_df = df[df["news_date"] >= "2023-10-01"]

如果数据量小,按时间切分后测试集类别分布失衡,那就改用GroupShuffleSplit,按股票代码分组切分,保证同一只股票的新闻不会同时出现在训练集和测试集。切分之后打印两边的类别分布,确认三个类别的比例大致接近,比例偏差太大会让测试评估失真。

4.2 评估指标:准确率在类别不平衡时会骗人

三分类任务里中性新闻通常占50%以上,模型全猜中性也能拿到50%以上的准确率,这个数字在答辩时是站不住脚的。我一般把准确率、宏平均F1、每个类别的精确率和召回率一起打印出来,重点看负面新闻的召回率——对金融场景来说,漏掉一条利空新闻的代价远大于把中性误判成利好。

from sklearn.metrics import classification_report, confusion_matrix pred = model.predict(X_test) print(classification_report(y_test, pred, target_names=["负面", "中性", "正面"])) print(confusion_matrix(y_test, pred))

classification_report输出的macro avg F1是三类别F1的算术平均,不受中性样本占多数的影响,我建议把它作为报告里的核心评价指标。混淆矩阵则用来观察错误形态,比如"负面被误判成中性"的格子占比是否符合预期。

预测结果落盘是个容易被忽略的实操细节:

import pandas as pd result = pd.DataFrame({ "stock_code": test_df["stock_code"], "title": test_df["title"], "label_true": y_test, "label_pred": pred }) result.to_csv("pred_result.csv", index=False, encoding="utf-8-sig")

utf-8-sig编码会让生成的CSV在Excel里直接打开不乱码,这一步看似小,但答辩老师现场打开文件看到乱码会很尴尬。还要追加两列predict_proba的正类概率,后续做行情联动分析时直接用概率值做排序和相关性计算,比只用离散标签信息量更大。

4.3 把模型和向量化器打包保存

模型训练完毕,测试集评估完成,接下来要能对新新闻进行预测。新手最容易犯的错误是把向量化器和模型分开保存后,预测时忘了做transform而直接对字符串调用model.predict。一个更稳的做法是用Pipeline把两个步骤串起来。

from sklearn.pipeline import Pipeline import joblib pipe = Pipeline([ ("tfidf", vectorizer), ("clf", model) ]) pipe.fit(train_corpus, y_train) joblib.dump(pipe, "news_pipeline.pkl") loaded_pipe = joblib.load("news_pipeline.pkl") def predict_new(title): tokens = tokenize(title) proba = loaded_pipe.predict_proba([" ".join(tokens)])[0] return loaded_pipe.classes_[proba.argmax()], max(proba)

Pipeline保证了transform和predict永远成对出现,少一个环节就会直接报错,不会产生"你以为预测了、其实特征全错"的静默失败。predict_proba返回的数组顺序对应classes_属性,取argmax时不要写死索引,用classes_[索引]反查标签名,避免类别顺序变动导致标签错位。

5. 避坑清单:金融文本分类最容易翻车的5个环节

5.1 现象:训练效果很好,测试集也高分,但手工拿几条新新闻预测却一塌糊涂

原因在于数据泄漏。同一事件的多篇报道因为标题改写了几个字,被当成不同样本;或者弱标签用了次日涨跌幅,但新闻本身发布时间与数据表里的日期字段不匹配,导致模型偷看到了未来信息。解决思路是两步走,先对正文字符串做MD5去重和SimHash近似去重,再按时间切分数据集。

import hashlib df["text_hash"] = df["content"].apply(lambda x: hashlib.md5(x.encode("utf-8")).hexdigest()) df = df.drop_duplicates(subset="text_hash")

去重后的样本量会下降一成到两成,这是正常损耗,不要心疼。时间的完整新闻数据相对难拿,如果只有标题字段,至少要确保news_date是从页面元信息里解析出来的,而不是自己编的采集日期。

5.2 现象:模型永远输出"中性",永远猜不到利好和利空

这是类别不平衡的典型症状。用涨跌幅阈值打弱标签时,大盘平稳期超过60%的新闻落在正负1%之间,模型为了降低整体损失,把所有样本都判成概率占优的多数类。解决办法有三个,按优先级排列:把阈值从正负1%改成正负2%,缩小中性区间;用class_weight="balanced"让少数类获得更高权重;对中性样本做欠采样,把三个类别的数量压到接近。

经验值上,正负2%的阈值生成的三类别分布大致为 30% / 40% / 30%,这个比例对逻辑回归最友好。如果调完模型仍然偏爱中性,再考虑对少数类用SMOTE过采样。

5.3 现象:爬虫跑半个小时后IP被封,之前抓的数据全断在半路

高频请求加固定UA是最容易被反爬识别的组合。解决方法是控制节奏加随机化,列表页与详情页都加time.sleep(random.uniform(2, 5)),UA从池子里随机选,遇到403或502时退避重试三次。

for attempt in range(3): try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() break except Exception as e: print(f"第{attempt + 1}次失败: {e}") time.sleep(5 * (attempt + 1))

另外建议把已抓到的数据每50条增量写入CSV,这样即使中途被封,已经落盘的数据不会丢。单机单线程爬几千条新闻大约需要一两个小时,没有必要上协程或并发,爬太快反而是给自己找麻烦。

5.4 现象:股票名称对不上代码,新闻和行情数据合并之后大量空值

财经新闻正文里经常出现公司简称,"贵州茅台"写成"茅台""贵州茅台酒股份有限公司",而行情数据表里只有600519这个代码。爬虫如果只存了名称没存代码,后面合并时就会丢行。解决方法是建一个别名映射表,把所有简称映射到标准代码,清洗阶段就把正文和标题里的别名统一替换成代码。

alias_map = { "茅台": "600519", "贵州茅台": "600519", "平安银行": "000001", "中国平安": "601318", } def replace_alias(text): for name, code in alias_map.items(): text = text.replace(name, code) return text

更省事的方案是在爬列表页时就把URL里的股票参数解析出来,很多财经网站的个股新闻URL里直接带stock_code,这一步比事后匹配可靠得多。匹配不上时宁可直接丢弃样本,也不要保留错误的代码,错误代码会让行情联动分析的结果变成一团噪声。

5.5 现象:想用BERT提升效果,但实验室机器一跑就内存溢出

BERT系列模型的参数量级对内存和显存的要求很现实,本科生毕设如果没有GPU资源,硬跑BERT容易把时间耗在环境配置和等待OOM上。解决思路是分清主次:主线用前面的逻辑回归方案保证论文完整,BERT只做小规模的对比实验。具体做法是选一个小参数预训练模型,把max_length限制在64,冻结权重只训练分类头,这样CPU上也能跑通,虽然F1可能只比逻辑回归高两三个点,但已经足够在论文里写"预训练模型在金融文本上具备潜力,但因计算资源受限未做大规模调优"这样的客观结论。

6. 进阶玩法:把预测结果和次日行情做联动,报告亮点就有了

6.1 从"预测对不对"到"预测有没有用"

分类预测的评估指标只能证明模型学到了文本规律,答辩时老师一定会追问:"预测出利好利空,然后呢?" 所以要把预测结果和真实行情连起来,做最简单的相关性验证。逻辑是:如果模型输出的利好概率与股票次日收益显著正相关,说明情感判断有经济意义,而不仅仅是统计意义的分类正确。

from scipy.stats import spearmanr merged = pd.merge(result, price_ret, on="stock_code", how="left") merged = merged.dropna(subset=["next_day_ret"]) rho, p_value = spearmanr(merged["pos_proba"], merged["next_day_ret"]) print(f"Spearman相关系数: {rho:.4f}, p值: {p_value:.4f}")

pos_proba是模型预测为"利好"的概率,next_day_ret是新闻发布后第一个交易日的个股收益率。Spearman 相关系数对收益率的非正态分布不敏感,适合这种金融数据。p值小于0.05就说明相关性在统计意义上显著,哪怕相关系数只有0.1,在金融预测里也已经是有价值的信号。这个p值写进报告,比任何模型指标都更有冲击力。

6.2 用分组对比画出答辩时最有说服力的图

除了相关系数,还可以按预测概率把合并后的样本分成三组:高利好组、中性组、高利空组,分别计算每组新闻发布后未来五日的平均累计超额收益。超额收益用个股当日收益率减去大盘指数当日收益率,这样能剔除普涨普跌的影响。三步就能出结论:分组计算均值、做三组之间的t检验或方差分析、画一个三柱状图或箱线图。

写报告时把这一节命名为"模型预测与市场反应的关联性验证",放在分类评估之后。三组的平均超额收益如果呈现出单调递增或递减的趋势,答辩现场就是一张非常直观的成果图;即使趋势不明显,把p值如实报告并分析原因,比如"弱标签噪声导致信号被稀释",也依然体现了你理解了金融数据的特点。

做这个题目前前后后踩过的坑里,最深刻的教训是数据链路的可靠性永远排在模型之前。爬虫字段缺一个、时间解析错一个小时、别名映射漏一只股票,最后反映在模型分数上都是莫名其妙的两三个点。先花时间把每条新闻的代码、日期、正文对齐验证好,再开始调参,整个项目会顺畅得多。希望这篇笔记帮你在毕设路上少走一段弯路。

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

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

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

立即咨询