恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
豆瓣图书数据分析可视化系统:爬虫+Flask+Echarts+机器学习完整实战
首页
资讯中心
/
豆瓣图书数据分析可视化系统:爬虫+Flask+Echarts+机器学习完整实战
豆瓣图书数据分析可视化系统:爬虫+Flask+Echarts+机器学习完整实战
发布时间:2026/9/26 11:37:22
我一直觉得爬虫 数据可视化 Flask 机器学习这套组合是国内计算机专业毕业设计里最稳的选题之一。它不依赖特殊的硬件环境不需要多高深的算法理论却能完整体现数据采集、清洗、建模、展示的全部流程答辩时有东西可讲、有图可看、有数据支撑。市面上大量培训机构和源码平台都在卖这类系统说明它确实是经过了大量验证的成熟模板。我准备用这一篇文章把我做过的豆瓣图书数据分析可视化系统的完整过程拆给你看。从爬虫采集、数据清洗、机器学习建模到Echarts图表展示和Flask后端整合每个环节我都会给出可直接落地的代码、关键参数和踩坑记录。这篇文章适合正在准备毕业设计的学生也适合想快速上手Python全栈数据应用的开发者。照着做你不仅能跑通一个完整项目更重要的是能理解每个模块为什么要这样设计。1. 系统框架与模块划分毕业设计的标准答案怎么搭1.1 整体架构三层分离让数据串起来这个项目在架构上其实非常朴素但恰恰是这种朴素让它特别适合教学演示和答辩。整个系统分三层数据采集层、分析处理层、展示应用层。数据采集层用Python写爬虫从豆瓣图书抓取书名、作者、评分、评价人数、出版信息等字段存入本地数据库。分析处理层对原始数据做清洗、去重、特征工程然后用机器学习模型做评分预测或图书分类产出分析结果。展示应用层Flask提供Web服务和JSON接口前端用Echarts绘制各类交互图表用户通过浏览器即可查看分析报告。数据在这三层之间单向流动爬虫写入MySQL分析模块读取MySQL并回写结果Flask再从数据库取数暴露给前端。这样设计最大的好处是模块解耦——爬虫挂了不影响展示模型调参不需要重启Web服务答辩时你甚至可以现场演示只改数据库里的数据图表自动更新这个亮点。我在指导学弟做这个项目时发现很多人一开始就把所有代码堆在一个文件里爬虫、数据处理、Flask路由全混在一起结果跑起来全是全局变量的冲突。我的建议是哪怕是一个Demo级项目也要从第一天就按包结构来组织代码后面调试和写论文都会省很多事。1.2 技术选型为什么是Python Flask Echarts先解释一下这套选型背后的逻辑答辩时老师一定会问为什么用这些技术。Python没什么好说的爬虫和数据分析生态最成熟requests、BeautifulSoup、pandas、scikit-learn都是经过验证的库学习成本低。Flask选择的原因有两个一是微框架够轻量一个Python文件就能起服务适合这种中小型项目二是它和pandas、pymysql配合特别自然直接在路由函数里查询数据库、返回JSON不需要像Django那样引入复杂的ORM。至于Echarts它是百度开源的可视化库中文文档全、社区案例丰富、图表类型多关键是纯前端渲染、配置简单对比D3.js的陡峭学习曲线Echarts对新手友好太多。我还想提一点有些人会用Django替代Flask或者用Plotly替代Echarts。这些方案本身没错但对毕业设计来说Flask Echarts的组合在答辩演示时更直观——你可以明确地指着图说这是前端Echarts渲染的数据来自Flask接口评审老师一听就懂。如果你用Plotly Dash虽然图也能做但爬虫—Flask—Echarts这个经典链条的展示感就被削弱了。2. 爬虫模块实战豆瓣图书数据采集的关键细节2.1 爬虫设计思路先定字段再写代码很多人一上来就写爬虫代码这是本末倒置。正确做法是先明确最终要展示什么图和做什么模型再倒推需要采集哪些字段。以这个项目为例我的目标图表包括评分分布直方图、评价人数Top榜单、图书年份出版趋势、评分与评价人数散点图机器学习部分做评分预测。于是需要采集的字段就很清晰了字段用途来源书名展示标签、榜单维度列表页链接的title属性作者作者维度分析列表页 p 标签评分核心数值图表Y轴、模型预测目标列表页 rating_nums评价人数热度指标散点图对比列表页 pl 标签简介词云素材列表页 quote标签出版社/出版年份时间趋势分析详情页价格模型特征之一详情页这个表一列出来你就知道列表页能拿到什么、哪些字段需要进详情页补抓。豆瓣图书Top250的列表页字段比较稳定但出版社、价格这类信息在列表页是拿不到的得请求每本书的详情页。这里涉及一个关键决策是否抓详情页。我的经验是如果只做Top250详情页全部抓一遍没太大压力250条数据限速合理的话几分钟搞定但如果你要扩展成几千本书就必须设计任务队列 失败重试机制。毕业设计建议控制在500~1000条数据既能满足机器学习训练的基本需求又不会把服务器搞到封IP。2.2 请求头伪装、限速与异常重试爬虫的细节决定你是一帆风顺还是五分钟就被封。豆瓣的反爬策略在同类网站里算温和但依然有频率限制和验证码机制所以请求头伪装和限速是必修课。基础的请求头要包含User-Agent和Refererimport requests import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://book.douban.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8 } def fetch_page(url, retry3): for attempt in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: resp.encoding utf-8 return resp.text elif resp.status_code 403: time.sleep(random.uniform(2, 5)) continue else: time.sleep(1) except requests.exceptions.RequestException: time.sleep(2) return None这里有两个细节值得展开。第一User-Agent为什么要写这么完整因为有些服务器会检查User-Agent的完整性纯requests库的默认UA形如python-requests/2.31.0一眼就能识别为脚本。随便复制一个真实浏览器的UA是最稳妥的做法。第二retry机制里的time.sleep非常关键。豆瓣的限速阈值大概是每10秒几次请求做得粗糙的爬虫容易被临时封禁。我通常会在每次请求后sleep一个随机时间范围控制在1~3秒。加random不是玄学而是为了让请求间隔不均匀更像人的操作节奏。2.3 解析与存储从HTML到结构化数据拿到HTML之后用BeautifulSoup解析。豆瓣图书Top250的页面结构这些年比较稳定核心选择器如下from bs4 import BeautifulSoup import pandas as pd def parse_book_list(html): soup BeautifulSoup(html, html.parser) items soup.select(tr.item) books [] for item in items: title_tag item.select_one(td[valigntop] a) if not title_tag: continue title title_tag.get(title, ).strip() link title_tag.get(href, ).strip() rating item.select_one(span.rating_nums) people_tag item.select_one(span.pl) quote_tag item.select_one(p.quote span.inq) book { 书名: title, 链接: link, 评分: float(rating.text.strip()) if rating else None, 评价人数: parse_people(people_tag.text) if people_tag else 0, 简介: quote_tag.text.strip() if quote_tag else } books.append(book) return books def parse_people(text): import re match re.search(r(\d)人评价, text) return int(match.group(1)) if match else 0解析环节最容易翻车的地方是标签选择器失效。豆瓣偶尔会调整HTML结构或者某个字段在部分条目里缺失。所以我在解析时给每个字段都加了None兜底防止一条数据解析失败导致整个循环崩溃。再看主函数把分页逻辑串起来def crawl_douban_books(max_pages10): all_books [] for page in range(max_pages): url fhttps://book.douban.com/top250?start{page * 25}filter html fetch_page(url) if html is None: print(f第{page1}页抓取失败) continue books parse_book_list(html) all_books.extend(books) print(f第{page1}页完成累计{len(all_books)}条) time.sleep(random.uniform(1, 3)) return pd.DataFrame(all_books)存数据库这一步我建议直接用pandas的to_sql方法简单高效from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:passwordlocalhost:3306/douban_books?charsetutf8mb4) df.to_sql(books, engine, if_existsreplace, indexFalse)这里有个新手很容易踩的坑数据库编码。豆瓣书名里全是中文如果建表时没指定utf8mb4写入会报Incorrect string value错误。连接串里的charset以及建库时的字符集都要设置成utf8mb4才能完整支持中文和生僻字。3. 数据清洗与机器学习建模让数据产生预测力3.1 缺失值、数据变换与特征工程采集完的数据直接喂给模型是行不通的绕不开数据清洗这一步。我常用的清洗流程是去重、处理缺失、编码转换、特征构造。import pandas as pd df pd.read_sql(SELECT * FROM books, engine) # 1. 去重同一个书可能存在多条记录不同分页 df df.drop_duplicates(subset[书名], keepfirst) # 2. 缺失值处理 df df.dropna(subset[评分]) df[简介] df[简介].fillna() # 3. 类型转换 df[评价人数] pd.to_numeric(df[评价人数], errorscoerce).fillna(0) # 4. 从链接提取豆瓣ID作为唯一标识 df[book_id] df[链接].str.extract(r(\d))缺失值处理这里要做一个决策是删行还是填充。我的原则是——核心指标评分缺失就删因为模型训练不能靠猜非核心文本简介缺失就填空字符串因为词云分析不要求每条都有内容。如果你采集了出版社、价格这类详情页字段那缺失率会明显上升这时候可以用众数填充或者直接将该字段转为未知不要硬删。特征工程是这个项目的加分项。机器学习模型不能直接用书名这种原始文本必须转成数值特征。我建议构造以下几类特征出版年份转成数值new_df[出版年份]缺失用中位数填充。价格转成连续数值定价: 49.00元需要正则提取。评分分档作为分类标签可选大于等于8.5为高分6到8.5为中分低于6为低分。文本特征可选进阶把简介做TF-IDF向量化作为辅助特征。这一步对毕业设计来说属于加分项不做也不影响主线。3.2 模型选型回归还是分类怎么选这个项目里机器学习部分最自然的切入点是基于已有图书的属性预测评分属于回归问题。我试过线性回归、决策树、随机森林实测下来随机森林的效果最稳。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_absolute_error from sklearn.preprocessing import LabelEncoder # 构造特征集 feat_df df[[评价人数, 出版年份, 价格]].copy() # 出版年份和价格可能有缺失统一填充 feat_df[出版年份] feat_df[出版年份].fillna(feat_df[出版年份].median()) feat_df[价格] feat_df[价格].fillna(feat_df[价格].median()) # 类别特征出版社做标签编码 df[出版社编码] LabelEncoder().fit_transform(df[出版社]) feat_df[出版社编码] df[出版社编码] X feat_df.fillna(0) y df[评分] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model RandomForestRegressor(n_estimators100, max_depth8, random_state42) model.fit(X_train, y_train) pred model.predict(X_test) print(R2:, round(r2_score(y_test, pred), 4)) print(MAE:, round(mean_absolute_error(y_test, pred), 4))关于模型我想多说两句为什么选随机森林而不是更复杂的模型。第一数据量只有几百条神经网络在这点样本上完全发挥不出来过拟合风险极高第二随机森林有内置的特征重要性评估答辩时你可以输出特征重要性排序直接说明评价人数对评分的预测贡献最大这是很加分的产出。第三随机森林对缺失值和异常值的容忍度比线性回归高省去很多数据工程的时间。如果你的导师要求体现机器学习含量你可以把模型对比做成一张表格线性回归的R2、随机森林的R2、决策树的R2分别列出来然后解释随机森林为什么在这个数据集上更好。这就构成了论文里一个有数据支撑的结论。3.3 模型结果如何落地到系统里模型训练出来不能只活在Jupyter Notebook里。为了让系统显得完整我会做两件事第一特征重要性可视化。把feature_importances_存到数据库前端用横向柱状图展示让机器学习过程变成图表的一部分。importance_df pd.DataFrame({ 特征: X.columns, 重要性: model.feature_importances_ }).sort_values(重要性, ascendingFalse) importance_df.to_sql(feature_importance, engine, if_existsreplace, indexFalse)第二模型结果回写。我没让用户实时调用模型因为页面是纯展示不是交互预测而是把模型输出的预测评分存回books表新增一列pred_score。这样前端图表可以同时展示真实评分和预测评分用一个双折线图对比直观体现模型的准确性。这里有个设计上的取舍为什么不搞一个交互式预测页面可行性角度Flask完全支持——一个表单页用户填页数、价格、出版社后端调用pickle保存的模型返回预测分数。但毕业设计通常不需要这种交互做好分析展示的闭环就够了。当然如果你时间充裕加一个交互页面绝对是答辩加分项工作量也不算大。4. Echarts图表展示从数据到图表的最后一公里4.1 图表选型什么数据配什么图Echarts的图表类型非常多但并不是每种都适合这个项目。我最终选了五张图每一张都有明确的业务含义图表类型展示内容说人话的解释柱状图评分Top20图书一眼看出哪些书最受好评直方图柱状图评分分布区间整体评分是偏高还是偏低散点图评价人数 vs 评分好书是否等于热度高折线图年度出版数量趋势哪些年份出版繁荣词云简介热词高频词展现图书主题偏好饼图出版社Top占比头部出版社的市场份额做Top榜单要用柱状图因为横向对比排行最直观评分分布用直方图因为要呈现数据集中于哪个区间评价人数和评分的关系必须用散点图才能看出是否存在相关趋势。这套组合逻辑你答辩时能讲得头头是道。4.2 核心图表配置与数据对接前端部分我建议用一个HTML模板承载所有图表每个图表对应一个div容器然后通过Flask的JSON接口取数。先看后端Flask接口怎么设计from flask import Flask, jsonify, render_template import pymysql app Flask(__name__) def get_conn(): return pymysql.connect( hostlocalhost, userroot, passwordpassword, databasedouban_books, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) app.route(/) def index(): return render_template(index.html) app.route(/api/top_books) def top_books(): conn get_conn() with conn.cursor() as cur: cur.execute(SELECT 书名, 评分, 评价人数 FROM books ORDER BY 评分 DESC LIMIT 20) data cur.fetchall() conn.close() return jsonify({books: data}) app.route(/api/score_dist) def score_dist(): conn get_conn() with conn.cursor() as cur: cur.execute( SELECT FLOOR(评分) as score_bin, COUNT(*) as cnt FROM books GROUP BY score_bin ORDER BY score_bin ) data cur.fetchall() conn.close() return jsonify({dist: data})这里特别推荐把所有查询封装成独立路由一个接口只做一件事。因为前端fetch请求天然是并发的如果接口耦合度太高某一个图表的数据出错会导致整页渲染失败。再看前端Echarts的核心调用。最不容易出错的方式是封装一个renderChart函数接受接口地址和配置模板!DOCTYPE html html langzh-CN head meta charsetUTF-8 title豆瓣图书数据分析系统/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script style .chart-box { width: 90%; height: 420px; margin: 20px auto; } /style /head body h2 styletext-align:center;豆瓣图书数据分析/h2 div idtopChart classchart-box/div script function initChart(domId, option) { var chart echarts.init(document.getElementById(domId)); chart.setOption(option); window.addEventListener(resize, function() { chart.resize(); }); } fetch(/api/top_books) .then(res res.json()) .then(data { var books data.books.map(item item.书名); var scores data.books.map(item item.评分); var option { title: { text: 评分TOP20图书, left: center }, tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: books, axisLabel: { rotate: 40, interval: 0 } }, yAxis: { type: value, name: 评分 }, series: [{ name: 评分, type: bar, data: scores, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #ff7e5f }, { offset: 1, color: #feb47b } ]) } }] }; initChart(topChart, option); }); /script /body /html这个模板里有两个容易被忽视的细节。第一个是axisLabel: { rotate: 40, interval: 0 }。书名是长文本如果X轴文字不旋转且不强制全部显示Echarts会默认跳过部分标签这样你看到的就是图上少了几本看不见名字的书。rotate40配合interval0是最常见的调法。第二个是resize监听。浏览器窗口变化时图表默认不会自适应宽高需要手动调用chart.resize()。不写这个演示时切到投影仪就能看出问题——图表溢出或者变形。4.3 词云的实现细节词云用Echarts的wordcloud扩展包实现这个图不在echarts核心库里需要单独引入script srchttps://cdn.jsdelivr.net/npm/echarts-wordcloud2.1.0/dist/echarts-wordcloud.min.js/script后端先把所有简介汇总用jieba分词并统计词频import jieba from collections import Counter def build_wordcloud_data(): conn get_conn() with conn.cursor() as cur: cur.execute(SELECT 简介 FROM books WHERE 简介 ! ) rows cur.fetchall() conn.close() text .join([row[简介] for row in rows]) words jieba.cut(text) # 过滤停用词和单字 stopwords {的, 了, 是, 在, 和, 有, 也, 不, 人, 等} filtered [w for w in words if w.strip() and w not in stopwords and len(w) 1] word_count Counter(filtered).most_common(100) return [{name: w, value: c} for w, c in word_count]这里要提醒一下jieba分词的结果里会有大量无意义的词必须维护一份停用词表。词云如果不做这个过滤展示出来的全是我们他们一个这类连接词数据感就被毁掉了。5. Flask应用整合把分析结果变成可访问的系统5.1 Flask项目结构与路由设计一个清晰的目录结构能让你在答辩前最后一天改bug时不至于抓狂。我推荐以下结构douban_analysis/ ├── app.py # Flask入口注册路由 ├── models/ │ ├── __init__.py │ ├── database.py # 数据库连接管理 │ └── train.py # 机器学习训练脚本 ├── spiders/ │ ├── __init__.py │ └── douban_spider.py # 爬虫模块 ├── static/ │ └── css/ style.css ├── templates/ │ └── index.html # 前端页面 └── requirements.txt路由设计遵循一条原则一个页面只对应一个模板一个图表只对应一个接口。这样页面渲染和接口调用完全分离前端出问题就查前端后端出问题就查后端不用两头猜。5.2 从数据库到图表的完整链路Flask应用跑起来后完整链路是这样浏览器访问首页时加载HTMLHTML里的JavaScript依次fetch各个API接口Flask收到请求后执行SQL查询并返回JSON前端拿到数据后调用Echarts渲染图表。这个链路里有一个必须注意的点SQL查询的返回字段和前端代码里引用的字段要保持完全一致。比如MySQL里字段名如果用了中文像书名评分前端item.书名才能取到值。如果数据库字段是英文的book_name前端就要改成item.book_name。这是一个很低级但高频的bug改代码时前后端要一起看。所有接口写好后跑起来非常简单python app.py # 浏览器访问 http://127.0.0.1:5000Flask默认端口是5000如果启动时报端口占用换到5001或者自定义端口app.run(host0.0.0.0, port5001, debugTrue)5.3 部署与演示前的最后准备毕业设计一般不需要真正的公网部署本地跑通即可。但演示时建议做三件事第一准备一份清洗好的数据库备份。演示现场机器环境不可控万一MySQL服务挂了你还能用sqlite3兜底。我的做法是把数据同时导出一份CSVFlask配置里做一个数据源切换开关——优先读MySQL读不到就自动切到CSV。这个高可用设计还能在答辩时作为亮点提一句。第二关闭debug模式。debugTrue虽然方便开发但演示时如果代码里有任何warning浏览器会自动显示调试页面这在答辩现场非常尴尬。正式演示用app.run(debugFalse)。第三预生成关键图表截图。万一现场前端图表加载不出来你还有静态截图可以放PPT里。这个不是滑头是务实的风险管理——我见过太多人答辩现场因为网络加载CDN超时图表白屏半分钟。6. 常见问题与排查技巧实录6.1 爬虫抓不到数据或解析为空这个问题出现频率最高。先确认请求是否返回了正常HTML再确认选择器是否匹配。html fetch_page(https://book.douban.com/top250?start0filter) print(len(html)) # 正常应该有数万字符 soup BeautifulSoup(html, html.parser) print(soup.select(tr.item)[:2]) # 列表为空就说明选择器失效如果状态码403就是被服务器拦截了优先检查User-Agent和请求频率。如果状态码200但解析为空大概率是页面结构变了。豆瓣偶尔会微调HTMLpayload里的数据不一定永远在tr.item里。这时候直接用浏览器开发者工具查看当前页面结构重新确认选择器即可。6.2 数据库写入报中文乱码或编码错误这个问题的根源几乎都是连接串charset设置与建库字符集不一致。常规解法是CREATE DATABASE douban_books DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接串里也明确写charsetutf8mb4。另外pandas的to_sql在首次建表时会自动推断字段类型中文文本可能被建成varchar(255)导致长的简介被截断。解决办法是先手动建表把简介这类长文本字段设成text类型再to_sql写入。6.3 Echarts图表不显示或X轴标签错位图表不显示先按顺序排查是否引入echarts.js看控制台有没有报Echarts is not defined、容器div是否设置了宽高Echarts对隐藏或0高度的容器会直接渲染失败、fetch是否真的返回了数据在浏览器Network面板看接口响应。X轴标签错位或重叠最常见原因就是长文本标签过多。处理方式我在前面提过设置axisLabel: { rotate: 40, interval: 0 }如果文本还是挤就把图表高度调大或者减少Top榜单的数量从20改成15。6.4 Flask启动失败或页面404启动失败先看端口占用lsof -i:5000找出占用进程并结束。404的话先检查路由字典——Flask的路由是精确匹配templates/index.html对应的路由是/但如果你写成/index就需要单独再定义一个路由或者把模板文件放在templates/下并用render_template(index.html)。6.5 答辩和演示现场的加分经验最后这一点算是我自己的心得。做这类系统真正能拿高分的往往不是技术多深而是逻辑闭环。我见过很多作品爬虫能跑、图表能出但问一句你的机器学习模型对系统有什么实际作用就答不上来。我的建议是让模型的输出真正进入展示链路。比如把特征重要性柱状图和真实评分与预测评分对比折线图放进页面里这样评审老师一进来就能看到机器学习的成果不需要你费力解释。数据闭环本身就说明你理解了整个系统的价值。另外演讲时记得准备一个翻车预案。我在实际演示中就遇到过一次MySQL突然连不上的情况当时靠CSV兜底一分钟内恢复了展示。你永远不知道现场设备会出什么幺蛾子提前把数据准备好几份永远不是坏事。做这个项目的过程其实就是把大学四年学的Python基础、数据库、前端、机器学习知识串成一条线。它的意义不止于毕业设计能过更在于让你完整经历一次从数据到产品的流程——爬虫采集、清洗建模、可视化展示每一步都会遇到课本上没有的坑而把坑填平的过程才是真正涨经验的时候。如果你正在做类似的系统按我上面的方法踏踏实实走一遍一定能在答辩时拿出一个有逻辑、有亮点、能落地的东西。