news 2026/10/8 19:01:30

Flask+SQLite博客系统:从架构设计到数据库迁移完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask+SQLite博客系统:从架构设计到数据库迁移完整实战

简介:基于Python的个人博客系统设计与实现毕业设计项目,完整提供后端源码、数据库脚本与软件说明书,面向正在学习Python Web开发或需要完成毕业设计的学生。项目参照主流Python Web框架的典型结构搭建,实现文章发布、评论互动、用户注册登录等博客核心功能;数据库部分使用SQL,配合ORM进行数据管理,并给出环境配置与部署运行说明。压缩包共包含7387个文件,大小约25.83MB,主要文件类型包括Python源代码及其编译文件、网页前端资源(html/js/css)、数据库脚本、Word使用文档以及若干依赖库文件,目录划分清晰,便于定位查找。目前已吸引775人学习下载,使用该资源可系统梳理博客系统从需求分析、数据模型设计、ORM数据访问、路由配置、用户认证到数据迁移的完整开发流程,所有代码和文档配合良好,既能作为毕业设计的完整参考,也能在此基础之上扩展个性化功能。

1. 一个 Python 博客系统 zip 包的价值:半天跑通带数据库的毕设项目

这个标题背后是一类很常见的交付物:基于 Python 的个人博客系统,把 Web 代码和数据库一起打成 zip。所谓设计与实现,落到代码上就是两件事:设计表结构,实现用户、文章、评论的增删改查。这类项目能解决的问题很直接——你拿到的是一个“能跑、能演示、能讲清”的博客,而不是一个三天打鱼两天晒网的半成品。

它适合三类人:准备毕设或课设答辩的学生,想拿现成项目练手 Web 开发的 Python 学习者,以及单纯想给自己搭一个本地博客或笔记站的人。前提是 Python 环境已经装好,然后按下面的章节把项目完整跑一遍,再按自己的需求去改字段、换数据库。我下面讲的顺序是:架构选型、核心功能代码、数据库初始化和备份、报错排查、最后是搜索和迁移 MySQL 的进阶做法。

2. 基于 Flask + SQLite 的架构选型:为什么不是 Django,也不是 MySQL

2.1 先看清 zip 里的文件结构

解压“基于 Python 的个人博客系统(包含数据库)”这类项目包,最常见的长这样:

blog_system/ ├── app.py # 应用入口,Flask 实例和所有路由定义 ├── models.py # 数据库模型:User / Post / Comment ├── init_db.py # 初始化数据库脚本,一键建表 ├── requirements.txt # Python 依赖清单 ├── blog.db # SQLite 数据库文件,单文件、直接携带 ├── templates/ # 页面模板 │ ├── base.html # 母版页,导航栏和页脚 │ ├── index.html # 首页文章列表 │ ├── article.html # 文章详情页 │ ├── login.html # 登录页 │ ├── register.html # 注册页 │ └── admin/ │ └── edit.html # 发布 / 编辑文章页 └── static/ # 静态资源 css / js / 图片

你拿到包以后要做的第一件事,是分清“代码”和“数据”两类文件。app.py、models.py、init_db.py 是代码,blog.db 是数据。在动手改代码之前,先打开数据库看一眼表结构。用命令行工具执行一行就能列出所有表:

sqlite3 blog.db ".tables"

如果看到 user、post、comment 三张表,那这个项目的功能边界就很清晰:用户管理、文章管理、评论管理。如果表更多,说明作者把标签、分类做成了独立表,功能更完整,但代码量和答辩证也会同步变多。先看懂这个文件结构,后面每一步才不会抓瞎。

2.2 Flask 在这个项目里是“标准答案”

我接触过的同类博客系统里,Flask + SQLite 出现的频率远高于 Django + MySQL。原因在于这几种选择的组合,刚好卡在“能完整演示增删改查”和“代码量适合讲清”的平衡点上。

对比项Flask + SQLiteDjango + MySQL
上手门槛低,一个 app.py 能看完全部路由高,settings、ORM、Admin 站点都是额外的概念
数据库交付单文件 blog.db 直接放 zip 里需要额外提供 sql 导入脚本和建库步骤
表单处理手动写表单、手动校验自带 Form 体系,封装较重
答辩叙事每个视图函数都能对应一个功能点框架自动完成的部分多,容易变成“背文档”

