简介:一套基于NLP的敏感文本识别与分类Python项目源码及配套数据库,面向计算机相关专业学生、毕业设计、课程设计等场景,适用于敏感词检测、不良文本分类、内容安全过滤等任务。压缩包共6个文件,其中两个Python程序分别负责敏感不良文本分类与非法信息收集,另含程序说明文档、SQL数据库脚本和快捷方式,整体大小约3.12MB,结构清晰、轻量易部署。目前已有132人学习下载。代码经过验证可运行,可直接作为毕设、课设或大作业的参考实现;SQL脚本可快速初始化数据库,说明文档有助于理解项目结构与运行流程。对于入门者,可借助该项目学习NLP文本分类的完整实现思路;对于进阶者,也可基于现有代码修改算法、扩充敏感词库,或接入更多业务场景,具备较高的学习与二次开发价值。
1. 敏感文本识别不是正则匹配:这个 NLP 小项目到底能干什么
这次拆的资源是一个基于 NLP 的敏感文本识别分类项目,Python 源码加 SQL 数据库打包好,很多人拿它当毕业设计、课程设计或期末大作业。初看“敏感文本分类”,容易想成拉一张敏感词表做正则;真跑起来会发现,词表命中很容易,误杀和漏杀却控制不住,而 NLP 分类能按整段上下文判断这条文本是不是不良内容,这正是这个项目值得复现的地方。
项目里真正干活的是三个文件:敏感不良文本分类.py、非法信息收集.py、stop.txt,外加一份 sexeducation.sql 数据库导出。整个链路覆盖中文分词、去停用词、TF-IDF 特征提取、分类、入库,不是一句“调个接口”能带过的。适合计算机相关专业学生做毕设和课程设计;也适合刚开始碰 NLP 分类、想完整跑通文本处理流程的初级工程师拿来练手。
2. 先拆包再动手:文件清单、数据流与 SQL 表结构
打开 zip,先别急着运行。这份资源文件数量不多,却覆盖了“样本预筛—文本预处理—分类—存储”四个环节。很多同学直接双击敏感不良文本分类.py,遇到缺库、缺表、路径不对就停在那里;正确姿势是先读文件清单,再把数据流理一遍。
2.1 从 py 文件和 sql 库反推项目结构
解压之后常见文件如下:
| 文件名 | 在项目里的角色 |
|---|---|
| stop.txt | 停用词表,每行一个词,用来过滤“的/了/是”等低信息量字词 |
| 非法信息收集.py | 初筛脚本,对原始文本做关键词预判,挑出可能敏感的候选样本 |
| 敏感不良文本分类.py | 核心脚本,负责分词、去停用词、特征化、分类决策 |
| sexeducation.sql | 数据库导出,存敏感词表、待分类样本、分类结果和复核状态 |
| 程序说明.txt | 原始作者留下的运行说明 |
| IDLE (Python 3.5 64-bit).lnk | Windows 环境入口快捷方式,不是代码,可以忽略 |
从这份清单能看出,项目作者是在 Windows 下用 Python 3.5 64 位搭的环境。我重新复现时没有继续用 3.5,而是新建了 Python 3.8 的虚拟环境,再把 pymysql、jieba、scikit-learn 装齐,核心逻辑不需要大改。注意IDLE (Python 3.5 64-bit).lnk出现在资源里不代表运行依赖它,它只是当时作者启动 IDLE 的快捷方式。
建议的复现顺序是:先导入 sexeducation.sql,确认 stop.txt 和两个 py 文件在同一目录;然后跑非法信息收集.py 写入候选样本;最后跑敏感不良文本分类.py。如果只想快速看分类效果,也可以先不连数据库,直接喂一批带标签的文本,但要演示“完整项目”,数据库是绕不开的一环。
整个数据流常见排布是:先由非法信息收集.py 读取文本源,匹配到若干关键词后写入 sql 的待分类表;敏感不良文本分类.py 再从库里取内容,读 stop.txt 做清洗,转成 TF-IDF 特征,交给分类模型返回 0/1 标签,最后把结果回写数据库。到这里,sexeducation.sql 里的增删改查四条路全走了一遍:keyword 表增词、sample 表插入新样本、更新 label、查统计结果。
2.2 敏感词库 vs 分类模型:为什么这个项目把 SQL 和文本分类放一起
敏感词库是规则,分类模型是概率判断。两者各有一头不好办:纯规则漏变形体,纯模型缺标注样本。这个项目把 SQL 放在中间,等于同时承担了三个功能:第一,存敏感词和别名,方便做规则初筛;第二,存人工标注或模型标注后的样本,作为分类器的训练数据;第三,存每条文本的来源和复核状态,让结果可以追溯。
sexeducation.sql 导入后,里面大概率有几张核心表。我按最常见的设计还原出结构,实际表名以你解压后的 sql 为准:
-- 敏感词库表:维护初筛用的关键词 CREATE TABLE IF NOT EXISTS keyword ( id INT AUTO_INCREMENT PRIMARY KEY, word VARCHAR(50) NOT NULL UNIQUE, category VARCHAR(20) DEFAULT 'default', -- 词所属类别,便于分组分析 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 样本表:存原始内容、来源、模型标签和复核状态 CREATE TABLE IF NOT EXISTS sample ( id INT AUTO_INCREMENT PRIMARY KEY, content TEXT NOT NULL, source VARCHAR(200) DEFAULT '', label TINYINT DEFAULT 0, -- 0 正常,1 敏感/不良 status TINYINT DEFAULT 0, -- 0 待分类,1 已分类,2 人工复核 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段设置有两个容易被忽略的地方。第一个是 label 用 TINYINT 而不是字符串,这样后面做GROUP BY label统计时方便,也方便模型直接读成 0/1 目标变量。第二个是 status 单独一位,它和 label 分开的意义在于:模型给的结果可能出错,人工改完 label 后把 status 置成 2,下次批量脚本就不会再覆盖这条记录。这个设计看起来简单,却是毕设答辩时能讲出“数据闭环”的关键。
为什么不直接调现成的内容审核 API?一是隐私和可控性,二是这份资源本身就是拿来学习分类原理的,用自带源码训练,参数、特征、阈值都能打开改,比调用黑匣子接口更有作业价值。SQL 在这里不是摆设,而是整个系统的记忆层——新敏感词、新样本、新结果,都靠它累积。额外收获是这套表结构可以直接导出 CSV 做后续数据分析,比如按 source 分类统计敏感文本比例。
3. 复现第一步:敏感文本预处理与停用词表 stop.txt 的正确用法
分类效果有一半由预处理决定。第一次做 NLP 毕设的人,容易把精力全放到训练模型上,结果准确率报告很好看,拿真实句子一测就翻车。翻车点往往不是模型参数,而是文本里混着空格、标点、零宽字符,特征矩阵里全是噪声。项目自带的 stop.txt 就是用来干这件事的。
3.1 中文分词、去停用词、去重:这一步决定后面分类精度
先看 stop.txt 的读取和分词过滤。这里用 jieba 做中文分词,这是当前最常见的做法,Python 3.5 也能跑,安装时用pip install jieba即可:
# -*- coding: utf-8 -*- import jieba import re def load_stopwords(path='stop.txt'): # 每行一个停用词,空行直接跳过 with open(path, 'r', encoding='utf-8') as f: stopwords = {line.strip() for line in f if line.strip()} return stopwords def clean_text(text): # 去掉普通空格、零宽字符和 BOM,防止“代 购”这类变形绕过 text = re.sub(r'[\s\u200b\u200c\u200d\ufeff]+', '', text) # 去掉中英文标点和特殊符号,\w 会保留数字、字母和汉字 text = re.sub(r'[^\w]+', '', text) return text def tokenize(text, stopwords): text = clean_text(text) words = jieba.lcut(text) # 过滤停用词和单字,单字后续用用户词典补回敏感词 words = [w for w in words if w not in stopwords and len(w.strip()) > 1] # set 去重让特征更规整;如果发现顺序有影响,可改为普通列表 return list(set(words)) stopwords = load_stopwords() sample_text = "这个商品的成分含有激素,长期使用会让人产生依赖" print(tokenize(sample_text, stopwords))关键点在两处。一是stopwords用 set 存,成员判断 O(1),几千甚至几万词的词表也不会拖慢速度;二是clean_text里正则把空格和不可见字符直接抹掉,避免“加 微信”被拆成两个 token。len(word.strip()) > 1会丢掉单字,这是有意为之,因为单字在 TF-IDF 里基本是噪声;但敏感词里如果有一个字的词,比如某个核心字,你需要把它加进 jieba 用户词典再保留,后面避坑章会专门说。
很多人忽略的一个细节是:停用词表不是越长越好。看到 stop.txt 有几千行就往里塞,容易把“不”“没”这类否定词也塞进去;对敏感文本分类来说,“不违法”“没问题”里的“不”是有语义的。我一般只删虚词、代词、语气词,保留否定词,这样误杀能少一层。
3.2 特征工程:TF-IDF 向量化与卡方校验:小样本下比词向量好用
预处理之后,下一步是把分词结果变成模型能读的向量。小样本毕设场景,词向量预训练模型太重,TF-IDF 是最稳的起点。核心脚本里大概率就是TfidfVectorizer + MultinomialNB这个组合;这个组合训练快、解释性强,答辩时说清参数就能讲明白。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import pymysql def load_data_from_db(): # 从 sample 表读取已标注数据,label 为 0/1 conn = pymysql.connect(host='127.0.0.1', user='root', password='123456', db='sexeducation', charset='utf8mb4') with conn.cursor() as cur: cur.execute("SELECT content, label FROM sample WHERE label IN (0, 1)") rows = cur.fetchall() conn.close() return [r[0] for r in rows], [int(r[1]) for r in rows] texts, labels = load_data_from_db() vectorizer = TfidfVectorizer( ngram_range=(1, 2), # 保留单个词和相邻双词组合 min_df=2, # 只在一条样本里出现的词直接忽略 max_features=5000, # 特征数上限,防止矩阵过稀疏 sublinear_tf=True # 用 1+log(tf) 压一压高频词 ) X = vectorizer.fit_transform(texts) X_train, X_test, y_train, y_test = train_test_split( X, labels, test_size=0.2, random_state=42 ) model = MultinomialNB(alpha=0.1) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))参数这么设的理由我给一张表:
| 参数 | 值 | 作用 |
|---|---|---|
| ngram_range | (1, 2) | 能抓住“私密视频”“加微信”这类双词组合,比纯单字特征有区分度 |
| min_df | 2 | 过滤只出现一次的偶发词,降低稀疏度 |
| max_features | 5000 | 限制特征矩阵规模,训练更快,内存更可控 |
| sublinear_tf | True | 原本 tf 是线性次数,改成 1+log(tf) 后,高频但无意义的词权重不会爆炸 |
alpha 是朴素贝叶斯的平滑系数,0.1 是我在这个项目里常用的起点。如果样本不均衡,比如敏感类只有几十条,正常类几百条,MultinomialNB 没有 class_weight 参数,我一般换LinearSVC(class_weight='balanced'),分类边界比朴素贝叶斯更抗偏斜。但那样拿不到 predict_proba,需要自己对 decision_function 调阈值。特征和阈值都需要看数据调,不要照抄任何人的参数。
顺便说一句:每次跑完模型,我都习惯把 feature_names 打印出来看一眼,挑选权重最高的 20 个词。曾经发现最高权重的词是“我们”,说明停用词表有遗漏;补上之后模型报告下降了一个点但误杀少了很多。这类经验比调参更重要。
4. 避坑手册:敏感文本识别最常见的四个翻车点
demo 跑通容易,跑出能用的结果难。敏感文本分类的坑集中在“漏检”“误杀”“分词被切”“数据库乱码”“模型重复训练”五类;每条我都按现象、原因、解决记录如下。
4.1 漏检:变形体与零宽字符把敏感词撕成碎片
现象:敏感词表里明明有“私密视频”,样本里写的是“私 密 视 频”或“私密视频”中间夹了不可见字符,代码if '私密视频' in text直接漏掉。
原因:文本里混入了空格、换行、零宽字符,敏感词被物理打断;纯词表匹配不会自动合并这些字符。
解决:在 clean_text 阶段用 NFKC 统一 Unicode 格式,再把不可见字符删干净:
import re import unicodedata def normalize(text): # NFKC 会把全角字母和部分格式字符转成半角或标准形式 text = unicodedata.normalize('NFKC', text) # 删空格、零宽字符、BOM text = re.sub(r'[\s\u200b\u200c\u200d\ufeff]+', '', text) return text.lower() # 英文统一小写注意 stop.txt 和分类脚本里的正则也要走这一层,否则初筛和分类两套清洗标准,结果对不上。另外还有同音字变形,比如“薇信”替代“微信”,这类不是字符层能解决的,我一般在 keyword 表加 alias 字段,把别名一起加载进敏感词列表。
4.2 误杀:科普文章里出现敏感字就被判成不良
现象:一篇讲“艾滋病预防”的科普文章,出现了“性生活”“安全套”等词,被模型打成 1;人工读起来完全正常。
原因:如果特征是敏感词出现次数,而不是整句上下文,模型很容易把“高风险词”和“不良文本”划等号。教育类、医学类文本天然包含这些词。
解决:不要直接统计关键词数量,而是用整段文本的 TF-IDF 特征,并在预测时加概率阈值:
proba = model.predict_proba(vectorizer.transform([text]))[0] # 只有概率足够高才判敏感,0.9 按你的混淆矩阵调 label = 1 if proba[1] >= 0.9 else 0我自己的血泪经验是:先打印混淆矩阵,看误杀数量而不是只看准确率。如果正常类样本大量落到预测为 1,就是阈值或者语料分类问题。不要急着调模型,先去看是不是文本清洗阶段把“不”字删掉了——我犯过这种错,把“没有违规”洗成“违规”。
4.3 分词拆词:jieba 把“大保健”切成“大”和“保健”
现象:预处理后敏感词被切成两半,特征只剩“大”“保健”,词表匹配和 TF-IDF 都失去这个关键信号。
原因:jieba 默认词典是按通用语料训练出来的,敏感词库里的网络词、行业词很多不在词典中,默认切分粒度对它们不友好。
解决:加载自定义用户词典:
import jieba # 方式一:逐词添加 jieba.add_word('大保健') jieba.add_word('私密视频') # 方式二:把 keyword 表导成每行一个词,再整体加载 jieba.load_userdict('sensitive_words.txt')我一般用第二种,因为敏感词库在持续增长。sensitive_words.txt可以直接从 SQL 里SELECT word FROM keyword导出来,但要控制列表规模,几十万词会让 jieba 初始化变慢。另外,加了用户词典后要重新跑一遍清洗,确认“大保健”没有被后面的过滤规则再过滤掉。
4.4 数据库:sexeducation.sql 导入乱码或 Python 读写错位
现象:MySQL 里source sexeducation.sql导入成功,查询时中文全是问号;Python insert 后再 select,中文变成乱码。
原因:字符集不一致。sql 文件里的中文用某个编码写,MySQL 客户端连接用的却是 latin1,或者表默认字符集不是 utf8mb4。
解决:导入前显式指定 utf8mb4:
mysql -uroot -p --default-character-set=utf8mb4 mysql> SET NAMES utf8mb4; mysql> source /path/to/sexeducation.sql;如果是在 Python 里连库,连接参数里也必须写charset='utf8mb4',并且建表语句要带DEFAULT CHARSET=utf8mb4。还有个小坑:旧版本 MySQL 5.5 不支持 utf8mb4,至少用 5.7;本地开发我建议直接用 MySQL 8,字符集默认就对了。
检查乱码时不要只看一条记录,用SHOW CREATE TABLE sample\G看表字符集,还不够就SELECT HEX(content) FROM sample LIMIT 1,把十六进制结果和手动 SET NAMES 之后的结果对比,定位是连接层还是存储层的问题。这个排查习惯能省半小时。
4.5 模型被重复训练:import 分类脚本时覆盖了已保存模型
现象:调用from 敏感不良文本分类 import classify_text后,预测结果全部变成同一个值,甚至报 “Model not fitted”。
原因:核心脚本里的训练代码没有放到if __name__ == '__main__'里,一 import 就把模型重新训练一遍,把之前保存的参数覆盖了。
解决:训练代码包进 main,模型用 joblib 保存,预测时只加载不重训:
import joblib # 训练完成后保存 joblib.dump(model, 'model.pkl') joblib.dump(vectorizer, 'vectorizer.pkl') # 预测时加载,避免每次重复 fit model = joblib.load('model.pkl') vectorizer = joblib.load('vectorizer.pkl')这个问题在 Jupyter 里跑还不太明显,一旦改成模块化导入就会爆发。凡是拿到别人源码,第一件事就是把“训练”和“预测”两个入口拆干净。
5. 把 SQL 库用起来:样本入库、批量分类与人工复核闭环
训练完模型,只有把数据库接上,整套代码才像一个完整的毕业设计。这一步要做的是:文本进库、批量分类、结果更新。项目自带的非法信息收集.py 负责前半段,敏感不良文本分类.py 负责后半段。
5.1 从「非法信息收集.py」看样本怎么来:先初筛再入库
这个脚本名字容易让人误解。我的理解是,它要做的是“收集可能存在非法或不良信息的候选样本”,不是让你去抓取私密数据。复现时我只在公开语料和自己整理的文本上跑。真要接生产数据,一定要确认数据来源合规。
初筛的逻辑一般是用一组警告词做粗过滤,命中多个再入库:
# 初筛脚本:只挑候选,不直接下结论 import re import pymysql ALERT_WORDS = ["视频", "资源", "加微信", "直播", "转账", "会员"] def pre_scan(text): # 清洗规则和分类脚本保持一致 text = re.sub(r'[\s\u200b-\u200d\ufeff]+', '', text) hits = sum(1 for w in ALERT_WORDS if w in text) return hits >= 2 # 命中两个及以上才进候选库 def insert_sample(content, source): conn = pymysql.connect(host='127.0.0.1', user='root', password='123456', db='sexeducation', charset='utf8mb4') try: with conn.cursor() as cursor: cursor.execute( "INSERT INTO sample(content, source, status) VALUES (%s, %s, 0)", (content, source) ) conn.commit() finally: conn.close() if __name__ == '__main__': with open('collected.txt', encoding='utf-8') as fp: for line in fp: line = line.strip() if line and pre_scan(line): insert_sample(line, 'collected.txt')这里的ALERT_WORDS只是我写出来方便演示的词组,真正跑的时候应该从 keyword 表动态读取,避免脚本里写死。初筛的标准可以放宽:宁可多挑,不能漏挑。多挑的样本最多占用人工复核时间,漏挑的样本会影响分类器覆盖范围。status=0表示新样本待分类,后面批量脚本只会处理这个状态。
5.2 批量分类并写回数据库:一条可跑的完整链路
接下来是把核心脚本包装成单条分类函数,循环处理库里的待分类样本。我习惯把中文文件名“敏感不良文本分类.py”重命名为sensitive_bad_text_classifier.py再导入,省得中文模块名在一些 IDE 里报错。如果项目里原函数名不叫classify_text,你自己改个名就行。
from sensitive_bad_text_classifier import classify_text import pymysql def batch_classify(batch_size=100): conn = pymysql.connect(host='127.0.0.1', user='root', password='123456', db='sexeducation', charset='utf8mb4') try: with conn.cursor() as cursor: cursor.execute( "SELECT id, content FROM sample WHERE status = 0 LIMIT %s", (batch_size,) ) rows = cursor.fetchall() print(f"本次处理 {len(rows)} 条") for rid, content in rows: label = classify_text(content) # 返回 0 或 1 cursor.execute( "UPDATE sample SET label = %s, status = 1 WHERE id = %s", (label, rid) ) conn.commit() finally: conn.close()batch_size我一般设 100 到 500。一次拉 10 万条很容易把内存吃满,尤其是 TF-IDF 向量化时还要再复制一份矩阵。分片处理的好处是,某一批挂了不会影响已经写完的部分,重新跑只会处理剩下的status=0。
批量入库前还要注意重复样本。同一个句子如果来源不同,会被插两次,模型训练时相当于把这条样本权重翻倍。常见做法是在 content 字段上加唯一索引,或者插入前先查一次SELECT COUNT(*)。我临时演示时习惯用后者,因为加唯一索引可能会撞上已有的重复数据。
整个批处理跑完,可以用一条统计 SQL 看模型判断和人工复核的差距:
SELECT status, label, COUNT(*) AS cnt FROM sample GROUP BY status, label;这条 SQL 是演示时最好使的产出:它能看出整批数据里有多少被模型判成 1、有多少被人工改成 0。答辩展示这一屏,比念一堆准确率数字更有说服力。人工复核时把status改成 2,下次批量更新就不会覆盖。
6. 还能怎么用:把分类模型封装成 HTTP 接口
脚本加数据库只能命令行演示,我一般会再包一层 Flask 接口,让前端或者测试工具能直接调用。这个项目本身是 Python 3.5 起步,Flask 兼容性没问题,接口代码也很短:
from flask import Flask, request, jsonify from sensitive_bad_text_classifier import classify_text app = Flask(__name__) @app.route('/check', methods=['POST']) def check(): payload = request.get_json() if not payload or 'text' not in payload: return jsonify({'error': 'missing text'}), 400 label = classify_text(payload['text']) return jsonify({'label': label}) if __name__ == '__main__': app.run(host='127.0.0.1', port=8000, debug=False)接口里做了输入校验,没有 text 字段直接返回 400,而不是让服务抛异常。启动后可以用 curl 快速验证:
curl -X POST http://127.0.0.1:8000/check \ -H "Content-Type: application/json" \ -d '{"text": "加微信看私密视频,在线发资源"}'返回{"label": 1}就是接口通了。但验证别只看这一条,要构造三组样本:正常科普、广告变体、普通闲聊,分别调用接口,打印每条的预测概率和最终标签。我第一次跑这个项目时偷懒只看报告里的准确率,结果演示时一条“性教育”被误杀,当场翻车。从那以后我每次复现分类项目都会强制打印 classification_report 和几条极端样例,确认阈值没调错。希望帮到你。
本文还有配套的精品资源,点击获取