Python Flask与Echarts实现豆瓣电影TOP250数据分析可视化网站
简介:基于Python Flask与Echarts插件构建的豆瓣电影TOP250数据分析可视化网站完整源码,适合有一定Python基础、希望系统学习Flask Web开发与数据可视化应用的开发者。项目综合运用爬虫采集豆瓣电影数据,并通过Echarts生成交互式图表,辅以WordCloud词云、地图文件等增强展示效果,可作为课程设计或毕业设计的参考蓝本。整个压缩包共92个文件,主要包含4个Python脚本、5个HTML模板、16个CSS样式、16个JavaScript脚本及11个地图文件,另有图片、字体、数据库与配置文件等,整体大小9.98MB,目录结构清晰,前端静态资源与后端逻辑划分明确。目前已有462人学习下载。通过学习这份源码,可掌握基于Flask搭建动态网站、使用Echarts绘制评分与评论分布图表、利用WordCloud分析文本关键词,以及爬虫数据入库的完整实现思路,便于后续迁移到其他数据分析场景。
1. 基于Python Flask与Echarts插件的豆瓣电影TOP250数据分析可视化网站:从小工具到完整项目
你是不是也有过这种经历:想研究豆瓣TOP250到底被哪些年份、哪些地区的电影霸榜,手动一页页翻网页、复制表格到Excel,折腾一晚上只得到一份乱七八糟的数据。这正是我做这个项目的起点——与其人工扒数据,不如写一个把抓取、存储、分析和可视化串起来的小网站。基于Python Flask与Echarts插件的豆瓣电影TOP250数据分析可视化网站,本质上就是一条完整的数据流水线:requests抓页面,BeautifulSoup解析,SQLite存数据,Flask提供后端接口和页面渲染,Echarts在浏览器里画出柱状图、饼图和排行表。这套方案不挑机器、不挑系统,Windows和Mac都能跑,适合刚学完Python基础想做一个能拿得出手的“数据类项目”的人,也适合已经在做爬虫但想补上可视化这一环的开发者。
2. 先把数据抓到本地:豆瓣TOP250的页面解析与字段清洗
2.1 目标数据长什么样:列表页结构、翻页规律与字段清单
豆瓣TOP250的列表页是出了名的简单规整,每页固定25条电影,一共10页。它的翻页参数就是URL里的start,从0开始,步长25,也就是说第1页是start=0,第2页是start=25,一直到最后start=225。每个电影条目被包裹在ol.grid_view列表的li.item节点里,你要的信息基本都在这个节点内部。
我一般会先打开页面源码确认字段节点,而不是凭经验写死选择器。每个li.item里能拿到的字段包括排名(em标签)、片名(span.title)、导演和主演(p里的第一行,文本以换行分隔)、年份和地区(同一段p里的第二行)、评分(span.rating_num)、评价人数(span.pl)、一句话短评(span.inq)。这些字段的结构经年累月基本稳定,但文字排版偶尔会变化,所以清洗逻辑一定要写得抗造。
| 字段 | 解析节点 | 清洗后格式 | 备注 |
|---|---|---|---|
| 排名 | li.item > div.pic > em | 整数 | 全局唯一,可作主键 |
| 片名 | span.title(第一个) | 字符串 | 中文名,别名不要 |
| 导演/主演 | p第一行文本,按/拆分 | 字符串 | 取第一位可得到导演 |
| 年份 | p第二行文本,正则提取 | 四位整数 | 如“1994” |
| 地区 | p第二行文本,按/拆分 | 字符串 | 可能多地区,取第一个 |
| 评分 | span.rating_num | 浮点数 | 保留一位小数 |
| 评价人数 | span.pl,去括号 | 整数 | 常见写法“1513423人评价” |
| 短评 | span.inq | 字符串 | 有的片子没有,需判空 |
2.2 抓取脚本:requests加BeautifulSoup的最小实现
爬虫这部分我习惯用requests做HTTP请求,BeautifulSoup做页面解析,而不是直接用Scrapy。原因很简单:总共只有10个页面,不需要调度器、中间件那一套重武器,单文件脚本足够清爽,出了问题也好定位。下面这个脚本就是整个项目的“数据入口”。
import requests import time from bs4 import BeautifulSoup 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" } def fetch_page(start): """抓取单个列表页,返回解析后的电影字典列表""" url = f"https://movie.douban.com/top250?start={start}&filter=" resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") movies = [] for item in soup.select("ol.grid_view li.item"): # 节点缺失时用 try 兜底,避免整页解析中断 rank = int(item.select_one("div.pic em").get_text()) title = item.select_one("span.title").get_text() rating = float(item.select_one("span.rating_num").get_text()) quote_node = item.select_one("span.inq") quote = quote_node.get_text() if quote_node else "" info_lines = item.select_one("div.bd p").get_text("\n", strip=True).split("\n") # info_lines[0] 是导演/主演,info_lines[1] 是年份/地区/类型 year_line = info_lines[1] if len(info_lines) > 1 else "" movies.append({ "rank": rank, "title": title, "rating": rating, "quote": quote, "year_raw": year_line, }) return movies def main(): all_movies = [] for start in range(0, 250, 25): all_movies.extend(fetch_page(start)) time.sleep(1) # 控制请求频率,降低被封概率 # 后续把 all_movies 写入数据文件,见下一节 print(f"共抓到 {len(all_movies)} 条")我特意用select_one而不是find,因为select_one配合CSS选择器写起来更像写选择器,而且天然返回单个节点或None,适合判空。resp.raise_for_status()的作用是状态码非200时直接抛异常,避免拿到错误页面还继续解析造成假数据。time.sleep(1)在本地抓250条时不算慢,但对豆瓣这种有反爬机制的站点来说,这个间隔是保命的。
2.3 数据清洗与落地:年份、地区、评分区间怎么处理
抓下来的year_raw是类似“1994 / 美国 / 犯罪 剧情”这种混合文本,不能直接用。我通常用正则提取年份,再按分隔符拆分地区和类型,最后把结果规整成一行一条的结构化数据。清洗逻辑放在解析函数之后、入库之前,单独写一个函数,方便后续换了数据源还能复用。
import re def clean_movie(movie): """从原始字段里提取年份和地区,补全结构化字段""" raw = movie.pop("year_raw", "") parts = [p.strip() for p in raw.split("/") if p.strip()] year = None region = "未知" for part in parts: # 年份优先:匹配四位数字 m = re.search(r"(\d{4})", part) if m: year = int(m.group(1)) continue # 非年份且非类型词,视为地区 if part not in ("剧情", "喜剧", "动作", "爱情", "科幻", "动画", "悬疑", "犯罪", "冒险", "奇幻", "惊悚", "恐怖", "纪录片", "音乐", "歌舞", "战争", "历史", "传记", "运动"): region = part movie["year"] = year movie["region"] = region return movie注意这里的类型词表是我按TOP250常出现的类型整理出来的,实际拿到的页面数据如果有新的类型词,就会误判成地区。这是小型爬虫项目里典型的“硬编码字典风险”,代码够用但不完美,遇到漏网之鱼时只需要往集合里补词。清洗后的数据我一般存成两路:一是movies.json方便调试时直接看,二是SQLite数据库供Flask后端查询,下一章会用到。写入JSON时别忘了一个关键参数:
with open("movies.json", "w", encoding="utf-8") as f: json.dump(all_movies, f, ensure_ascii=False, indent=2)ensure_ascii=False必须加,否则中文片名和地区全部会变成\u4f60这种转义序列,打开文件根本没法看。这一步我在早期项目里翻过车,当时还以为是爬虫解析出了问题,排查了半天才发现是序列化参数没设对。
3. Flask后端与API设计:动态页面渲染和数据接口两条路线
3.1 为什么选Flask而不选FastAPI:轻量、模板生态和上手成本
做这种规模的个人项目,Flask和FastAPI其实都能胜任,但它们的气质完全不同。FastAPI的异步支持、自动OpenAPI文档和Pydantic校验都很现代,适合前后端分离、接口文档需求明确的生产级应用。但我的场景是单机自用、带页面模板、需要快速跑起来,Flask的render_template配合Jinja2模板直接一个函数出页面,省掉了“前端单独部署”这一步。更重要的一点是,Flask的资料和踩坑记录遍地都是,新手遇到问题很容易搜到同款报错。
如果有朝一日项目要加实时推送、要做高并发接口,那时候再迁移到FastAPI也不迟,那是“数据量真大了”的问题,不是“还没跑起来”的问题。做这个TOP250项目,我不建议在框架选型上过度纠结,先跑通再优化,比什么都强。
3.2 源码工程的目录结构:一个Flask应用的标准打开方式
拿到这类“设计源码”时,最常见的目录组织方式是这样的,不管源码作者怎么命名,核心就这几个部分:
project_root/ ├── app.py # Flask主程序,路由和API都在这 ├── spider.py # 爬虫脚本,独立运行 ├── data/ │ ├── movies.json # 爬虫输出的原始数据 │ └── movies.db # SQLite数据库 ├── templates/ │ └── index.html # Echarts图表容器页面 └── static/ ├── echarts.min.js # Echarts库文件(本地引入) └── style.css # 页面样式app.py和spider.py分开写,是因为爬虫部分和网站服务部分是两条生命周期:爬虫是“偶尔跑一次”的采集任务,而Flask应用是“持续挂着”的服务。如果把两者写进同一个文件,每次启动网站都会执行一遍爬虫逻辑,既慢又容易触发反爬。templates和static是Flask约定的目录名,不要改动,否则框架找不到文件。
3.3 两条渲染路线:render_template服务端渲染与/api接口前端取数
Flask写页面有两条路。一条是服务端渲染:在app.py里读数据库,把数据整理成列表或字典,直接传给模板,模板里的Jinja2语法把它嵌入页面。另一条是API取数:后端只提供JSON接口,前端页面加载后用JavaScript的fetch去拿数据,再由Echarts把数据画成图表。我在这类项目里的经验是:静态页面上做模板渲染没问题,但图表数据一定要走API接口。原因是Echarts的setOption需要接收一个JavaScript对象,服务端渲染出来的数据要先塞进tojson过滤器再交给前端,形态别扭;而API天然返回JSON,前端拿过来直接用,代码心智负担小得多。
from flask import Flask, render_template, jsonify, g import sqlite3 app = Flask(__name__) DB_PATH = "data/movies.db" def query_db(sql, args=()): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cur = conn.execute(sql, args) rows = cur.fetchall() conn.close() return rows @app.route("/") def index(): # 页面首次访问时渲染空壳,图表由前端JS拉取数据后绘制 return render_template("index.html") @app.route("/api/year-distribution") def year_distribution(): """按年份统计电影数量,返回柱状图数据""" rows = query_db( "SELECT year, COUNT(*) AS cnt FROM movies " "WHERE year IS NOT NULL GROUP BY year ORDER BY year" ) return jsonify([{"year": r["year"], "count": r["cnt"]} for r in rows]) @app.route("/api/rating-distribution") def rating_distribution(): """按评分区间统计电影数量,返回饼图数据""" rows = query_db("SELECT rating FROM movies") buckets = {"9分以上": 0, "8.5-9分": 0, "8-8.5分": 0, "8分以下": 0} for r in rows: rating = r["rating"] if rating >= 9: buckets["9分以上"] += 1 elif rating >= 8.5: buckets["8.5-9分"] += 1 elif rating >= 8: buckets["8-8.5分"] += 1 else: buckets["8分以下"] += 1 return jsonify([{"name": k, "value": v} for k, v in buckets.items()])要注意jsonify在Flask里会自动把响应Content-Type设成application/json,前端fetch拿到的res.json()可以直接解析,不需要额外处理编码。sqlite3.Row的作用是让查出来的每一行能通过r["year"]这种字典式访问拿值,比默认的元组形式可读性强很多。我在query_db里每次查询都新建连接,项目规模小所以无所谓;如果数据量大到上百MB,你再考虑连接池也不迟。
3.4 中文编码、JSON序列化与排序的边界问题
Flask的jsonify在返回中文时不需要担心乱码,框架默认会处理UTF-8编码。真正会翻车的点是:如果你不用jsonify而是手动json.dumps,一定要带上ensure_ascii=False,否则你辛辛苦苦抓来的“肖申克的救赎”就变成了\u8096,浏览器里展示出来的就是一串乱码。
另外还有一个排序问题。jsonify默认按字典的Key排序输出,也就是count会跑到year前面。这也就算了,关键是某些Echarts配置对数据顺序敏感,比如年份柱状图横轴期望按年份升序排,如果后端没加ORDER BY year,图表上的柱子顺序就会是乱的。我一般会在SQL层就把排序做掉,别指望前端再去排序——减少一个出错环节就少一次排查。
4. Echarts图表怎么接:从后端JSON到柱状图、饼图与排行的完整配置
4.1 引入方式:本地静态文件优先,CDN不是不可以但要有备选
Echarts的引入方式直接决定项目在无外网环境还能不能跑。用bootcdn或jsdelivr的CDN地址确实省事,但这类个人项目经常要拿到别人的电脑上演示,对方一旦没联网或者CDN被解析异常,页面就是一片空白。我的惯例是:把echarts.min.js下载后放进static/目录,用本地路径引用。代价是工程目录里多一个约1MB的静态文件,换来的却是绝对的可移植性。
4.2 页面骨架与基础配置:容器高度、宽度与初始化时序
Echarts图表不渲染,十次有九次是容器高度为0。div默认高度由内容撑开,而Echarts的init方法要求容器必须有明确的宽高。所以页面里每个图表容器都要显式指定高度,哪怕是一行style="height:400px",也比不给强。
<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>豆瓣电影TOP250数据分析</title> <script src="{{ url_for('static', filename='echarts.min.js') }}"></script> <style> .chart { width: 90%; height: 420px; margin: 20px auto; } .chart-row { display: flex; gap: 16px; } .chart-row .chart { flex: 1; } </style> </head> <body> <h2>豆瓣电影TOP250 · 可视化看板</h2> <div id="yearChart" class="chart"></div> <div class="chart-row"> <div id="ratingChart" class="chart"></div> <div id="regionChart" class="chart"></div> </div> <script src="{{ url_for('static', filename='main.js') }}"></script> </body> </html>这里有个值得注意的细节:main.js必须放在</body>之前,而不是<head>里。因为图表初始化需要DOM节点已经存在,脚本放前面会找不到yearChart这个id。如果你非要放头部,那就得包一层window.onload或DOMContentLoaded,没必要。模板里的url_for('static', filename=...)是Flask的标准静态资源路径生成方式,换成写死的/static/...也能用,但前者在项目换目录部署时不用改代码。
4.3 年份分布柱状图:坐标轴、标签旋转与渐变色
柱状图是这个项目里信息量最大的图表,横轴是年份(1980到2024),纵轴是该年份入围的电影数量。年份跨度大,横轴标签如果不做处理,每根柱子下面挤满数字,根本看不清。我一般会把axisLabel的rotate设为45,并开启interval自动抽稀,让Echarts自己决定每几个年份显示一个标签。另外可以做一个隐藏的细节优化:用itemStyle.color配合linearGradient做渐变色柱体,视觉上会更有“数据看板”的气质。
// 这个文件是 static/main.js fetch("/api/year-distribution") .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById("yearChart")); chart.setOption({ title: { text: "豆瓣TOP250电影年份分布", left: "center" }, tooltip: { trigger: "axis" }, grid: { left: 50, right: 20, bottom: 60 }, xAxis: { type: "category", data: data.map(item => item.year), axisLabel: { rotate: 45, interval: "auto" } }, yAxis: { type: "value", name: "影片数量" }, series: [{ type: "bar", data: data.map(item => item.count), itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: "#ff6b35" }, { offset: 1, color: "#ffd166" } ]) }, barMaxWidth: 30 }] }); }) .catch(err => console.error("年份数据加载失败", err));这段配置里grid的bottom留了60像素,不是随手写的——因为rotate:45后的年份标签会向下探出40到50像素,底部空间不够的话标签会被裁掉。barMaxWidth:30是为了防止某个年份数量特别大时单根柱子过宽,把其他年份挤得显示不下。这些都是实践里一点一点试出来的参数,先抄下来用,后面再按自己的审美微调。
4.4 评分分布环形图:饼图中间的文字和颜色列表
评分分布的饼图适合用环形图展示,视觉上比实心饼图轻巧。Echarts里只需要在series里加一个radius: ["40%", "70%"],内径外径一设,饼图就变成了环。环中间默认是空的,很多人想在那里放一个总数,这就涉及一个常见问题——饼图中间的字怎么加。做法是在series平级的位置添一个graphic组件,用text元素定位到画布中心。
fetch("/api/rating-distribution") .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById("ratingChart")); const total = data.reduce((sum, item) => sum + item.value, 0); chart.setOption({ title: { text: "评分区间分布", left: "center" }, tooltip: { trigger: "item" }, legend: { orient: "vertical", right: 10, top: "middle" }, series: [{ type: "pie", radius: ["40%", "70%"], data: data, label: { show: false }, color: ["#5470c6", "#fac858", "#ee6666", "#73c0de"] }], graphic: [{ type: "text", left: "center", top: "45%", style: { text: `总计\n${total} 部`, textAlign: "center", fontSize: 18, fontWeight: "bold" } }] }); });graphic组件是Echarts里容易被忽略但很好用的能力,它能在canvas上叠加任意图形或文字,而且不参与图表的数据体系。设置left:"center"和top:"45%"时,Echarts会自动换算成百分比坐标,效果大致是文字垂直居中于环形区域。color数组的顺序对应data数组的顺序,注意饼图扇区的默认排序会把数据从大到小排,颜色可能和你预期的分组顺序对不上,如果希望颜色严格对应,可以在series里加sort: (a, b) => a.value - b.value控制。
4.5 地区分布横向条形图:解决国家名过长的问题
地区分布我习惯用横向条形图,而不是纵向柱状图。原因是有些地区名称很长(“中国大陆 / 中国香港”这种组合),在纵向柱状图的横轴下面会被压缩成斜着的省略号,根本读不出来。横向条形图的Y轴天然适合放长文本。实现上只需要把xAxis和yAxis对调,series类型仍然是bar。这个图表的接口/api/region-distribution在上一章没写,你可以照着year-distribution的写法自己补一个,核心就是GROUP BY region ORDER BY cnt DESC LIMIT 10,重点在前十名地区,后面的地区数量少画出来没意义。
图的渲染顺序也值得注意:页面上一共有三个图表,三个fetch是并行发出的,互不等待。如果某个接口偶发超时,对应图表会保持空白,而其他图表正常渲染。这不是框架问题,是异步加载的正常表现。如果你希望“要么全出,要么给个错误提示”,可以用Promise.all统一加载,但那通常不是必要优化。
5. 常见坑与排查:编码、反爬、图表白屏的四个血泪现场
5.1 豆瓣返回418或403:不是代码写错了,是请求太像爬虫
现象:爬虫第一次运行能抓到几十条,多跑几次后resp.status_code变成403或者418,页面内容变成了“请求过于频繁”的验证页面。
原因:豆瓣的反爬机制识别到了高频请求特征,封禁了当前IP的访问权限。你做了UA伪装但没做频率控制,或者前几次请求间隔太短,被封是必然的。
解决:把请求间隔从time.sleep(0.5)拉长到time.sleep(2)到time.sleep(3),并给请求头里的Accept-Language和Referer都补上。如果你想稳妥一点,可以加一个简单的重试逻辑:状态码非200时,等5秒重试一次,最多重试3次。爬虫这东西不是越快越好,慢就是快。
5.2 JSON文件里全是\uXXXX转义:看着像乱码,其实是没设ensure_ascii
现象:打开movies.json,中文电影名全是\u8096\u514b\u7684\u6551\u8d4e,但程序能正常读取,页面也能输出正常中文。
原因:json.dump没有指定ensure_ascii=False,默认把所有非ASCII字符转义成了Unicode编码。这个现象只在json.dump和json.dumps里出现,写到SQLite里的数据不受影响。
解决:写入时统一加ensure_ascii=False, encoding="utf-8",再检查一遍打开文件是否正常。这个坑我在不止一个项目里见过,包括有些网上的源码包本身就没处理,你拿到手觉得“怎么全是乱码”,其实不是数据坏了。
5.3 图表白屏但接口正常:容器高度为0或初始化时序不对
现象:浏览器控制台没有任何报错,fetch能拿到数据,console.log(data)也能打印出数组,但页面上图表区域就是一片空白。
原因:两种可能,一是容器的CSS高度没有设置,div的默认高度是0;二是echarts.init执行时DOM节点还没渲染完成。前者最常见,后者发生在你把main.js放在了<head>里。
解决:给每个图表容器加style="height:420px"这种显式高度;把main.js的script标签移到body底部。如果改完还白屏,打开开发者工具,在Elements面板里看容器元素的计算样式,height是不是有值——这一步能定位90%的白屏问题。
5.4 分析数据比豆瓣页面少了十几条:页面结构微调导致解析漏掉节点
现象:网页上明明250条,程序抓完只有235条,少的主要集中在某些特定年份。
原因:个别列表项里的span.inq不存在(没有一句话短评),或者p文本里的换行符数量不一致,导致info_lines[1]索引越界,整条被continue跳过了。这是典型的“解析逻辑容错性差”。
解决:把每个字段的解析都独立判空,解析失败时该字段置为None而不是跳过整条记录。另外可以在清洗前打印一下len(movies),对比页面总数,抓少了就检查是不是某类节点结构变了。这种问题没有什么一劳永逸的解法,因为豆瓣前端偶尔改版,你的解析代码需要具备“一个字段拿不到不影响其他字段”的弹性。
5.5 Flask应用启动后页面能开但样式全乱:静态文件路径被硬编码
现象:本地开发时一切正常,把整个目录放到服务器或者别人的电脑上,页面HTML出来了但图表和CSS都加载不出来。
原因:模板里写了绝对路径/static/echarts.min.js,而不是用url_for('static', filename='...')生成。Flask的static路径在开发服务器、gunicorn、nginx反代三种场景下前缀可能会有差异,硬编码路径是最脆弱的写法。
解决:模板里统一用{{ url_for('static', filename='echarts.min.js') }}。这个函数会根据Flask的配置自动生成正确路径,项目挪位置、加前缀都不需要改页面代码。同理,API请求地址也尽量用相对路径/api/year-distribution,不要在JavaScript里写死http://localhost:5000。
6. 再往前走一步:从单页图表到部署上线与自动更新
项目跑通只是起点,到这个阶段你应该已经有一份完整可运行的代码,但我建议你再往前走三步,把项目从“能跑”变成“有用”。
第一步是部署。Flask自带的开发服务器只适合调试,真正对外提供服务时我习惯用gunicorn做WSGI容器,一条命令搞定:gunicorn -w 4 -b 0.0.0.0:8000 app:app。注意-w的进程数和服务器CPU核数相关,不是越多越好,4核机器开4个worker是常见起步配置。部署后如果遇到静态文件404,优先检查app.secret_key和static_folder这些配置项,多数问题都出在这。
第二步是数据更新自动化。TOP250榜单不是一成不变的,新片入围、老片退出都是常态。我给爬虫加了一个“幂等更新”逻辑:按rank字段作为唯一键,重复抓取时先删后插,保证数据库里永远是当前最新榜单。配合系统的cron或Windows的任务计划程序,每月自动跑一次spider.py,数据就永远是新鲜的。
第三步是图表交互深度。现在的三个图表是彼此孤立的,进阶一点的做法是加一个click事件——点击年份柱状图里的某根柱子,右侧的排行榜表格就只显示该年份的电影。Echarts的chart.on("click", params => ...)可以拿到柱子对应的params.name,然后带着这个参数重新请求一个/api/movies?year=1994接口,就能实现真正的联动。这一步做完,这个项目就不再是一个“展示页”,而是一个可交互的分析工具。
我在自己折腾这类项目的过程中,最大的教训是别急着写代码。先把数据字段确认清楚、把目录结构想明白、把接口路径定下来,后面每一步都会很顺。反过来,一上来就写页面、写图表,最后经常因为数据格式对不上反复重构,那种返工才是最费时间的。
这套方案我前后整理过好几个版本,从最早的纯表格展示,到后来的多图表联动,再到现在的自动更新部署,每一步都是被真实需求推着走的。希望这套基于Python Flask与Echarts插件的豆瓣电影TOP250数据分析可视化网站设计,能帮你少走几步弯路,早点把自己想看的数画出来。
本文还有配套的精品资源,点击获取