再加上“包含数据库”这个交付要求:如果数据库选 MySQL,老师打开你的项目要先装 MySQL 服务、建库、改密码、执行 sql 脚本,任一步骤失败项目都跑不起来。用 SQLite 则不存在这个问题,Python 自带驱动,文件放哪都能跑。SQLite 的另外一个隐性好处在第 4 章会体现:备份就是复制一个文件。

2.3 三张表支撑整个博客:字段设计和两个容易忽略的索引

核心表结构通常是这样的:

表字段
userid、username、password_hash、created_at
postid、title、content、summary、category、tags、views、created_at、updated_at、user_id
commentid、post_id、user_id、content、created_at

三个字段设计上的细节值得说透。第一,password 字段应该存密码哈希而不是明文,字段名写成 password_hash 能时刻提醒自己别踩明文存储的坑。第二,post 表里冗余了 summary 字段,列表页只取 id、title、summary,就不需要把整篇 content 从数据库拖出来再截断,列表页性能会好很多。第三是索引,给 post.user_id 和 comment.post_id 都加上 index=True,否则数据量到几百条时关联查询还能忍,到几千条时文章详情页会明显变慢。

数据流向也很固定:首页是 post 表按创建时间倒序分页;文章详情页通过 post_id 去 comment 表查评论;个人中心通过 user_id 反查文章列表。方向永远是从 user、post 这种主表流向 comment 这种从表,不要在评论列表里循环查询文章表,那是典型的 N+1 查询,新手在这里翻车的概率极高。

3. 用户与文章的增删改查:注册登录、发布、编辑、删除的完整代码

3.1 注册登录:用 werkzeug 管理密码,而不是自制加密

注册和登录是几乎所有 Web 项目的入口,这里最需要注意的是密码处理。自己写一个 hash 函数、或者用 base64 编码一下,都是错误的做法。标准做法是使用 Werkzeug 自带的密码工具,代码量最少,安全性也有保障。

from flask import Flask, render_template, request, session, redirect, url_for from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) app.secret_key = "dev-secret-key-change-in-production" # 部署前改成环境变量 app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db" db = SQLAlchemy(app) class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(32), unique=True, nullable=False) password_hash = db.Column(db.String(256), nullable=False) def set_password(self, raw_password): self.password_hash = generate_password_hash(raw_password) def check_password(self, raw_password): return check_password_hash(self.password_hash, raw_password) @app.route("/register", methods=["GET", "POST"]) def register(): if request.method == "POST": username = request.form.get("username", "").strip() password = request.form.get("password", "") if len(username) < 3 or len(password) < 6: return render_template("register.html", error="用户名至少3位,密码至少6位") if User.query.filter_by(username=username).first(): return render_template("register.html", error="用户名已存在") user = User(username=username) user.set_password(password) # 只存哈希值,不存明文 db.session.add(user) db.session.commit() return redirect(url_for("login")) return render_template("register.html") @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form.get("username", "") password = request.form.get("password", "") user = User.query.filter_by(username=username).first() if user and user.check_password(password): session["user_id"] = user.id session["username"] = user.username return redirect(url_for("index")) return render_template("login.html", error="用户名或密码错误") return render_template("login.html")

代码逻辑说明:generate_password_hash 会自动加随机盐,所以同一密码每次生成的哈希值都不同,这是正常现象,不是 bug。check_password_hash 负责校验。整个过程中,数据库里永远只保存哈希后的字符串,密码原文只在内存里存活一瞬间。两个请求处理函数都同时支持 GET 和 POST,GET 返回页面,POST 处理表单。

参数说明:username.strip() 是为了去掉首尾空格,防止用户注册“admin ”这种带空格的账号;密码最小长度限制在 6 位,是因为演示密码 123456 正好 6 位,太短校验没有意义,太长会给演示带来无谓麻烦。

3.2 文章发布和编辑:一条路由同时处理 GET 和 POST

文章的发布和编辑在操作上非常像,所以常见做法是用一条路由同时管两件事:URL 上没有文章 id 时是新增,有 id 时是编辑。这样模板也可以复用同一份 admin/edit.html。

