☰
基于SVM与随机森林的上市公司新闻文本分类实战:从爬虫到模型部署
2026/9/30 2:59:05 网站建设 项目流程

简介:这份资源是面向金融数据分析初学者与文本挖掘实践者的完整项目源码,围绕上市公司新闻的抓取、文本分析与分类预测展开,帮助读者理解从数据采集到模型落地的全流程。项目通过爬虫脚本从财经、每经网、金融界、中国证券网、证券时报网等平台获取历史新闻,再经分词、停用词过滤、财经词典辅助等预处理,提取词袋与TF-IDF特征,并训练SVM、随机森林等分类器实现新闻自动归类。资源包共21个文件,以17个Python源代码文件为主,覆盖爬虫、文本处理、特征提取、模型训练与评估等模块,另含3个txt文本文件(财经词典与中文停用词表)及1个许可证文件,压缩包约180KB,结构清晰便于按模块学习。目前已有350人学习下载,适合希望掌握Python文本分析与机器学习分类实战的读者参考复用。

1. 从 21 个文件说起:这套上市公司新闻文本分析与分类预测源码能跑出什么

拿到一个压缩包,先别急着解压看代码,我习惯先扫一遍文件清单。这个包一共 21 个文件,17 个 Python 源码、2 个文本词典、1 个 LICENSE、1 个 readme,结构非常清晰:Crawler/负责抓,Text_Analysis/负责算,run_main.py是总入口。它解决的是一个很具体的问题——把财经、每经、金融界、中国证券网、证券时报网这几家平台的上市公司历史新闻批量抓下来,做中文分词、去停用词、特征提取,再用 SVM 和随机森林训练分类器,最终对新闻做自动归类预测。适合谁?正在做 Python 数据分析、文本挖掘、机器学习课程设计的学生,以及想跑通「爬虫→NLP→分类模型」完整链路的初级算法工程师。它不是一个生产级系统,但作为一套可复现的端到端练手项目,链路完整度是够的。

2. 拆开 Crawler 目录:五个爬虫脚本的选型逻辑与启动方式

2.1 为什么是这五家,而不是随便抓

Crawler/下对应五个平台各一个模块:crawler_cnstock.py、crawler_tushare.py、crawler_nbd.py、crawler_stcn.py、crawler_jrj.py,另外还有crawler_sina.py。每个模块配一个run_crawler_xxx.py作为独立启动入口,这种「一个平台一个模块 + 一个 run 脚本」的拆法,好处是某个平台改版时只动那一个文件,不会牵连全局。

选这五家是有讲究的。中国证券网和证券时报网偏官方公告与政策解读,文本规范、噪声少;每经网和金融界偏市场化报道,标题党多、口语化重;Tushare 严格说不是爬虫目标,它是财经数据接口,crawler_tushare.py大概率是拉股票列表或行情做辅助关联。这几类语料混在一起,正好覆盖了「规范文本 + 噪声文本」两种分布,训练出来的分类器泛化能力比单一来源强。如果你只抓一家,模型很容易过拟合到那家的写作风格上。

2.2 启动爬虫前必须确认的三件事

直接python run_crawler_cnstock.py大概率会翻车,先做三件事。

第一,确认依赖。常见做法是建虚拟环境再装:

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install requests beautifulsoup4 lxml pandas tushare jieba scikit-learn

requests负责请求,beautifulsoup4+lxml负责解析 HTML,jieba是后面分词要用的,scikit-learn提供 SVM 和随机森林。Tushare 单独装,因为它有自己的一套 token 机制。

第二,确认目标网站的页面结构。爬虫模块里通常写死了 CSS 选择器或 XPath,比如soup.select('.news-list li a')这种。网站一改版,选择器就失效,报错往往是AttributeError: 'NoneType' object has no attribute 'text'。遇到这个,打开浏览器 F12 重新定位列表容器和标题节点,把选择器换掉即可。

第三,控制请求频率。财经网站对高频访问敏感,脚本里如果没有time.sleep(),跑几百条就可能被限流甚至封 IP。我一般会在循环里加:

import time import random for page in range(1, 51): url = f"https://example.com/news?page={page}" resp = requests.get(url, headers=headers, timeout=10) # 解析逻辑... time.sleep(random.uniform(1.5, 3.5)) # 随机间隔,降低被识别为机器的概率

random.uniform(1.5, 3.5)比固定sleep(2)更稳,固定间隔反而容易被风控识别出规律。timeout=10必须有,否则某个请求卡死,整个脚本就挂在那了。

2.3 抓下来的数据长什么样

爬虫模块一般会把结果写成 CSV 或直接存成列表供后续调用。字段通常包括标题、正文、发布时间、来源平台、股票代码。这里有个容易忽略的点:正文里往往混着 HTML 标签、 、全角空格、编辑署名等噪声。如果爬虫阶段不清,后面分词阶段就得补,建议在爬虫模块里就做一次粗清洗:

import re def clean_text(raw): raw = re.sub(r'<[^>]+>', '', raw) # 去 HTML 标签 raw = raw.replace('&nbsp;', ' ').replace('\u3000', ' ') raw = re.sub(r'\s+', ' ', raw) # 合并连续空白 raw = re.sub(r'(编辑[::].*?)', '', raw) # 去编辑署名 return raw.strip()

re.sub(r'<[^>]+>', '', raw)是通用去标签,\u3000是全角空格,中文网页里极常见。编辑署名那条正则按实际文本调整,不同平台格式不一样。

3. Text_Analysis 里的文本挖掘链路:从停用词到 TF-IDF 特征矩阵

3.1 两个词典文件决定了预处理质量

Chinese_Stop_Words.txt和finance_dict.txt是这套源码里最容易被低估的两个文件。停用词表负责过滤「的、是、了、在」这类高频但无区分度的词;财经词典则是自定义词典,喂给 jieba 用的,保证「市盈率」「资产负债率」「归母净利润」这类术语不被切碎。

jieba 加载自定义词典的方式:

import jieba jieba.load_userdict('finance_dict.txt') # 财经术语不被切碎 def load_stopwords(path): with open(path, 'r', encoding='utf-8') as f: return set(line.strip() for line in f if line.strip()) stopwords = load_stopwords('Chinese_Stop_Words.txt')

load_userdict接受的文件格式是「词语 词频 词性」三列,词频和词性可省略。如果你发现「上市公司」被切成「上市」「公司」,就是词典没生效或词条没加进去。停用词用set而不是list,因为后面过滤时是if word not in stopwords,集合查找是 O(1),列表是 O(n),几万条文本下差距很明显。

3.2 text_processing.py 与 text_mining.py 的分工

按命名推断,text_processing.py管清洗和分词,text_mining.py管特征提取和向量化。典型的分词函数长这样:

import jieba.posseg as pseg def segment(text, stopwords): words = [] for word, flag in pseg.cut(text): # 只保留名词、动词、形容词,过滤虚词 if flag in ('n', 'vn', 'v', 'a') and word not in stopwords and len(word) > 1: words.append(word) return words

pseg.cut带词性标注,flag是词性。只留n(名词)、vn(动名词)、v(动词)、a(形容词),是因为新闻分类真正有区分度的是实词,虚词和标点只会增加维度噪声。len(word) > 1过滤单字,中文单字歧义太大。

特征提取这一步,源码描述里提到了词袋和 TF-IDF。实际用 TF-IDF 更合理,因为它能压低「公司」「股票」这种到处都出现的词的权重:

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( tokenizer=lambda x: x, # 传入的已经是分好词的列表 preprocessor=lambda x: x, token_pattern=None, max_features=5000, # 只保留权重最高的 5000 个词 ngram_range=(1, 2) # 一元词 + 二元词组 ) X = vectorizer.fit_transform(tokenized_corpus)

max_features=5000是控制维度的关键,不设的话词表可能上万维,训练慢且容易过拟合。ngram_range=(1, 2)让「新能源」「汽车」也能组合成「新能源汽车」这样的二元特征,对新闻分类有实际提升。注意tokenizer和preprocessor都设成恒等函数,是因为输入已经是分好词的列表,不能再让 vectorizer 自己切。

3.3 标签从哪来

分类预测要有标签才能训练。这套源码的标签来源通常有两种:一是按新闻来源平台打标(财经 vs 每经 vs 金融界),二是按股票行业板块打标。前者简单,爬虫时顺手就记了;后者需要额外关联股票代码和行业表。如果你拿到的数据没有标签列,先确认run_main.py里是怎么构造y的,别急着跑模型。

4. 训练 SVM 与随机森林:参数怎么设、结果怎么看

4.1 两个分类器的定位差异

SVM 在文本这种高维稀疏数据上表现稳定,尤其是线性核,训练快、不容易过拟合。随机森林是集成方法,对特征尺度不敏感、能输出特征重要性,但维度上万时训练比 SVM 慢。源码同时用这两个,意图很明显:做对比实验,看哪个在当前语料上更好。

SVM 的典型配置:

from sklearn.svm import LinearSVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) svm = LinearSVC(C=1.0, max_iter=2000) svm.fit(X_train, y_train) y_pred = svm.predict(X_test) print(classification_report(y_test, y_pred))

LinearSVC比SVC(kernel='linear')快得多,文本分类首选。C=1.0是正则强度,C 越大越容易过拟合,文本任务一般从 0.1 到 10 之间调。stratify=y保证训练集和测试集里各类别比例一致,类别不均衡时必须有,否则某类可能全跑测试集里去了。

随机森林:

from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=200, # 树的数量 max_depth=None, # 不限制深度,让树充分生长 min_samples_leaf=2, # 叶子最少 2 个样本,防过拟合 n_jobs=-1, # 用满所有 CPU 核 random_state=42 ) rf.fit(X_train, y_train)

