简介:这是一份基于机器学习的微博恶意用户识别系统完整项目资料,面向人工智能、计算机及相关专业的学生和开发者,尤其适合作为毕业设计、课程设计或初期项目演示。资源涵盖数据采集、特征处理、模型训练与Web展示等环节,能够帮助读者快速搭建并理解一套完整的恶意用户识别方案。包内共56个文件,主要包括Python脚本(模型训练与爬虫采集)、npy/dat数据文件、SQL数据库脚本、YAML配置、HTML/CSS前端页面、Markdown说明文档及Shell脚本等,各类文件按功能组织,便于按需查阅与二次开发,压缩包整体仅8.8MB。项目文档还标注了环境配置与运行要点,配合测试通过的代码,可快速复现并继续改进;已有65人学习,适合不同基础的学习者在此基础上修改扩展,也可直接用于相关课程作业或项目答辩。
1. 微博恶意用户识别:一套能跑通全流程的95分毕设
接手“微博恶意用户识别”这套资料时,我最关心的是两件事:数据从哪来,模型有没有真跑通。拆解之后两个答案都很明确——它不是一个只给训练脚本的单项作业,而是把爬虫采集、MySQL存储、特征工程、逻辑回归训练、Flask 展示串成一条完整链路。从微博抓用户关系数据,清洗落库,再交给机器学习模型做二分类,最终在 Web 页面完成单用户识别。技术栈覆盖标准,数据链路完整,这正是答辩时最容易被追问的“全流程”能力,也是 95 分的来源。机器学习和 Python 的热门词背后,其实都是这张表里实实在在的字段计算。适合做毕设、课设,或者想完整走一遍“数据采集—特征—模型—服务”这条路的同学。下面按数据层、算法层、工程层逐层拆解,把最容易翻车的地方单独拎出来讲。
2. 数据层拆解:爬虫采集、SQL 落库与训练集构造
2.1 微博爬虫模块:关注关系采集与 Cookie 维护
项目里爬虫文件分得很清楚:weiboCrawler.py负责基础抓取,concern.py和fans.py分别抓用户关注列表和粉丝列表,cookies.txt存放登录态。恶意用户识别最常用的行为特征就是关注数、粉丝数比例,所以这两个文件是整个特征体系的源头。
爬虫的核心逻辑通常是这个风格:
# concern.py 核心逻辑示意 import requests import time import json def fetch_following(uid, cookie): url = f"https://weibo.com/ajax/friendships/friends?uid={uid}&page=1" headers = { "cookie": cookie, "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } resp = requests.get(url, headers=headers, timeout=10) data = resp.json() return [item["id"] for item in data.get("data", {}).get("list", [])] if __name__ == "__main__": uid = sys.argv[1] cookie = open("cookies.txt").read().strip() following_list = fetch_following(uid, cookie) print(json.dumps(following_list, ensure_ascii=False))逻辑说明:这里抓的是用户关注的账号 ID 列表,后续可以计算“互粉率”“关注列表里有多少疑似营销号”等特征。参数说明:timeout=10表示单次请求 10 秒超时,避免某个用户数据异常时把爬虫卡死;cookie 从cookies.txt统一读取,保证登录态集中维护。真实运行时还需要加请求延时,我一般会在循环里time.sleep(0.5)到time.sleep(1),微博的反爬策略对高频请求很敏感,赶时间可以压到 0.3 秒,但不要取消。
fans.py的逻辑与concern.py几乎对称,只是把 url 换成粉丝列表接口,返回的字段里多了一个followed_by布尔值,用来判断对方是否关注了当前用户。这个字段能直接算出“互粉数”,也是恶意用户识别里区分真人和机器号的重要信号——机器号的互粉率通常极低。
2.2 数据表设计:从 SQL 文件看存储结构
项目里的xinan.sql和xinan_with_data.sql两个文件分工很明确:前者是空表结构,适合先看字段;后者是带数据的完整库,导入后可以直接复现训练。数据表核心是一张用户主表加一张恶意样本标签表,用户的画像信息都在这两个文件里。
导入数据库时有个字符集坑,先给命令:
# 创建数据库时必须指定 utf8mb4,否则中文昵称直接变乱码 mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS weibo DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_bin;" mysql -u root -p weibo < xinan_with_data.sql注意那条COLLATE utf8mb4_bin,微博昵称包含大量 emoji 和特殊符号,用默认的utf8mb4_general_ci在某些 MySQL 版本下会报排序规则错误。bin排序规则按字节比较,能完整保留原始数据。
用户主表最关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| uid | bigint | 微博用户 ID,主键 |
| screen_name | varchar(64) | 昵称 |
| followers_count | int | 粉丝数 |
| follow_count | int | 关注数 |
| statuses_count | int | 微博总数 |
| created_at | datetime | 注册时间 |
| is_malicious | tinyint | 标签,1 为恶意用户 |
| latest_text | text | 最近微博文本 |
is_malicious的来源通常是人工标注加上evil1.txt这类黑名单账号匹配的结果。evil1.txt里存放的是已经确认的恶意用户 ID 清单,清洗时拿用户表和这份清单做 JOIN,能自动生成一部分标签。
2.3 数据处理与训练集构造
从数据库到训练集,核心是把原始字段转成模型能吃的数值矩阵。insertUser.py负责把爬虫抓到的数据写进用户表,newUser.py做增量更新,两者都只解决“数据进库”的问题;真正构造训练集还需要一个查询清洗脚本。
# 构造训练集:从 MySQL 读取并做最基础的清洗 import pymysql import pandas as pd conn = pymysql.connect( host="localhost", user="root", password="123456", database="weibo", charset="utf8mb4" ) df = pd.read_sql( "SELECT followers_count, follow_count, statuses_count, " "is_malicious FROM user_profile WHERE followers_count IS NOT NULL", conn ) # 基础清洗:排除负数和明显异常数据 df = df[df["followers_count"] >= 0] df = df.drop_duplicates(subset=["uid"]) # 实际场景要保留 uid 列 X = df[["followers_count", "follow_count", "statuses_count"]].values y = df["is_malicious"].values逻辑说明:取出三列数值特征和标签列,drop_duplicates按 uid 去重,避免同一用户被抓多次造成数据泄漏。参数说明:charset="utf8mb4"必须和建库时一致,否则读中文会报UnicodeDecodeError;WHERE followers_count IS NOT NULL是为了过滤脏数据,爬虫抓取失败时最容易产生 NULL。
这里要特别提醒:训练集里不要直接放 uid。uid 是类别型标识,放进逻辑回归会被当成数值,导致模型学到“某个特定 ID 是恶意用户”这种完全不可泛化的规律。uid 只能用于去重和回查,不能作为特征。
3. 算法层拆解:特征工程与逻辑回归识别模型
3.1 特征工程:恶意用户长什么样
恶意用户和正常用户的行为差异,最直观的几条:关注数极高但粉丝数极低,典型的“广撒网”式关注;微博内容多为转发或刷屏,原创率低;注册时间短但发博频率异常;昵称带有一串随机数字或营销关键词。这些领域知识直接决定了特征怎么构造。
基于这些先验,把原始字段转成真正有区分度的特征:
| 特征名 | 计算公式 | 含义 |
|---|---|---|
| follow_fan_ratio | 关注数 / (粉丝数 + 1) | 恶意用户通常大于 5 |
| status_freq | 微博数 / (注册天数 + 1) | 检测刷屏行为 |
| original_rate | 原创微博数 / 总微博数 | 营销号普遍偏低 |
| has_mobile | 是否绑定手机 | 批量注册号常缺失 |
| nickname_entropy | 昵称字符熵 | 随机生成昵称的特征 |
特征构造代码:
# 特征构造:ratio 和 freq 必须在训练前算好 df["follow_fan_ratio"] = df["follow_count"] / (df["followers_count"] + 1) df["register_days"] = (pd.Timestamp("now") - pd.to_datetime(df["created_at"])).dt.days df["status_freq"] = df["statuses_count"] / (df["register_days"] + 1) df["original_rate"] = df["original_count"] / (df["statuses_count"] + 1) # 构造最终特征矩阵 feature_cols = ["follow_fan_ratio", "status_freq", "original_rate", "has_mobile", "nickname_entropy"] X = df[feature_cols].fillna(0).values逻辑说明:分母加 1 是同时解决除零和避免 NaN 的一种常见做法;register_days为 0 的新账号在分母加 1 后不会被计算成无穷大。参数说明:fillna(0)用 0 填充缺失值,但这只在缺失比例低于 5% 时合理;如果某列缺失超过 30%,我一般会直接丢弃这一列,而不是硬填。
从实际训练结果看,follow_fan_ratio通常是逻辑回归系数最大的特征,说明它对恶意用户判别贡献最高。如果你拿不到文本特征,优先保证这个特征质量最可靠。
3.2 模型选择与训练:逻辑回归是这类任务的默认答案
恶意用户识别本质是二分类。learner目录下训练代码实现了逻辑回归加交叉验证的经典组合。逻辑回归相比树模型的最大优点是特征系数完全可解释,答辩时可以直接展示每个特征对恶意判别的权重,比神经网络更适合教学场景。
# 训练逻辑回归模型,含交叉验证和特征标准化 from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score, train_test_split from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) model = make_pipeline( StandardScaler(), LogisticRegression(C=1.0, class_weight="balanced", max_iter=1000) ) scores = cross_val_score(model, X_train, y_train, cv=5, scoring="f1") print("5-Fold F1: %.3f +- %.3f" % (scores.mean(), scores.std()))逻辑说明:StandardScaler会把关注数、粉丝数这类量纲差异巨大的特征压缩到同一尺度,逻辑回归的梯度下降才能正常收敛;class_weight="balanced"用于缓解恶意用户样本少导致的“全部预测为正常用户”问题;交叉验证选 F1 而不是 accuracy,是因为该类别的正样本比例通常很低。参数说明:C=1.0是正则化强度的默认值,数据量在几千到几万时不需要刻意改;max_iter=1000必须设大,默认 100 在标准化后也容易警告不收敛,我一般直接给 1000。
3.3 模型评估:别只看准确率
恶意用户识别里最容易栽的坑是准确率虚高。如果恶意用户只占 5%,把所有用户判为正常,准确率也有 95%。所以要重点看混淆矩阵、召回率、F1。
from sklearn.metrics import classification_report, confusion_matrix # 用概率输出而不是直接 predict,方便后面调阈值 proba = model.predict_proba(X_test)[:, 1] y_pred = (proba >= 0.5).astype(int) print(classification_report(y_test, y_pred, target_names=["normal", "malicious"])) print(confusion_matrix(y_test, y_pred))逻辑说明:proba是恶意类概率,threshold=0.5是默认阈值;如果业务目标是“宁可误伤也要多抓”,可以把阈值降到 0.3。参数说明:classification_report里malicious行的 recall 表示恶意用户被识别出来的比例,precision 表示被抓出来的里面有多少是真恶意用户。答辩时常被追问“为什么不用准确率”,回答“准确率受样本不平衡影响,F1 和召回率更贴合业务目标”就能直接命中采分点。
4. 工程层拆解:从训练脚本到 Flask 接口
4.1 Flask Demo:加载模型对外提供预测
flask_demo目录的作用是把训练好的模型包装成简单 Web 服务,配合content.html页面做交互。核心逻辑是接收用户特征,返回恶意概率。
# app.py 核心代码:加载模型并接收特征返回预测结果 import joblib from flask import Flask, request, jsonify app = Flask(__name__) model = joblib.load("model.pkl") # 训练完成后保存的模型文件 @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json() # 前端已经算好了特征值,后端不承担特征计算 feats = [data["follow_fan_ratio"], data["status_freq"], data["original_rate"]] proba = model.predict_proba([feats])[0][1] return jsonify({ "proba": proba, "malicious": int(proba >= 0.5) }) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)逻辑说明:host="0.0.0.0"让服务可被局域网其他机器访问,方便答辩时从自己的笔记本演示;predict_proba返回恶意类的概率,比例子直接返回类别更灵活,方便随时调阈值。参数说明:debug=False必须保持,否则生产环境暴露调试器有安全风险;端口 5000 是 Flask 默认端口,被占用时可以换 5001。
一个容易被忽略的细节:joblib.load在启动时加载模型,但 sklearn 的LogisticRegression第一次predict_proba才初始化底层 liblinear 库,所以第一次请求会卡顿。解决方法是启动后先调一次缓存接口:
# 启动时预热模型,避免第一个请求慢 5-10 秒 model.predict_proba([[0, 0, 0]])4.2 全流程串联:从采集到预测的管道
顺着项目的文件结构走一遍完整数据流,能帮你快速定位每一个文件该在哪个环节运行:
weiboCrawler.py加concern.py、fans.py抓取原始用户关系数据,结果写入文本文件或直接落 MySQLinsertUser.py把新抓到的用户数据插入user_profile表newUser.py做增量更新,避免每天全量重爬- 从 MySQL 导出特征训练集,
learner目录完成模型训练与评估 flask_demo加载模型,content.html提供录入界面,浏览器调用/predict接口
mysqldump.sh是数据库备份脚本,nohup.out是后台运行日志。从工程角度看,爬虫和 Flask 服务都应该用nohup或systemd守护,避免 SSH 断开后进程被杀:
# 后台启动 Flask 服务,日志写入 nohup.out nohup python app.py > nohup.out 2>&1 &4.3 关键文件与配置清单
拿到资源后按这张表对号入座,能快速知道该先看哪个文件:
| 文件/目录 | 作用 | 使用建议 |
|---|---|---|
| weiboCrawler.py | 微博基础爬虫 | 先验证 cookies.txt 是否有效 |
| concern.py / fans.py | 抓关注、粉丝列表 | 请求频率控制在每秒一次以内 |
| cookies.txt | 登录 Cookie | 失效后爬虫返回登录页 HTML |
| xinan.sql | 空表结构 | 用于理解字段含义 |
| xinan_with_data.sql | 带数据完整库 | 导入后可直接复现训练 |
| evil1.txt | 恶意用户 ID 黑名单 | 用于生成标注 |
| learner | 训练和评估代码 | 模型保存为 model.pkl |
| flask_demo | Web 服务 | 先本地跑通再部署 |
| insertUser.py / newUser.py | 入库与增量更新 | 注意唯一索引冲突处理 |
| mysqldump.sh | 数据库备份 | 建议 crontab 每天执行 |
注意cookies.txt是爬虫的生命线。Cookie 过期后爬虫不会立刻报错,而是返回登录页的 HTML,解析结果为空,很多人会误以为是目标用户没有数据。我一般会写个脚本每隔一段时间请求一次用户接口,返回内容里如果检测不到 “screen_name” 字段就说明 Cookie 已失效,需要手动更新。
5. 避坑指南:微博恶意用户识别的五个典型翻车现场
5.1 SQL 导入失败,数据全部丢失
现象:导入xinan_with_data.sql时提示Unknown collation,或导入成功后中文昵称全是乱码。
原因:SQL 文件里表结构用的是utf8mb4_bin字符集,而 MySQL 客户端连接时用了系统默认的latin1或utf8,两边排序规则对不上。
解决:导入前在 mysql 客户端先执行SET NAMES utf8mb4;,再指定字符集重新导入。
mysql -u root -p --default-character-set=utf8mb4 weibo < xinan_with_data.sql从那以后我每次导入 MySQL 都会先看一眼 init_connect 字符集,宁可多写一句也不赌默认配置。
5.2 模型在验证集上分数很高,新用户预测全是正常用户
现象:交叉验证 F1 有 0.85,接进 Flask 后连续预测 10 个用户全部返回malicious: 0。
原因:训练集里恶意用户样本占比太低,模型决策面偏向多数类;或者新用户数据分布变了,比如注册时间、昵称风格都和训练集不一致。
解决:class_weight="balanced"已加的情况下,尝试对正常用户做人工下采样;同时把阈值从 0.5 降到 0.3,用最近一周新增的标注数据做增量训练。最典型的案例是训练数据里全是 2019 年的恶意脚本特征,而 2023 年的恶意脚本早已换了策略,所以这个任务里周期性重训不是可选项,是必选动作。
5.3 爬虫抓到的粉丝数全是 0
现象:concern.py/fans.py运行正常,没有报错,但抓回来的数据里followers_count全是 0。
原因:微博前端改版后旧接口返回字段被移除,或者 Cookie 有效但请求 URL 里没带正确的 uid 参数。
解决:用浏览器开发者工具打开一个真实用户主页,重新抓取 Ajax 接口地址;确认 Cookie 是否包含SUB字段;nohup.out日志中如果返回 200 但 JSON body 为空,基本就是接口路径过期。这类问题靠断点调试定位不了,直接看响应内容最省时间。
5.4 Flask 接口第一次请求要等 10 秒
现象:本地运行时第一次请求/predict卡很久,后面的请求又正常变快。
原因:joblib.load只负责把磁盘文件反序列化到内存,sklearn 底层 liblinear 资源要等第一次预测时才初始化,叠加了模型冷启动。
解决:在 Flask 启动后手动调用一次model.predict_proba([[0, 0, 0]])做预热。如果部署在公网服务器上,用 Nginx 反向代理并开启 gzip,首屏响应也能明显改善。
5.5 MySQL 连接字符集不一致导致训练数据缺失
现象:用 pymysql 读出的 DataFrame 里statuses_count大量为NaN,模型训练直接报 ValueError。
原因:数据库字段允许 NULL,爬虫没抓到某些微博数时会插入 NULL,pandas 读入后就是 NaN;字段类型定义过窄也会导致导入时被截断为 NULL。
解决:SQL 查询里用COALESCE(statuses_count, 0)做兜底,或者读入后用fillna(0)填充。如果特征缺失超过 30%,不要填充,直接删除整列。数据质量比算法复杂度更影响这个项目的最终效果。
6. 验证与进阶:阈值扫描与增量更新策略
6.1 阈值扫描:把模型调到业务需要的强度
训练完模型后最该做的不是看准确率,而是跑一遍阈值扫描。默认 0.5 是数学上的分界线,不是业务上的最优解。恶意用户识别这种场景,漏放一个恶意账号的成本远高于误伤一个正常账号,所以要主动降阈值去换召回率。
import numpy as np from sklearn.metrics import precision_recall_curve proba = model.predict_proba(X_test)[:, 1] precision, recall, thresholds = precision_recall_curve(y_test, proba) # 在召回率不低于 0.8 的前提下,选 precision 最高的阈值 valid = np.where(recall >= 0.8)[0] best_idx = valid[np.argmax(precision[valid])] best_thr = thresholds[best_idx] print("suggest threshold: %.3f -> recall=%.3f precision=%.3f" % ( best_thr, recall[best_idx], precision[best_idx]))逻辑说明:precision_recall_curve返回每个阈值下的精确率和召回率,np.where(recall >= 0.8)把小于业务底线的阈值全部排除,再从剩下候选里选精确率最高的那个。参数说明:0.8 这个底线不是固定值,如果业务上可以接受 70% 召回,就改成 0.7,这是个业务参数不是算法参数。
6.2 增量更新:避免模型上线即衰减
微博这类数据变化很快,每个月都会出现新的恶意用户模式。我的更新策略是:
- 每天把新增的已标注样本写入
new_user_label表 - 每周把新数据和原训练集合并,重跑一次逻辑回归
- 用
warm_start=True保留旧权重作为初始值,训练速度能快 20% 左右 - 每次更新前先跑交叉验证对比 F1,如果下降超过 0.05 就回滚到上一版模型
一个迭代里最容易被忽略的是特征对齐。新增样本的字段如果和旧模型不一致,predict_proba会直接报维度错误。每次训练前我会打印一次model.n_features_in_和训练集列名,确保线上线下特征完全一致。
这组方法就是从这套微博恶意用户识别项目里反复验证出来的。最开始我拿到模型只跑了一下准确率就交付,结果真实环境里误报率极高。现在已经形成习惯:任何二分类模型交付前,强制走一遍阈值扫描,再把召回率、精确率曲线和特征系数表一起提交。答辩时评委看到的不只是一个模型文件,而是你清楚知道业务上线后会怎么衰减、阈值该怎么调,这才是高分项目最值钱的部分。希望帮到你。
本文还有配套的精品资源,点击获取