@app.route("/post/new", methods=["GET", "POST"]) @app.route("/post/<int:post_id>/edit", methods=["GET", "POST"]) def edit_post(post_id=None): if "user_id" not in session: return redirect(url_for("login")) post = None if post_id: # 双条件查询:必须同时满足文章 id 和当前登录用户 id post = Post.query.filter_by(id=post_id, user_id=session["user_id"]).first() if not post: return "这篇文章不存在或无权编辑", 404 if request.method == "POST": title = request.form.get("title", "").strip() content = request.form.get("content", "").strip() summary = request.form.get("summary", "").strip() if not title or not content: return render_template("admin/edit.html", error="标题和正文不能为空", post=post) if post is None: post = Post(user_id=session["user_id"]) db.session.add(post) post.title = title post.content = content post.summary = summary[:100] if summary else content[:100] db.session.commit() return redirect(url_for("article_detail", post_id=post.id)) return render_template("admin/edit.html", post=post)

逻辑说明:新增和编辑共用 validate 逻辑,前端传过来的 title 和 content 都先 strip 再去判断是否为空,避免全空格内容通过校验。post_id 判断的关键在于 filter_by 里同时传了 user_id=session["user_id"],这是一层很基础的越权保护:你可以编辑自己文章,但直接手改 URL 去 edit 别人的文章时,查询结果为空,返回 404。

参数说明:summary 字段手动填了就存手动内容,没填则截取正文前 100 个字符兜底。这样列表页始终有摘要可显示,不需要在模板层再写截断逻辑。

3.3 删除、列表分页与评论:跨浏览器支持与三个设计细节

这三个功能看着简单,但细节里藏着最容易在验收时暴露的问题。

@app.route("/") def index(): page = request.args.get("page", 1, type=int) pagination = Post.query.order_by(Post.created_at.desc()).paginate(page=page, per_page=5) return render_template("index.html", pagination=pagination) @app.route("/post/<int:post_id>") def article_detail(post_id): post = Post.query.get_or_404(post_id) comments = Comment.query.filter_by(post_id=post_id).order_by(Comment.created_at.asc()).all() return render_template("article.html", post=post, comments=comments) @app.route("/post/<int:post_id>/delete", methods=["POST"]) def delete_post(post_id): if "user_id" not in session: return redirect(url_for("login")) post = Post.query.filter_by(id=post_id, user_id=session["user_id"]).first() if post: Comment.query.filter_by(post_id=post_id).delete() # 先删评论 db.session.delete(post) # 再删文章 db.session.commit() return redirect(url_for("index"))

逻辑说明:分页的 page 参数用 type=int 强转,用户把 page 写成 abc 时不会报 500,而是回落到默认值 1。per_page=5 适合 demo,展示列表时刚好一屏一页。删除评论使用批量 delete 而不是循环遍历,执行效率更高,也能避免评论数量多时逐条删带来的事务长持有。

删除操作强制使用 POST 而不是 GET,这是很多人不会特意讲、但实际非常关键的一点:搜索引擎爬虫和浏览器预取机制可能会自动 GET 一个删除链接,如果你用 GET 删除,文章可能被“意外”删掉。这个设计细节在答辩时讲出来,分量很足。

跨浏览器支持方面,我一般会强调模板里尽量用 Bootstrap 栅格代替手写 flex,表单提交按钮用常规 input type="submit",不要用“点击某个 div 再触发 JS 提交”的花哨写法。评审老师的浏览器版本可能很老,学校机房的 IE 兼容模式也未必支持最新 CSS 特性,标准 form 提交永远最稳。

4. 数据库设计与初始化:建表、测试数据、.db 文件的备份恢复

4.1 初始化数据库:用脚本建表,不要手动敲 CREATE TABLE

拿到一个带数据库的项目包,第一件事是确认 blog.db 里的表结构和 models.py 是否对得上。对不上的情况在改过代码之后特别常见。如果你改动了模型,需要重新初始化数据库。

# init_db.py from app import app, db from models import User, Post, Comment with app.app_context(): db.drop_all() # 删除所有表,仅限开发环境使用 db.create_all() # 按 models 里的定义重新建表 print("数据库初始化完成")