n_estimators=200是精度和耗时的平衡点,再往上收益递减。min_samples_leaf=2是我一般会加的,默认 1 时每棵树容易长出只含一个样本的叶子,噪声全被记住。n_jobs=-1并行训练,能省不少时间。

4.2 评估指标别只看准确率

classification_report会输出 precision、recall、f1-score。新闻分类如果类别不均衡(比如某平台新闻量远大于其他),准确率会骗人——全预测成多数类也能有 70%+。重点看 macro avg 的 f1,它给每个类别等权重。如果某类 recall 特别低,说明模型把这类样本大量误判成别的类,要么加这类数据,要么调class_weight='balanced'。

4.3 特征重要性反查

随机森林训练完可以看哪些词贡献最大:

import numpy as np feature_names = vectorizer.get_feature_names_out() importances = rf.feature_importances_ top_idx = np.argsort(importances)[-20:][::-1] for i in top_idx: print(feature_names[i], round(importances[i], 4))

这一步很实用。如果 top 词里出现「编辑」「转载」「来源」这种,说明清洗没做干净,模型在学噪声而不是内容。我见过最离谱的一次,top1 特征是「责任编辑」,因为某平台每篇都有这四个字,模型直接靠它分类。

5. 避坑与排查:这套源码跑不通时先查这五处

5.1 现象:爬虫返回空列表,程序不报错但没数据

原因:目标网站改版,CSS 选择器失效,select()返回空列表,循环体不执行,自然没数据也不报错。

解决:打开目标页面 F12,重新定位新闻列表的容器节点,更新选择器。加一行print(len(items))在解析后,能第一时间发现是 0 条。

5.2 现象:jieba 分词后全是单字,术语被切碎

原因:finance_dict.txt没加载,或文件编码不是 UTF-8,load_userdict静默失败。

解决:确认jieba.load_userdict在分词之前调用,且文件路径正确。用jieba.lcut('市盈率')单独测一下,如果输出['市', '盈', '率']就是词典没生效。

5.3 现象:TF-IDF 矩阵维度上万,训练极慢甚至内存溢出

原因:max_features没设或设得太大,词表爆炸。

解决:设max_features=5000到 10000 之间,同时用min_df=2过滤只出现一次的词。min_df=2能砍掉大量拼写错误和低频噪声词。

5.4 现象:模型准确率虚高,但换一批数据就崩

原因:训练集和测试集来自同一时间段、同一平台,分布太像,模型记住了平台特征而非内容特征。

解决:按时间切分而非随机切分,用早期数据训练、后期数据测试。或者按平台切分,用 A 平台训练、B 平台测试,看真实泛化能力。

5.5 现象:Tushare 模块报 token 错误

原因:crawler_tushare.py需要 Tushare 的 API token,源码里通常是占位符。

解决:去 Tushare 注册获取 token,替换脚本里的占位字符串。如果只是跑文本分类,这个模块可以暂时跳过,不影响主链路。

6. 让这套源码真正可用:从跑通到跑好的三个进阶动作

跑通run_main.py只是起点。我拿到这类项目,习惯做三件事让它从「能跑」变成「能用」。

第一件,把爬虫结果落盘成中间层。别让爬虫和训练耦合在一次运行里,爬完存成 CSV,训练时读 CSV。这样调模型参数时不用反复爬,省时间也少给目标网站添麻烦。存的时候用df.to_csv('news_raw.csv', index=False, encoding='utf-8-sig'),utf-8-sig带 BOM,Excel 打开不乱码。

第二件,加一个基线对比。别只跑 SVM 和随机森林就下结论,加一个朴素贝叶斯或逻辑回归做基线。如果复杂模型比基线高不了两三个点,说明特征质量才是瓶颈,该回去优化分词和停用词表,而不是继续调模型参数。我一般会用一个简单的MultinomialNB做基线,几行代码的事:

from sklearn.naive_bayes import MultinomialNB nb = MultinomialNB(alpha=0.1) nb.fit(X_train, y_train) print('NB baseline:', nb.score(X_test, y_test))

alpha=0.1是平滑系数,文本分类常用 0.01 到 1 之间。基线跑出来如果和 SVM 差距在 3 个点以内,优先查特征而不是换模型。

第三件,把模型和向量器一起持久化。训练完不保存,下次预测又得重训,浪费时间。用 joblib:

import joblib joblib.dump(vectorizer, 'tfidf.pkl') joblib.dump(svm, 'svm_model.pkl') # 预测新新闻时 vectorizer = joblib.load('tfidf.pkl') model = joblib.load('svm_model.pkl') new_vec = vectorizer.transform([segment(new_text, stopwords)]) print(model.predict(new_vec))

注意预测时transform而不是fit_transform,用训练好的向量器,否则词表对不上,维度不一致直接报错。这个坑我踩过不止一次,训练时fit_transform、预测时手滑写成fit_transform,结果每次预测词表都在变,模型输出全是乱的。

从那以后我每次保存模型,都会顺手写一个predict.py单独测一条新样本,确认加载、向量化、预测整条链路通,再收工。希望帮到你。

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

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

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

立即咨询