
简介基于KNN算法的新闻文本分类是毕业设计、课程设计中的常见方向。该项目以Python语言为基石完整覆盖了从新闻网站数据采集、pandas清洗与统计分析、TF-IDF文本特征提取、KNN分类器构建到Flask Web前后端联动展示的整个流程适合计算机相关专业学生作为实战参考。压缩包共56个文件约54.8MB内含源代码、配置文件、文本数据集、停用词表、前端样式与脚本、数据库文件并附有MOV格式的操作演示视频方便对照运行。目前已有118人学习使用对于正在准备毕设或课设的学生来说可省去从零搭建环境与摸索算法细节的时间。通过该项目能够理解KNN文本分类的具体实施步骤文本预处理与向量化、通过TF-IDF计算特征词权重、使用余弦相似度度量文本距离、按K近邻少数服从多数得到类别结果同时掌握Flask路由设计、ECharts图表展示等Web可视化技巧便于在此基础上进行功能扩展。1. 基于KNN的新闻数据分析项目毕业设计、文本分类与可视化一次打通如果你正在找毕业设计或课程设计题目又不想只做个“调包跑通”的Demo这个项目是很好的参考样板它用爬虫从某新闻网站抓取真实新闻文本基于KNN算法做文本分类再用Flask搭建Web后台配合Bootstrap和ECharts把“分类结果、热词分布、新闻统计”全部可视化出来。换句话说它把数据分析、机器学习建模、Web开发三件事揉进了一个完整工程里既能写进论文也能现场演示。项目里自带源代码、演示视频和依赖清单拿到手就能跑对想快速完成毕设又希望代码有解释深度的同学尤其友好。2. 项目架构与目录拆解Flask Web 爬虫 KNN建模的完整链路2.1 整体技术选型为什么是KNN而不是深度学习新闻文本分类有很多算法可选比如朴素贝叶斯、SVM、LSTM、BERT。但这个项目选择了KNN原因很实际KNN是懒惰学习不需要显式训练模型的过程对文本这类高维稀疏数据天然友好同时它原理简单论文里好解释答辩时能讲清楚“距离度量”和“多数投票”这两个核心概念不会陷入深度学习的黑匣子。整个技术栈是这样的Python负责所有逻辑requests BeautifulSoup常见做法写爬虫pandas做统计scikit-learn或自写的TF-IDF计算做文本向量化KNN分类器自己实现一遍这样代码更有教学价值Flask提供Web服务前端用Bootstrap布局、ECharts画图。数据库用了SQLiteuser_info.db保存用户操作和新闻数据轻量且免安装。我拆这个项目时最深的感受是它不是“为了用算法而用算法”而是把每个环节都串起来了——爬虫抓到新闻存入JSON和数据库预处理代码读出来做清洗分词TF-IDF计算特征词权重KNN按余弦相似度找最近邻最终结果通过Flask接口交给前端渲染。每一步都有落点而不是孤立的算法实验。2.2 目录结构逐层看每个文件在工程里扮演什么角色拿到压缩包后先别急着跑建议按照下面这张“地图”理解每个文件。文件/目录作用app.pyFlask应用入口定义路由、视图函数和分类预测接口config.py全局配置比如数据库路径、K值、特征词数量上限util.py工具函数集合文本清洗、分词、TF-IDF计算等news_spider.py新闻爬虫从某新闻网站采集文本数据KNN_TFIDF_Text_Classify核心分类器目录内含KNN和TFIDF相关实现templates/HTML模板含index.html、news_category.html、hot_words.html、lda_classify.htmlstatic/前端静态资源css、js、img、字体stopwords.txt中文停用词表用于过滤“的、了、和”等无意义词all_news_info.json爬虫采集的新闻数据存档JSON格式user_info.dbSQLite数据库存储分类记录和用户信息requirements.txtPython依赖清单user-agent.txt爬虫请求头文本用于伪装浏览器test.txt测试文本样本.idea/PyCharm工程配置不用管这个结构很典型入口文件、工具模块、分类器模块、模板层、静态资源层分明。二次开发时最常改的是util.py和分类器目录前端修改集中在templates和static。2.3 数据流向从新闻爬取到页面展示的完整流程整个系统的数据流可以概括为一条单向链路爬虫采集新闻 - 清洗与分词 - TF-IDF向量化 - KNN分类建模 - 统计聚合 - Flask接口 - ECharts渲染具体步骤是news_spider.py先按user-agent.txt里的请求头伪装浏览器从目标新闻网站爬取文章标题和正文存成all_news_info.json。util.py读取这些JSON去掉HTML标签、数字、特殊符号按jieba分词常见做法再用stopwords.txt删除停用词。接着计算每篇文章的TF-IDF特征向量把文本变成模型能吃的数值矩阵。KNN分类器接收这些向量在预测时计算待分类文本与所有训练样本的余弦相似度取前K个最近邻做多数投票输出类别。最后Flask把类别结果和统计数据渲染进模板ECharts以前端图表的形式展示分类占比和热词排行。3. 核心模块实现爬虫、TF-IDF向量化与KNN分类器3.1 新闻爬虫请求头伪装与数据落盘爬虫部分代码不长但细节多。下面是我的简化版核心逻辑import requests from bs4 import BeautifulSoup def get_news_list(url, user_agent_file): # 从user-agent.txt读取请求头避免被服务器识别为脚本 with open(user_agent_file, r, encodingutf-8) as f: ua f.read().strip() headers {User-Agent: ua} resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) # 选取新闻列表中的标题和链接常见做法是按特定class过滤 items soup.select(.news-item a) news_list [] for item in items: title item.get_text().strip() link item.get(href) if title and link: news_list.append({title: title, url: link}) return news_list def save_news(news_list, json_path): import json # 追加写入保留历史数据 with open(json_path, a, encodingutf-8) as f: for news in news_list: f.write(json.dumps(news, ensure_asciiFalse) \n)逻辑说明get_news_list里最关键的是请求头伪装——很多新闻站对非浏览器请求会直接拒绝从user-agent.txt读UA而不是写死方便自己换。save_news用追加模式逐行写JSON好处是断点续爬时不会覆盖已有数据坏处是文件会越来越大所以项目里通常配合去重逻辑。参数说明timeout10表示超过10秒请求失败encodingutf-8适配大多数中文站点.news-item a是CSS选择器不同网站要改成对应的HTML结构。3.2 TF-IDF文本向量化特征词权重计算KNN不能直接吃中文文本必须先把它转成数值向量。项目用的是TF-IDF核心思想是一个词在当前文章里出现频率高但在整个语料里很少见说明它对这篇文章有很强的区分度。import math from collections import Counter import jieba def load_stopwords(path): with open(path, r, encodingutf-8) as f: return set(line.strip() for line in f) def text_to_tfidf(texts, stopwords_path): # texts是分词后的文档列表每项是一篇文章的全部词语 corpus [] for t in texts: words [w for w in jieba.lcut(t) if w not in stopwords] corpus.append(words) # 计算每个词的文档频率DF df Counter() for words in corpus: for w in set(words): df[w] 1 total_docs len(corpus) # 为每个文档生成TF-IDF向量 vectors [] all_vocab sorted(df.keys()) vocab_index {w: i for i, w in enumerate(all_vocab)} for words in corpus: tf Counter(words) vec [0.0] * len(all_vocab) for w, count in tf.items(): # TF取词频IDF取log(总文档数/含该词的文档数) idf math.log((total_docs 1) / (df[w] 1)) 1 vec[vocab_index[w]] count * idf vectors.append(vec) return vectors, vocab_index逻辑说明这段代码先分词并去停用词再统计每个词出现在多少篇文档中TF是词频IDF是“稀有度”的对数变换。1是平滑处理避免某个词在所有文档都没出现时除零报错。最后生成的vectors是二维数值矩阵每行代表一篇文章每列代表一个特征词。参数说明log((total_docs 1) / (df[w] 1)) 1是常见的平滑IDF公式比原始log(N/df)更稳定vocab_index把词语映射到向量下标预测新文本时要用同一个映射否则维度对不上。3.3 KNN分类器余弦相似度与多数投票KNN的核心是三个步骤算相似度、找K个最近邻、按多数投票定类别。项目里相似度用余弦值公式是向量的点积除以模长乘积。import math def cosine_similarity(vec1, vec2): # 计算两个向量夹角的余弦值取值范围[-1,1]越大越相似 dot sum(a * b for a, b in zip(vec1, vec2)) norm1 math.sqrt(sum(a * a for a in vec1)) norm2 math.sqrt(sum(b * b for b in vec2)) if norm1 0 or norm2 0: return 0 return dot / (norm1 * norm2) def knn_classify(train_vectors, train_labels, test_vector, k): # 计算测试向量与所有训练向量的相似度 distances [] for i, vec in enumerate(train_vectors): sim cosine_similarity(vec, test_vector) distances.append((sim, train_labels[i])) # 按相似度从大到小排序取前K个 distances.sort(keylambda x: x[0], reverseTrue) top_k distances[:k] # 多数投票统计每个类别出现的次数 vote {} for sim, label in top_k: vote[label] vote.get(label, 0) 1 # 返回票数最多的类别 best_label max(vote, keyvote.get) return best_label, top_k逻辑说明cosine_similarity中如果向量全零文章只有停用词模长是0直接返回0表示无相似度。knn_classify先算所有距离再排序取Top K最后用字典统计票数。注意这里用的是普通投票实际项目中可以考虑按相似度加权投票比如相似度越高的样本票重越大效果通常更好。参数说明K值是个玄学K太小容易受噪声影响K太大会把低频类别淹没。这个项目里K默认取5调参时可以用交叉验证观察F1值随K的变化曲线。3.4 Flask路由设计分类预测接口怎么暴露给前端爬虫和分类器是后端核心但没有Web接口用户就没法交互。app.py里定义了两个关键路由一个是首页展示统计图表另一个是接收用户输入的新闻文本并返回分类结果。from flask import Flask, request, jsonify, render_template app Flask(__name__) app.route(/) def index(): # 渲染首页模板同时把新闻统计摘要传过去 return render_template(index.html) app.route(/classify, methods[POST]) def classify_news(): # 获取前端传过来的待分类文本 text request.form.get(news_text, ) if not text.strip(): return jsonify({error: 文本不能为空}), 400 # 调用工具函数进行清洗、分词、向量化 vec util.text_to_vector(text) # 调用KNN分类器返回预测类别和相似样本 label, neighbors util.knn_predict(vec, k5) return jsonify({ category: label, neighbors: neighbors })逻辑说明/classify是典型的POST接口前端通过表单把文本传到后端后端先做非空校验再利用之前构建好的词表和分类器做预测。util.text_to_vector必须复用训练时生成的vocab_index否则新文本向量和训练向量长度不一致这一点是最容易踩的坑。参数说明request.form.get(news_text)读取的是表单字段如果你前端用JSON传就得换成request.json[news_text]k5和训练时保持一致不要训练用5预测用7常见错误。4. 可视化页面与前端渲染Bootstrap ECharts 怎么展示分析结果4.1 模板文件体系index、news_category、hot_words各自干什么templates目录下四个HTML文件职责划分很清楚index.html是主页面用卡片形式展示项目说明和统计概览news_category.html展示分类结果通常带一个饼图和一个新闻列表hot_words.html展示热词分析用词云或柱状图呈现高频词lda_classify.html是扩展模块页面上有一个输入框和提交按钮用户粘贴一段新闻文本后点击预测就能立刻看到它属于哪个类别。布局方面用了Bootstrap栅格系统一行分三列左侧放说明文字中间放图表容器右侧放结果表格。这样在小屏设备上也能正常展示不用额外写响应式样式。我给这个项目的建议是如果你只想完成毕设重点看index和lda_classify两个页面如果你想把热词分析做得更出彩可以扩展hot_words.html把ECharts的词云组件换成语料库中真正的高频词排名。4.2 ECharts图表接入分类分布与热词统计ECharts是纯前端图表库通过script标签引入核心用法是先定义容器div再初始化图表实例并设置option。div idcategoryChart stylewidth: 100%; height: 400px;/div script src{{ url_for(static, filenamejs/echarts.min.js) }}/script script var chart echarts.init(document.getElementById(categoryChart)); var option { tooltip: { trigger: item }, series: [{ name: 新闻类别, type: pie, radius: 60%, data: [ { value: {{ sports_count }}, name: 体育 }, { value: {{ tech_count }}, name: 科技 }, { value: {{ finance_count }}, name: 财经 }, { value: {{ edu_count }}, name: 教育 } ] }] }; chart.setOption(option); /script逻辑说明模板里用了{{ }}语法这是Flask的Jinja2模板变量后端把统计数字传进来前端直接拼到图表数据里。饼图的radius控制半径大小tooltip控制鼠标悬停显示的信息。ECharts的好处是图表类型切换非常方便把type: pie改成type: bar就是一个柱状图适合论文里做对比实验。参数说明url_for(static, filenamejs/echarts.min.js)是Flask生成静态文件URL的标准写法不要直接写死路径否则部署时容易404。sports_count等变量是在app.py中通过render_template传入的前端不能自己造数据必须后端给。4.3 表单交互用户输入新闻文本后如何拿到分类结果lda_classify.html里的交互是典型的前后端配合用户输入文本点击按钮JavaScript通过AJAX把文本发给后端后端返回JSON前端再更新页面。$(#predictBtn).on(click, function () { var text $(#newsText).val(); if (!text.trim()) { alert(请输入新闻内容); return; } $.post(/classify, { news_text: text }, function (data) { if (data.category) { $(#result).text(预测类别 data.category); // 把相似新闻列表渲染到页面上 var html ; data.neighbors.forEach(function (item) { html li item.title item.label /li; }); $(#neighborList).html(html); } }); });逻辑说明这里用了jQuery的$.post发送表单数据success回调里的data是后端返回的JSON对象。注意要先判断data.error是否存在否则后端返回400错误信息时页面会显示未定义。forEach遍历相似样本拼出列表HTML简单直接适合毕设展示。参数说明news_text是字段名必须和后端request.form.get(news_text)一致#predictBtn是按钮ID和HTML里idpredictBtn对应。如果页面加载后$.post报404优先检查后端路由是否被装饰器正确注册。5. 运行部署与避坑指南从零跑起来最常见的6个坑5.1 环境准备与依赖安装拿到项目后第一步是创建虚拟环境并安装依赖。项目自带requirements.txt命令如下python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install -r requirements.txt不过这个requirements.txt是作者当时的版本快照直接安装可能因为Python版本不同而出问题。我一般会把它升级一下重点装这几项flask、pandas、jieba、requests、beautifulsoup4、lxml、scikit-learn。如果不想折腾版本用pip install flask pandas jieba requests beautifulsoup4 lxml scikit-learn一次性装齐。安装完依赖后检查当前Python版本。这个项目的pycache文件显示是cpython-37说明当初用Python 3.7开发但你用3.10或3.11也大概率能跑个别语法差异比如collections.Iterable需要改成collections.abc.Iterable。5.2 数据库与数据文件检查启动Flask前先确认user_info.db是否存在不存在也没关系项目里的建表逻辑会在首次运行时自动创建通常写在util.py的初始化函数里。但all_news_info.json必须存在否则爬虫模块和分类器会读不到数据直接报错。建议第一次运行前先跑一次爬虫或直接使用压缩包内自带的JSON数据。如果JSON为空后续训练向量就是空列表KNN分类时会出现“list index out of range”。判断方法很简单用编辑器打开JSON文件看里面有没有实质性的新闻记录。5.3 常见问题排查现象、原因、解决问题一启动app.py后浏览器访问http://127.0.0.1:5000显示404。现象首页能打开但点击“分类预测”页面找不到。原因路由没注册或者模板文件名和render_template里的参数不一致。解决检查app.py中app.route装饰器的路径再看templates目录下是否有对应HTML文件注意大小写。问题二输入新闻点预测界面一直没反应。现象浏览器控制台显示Uncaught ReferenceError: $ is not defined。原因jQuery库没有正确引入常见做法是模板里漏了script src{{ url_for(static, filenamejs/jquery.min.js) }}/script。解决在HTML头部引入jQuery并且放在调用代码之前。问题三预测结果所有新闻都被分成同一个类别。现象无论是科技新闻还是体育新闻输出都是“体育”。原因特征词表构建不完整训练数据里类别样本严重不平衡或者停用词表把有区别力的词全过滤了。解决检查all_news_info.json中各类别数量把样本少的类别补几条数据同时确认stopwords.txt没有把“手机”“股市”这类特征词误删。问题四Flask启动时报ModuleNotFoundError: No module named jieba。现象代码在import jieba就直接崩溃。原因虚拟环境没装jieba或者source错了环境。解决用pip list先看已安装包再执行pip install jieba不要依赖IDEA自带的Python解释器。问题五爬虫运行后抓到的全是空内容。现象all_news_info.json每行都是{title: , url: }或者直接抓不到。原因目标网站结构变了CSS选择器.news-item a匹配不到任何节点或者服务器返回了验证码页面。解决用浏览器开发者工具查看当前页面结构更新news_spider.py中的selector同时确认user-agent.txt里的UA是最新浏览器的UA。问题六KNN分类时提示ValueError: operands could not be broadcast together with shapes。现象预测阶段向量维度和训练阶段不一致。原因预测时重新构建了词表而不是复用训练的词表。解决在util.py中把训练好的vocab_index保存到pickle文件常见做法预测前加载它保证特征维度统一。这个坑我几乎每次做文本分类都会遇到强烈建议一开始就设计成“一次训练、多次预测”的模式。6. 二次开发思路把KNN新闻分类改造成自己的数据分析项目6.1 改造点1替换语料与类别体系原项目用的是新闻网站几个固定类别你可以换成自己的数据源比如微博热搜评论、商品评论、论文摘要。需要修改三处爬虫的选择器和目标URL类别标签映射表all_news_info.json的数据结构。如果不想动爬虫也可以把外部CSV转成JSON格式写一个load_data函数兼容自己的文件。建议保留TF-IDF和KNN的核心代码不变只替换数据入口这样风险最小。替换后重新训练看分类准确率是否下降如果下降明显大概率是语料太杂需要增加停用词表或做更激进的清洗。6.2 改造点2引入算法对比增强论文说服力同一个数据集上KNN可以和朴素贝叶斯、逻辑回归做对比。在util.py里加一个train_baseline_models函数用scikit-learn的TfidfVectorizer和MultinomialNB输出三张表的F1值对比放进论文实验结果章节。这样做的好处是毕设答辩时能回答“为什么选KNN”的问题——虽然朴素贝叶斯更快但KNN在短文本场景下容错性更好尤其适合小样本。对比时注意朴素贝叶斯对特征分布有假设文本数据通常符合多项式分布KNN没有分布假设但每次预测都要遍历全部训练样本复杂度是O(N)数据量超过一万条就会明显变慢。6.3 改造点3增加一个JSON API接口原项目前端是页面渲染但你以后可能想接App或小程序。我一般会加一个/api/classify接口用Flask的jsonify返回结构化数据不再依赖模板。改造步骤如下app.route(/api/classify, methods[POST]) def api_classify(): # 接收JSON请求体而不是表单 data request.get_json() text data.get(text, ) if not text: return jsonify({code: 400, message: 文本不能为空}), 400 label, _ util.knn_predict(text, k5) # 统一返回codedata结构方便调用方解析 return jsonify({code: 0, data: {category: label}})逻辑说明接口层和应用层分离前端可以继续用/classify带页面跳转App端走/api/classify拿纯JSON。code:0表示成功非0表示业务异常这样调用方不需要依赖HTTP状态码判断逻辑。参数说明request.get_json()要求请求头带Content-Type: application/json用curl -H Content-Type: application/json -d {text:... }测试很方便。记住加个try...except捕获JSON解析错误不然前端乱传格式会导致500。从那次以后我每次拿到一个Flask项目都会先看路由注册表、再跑通数据流、最后改接口这套流程已经成了习惯。改造这个项目也是一样先把爬虫和分类器跑通再谈换算法加接口顺序反了会把自己绕晕。希望帮到你祝你顺利跑起来。本文还有配套的精品资源点击获取