逻辑说明:drop_all 和 create_all 是成对出现的,先清理旧表再创建新表,保证模型和表结构完全一致。这条脚本只适合开发阶段反复改模型时使用,生产环境一旦有了真实数据,绝对不能直接跑 drop_all。

create_all 的特性需要特别注意:它只会新建不存在的表,不会给已有表增加缺失的字段。你给 Post 模型新增一个 category 字段后,光跑 create_all 没有任何效果,旧表里没有这一列,查询时直接报 OperationalError: no such column。这段时期你要处理的其实就是数据库修改结构的问题。SQLite 的常见解法是执行 ALTER TABLE post ADD COLUMN category VARCHAR(32),或者干脆把旧库备份后删掉重建。第一次跑通项目,我建议直接重建,省事且干净。

4.2 造一批能演示的测试数据

空表项目交给老师,老师还得自己注册账号、慢慢发文章才能看到首页效果,体验很差。正确做法是在初始化之后顺手造一批测试数据,让首页和详情页打开就有东西看。

# seed.py from datetime import datetime, timedelta from app import app, db from models import User, Post, Comment with app.app_context(): demo_user = User(username="demo") demo_user.set_password("123456") db.session.add(demo_user) admin = User(username="admin") admin.set_password("admin123") db.session.add(admin) db.session.flush() # 先落库拿到两个用户的 id for i in range(10): post = Post( user_id=demo_user.id if i % 2 == 0 else admin.id, title=f"示例文章{i+1}:Flask 博客开发记录", content="这是一段用于演示的正文内容,用来让详情页看起来有足够长度。" * 20, summary=f"示例文章{i+1} 的摘要", created_at=datetime.now() - timedelta(days=i), ) db.session.add(post) db.session.add(Comment(post_id=1, user_id=admin.id, content="第一条演示评论")) db.session.commit()

逻辑说明:flush 的作用是把新用户的 id 立刻从数据库取回来,后续构造文章时才能正确写入 user_id。如果不 flush,SQLAlchemy 也会在最终 commit 时自动处理,但显式 flush 对阅读代码的人来说更直观。created_at 用 now 减去 i 天,首页按时间倒序排列时,10 篇文章的先后次序一眼就能看出来。

参数说明:密码用 123456 和 admin123,是为了现场演示时输入方便;正式部署必须改掉。正文用字符串乘法制造长度,纯粹是为了让摘要截断效果可见,演示完可以替换成真实内容。

4.3 .db 文件的备份、恢复和 WAL 三个坑

SQLite 的备份在直觉上极其简单:把 blog.db 复制走就完了。实际并没那么简单,还得看数据库有没有开启 WAL 模式。SQLite 从较新版本开始默认在部分场景下使用 WAL 日志模式,数据库目录里会多出 blog.db-wal 和 blog.db-shm 两个伴随文件。这时候只复制主文件,最近写入的数据可能还躺在 -wal 文件里,备份并不完整。

所以备份前先执行 checkpoint,把 WAL 内容合并回主库文件:

sqlite3 blog.db "PRAGMA wal_checkpoint(TRUNCATE);" cp blog.db /path/to/backup/blog_$(date +%F).db

逻辑说明:wal_checkpoint(TRUNCATE) 有两个动作:把 WAL 文件里的数据写回主库,然后把 -wal 文件清空。执行后再复制,blog.db 才是完整状态。恢复操作就是把备份文件覆盖回项目目录,但前提是数据库版本和 models.py 对得上,否则会触发 5.1 节那个 no such column 问题。

这里还有一个人多遇到过但我必须提的坑:别把 .db 文件放到 U 盘、共享目录或云盘同步盘里直接运行。SQLite 依赖操作系统的文件锁机制,网络盘和同步盘的锁状态非常不稳定,跑着跑着就会报 database is locked。这不是玄学,是 SQLite 架构决定的。开发调试就把文件放本地,部署才考虑换 MySQL。

5. 避坑/常见问题/排查:五个让我熬夜的经典报错

5.1 no such column:改了模型字段,旧库没跟上

