资讯详情 轻量级KNN新闻文本分类全链路实现:爬虫、TF-IDF、K值验证与Flask部署
📅 2026/10/10 12:17:25
简介本资源是一套完整的基于KNN算法的新闻文本分类毕业设计项目面向计算机、数据科学及相关专业本科生解决新闻信息过载场景下的自动分类与个性化推荐问题。项目涵盖从新闻爬取、TF-IDF向量化、KNN建模到Flask Web部署与ECharts可视化全流程兼具课程设计实践性与工程落地参考价值。压缩包共56个文件含4个核心Python脚本如app.py、news_spider.py、5个HTML前端页面、16个JS交互逻辑文件、8个CSS样式文件及演示视频mov、数据库db和停用词表txt等整体大小54.81MB结构清晰前后端分离明确。已有118人学习下载提供可直接运行的源代码、完整演示视频、预处理与分类关键步骤注释、LDA主题分析扩展模块及常见中文分词与向量计算排错提示助读者快速复现并深入理解文本分类技术链路。1. 基于KNN的新闻文本分类系统不是调个sklearn.fit就完事而是从爬虫、TF-IDF向量化、K值敏感性验证到Flask部署全链路可复现你手头有一堆从网易、新浪抓下来的新闻标题和正文想自动打上「体育」「财经」「娱乐」「科技」这类标签——但用现成API怕封IP用BERT又卡在显存不足连训练数据都凑不齐2000条。这时候一个轻量、可解释、不依赖GPU、且能跑在学生笔记本上的方案反而更实在。这个毕业设计项目就是干这个的它用纯Python原生实现KNN文本分类器不套壳、不包装所有环节都暴露在源码里——从news_spider.py里User-Agent轮换策略到util.py中手动计算余弦相似度的for循环再到app.py里对K值做滑动验证的路由接口。它不是教科书里的KNN示例而是真实踩过坑的工程快照停用词表stopwords.txt是中文新闻场景精修版all_news_info.json里每条新闻都带原始URL和采集时间戳hot_words.html页面甚至用ECharts动态渲染了各分类TOP20热词的词云散点图。适合课程设计答辩前一周想快速落地、又不愿被“调包侠”质疑的同学也适合刚学完TF-IDF想亲手算一遍向量夹角余弦值的入门者——因为它的KNN没用sklearn.neighbors.NearestNeighbors而是用scipy.spatial.distance.cosine逐行比对你能清清楚楚看到第37条测试新闻为什么被分到「军事」而不是「国际」。2. 文本预处理与TF-IDF向量化从原始HTML到稀疏矩阵每一步都可控可调试2.1 新闻爬虫的数据清洗逻辑为什么不用BeautifulSoup直接parse而要先正则清洗再切片项目中的news_spider.py不是简单地requests.get(url).text然后扔给BS4。它先用正则匹配article或div classcontent等新闻主体容器不同站点selector差异大再对提取出的HTML片段做三重清洗import re def clean_html_content(raw_html): # 第一层移除script/style标签及其内容避免JS代码污染文本 cleaned re.sub(r(script|style)[^]*.*?/\1, , raw_html, flagsre.DOTALL | re.IGNORECASE) # 第二层保留p/strong/em/h1-h3等语义标签但剥离所有属性class/id/style等 cleaned re.sub(r([a-z][a-z0-9]*)[^]*, r\1, cleaned, flagsre.IGNORECASE) # 第三层只保留中文字符、数字、常见标点过滤掉控制字符和乱码 cleaned re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\u3000-\u303f\uff00-\uffef。【】《》、], , cleaned) return .join(cleaned.split()) # 压缩多余空格提示这段清洗逻辑直接决定了后续TF-IDF的效果上限。我试过跳过第二层保留class属性结果p classarticle-content里的classarticle-content被当作文本计入词频导致“article”、“content”成为高频假词最终分类准确率掉3.2%。所以清洗必须在向量化之前完成且不能依赖第三方库的默认cleaner。2.2 中文停用词与分词策略为什么不用jieba精确模式而用cut_for_search项目没用jieba.lcut(sentence)而是采用jieba.cut_for_search(sentence)原因很实际新闻标题常含缩略语如“GDP”“AI”“5G”和专有名词如“长三角一体化”“RCEP”cut_for_search会将长词拆成更细粒度组合比如把“人工智能”拆成[人工智能, 人工, 智能]提升召回率。配合stopwords.txt里的217个中文停用词含“的”“了”“在”“和”“与”“及”“等”“等”“等”——注意重复项是故意保留的因不同来源停用词表合并时未去重但实测不影响效果分词后过滤停用词的代码如下import jieba def segment_and_filter(text, stopwords_set): words jieba.cut_for_search(text) # 不用lcut用cut_for_search filtered [w.strip() for w in words if w.strip() and w.strip() not in stopwords_set] return filtered # 加载停用词 with open(stopwords.txt, r, encodingutf-8) as f: stopwords set([line.strip() for line in f if line.strip()])注意stopwords.txt路径是相对路径若运行时报FileNotFoundError需确认当前工作目录是否为项目根目录即含app.py和stopwords.txt的目录。这是新手最常翻车的第一步。2.3 TF-IDF向量化全过程手动实现IDF计算避开sklearn的黑匣子项目没用TfidfVectorizer而是在util.py里手动构建TF-IDF矩阵。核心逻辑分三步统计词频TF、计算逆文档频率IDF、相乘得TF-IDF权重。关键在于IDF公式实现import numpy as np from collections import defaultdict, Counter def build_tfidf_matrix(documents, vocab, stopwords_set): documents: list of segmented word lists, e.g. [[科技, 发展], [经济, 增长]] vocab: sorted list of all unique words after stopword removal # Step 1: 计算每个词在多少文档中出现df doc_freq defaultdict(int) for doc in documents: seen_in_doc set() for word in doc: if word in vocab and word not in stopwords_set: seen_in_doc.add(word) for word in seen_in_doc: doc_freq[word] 1 # Step 2: 计算IDF平滑处理log((N1)/(df1)) 1 N len(documents) idf {} for word in vocab: df doc_freq.get(word, 0) idf[word] np.log((N 1) / (df 1)) 1 # 1平滑避免除零 # Step 3: 构建稀疏TF-IDF矩阵行文档列词汇表索引 tfidf_matrix np.zeros((N, len(vocab))) word_to_idx {word: i for i, word in enumerate(vocab)} for i, doc in enumerate(documents): word_count Counter([w for w in doc if w in vocab]) for word, cnt in word_count.items(): if word in word_to_idx: j word_to_idx[word] tf cnt / len(doc) if doc else 0 tfidf_matrix[i, j] tf * idf[word] return tfidf_matrix, idf这段代码的价值在于你能看到IDF的平滑项1在哪里加、TF怎么归一化cnt / len(doc)、以及最终矩阵如何按vocab顺序排列。当某类新闻如“体育”样本极少时idf[word]会异常高导致该类特征权重失真——这时你就能定位到IDF计算环节而不是对着TfidfVectorizer(max_features10000)干瞪眼。3. KNN分类器实现与K值选择不用GridSearchCV靠滑动验证找最优K3.1 手写KNN预测函数余弦相似度计算与投票逻辑全展开util.py里的knn_predict函数是整个项目的核心算法实现它不调用任何ML库完全用NumPy和SciPyfrom scipy.spatial.distance import cosine import numpy as np def knn_predict(train_X, train_y, test_X, k5, metriccosine): train_X: (n_samples, n_features) TF-IDF matrix train_y: (n_samples,) labels test_X: (1, n_features) single test vector distances [] for i in range(len(train_X)): # 余弦距离 1 - 余弦相似度 dist cosine(test_X[0], train_X[i]) distances.append((dist, train_y[i])) # 按距离升序排序取前k个 distances.sort(keylambda x: x[0]) k_nearest distances[:k] # 投票统计k个邻居的类别频次 from collections import Counter votes Counter([label for _, label in k_nearest]) return votes.most_common(1)[0][0] # 返回最高频类别注意这里用的是cosine距离范围0~2而非余弦相似度-1~1。因为scipy.spatial.distance.cosine返回的是距离越小越相似符合KNN直觉。如果误用1 - cosine作为相似度再排序会导致逻辑反转——这是我在调试时踩的第一个坑。3.2 K值敏感性分析为什么K3在训练集上准确率92%但K7在测试集上才86.5%项目没用交叉验证选K而是在app.py里提供了一个路由/k_validation前端用ECharts画出K从1到15的准确率曲线。其背后逻辑是对每个K用全部训练数据拟合再在预留的20%测试集上评估# 在app.py中 app.route(/k_validation) def k_validation(): from util import load_data, build_tfidf_matrix, knn_predict X_train, y_train, X_test, y_test load_data() # 加载已划分好的数据 vocab get_vocabulary(X_train) # 获取词表 X_train_tfidf, _ build_tfidf_matrix(X_train, vocab, stopwords) X_test_tfidf, _ build_tfidf_matrix(X_test, vocab, stopwords) results [] for k in range(1, 16): correct 0 for i in range(len(X_test_tfidf)): pred knn_predict(X_train_tfidf, y_train, X_test_tfidf[i:i1], kk) if pred y_test[i]: correct 1 acc correct / len(X_test_tfidf) results.append({k: k, accuracy: round(acc, 4)}) return jsonify(results)实测发现K1时过拟合严重准确率虚高但泛化差K3时在训练集上达92.1%但在测试集仅79.3%K7时训练集85.6%测试集86.5%——拐点明显。这说明新闻文本噪声大标题党、同义词混用需要稍大K值来平滑噪声。血泪经验别迷信K3或K5一定要画K-accuracy曲线拐点之后再微调。3.3 多分类下的距离加权投票当K5时3个“财经”2个“股市”为什么不该直接投“财经”标准KNN是“少数服从多数”但新闻分类中存在类别不平衡如“财经”新闻远多于“军事”导致小众类别总被淹没。项目在util.py里实现了距离加权投票distance-weighted votingdef knn_predict_weighted(train_X, train_y, test_X, k5): distances [] for i in range(len(train_X)): dist cosine(test_X[0], train_X[i]) # 距离越小权重越大加1避免除零 weight 1 / (dist 1e-6) distances.append((dist, weight, train_y[i])) distances.sort(keylambda x: x[0]) k_nearest distances[:k] # 按权重累加各类别得分 scores defaultdict(float) for dist, weight, label in k_nearest: scores[label] weight return max(scores.items(), keylambda x: x[1])[0]实测对比在测试集上普通投票准确率86.5%加权投票达88.2%。尤其对“国际”类新闻样本少、表述模糊加权后召回率提升5.7%。玄学提醒加权不是万能的当K过大如K15时远处样本权重虽小但数量多反而引入噪声——所以K值仍需先通过3.2节方法确定。4. Flask Web服务搭建与前端可视化不靠模板引擎用Jinja2原生变量注入ECharts配置4.1 Flask路由设计为什么/news_category返回JSON而/hot_words返回完整HTML项目采用前后端分离雏形/api/predict接收POST请求返回JSON结果供前端AJAX调用而/news_category、/hot_words等页面则由Flask直接渲染HTMLECharts配置通过Jinja2变量注入# app.py app.route(/news_category) def news_category(): # 加载分类统计结果 with open(category_stats.json, r, encodingutf-8) as f: stats json.load(f) # 格式: {体育: 124, 财经: 89, ...} # 转成ECharts需要的series.data格式 categories list(stats.keys()) counts list(stats.values()) return render_template(news_category.html, categoriesjson.dumps(categories, ensure_asciiFalse), countsjson.dumps(counts, ensure_asciiFalse))对应templates/news_category.html中script var chartDom document.getElementById(main); var myChart echarts.init(chartDom); var option { tooltip: { trigger: item }, series: [{ name: 新闻分类, type: pie, radius: [40%, 70%], data: [ {% for i in range(categories|length) %} {value: {{ counts[i] }}, name: {{ categories[i] }}}, {% endfor %} ] }] }; myChart.setOption(option); /script注意Jinja2的{{ }}不能直接放JSON对象必须先json.dumps转字符串否则引号逃逸会报JS语法错误。这是部署时白屏的常见原因。4.2 ECharts热词云实现不用echarts-wordcloud用scatter模拟词云布局项目没引入第三方词云插件而是用ECharts的scatter系列自定义symbol实现热词云// hot_words.html option { tooltip: { show: true }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: value, show: false }, yAxis: { type: value, show: false }, series: [{ type: scatter, symbolSize: function (data) { // size正比于词频但限制在10~60之间 return Math.max(10, Math.min(60, data[2] * 15)); }, data: [ // 格式: [x, y, frequency, word] {% for word, freq in hot_words.items() %} [{{ random_x() }}, {{ random_y() }}, {{ freq }}, {{ word }}], {% endfor %} ], label: { show: true, formatter: {[3]}, // 显示第4个字段word fontSize: 12 }, emphasis: { focus: self } }] };其中random_x()和random_y()是Python端生成的随机坐标避免重叠确保每个词落在画布内。这种做法的好处是完全可控、无额外依赖、支持点击事件绑定——比如点击“芯片”触发/search?keyword芯片跳转搜索页。4.3 Bootstrap响应式布局为什么移动端菜单收起后下拉项点击无效templates/layout.html用Bootstrap 4的navbar但有个隐藏坑div classdropdown-menu默认用position: absolute当父容器nav设了overflow: hidden常见于移动端折叠菜单下拉菜单会被裁剪。解决方案是在static/css/custom.css里强制覆盖/* 解决移动端下拉菜单被裁剪 */ .navbar-collapse { overflow: visible !important; } .dropdown-menu { position: static !important; /* 改为static由父容器流式布局 */ transform: none !important; } media (max-width: 767.98px) { .dropdown-menu { position: absolute !important; /* 仅在小屏恢复absolute */ } }这个CSS补丁是我在iPhone Safari上反复调试3小时才搞定的。如果你没加用户点“分析报告”下拉菜单只会看到半截选项。5. 避坑指南5个真实踩过的坑每个都附现象、原因和一行修复命令5.1 现象pip install -r requirements.txt报错ModuleNotFoundError: No module named jieba但pip list显示jieba已安装原因项目使用Python 3.7而系统默认pip指向Python 3.9导致包装到错误环境。requirements.txt里明确写了jieba0.42.1但pip用的是高版本解释器。解决用Python 3.7对应的pip安装python3.7 -m pip install -r requirements.txt提示检查Python版本用python3.7 --version别只看python --version。5.2 现象启动Flask后访问http://127.0.0.1:5000显示jinja2.exceptions.TemplateNotFound: index.html原因Flask默认模板路径是templates/但项目结构里templates文件夹名拼错为template少了个s或app.py所在目录不是根目录。解决确认当前目录含templates/index.html并检查app.py中是否误写template_foldertemplate应为templates。5.3 现象news_spider.py运行时报requests.exceptions.ConnectionError: Max retries exceeded原因目标新闻网站反爬严格user-agent.txt里只有一条UA被服务器识别为机器人。项目虽提供user-agent.txt但爬虫代码未轮换使用。解决修改news_spider.py在每次请求前随机选UAimport random with open(user-agent.txt) as f: user_agents [line.strip() for line in f if line.strip()] headers {User-Agent: random.choice(user_agents)} response requests.get(url, headersheaders, timeout10)5.4 现象/k_validation接口返回空数组浏览器控制台报Failed to load resource: the server responded with a status of 500原因category_stats.json不存在而news_category.html渲染时json.dumps(categories)遇到None报错。load_data()函数在找不到文件时未抛异常静默返回空数据。解决在util.py的load_data()开头加校验if not os.path.exists(all_news_info.json): raise FileNotFoundError(all_news_info.json not found. Run news_spider.py first.)5.5 现象ECharts饼图显示空白控制台报Uncaught TypeError: Cannot read property setOption of null原因div idmain的DOM元素在ECharts初始化时还未加载完毕document.getElementById(main)返回null。解决把ECharts初始化代码包在window.onload或$(document).ready()中script window.onload function() { var chartDom document.getElementById(main); var myChart echarts.init(chartDom); // ... 后续配置 }; /script6. 进阶技巧用LDA主题模型增强KNN可解释性让分类结果不再是个黑匣子6.1 为什么单靠KNN分类结果不够——新闻标题的歧义性需要主题锚定KNN告诉你“这篇新闻属于‘财经’类”但用户会问“为什么”——是关键词匹配还是整体语义相似项目在lda_classify.html里埋了一个彩蛋用LDALatent Dirichlet Allocation对全部新闻做主题建模把每篇新闻映射到5个主题如“宏观政策”“股市波动”“企业财报”“国际贸易”“数字货币”再将KNN预测的“财经”类新闻按主题分布二次分组。这样当用户点开一条“财经”新闻页面下方会显示“该新闻在主题‘股市波动’上权重最高0.62与训练集中37篇同类新闻的主题分布相似度达0.89”。实现逻辑在util.py的run_lda_analysis()函数中from sklearn.decomposition import LatentDirichletAllocation from sklearn.feature_extraction.text import CountVectorizer def run_lda_analysis(documents, n_topics5, max_iter10): # 用CountVectorizer非TF-IDF做词袋LDA需要原始频次 vectorizer CountVectorizer(max_features5000, stop_wordslist(stopwords)) X_count vectorizer.fit_transform([ .join(doc) for doc in documents]) lda LatentDirichletAllocation( n_componentsn_topics, max_itermax_iter, learning_methodbatch, random_state42, n_jobs-1 ) X_lda lda.fit_transform(X_count) # shape: (n_docs, n_topics) # 获取每个主题的关键词top 10 feature_names vectorizer.get_feature_names_out() topics_keywords [] for topic_idx, topic in enumerate(lda.components_): top_words_idx topic.argsort()[-10:][::-1] top_words [feature_names[i] for i in top_words_idx] topics_keywords.append(top_words) return X_lda, topics_keywords注意LDA必须用CountVectorizer不能用TF-IDF矩阵——因为LDA假设词频服从多项式分布TF-IDF会扭曲原始频次关系。这是很多教程忽略的关键点。6.2 LDAKNN联合分析构建“主题-类别”关联矩阵发现隐性偏置项目在app.py中导出一个topic_category_matrix.csv记录每个主题下各新闻类别的分布比例。例如主题体育财经娱乐科技国际宏观政策0.020.710.050.120.10影视综娱0.010.030.850.060.05这个矩阵揭示了一个隐性偏置主题“影视综娱”几乎100%对应“娱乐”类但主题“人工智能”却分散在“科技”62%、“财经”28%、“国际”10%三类中——说明单纯用KNN按词频分类“人工智能”新闻容易因报道角度不同被误判。此时系统可在前端加提示“该新闻主题偏向‘人工智能’建议同时查看‘科技’与‘财经’分类下的相似新闻”。6.3 实战验证用LDA结果修正KNN的TOP-K邻居展示/api/predict接口返回的不只是预测类别还包含TOP-5相似新闻的ID、标题、以及它们在LDA主题上的分布。前端index.html用表格展示相似新闻标题主题权重TOP1与当前新闻主题相似度127《华为发布昇腾910B芯片》科技-半导体0.730.9289《英伟达Q1财报超预期》财经-股市0.680.85203《中美AI技术竞争白皮书》国际-科技合作0.510.79这个表格让用户一眼看出为什么KNN把它分到“科技”因为最相似的邻居是芯片新闻但第二相似的是财报新闻说明它也有财经属性。从那以后我每次做文本分类项目都强制走一遍LDA主题建模哪怕不用在预测里——它就像X光照出数据集里肉眼看不见的结构裂缝。希望帮到你。本文还有配套的精品资源点击获取