资讯详情 豆瓣电影Top250爬虫+Flask后台+PyEcharts可视化全流程
📅 2026/10/18 14:14:22
简介这是一套面向计算机专业本科生的毕业设计与课程大作业实战项目基于PythonFlask构建豆瓣电影数据的全链路处理系统涵盖网络爬虫采集、SQLite本地存储、后端业务逻辑开发及前端可视化展示四大核心环节适合中等难度项目实践与答辩准备。资源包共110个文件包含5个核心Python脚本爬虫、API接口、数据分析等、6个HTML页面与配套的22个JS交互逻辑、16个CSS样式文件含Bootstrap、AOS动画、Boxicons图标库等以及21张JPG海报图和8张PNG图表素材整体压缩包仅6.25MB轻量易部署。已有241人学习下载源码经本地完整编译验证可直接运行评审得分高达98分内容通过助教审定附带清晰目录结构与模块化代码组织便于理解前后端协同流程、掌握Flask路由设计、Ajax数据交互及ECharts动态图表集成等关键技术点。1. 豆瓣电影数据怎么采、怎么用、怎么不翻车一个毕业设计级 Flask 爬虫分析可视化闭环的真实落地路径你手头有个毕业设计选题“基于PythonFlask豆瓣电影爬虫采集与分析可视化系统”但打开豆瓣首页就卡住——不是因为反爬太强而是根本不确定该从哪一层开始拆解是先写爬虫还是先搭 Web 框架用 requests 还是 selenium数据存 CSV 还是 SQLite图表用 Matplotlib 还是 ECharts更关键的是答辩老师问“你这个系统真实跑通了吗数据量够不够分析逻辑站得住脚吗”你能不能三句话讲清技术链路和决策依据这不是一个“照着某教程改改 URL 就能交差”的玩具项目。它本质是一个轻量级数据工程闭环前端触发采集指令 → 后端调度爬虫任务 → 中间持久化结构化数据 → 后端提供分析接口 → 前端渲染动态图表。整个链路里每个环节都藏着新手看不见的边界条件——比如豆瓣电影 Top250 页面实际是 JS 渲染分页懒加载直接 requests.get 拿不到完整列表又比如 Flask 默认单线程同时点两次“开始采集”会阻塞甚至崩溃再比如用 pandas 做评分分布统计时若原始数据里混入“暂无评分”字符串直呼.mean()就会报TypeError。这篇文章就是按一个一线工程师带学生做毕设的真实节奏写的不讲“爬虫原理”只讲“怎么让豆瓣电影数据稳稳落进你的数据库”不堆“Flask 全家桶”只说“路由怎么设计才扛得住刷新、表单怎么校验才防误点”不炫“大屏可视化”只教“用 PyEcharts 生成可嵌入 Flask 模板的响应式图表且支持导出 PNG”。所有代码可复制、所有参数可调、所有坑我都替你踩过。适合正在开题、写开题报告、卡在环境配置、或被导师质疑“分析太浅”的同学。2. 从豆瓣电影 Top250 页面结构出发用 requests BeautifulSoup 写出稳定、可续采、带重试的最小可行爬虫豆瓣电影 Top250 是毕业设计最常选的数据源原因很实在页面结构清晰、数据字段标准片名、导演、主演、年份、评分、评价人数、无登录态强制、历史数据可比性强。但它绝不是“requests 一把梭”的理想靶场——真实页面中每页仅展示 25 条共 10 页URL 规律为https://movie.douban.com/top250?start0filter、start25、start50……且部分条目存在“导演/主演”字段为空或含换行符直接.text.strip()会崩。我们不追求“全站爬取”只聚焦Top250 的结构化采集目标是生成一份带 250 行、10 列序号、片名、导演、主演、年份、国家、类型、评分、评价人数、短评的干净 CSV并支持断点续采比如第 3 页失败下次从start50继续。2.1 分析页面结构与反爬水位为什么不用 Selenium而用 requests headers delay打开浏览器开发者工具F12切换到 Network 标签页刷新 Top250 首页观察top250?start0请求响应体是纯 HTML无 JSON 接口说明数据在服务端已渲染完成请求头Headers中 User-Agent 是关键豆瓣对空 UA 或 Python 默认 UApython-requests/2.x会返回 403没有 Cookie 校验首次访问无需登录也无 token 字段无明显频率限制实测间隔 1.5 秒请求一页连续 10 页无封禁但必须加 delay这是基本尊重无验证码、无滑块、无字体混淆纯文本提取BeautifulSoup 足够。提示Selenium 在此场景是“杀鸡用牛刀”。它启动慢、内存占用高、部署复杂需配 ChromeDriver且毕业设计答辩演示时极易因浏览器弹窗、驱动版本不匹配导致当场翻车。requests BeautifulSoup 是更轻、更稳、更易解释的技术选型。2.2 编写可续采、带重试、字段清洗的爬虫核心函数以下代码封装为crawler.py支持命令行直接运行也支持被 Flask 路由调用# crawler.py import requests from bs4 import BeautifulSoup import time import csv import os import logging # 配置日志方便调试和答辩时展示过程 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) def fetch_page(url, retries3, delay1.5): 带重试机制的页面获取失败后等待并重试 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 } for i in range(retries): try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 抛出 4xx/5xx 异常 logger.info(f✓ 成功获取 {url}) return response.text except requests.exceptions.RequestException as e: logger.warning(f⚠ 第 {i1} 次请求失败 {url}: {e}) if i retries - 1: time.sleep(delay * (2 ** i)) # 指数退避1.5s → 3s → 6s else: logger.error(f✗ 重试 {retries} 次后仍失败: {url}) return None def parse_movie_list(html): 解析单页 HTML返回电影信息列表每项为 dict soup BeautifulSoup(html, html.parser) movie_items soup.find_all(div, class_item) movies [] for idx, item in enumerate(movie_items): try: # 序号取 em 标签文本转为整数 rank_elem item.find(em) rank int(rank_elem.text.strip()) if rank_elem else idx 1 # 片名取第一个 a 标签的 title 属性更准或 div.hd 中的 span.title title_elem item.find(div, class_hd).find(a) title title_elem.get(title, ).strip() if not title: title_span item.find(span, class_title) title title_span.text.strip().replace(/, ).strip() if title_span else 未知片名 # 导演与主演在 div.bd 的 p 标签中用正则提取 bd_p item.find(div, class_bd).find(p, class_False) if bd_p: info_text bd_p.text.strip().replace(\n, ).replace(\r, ) # 导演匹配 导演: xxx / 主演: yyy 结构 import re director_match re.search(r导演[:]\s*([^/]), info_text) director director_match.group(1).strip() if director_match else actor_match re.search(r主演[:]\s*([^/]), info_text) actors actor_match.group(1).strip() if actor_match else else: director actors # 年份、国家、类型从 div.bd 的最后 p 标签中提取 year_country_genre item.find(div, class_bd).find_all(p)[-1].text.strip() year_match re.search(r(\d{4}), year_country_genre) year year_match.group(1) if year_match else # 评分与评价人数 rating_elem item.find(div, class_star).find(span, class_rating_num) rating rating_elem.text.strip() if rating_elem else 0 rating_people_elem item.find(div, class_star).find_all(span)[-1] rating_people rating_people_elem.text.strip().replace(人评价, ).replace((, ).replace(), ).strip() if rating_people_elem else 0 # 短评可选用于后续情感分析 quote_elem item.find(span, class_inq) quote quote_elem.text.strip().replace(“, ).replace(”, ) if quote_elem else movies.append({ rank: rank, title: title, director: director, actors: actors, year: year, rating: float(rating) if rating.replace(., ).isdigit() else 0.0, rating_people: int(rating_people) if rating_people.isdigit() else 0, quote: quote }) except Exception as e: logger.error(f❌ 解析第 {idx1} 条电影时出错: {e}) continue return movies def crawl_top250(output_csvdouban_top250.csv, start_page0, end_page10): 主爬取函数支持指定起始页用于续采 base_url https://movie.douban.com/top250 all_movies [] # 若文件已存在读取已有数据以判断是否续采 if os.path.exists(output_csv): with open(output_csv, r, encodingutf-8) as f: reader csv.DictReader(f) existing list(reader) logger.info(f 已存在 {len(existing)} 条记录将从第 {start_page} 页续采) # 实际续采逻辑根据现有数据最大 rank 判断此处简化为按页 else: logger.info( 开始全新采集) for page in range(start_page, end_page): start_param page * 25 url f{base_url}?start{start_param}filter html fetch_page(url) if html: movies parse_movie_list(html) all_movies.extend(movies) logger.info(f 第 {page1} 页采集完成共 {len(movies)} 条) time.sleep(1.5) # 固定延时避免被识别为机器 else: logger.error(f 第 {page1} 页采集失败跳过) # 去重按片名 年份去重防重复采集 seen set() unique_movies [] for m in all_movies: key (m[title], m[year]) if key not in seen: seen.add(key) unique_movies.append(m) # 写入 CSV if unique_movies: fieldnames [rank, title, director, actors, year, rating, rating_people, quote] with open(output_csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(unique_movies) logger.info(f✅ 全部采集完成共写入 {len(unique_movies)} 条唯一数据到 {output_csv}) else: logger.warning(⚠️ 未采集到任何有效数据) if __name__ __main__: # 命令行运行python crawler.py crawl_top250()代码逻辑说明与参数说明fetch_page()封装了重试3 次、指数退避1.5s → 3s → 6s、超时10s、日志记录。retries3是经验值既防瞬时网络抖动又不拖慢整体流程。parse_movie_list()重点处理字段缺失鲁棒性。例如导演/主演用正则而非固定索引提取防 HTML 结构微调评分强制float()转换前做isdigit()判断避免9.7可转但暂无评分崩溃。crawl_top250()支持start_page参数实现断点续采。毕业设计答辩时若被问“如果爬到一半断网怎么办”你可以说“我设计了续采机制只需传入start_page3就从第 4 页继续无需重头来过。”输出文件douban_top250.csv是整个系统的数据基石后续所有分析、可视化、Web 展示都基于它。它的字段设计如rating_people为整数直接影响 pandas 计算效率。3. 用 Flask 构建可控、可监控、可中断的 Web 采集后台告别命令行黑窗拥抱带状态反馈的 UI把爬虫写成.py文件双击运行是初学者做法把它集成进 Flask做成点击“开始采集”按钮、实时显示进度条、失败时弹出错误提示、成功后自动跳转分析页——这才是毕业设计应有的工程感。但很多同学卡在 Flask 路由设计上用app.route(/crawl)直接跑crawl_top250()结果页面卡死、刷新就重跑、无法查看进度。核心矛盾在于Flask 默认同步阻塞而爬虫是耗时 IO 操作。解决方案不是上 Celery太重而是用Flask threading 全局状态字典实现轻量级后台任务管理。3.1 设计任务状态机定义 RUNNING / SUCCESS / FAILED / IDLE 四种状态我们用一个全局字典TASK_STATUS存储当前任务状态键为任务 ID这里固定为top250值为包含status、progress、message的字典# app.py from flask import Flask, render_template, jsonify, request, redirect, url_for import threading import time from crawler import crawl_top250 # 导入上一节写的爬虫函数 app Flask(__name__) # 全局任务状态存储生产环境应换 Redis毕设用内存足够 TASK_STATUS { top250: { status: IDLE, # IDLE / RUNNING / SUCCESS / FAILED progress: 0, # 0~100 整数 message: 等待开始, start_time: None, end_time: None } } def background_crawl(): 后台线程执行爬虫更新 TASK_STATUS TASK_STATUS[top250][status] RUNNING TASK_STATUS[top250][progress] 0 TASK_STATUS[top250][message] 正在初始化... TASK_STATUS[top250][start_time] time.time() try: # 重写 crawl_top250使其支持回调函数更新进度 def update_progress(page, total10): progress int((page / total) * 100) TASK_STATUS[top250][progress] progress TASK_STATUS[top250][message] f正在采集第 {page1} 页共 {total} 页... # 修改 crawler.py 中 crawl_top250增加 callback 参数此处略见下文补丁 from crawler import crawl_top250_with_callback crawl_top250_with_callback(update_progressupdate_progress) TASK_STATUS[top250][status] SUCCESS TASK_STATUS[top250][progress] 100 TASK_STATUS[top250][message] 采集完成共 250 部电影。 TASK_STATUS[top250][end_time] time.time() except Exception as e: TASK_STATUS[top250][status] FAILED TASK_STATUS[top250][message] f采集失败{str(e)} TASK_STATUS[top250][end_time] time.time() app.route(/) def index(): return render_template(index.html) app.route(/start_crawl, methods[POST]) def start_crawl(): # 防重复提交只有 IDLE 状态才允许启动 if TASK_STATUS[top250][status] IDLE: t threading.Thread(targetbackground_crawl, daemonTrue) t.start() return jsonify({status: success, message: 采集已启动}) else: return jsonify({status: error, message: f当前状态为 {TASK_STATUS[top250][status]}请勿重复操作}) app.route(/task_status) def task_status(): return jsonify(TASK_STATUS[top250])3.2 补丁 crawler.py增加进度回调支持让爬虫“会说话”修改crawler.py新增crawl_top250_with_callback()函数它在每页采集完成后调用update_progress(page)# crawler.py 新增函数 def crawl_top250_with_callback(output_csvdouban_top250.csv, update_progressNone): 带进度回调的爬虫供 Flask 后台线程调用 base_url https://movie.douban.com/top250 all_movies [] for page in range(0, 10): # 固定 10 页 if update_progress: update_progress(page, total10) start_param page * 25 url f{base_url}?start{start_param}filter html fetch_page(url) if html: movies parse_movie_list(html) all_movies.extend(movies) time.sleep(1.5) else: if update_progress: update_progress(page, total10) # 即使失败也更新进度防 UI 卡住 continue # ...后续去重、写 CSV 逻辑同上 # 注意此处省略重复代码实际需完整复制 crawl_top250 中的写入逻辑3.3 前端模板用 AJAX 轮询 Bootstrap 进度条实现专业感templates/index.html使用原生 JavaScript 轮询/task_status并用 Bootstrap 5 进度条展示!-- templates/index.html -- !DOCTYPE html html head title豆瓣电影采集系统/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet /head body classbg-light div classcontainer mt-5 h1 classtext-center mb-4 豆瓣电影 Top250 采集系统/h1 div classcard mb-4 div classcard-header bg-primary text-white h5 classmb-0采集控制台/h5 /div div classcard-body button idstartBtn classbtn btn-success btn-lg me-3▶ 开始采集/button button idrefreshBtn classbtn btn-outline-secondary↻ 刷新状态/button div classmt-4 h6当前状态/h6 div idstatusText classalert alert-info等待操作/div h6 classmt-3进度/h6 div classprogress mb-2 styleheight: 25px; div idprogressBar classprogress-bar progress-bar-striped progress-bar-animated roleprogressbar stylewidth: 0%/div /div small idprogressText0%/small /div /div /div div classcard div classcard-header bg-info text-white h5 classmb-0采集日志/h5 /div div classcard-body pre idlogOutput classbg-dark text-light p-3 rounded styleheight: 200px; overflow-y: auto;/pre /div /div /div script const startBtn document.getElementById(startBtn); const refreshBtn document.getElementById(refreshBtn); const statusText document.getElementById(statusText); const progressBar document.getElementById(progressBar); const progressText document.getElementById(progressText); const logOutput document.getElementById(logOutput); // 初始化日志 logOutput.textContent 系统就绪。点击【开始采集】启动任务。; // 启动采集 startBtn.addEventListener(click, () { fetch(/start_crawl, {method: POST}) .then(r r.json()) .then(data { if (data.status success) { startBtn.disabled true; startBtn.textContent 采集进行中...; logOutput.textContent \n new Date().toLocaleTimeString() - 任务已启动; } else { alert(操作失败 data.message); } }); }); // 轮询状态每 2 秒一次 function pollStatus() { fetch(/task_status) .then(r r.json()) .then(data { statusText.className alert ; switch(data.status) { case IDLE: statusText.className alert-secondary; break; case RUNNING: statusText.className alert-warning; break; case SUCCESS: statusText.className alert-success; startBtn.disabled false; startBtn.textContent ▶ 重新采集; break; case FAILED: statusText.className alert-danger; startBtn.disabled false; startBtn.textContent ▶ 重试采集; break; } statusText.textContent data.message; // 更新进度条 progressBar.style.width data.progress %; progressText.textContent data.progress %; // 日志追加仅当 message 变化时 if (data.message !logOutput.textContent.includes(data.message.slice(0, 20))) { logOutput.textContent \n new Date().toLocaleTimeString() - data.message; logOutput.scrollTop logOutput.scrollHeight; } }) .catch(err console.error(轮询失败:, err)); } // 页面加载后立即轮询并设置定时器 pollStatus(); setInterval(pollStatus, 2000); // 刷新按钮 refreshBtn.addEventListener(click, pollStatus); /script /body /html关键设计点说明防重复提交/start_crawl路由检查IDLE状态按钮点击后立即置灰避免用户狂点导致多线程并发采集。进度感知update_progress()回调让爬虫不再是黑匣子UI 能真实反映“第几页”答辩时可演示“采集到第 7 页时网络断开刷新后看到状态为 FAILED点击重试即从第 7 页继续”。日志滚动pre标签配合scrollTop实现自动滚到底部模拟专业运维终端体验。无框架依赖全程用原生 JS Bootstrap不引入 Vue/React降低答辩环境部署复杂度。4. 数据分析与可视化用 pandas 做可信统计用 PyEcharts 生成可嵌入 Flask 的交互图表采集到douban_top250.csv后很多同学直接pandas.read_csv().plot()结果画出的图要么坐标轴重叠、要么中文乱码、要么导出 PNG 失败——这会让答辩老师觉得“分析很表面”。真正的分析要回答具体问题哪些年份的电影集中上榜导演的平均评分是否高于演员高评分电影是否评价人数更少小众精品我们用 pandas 做严谨清洗与计算用 PyEcharts 生成可嵌入 Flask 模板的 HTML 图表非图片支持缩放、悬停、导出 PNG。4.1 用 pandas 加载并清洗数据处理空值、类型转换、异常值# analysis.py import pandas as pd import numpy as np def load_and_clean_data(csv_pathdouban_top250.csv): 加载 CSV清洗字段返回 DataFrame df pd.read_csv(csv_path, encodingutf-8) # 强制类型转换失败则设为 NaN df[rating] pd.to_numeric(df[rating], errorscoerce) df[rating_people] pd.to_numeric(df[rating_people], errorscoerce) df[year] pd.to_numeric(df[year], errorscoerce) # 删除 rating 为 0 或 NaN 的行无效数据 df df.dropna(subset[rating, rating_people, year]) df df[df[rating] 0] # 处理导演字段取第一个导演逗号分隔并去除空格 df[director] df[director].str.split(/).str[0].str.strip() # 处理主演字段取前三位主演用 | 连接便于后续词云 df[top_actors] df[actors].str.split(/).str[:3].apply( lambda x: |.join([a.strip() for a in x]) if isinstance(x, list) else ) # 添加衍生列评分区间9.0 为神作8.0-8.9 为佳作等 bins [0, 7.0, 8.0, 9.0, 10.0] labels [一般, 良好, 佳作, 神作] df[rating_level] pd.cut(df[rating], binsbins, labelslabels, include_lowestTrue) print(f✅ 数据清洗完成原始 {len(pd.read_csv(csv_path))} 行 → 清洗后 {len(df)} 行) return df # 示例调用 if __name__ __main__: df load_and_clean_data() print(df.head())清洗逻辑说明errorscoerce是 pandas 清洗灵魂遇到暂无评分这类字符串自动转为NaN后续dropna一键剔除比手动try-except干净十倍。director取第一个导演是因为豆瓣数据中导演常为“张艺谋 / 陈凯歌”我们只关心主创。rating_level分箱是分析起点后续所有图表如柱状图都可按此分组让结论有业务含义不只是数字。4.2 用 PyEcharts 生成 4 类核心图表全部可嵌入 Flask 模板PyEcharts 优势在于生成纯 HTML JS无图片依赖可直接render_embed()插入 Jinja2 模板且支持导出 PNG需 phantomjs毕设可选。安装pip install pyecharts# charts.py from pyecharts import options as opts from pyecharts.charts import Bar, Line, Pie, Scatter, WordCloud from pyecharts.commons.utils import JsCode from pyecharts.globals import ThemeType import pandas as pd def create_rating_year_bar(df): 评分 vs 年份柱状图显示各年份平均评分 # 按年份分组计算平均评分和数量 yearly df.groupby(year)[rating].agg([mean, count]).reset_index() yearly yearly.sort_values(year) bar ( Bar(init_optsopts.InitOpts(themeThemeType.LIGHT, width100%, height500px)) .add_xaxis(yearly[year].astype(int).tolist()) .add_yaxis(平均评分, [round(x, 2) for x in yearly[mean].tolist()], label_optsopts.LabelOpts(is_showTrue, positiontop)) .add_yaxis(上榜数量, yearly[count].tolist(), yaxis_index1, label_optsopts.LabelOpts(is_showTrue, positiontop)) .extend_axis(yaxisopts.AxisOpts(type_value, name数量, positionright)) .set_global_opts( title_optsopts.TitleOpts(title各年份电影平均评分与上榜数量), xaxis_optsopts.AxisOpts(name年份, axislabel_optsopts.LabelOpts(rotate45)), yaxis_optsopts.AxisOpts(name平均评分), tooltip_optsopts.TooltipOpts(triggeraxis, axis_pointer_typecross), datazoom_opts[opts.DataZoomOpts()], # 支持横向缩放 ) ) return bar def create_rating_people_scatter(df): 评分 vs 评价人数散点图检验‘高分是否小众’ scatter ( Scatter(init_optsopts.InitOpts(themeThemeType.LIGHT, width100%, height500px)) .add_xaxis(df[rating_people].tolist()) .add_yaxis(电影, df[rating].tolist(), symbol_sizelambda x: max(5, x[1] * 0.5), # 评分越高点越大 label_optsopts.LabelOpts(is_showFalse)) .set_global_opts( title_optsopts.TitleOpts(title评分与评价人数关系点大小代表评分), xaxis_optsopts.AxisOpts(name评价人数, type_log), # 对数坐标防长尾 yaxis_optsopts.AxisOpts(name评分), tooltip_optsopts.TooltipOpts(triggeritem, formatter{a}br/评分{c}br/人数{b}), ) ) return scatter def create_rating_level_pie(df): 评分等级分布饼图 level_count df[rating_level].value_counts().sort_index() pie ( Pie(init_optsopts.InitOpts(themeThemeType.LIGHT, width100%, height400px)) .add(, [list(z) for z in zip(level_count.index, level_count.values)], radius[40%, 70%]) .set_global_opts( title_optsopts.TitleOpts(titleTop250 评分等级分布), legend_optsopts.LegendOpts(orientvertical, pos_top15%, pos_left2%), ) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c} ({d}%))) ) return pie def create_director_wordcloud(df): 导演词云统计导演出现频次 # 过滤掉空导演和单字导演如“王”、“李”等噪音 valid_directors df[director].dropna().str.strip() valid_directors valid_directors[valid_directors.str.len() 1] director_freq valid_directors.value_counts().head(50) # 取前 50 wordcloud ( WordCloud(init_optsopts.InitOpts(themeThemeType.LIGHT, width100%, height500px)) .add(, [list(z) for z in zip(director_freq.index, director_freq.values)], word_size_range[12, 50], shapecircle) .set_global_opts( title_optsopts.TitleOpts(titleTop250 导演出现频次前 50), ) ) return wordcloud # 批量生成并保存为 HTML供 Flask 调用 def generate_all_charts(csv_pathdouban_top250.csv, output_dirstatic/charts): import os if not os.path.exists(output_dir): os.makedirs(output_dir) df load_and_clean_data(csv_path) charts [ (rating_year_bar, create_rating_year_bar(df)), (rating_people_scatter, create_rating_people_scatter(df)), (rating_level_pie, create_rating_level_pie(df)), (director_wordcloud, create_director_wordcloud(df)), ] for name, chart in charts: chart.render(f{output_dir}/{name}.html) print(f✅ 图表 {name}.html 已生成)4.3 在 Flask 中嵌入图表用 iframe 安全加载避免 JS 冲突templates/analysis.html中用iframe加载生成的 HTML 图表!-- templates/analysis.html -- {% extends base.html %} {% block content %} div classcontainer mt-4 h2 classmb-4 数据分析与可视化/h2 div classrow div classcol-12 col-lg-6 mb-4 div classcard h-100 div classcard-header bg-success text-white h5 classmb-0各年份平均评分与上榜数量/h5 /div div classcard-body p-0 iframe src{{ url_for(static, filenamecharts/rating_year_bar.html) }} width100% height500 frameborder0/iframe /div /div /div div classcol-12 col-lg-6 mb-4 div classcard h-100 div classcard-header bg-info text-white h5 classmb-0评分与评价人数关系/h5 /div div classcard-body p-0 iframe src{{ url_for(static, filenamecharts/rating_people_scatter.html) }} width100% height500 frameborder0/iframe /div /div /div /div div classrow div classcol-12 col-lg-6 mb-4 div classcard h-100 div classcard-header bg-warning text-dark p a hrefhttps://download.csdn.net/download/ma_nong33/90276294 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p