现象:给 Post 模型加了 category 字段,重启应用访问首页,直接报 sqlite3.OperationalError: no such column: post.category。

原因:create_all 只创建不存在的表,绝不修改已有表结构。SQLite 对删除列、改列类型的支持又很弱,导致数据库结构落后于代码结构。

解决:先判断库里的数据是否重要。不重要就备份后删除 blog.db,重新执行 init_db.py。有数据就执行 ALTER TABLE post ADD COLUMN category VARCHAR(32) DEFAULT 'python',再用 sqlite3 blog.db "PRAGMA table_info(post);" 确认列已经存在,最后再启动应用。

5.2 database is locked:单用户博客也逃不过的写锁

现象:发布文章或提交评论时偶尔报 sqlite3.OperationalError: database is locked。多刷新几个页面之后出现的概率变高。

原因:SQLite 同一时刻只允许一个写事务。Flask 开发服务器默认开启多线程,两个请求几乎同时发起写操作,后到的那一个拿不到写锁就直接失败。

解决:在数据库连接串上加超时参数,sqlite:///blog.db?timeout=10,让连接等待 10 秒而不是立刻报错。同时把写操作尽量收敛到同一个函数里,不要在两次表单提交之间开多个 session。如果并发量继续上涨,就该准备迁移到 MySQL,这是 SQLite 的定位问题,不是配置问题。

5.3 中文乱码:meta charset 和文件编码一起查

现象:首页标题正常,正文里中文变成“鍖呭惈鏁版嵁”这类乱码;或者表单提交后中文全部变成问号。

原因:三层没对齐。模板文件本身不是 UTF-8 编码,模板里没有声明 meta charset,或者数据库连接的字符集被强行改成了别的编码。SQLite 本身存的就是 UTF-8 字节,乱码大概率出在模板层。

解决:用编辑器把所有模板文件另存为 UTF-8 编码,在 base.html 的 head 区域第一行写上 。然后检查应用里有没有手动设置 response 的 Content-Type charset,Flask 默认走 UTF-8,不要画蛇添足。

5.4 密码明文入库:比乱码更严重的一类问题

现象:打开数据库一看,user 表里的 password 字段存的是明文 123456,或者一串看似加密但每次登录都不对的值。

原因:项目作者对密码存储不了解,自己写了 base64 或一个简单的自定义加密函数。更糟的是用了随机盐却没有把盐存下来,导致校验时永远算不出相同哈希。

解决:回到 3.1 节的写法,用 generate_password_hash 加密、check_password_hash 校验,盐由 Werkzeug 内部管理,根本不存在“存盐”这件事。如果你接手的是已经明文入库的系统,上线前必须让全部用户重置密码,或者提供一个一次性改密链接。这个坑不补,项目做得再漂亮也是白搭。

5.5 模板 404、静态文件 404:启动路径不对,不是代码问题

现象:python app.py 启动成功,浏览器访问首页却报 jinja2.exceptions.TemplateNotFound: index.html,或者 CSS 文件全部 404。

原因:Flask 实例化时用了相对路径指定模板目录,比如 Flask(name, template_folder="templates")。当你在项目根目录以外的位置启动 app.py 时,这个相对路径指向的工作目录里根本没有 templates 文件夹。

解决:用file拼绝对路径,让模板目录只跟 app.py 所在位置有关,跟启动命令所在目录无关。

import os basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__, template_folder=os.path.join(basedir, "templates"), static_folder=os.path.join(basedir, "static"))

逻辑说明:file是当前文件的完整路径,os.path.dirname 拿到 app.py 所在目录,再拼上 templates 和 static。这样你在任何目录下执行 python app.py,Flask 都能找到模板和静态文件。遇到 TemplateNotFound 和静态资源 404,先检查这段,再检查文件是不是真的存在。

6. 进阶:给博客加搜索,并把 SQLite 平滑迁到 MySQL

6.1 一条 LIKE 查询完成搜索

个人博客的文本量,撑不起搜索引擎组件,一个 LIKE 查询就够用。

@app.route("/search") def search(): keyword = request.args.get("q", "").strip() if not keyword: return redirect(url_for("index")) results = Post.query.filter( db.or_(Post.title.contains(keyword), Post.content.contains(keyword)) ).order_by(Post.created_at.desc()).all() return render_template("search.html", posts=results, keyword=keyword)

逻辑说明:contains 在 SQLAlchemy 里生成 LIKE %keyword%,对几百篇文章的库毫秒级返回。等文章量真到几十万条,再谈分词和倒排索引不迟。

6.2 SQLite 到 MySQL:类型对照与迁移两条路

如果部署到云服务器需要换 MySQL,先把类型对照记清楚:

SQLiteMySQL
INTEGER PRIMARY KEYINT PRIMARY KEY AUTO_INCREMENT
TEXTTEXT 或 LONGTEXT
DATETIMEDATETIME
FLOATFLOAT / DOUBLE

迁移有两条路。第一,写 Python 脚本从 SQLite 逐表读数据,再逐条写入 MySQL 连接,适合一次性搬迁;第二,项目本身长期换库,可以改用 SQLAlchemy 指向 MySQL 的 URL,ORM 自动建表后再导数据。注意 SQLite 的 INTEGER PRIMARY KEY 自带自增,换成 MySQL 后要改回 AUTO_INCREMENT,同时把 sqlite_sequence 表里的当前自增值读出来,写进 MySQL 的自增值,否则新文章 id 会撞车。市面上那些数据库同步软件大多面向 MySQL 实例之间,对这种跨类型的单库迁移帮不上忙,老实写脚本才是正路。

6.3 三个必须跑通的验证路径

交付前跑一遍这三点,比任何检查清单都实在。第一,注册新用户,登录后首页右上角出现用户名,刷新登录页自动跳回首页,说明 session 生效。第二,发布一篇中文标题、正文超过 200 字的文章,列表页只显示摘要,详情页显示完整正文,删除后列表页少一篇。第三,在文章详情页发两条评论,换一个用户登录确认评论可见,然后删除文章,确认评论被一并清理。

这三个动作分别验证权限控制、增删改查、级联删除,也是答辩评委最常上手操作的三件事。项目值不值得交付,就看你敢不敢让老师现场点这三下。

我在交付这类项目前,总会多花半小时把需求清单之外的边界补上:删除文章时评论怎么办、密码忘记怎么办、数据库连不上时页面是否给友好提示。这些位置才是真正劝退使用者的地方。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 18:57:18

SCADA北向接口全解析:从数据建模到MQTT/OPC UA实操

1. 北向接口到底在解决什么问题做SCADA项目最容易被忽略、又最绕不开的一个环节&#xff0c;就是数据怎么从系统里拿出来给别家用。很多人一开始接触SCADA&#xff0c;都以为它就是个“画面上显示数据、做报警、存历史曲线”的组态软件&#xff0c;等真正进入到工厂数据集成阶段…

作者头像 李华
网站建设 2026/10/8 18:55:20

Travel (FuJian) 2026.10.07

Travel (FuJian) 2026.10.072026年10月07日 永泰博物馆 旧建筑 也不知道古建筑还有多少了

作者头像 李华
网站建设 2026/10/8 18:54:04

软件测试5 测试分类

&#x1f4d1;目录&#xff08;俏皮版&#xff09; &#x1f914;为啥测试要搞这么多分类&#xff1f;&#x1f3af;按测试目标分&#xff1a;我们要测软件哪些方面&#xff1f; 2.1 UI 界面测试&#xff5c;软件的 “颜值质检员”2.2 功能测试&#xff5c;软件会不会干活2.3 性…

作者头像 李华
网站建设 2026/10/8 18:52:38

DAY6CSS学习4

56.浮动&#xff08;二&#xff09;元素浮动后的特点1.脱离文档流2.无论浮动前是什么元素&#xff0c;浮动后的默认宽与高都是被内容撑开&#xff08;尽可能小&#xff09;&#xff0c;而且可以设置宽高3.不会独占一行&#xff0c;可以与其他元素共用一行4.不会margin合并&…

作者头像 李华
网站建设 2026/10/8 18:52:08

【计算机毕设选题推荐】基于Spark的跨平台消费者评论情感分析与数据可视化系统源码 毕业设计 选题推荐 数据分析 机器学习

> ✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡如果你遇到…

作者头像 李华