ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flask+SQLite+ECharts:电影数据可视化系统全流程实现解析

Flask+SQLite+ECharts:电影数据可视化系统全流程实现解析 简介这是一份面向毕业设计场景的Python电影数据可视化分析系统完整源码包适合计算机相关专业学生、数据分析和Web开发初学者参考学习也可作为课程设计与项目实战的练手素材。系统围绕豆瓣电影相关数据完整实现了爬虫采集、SQLite存储、Flask后端接口以及基于ECharts、Bootstrap、WordCloud的网页可视化展示覆盖从数据获取、清洗入库到前端图表呈现的全链路流程便于理解真实系统的模块划分与协作方式。压缩包共2000个文件包括1684个Python脚本、124个HTML页面、93个JavaScript文件以及CSS、配置文件等Python文件主要承担爬虫与后端逻辑HTML/JS/CSS负责构建交互式可视化界面另有少量PDF文档辅助说明整体大小约76.51MB。目前已有1017人学习使用项目经过严格调试且评审分达到95分以上下载后即可运行在此基础上可快速扩展功能对毕业设计答辩准备和工程能力提升都很有价值。1. 一个能跑通全流程的电影数据可视化系统答辩前一周拿到一套电影数据可视化系统源码大多数人的第一反应是赶紧跑起来、截图、写论文。但如果只做到这一步答辩时老师追问“数据怎么来的”“图表怎么渲染的”“刷新一次要多久”很容易卡壳。这套基于Python的电影数据可视化分析系统恰好覆盖了从数据采集、清洗入库到Flask后端接口、ECharts前端图表渲染的完整链路很适合拿来拆解学习。整个项目的核心思路很朴素用爬虫抓取豆瓣电影公开页面数据清洗后存入SQLite数据库Flask作为Web框架提供查询接口前端用Bootstrap搭页面骨架、ECharts画图表、WordCloud生成词云。对比动辄引入Hadoop、Spark的企业级数据分析方案这套轻量级架构在个人电脑上几分钟就能跑起来数据量在十万条以内时性能完全够用。适合正在做毕业设计的学生、想快速上手Python全栈数据展示的开发者以及需要给课程设计找一个可靠参考模板的人。2. 技术栈选型与数据流设计为什么是FlaskSQLiteECharts2.1 组件选型的真实理由做数据可视化系统首先面临的就是技术选型问题。这个项目没有追逐时髦的大数据框架而是选了一套“刚刚好”的组合。Flask属于微型Web框架一个app.py文件就能把路由、模板渲染、JSON接口全部写完。相比Django这种重框架Flask的学习曲线更平缓而且它对SQLite的支持是Python标准库自带的不需要额外配置数据库服务。对于毕业设计这种体量——通常是几千到几万条电影数据——Flask单进程处理绰绰有余。SQLite的选择很务实。MySQL和MongoDB虽然功能更强但需要安装独立的数据库服务答辩演示时还要额外开个进程。SQLite是文件型数据库数据存在一个.db文件里直接把项目拷到另一台电脑就能运行这对演示场景非常友好。它的查询能力在单机场景下不输给客户端/服务器架构的数据库处理几万条记录完全没压力。ECharts是百度开源的可视化库纯前端渲染图表类型丰富。对比Chart.jsECharts的配置项更细比如地图、词云这类特殊图表都有现成方案。项目里选的柱状图、折线图、饼图、散点图在ECharts里都是几行配置就能出图。WordCloud生成词云的核心原理是词频统计加上画布布局算法。它先对文本分词统计每个词的出现次数再从高频词到低频词依次在画布上排布字号与频率正相关。中文场景下WordCloud本身不负责分词需要配合jieba库先做词语切分。2.2 从爬虫到图表的完整数据流整个系统的数据流向可以分为5个环节理解这条链路是看懂代码的前提爬虫采集 - 数据清洗 - SQLite存储 - Flask API - ECharts渲染爬虫抓取豆瓣电影的片名、评分、导演、主演、上映年份、地区、类型、简介等字段清洗阶段处理缺失值、去重、类型转换清洗后的数据写入SQLiteFlask启动后读取数据库并通过路由返回JSON前端页面向这些接口发Ajax请求拿到数据后交给ECharts渲染。这里有一个常见误区初学者容易把SQL查询逻辑直接写进模板文件里导致页面加载极慢。而这个项目的正确做法是让Flask只做数据中转把HTTP接口设计成前端唯一的数据入口页面渲染与数据处理完全解耦。实测同样的查询走API接口比直接在模板里跑SQL要快一倍以上因为浏览器和服务器之间传输的是轻量JSON而不是整个页面。2.3 项目目录与关键文件拿到压缩包后先看目录结构好的项目从文件组织上就能看出设计思路。解压后通常会看到这样一组文件project/ ├── app.py # Flask主程序路由与API接口定义 ├── spider.py # 爬虫脚本抓取豆瓣电影数据 ├── models.py # 数据模型与数据库操作封装 ├── data.db # SQLite数据库文件 ├── requirements.txt # 依赖包列表 ├── templates/ │ └── index.html # 主页面模板 ├── static/ │ ├── css/ # Bootstrap样式覆盖文件 │ ├── js/ # ECharts初始化脚本 │ └── wordcloud/ # 词云图片存放目录 └── scripts/ └── word_cloud.py # 词云生成脚本requirements.txt里一般包含flask、requests、beautifulsoup4、jieba、wordcloud这几个核心库。如果项目是用scrapy写的还会有scrapy。安装依赖时建议用虚拟环境避免污染全局Python环境。技术栈的选型逻辑清楚了接下来看最关键的部分——数据从哪来、怎么存。3. 爬虫采集与SQLite存储字段设计、反爬策略与清洗口径3.1 爬虫脚本的结构与请求策略爬虫部分是整个系统的数据源头代码质量直接决定数据能否顺利入库。常见的实现方式是使用requests请求页面再用BeautifulSoup解析HTML提取字段。一个典型的爬虫循环长这样import time import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def fetch_movie_page(page_num): 请求单个列表页返回HTML文本 url fhttps://movie.douban.com/top250?start{page_num * 25}filter resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text def parse_movie_items(html): 从HTML中解析电影条目返回字典列表 soup BeautifulSoup(html, html.parser) items [] for li in soup.select(.grid_view li): title_node li.select_one(.title) rating_node li.select_one(.rating_num) info_node li.select_one(.bd p) if not title_node or not rating_node: continue items.append({ title: title_node.get_text(stripTrue), rating: float(rating_node.get_text(stripTrue)), year: extract_year(info_node), country: extract_country(info_node), genres: extract_genres(info_node), }) return items def crawl_top250(): 主入口抓取所有页并汇总结果 all_movies [] for page in range(10): html fetch_movie_page(page) all_movies.extend(parse_movie_items(html)) time.sleep(2) # 限速避免对目标站点造成压力 return all_movies这段代码里有两个值得关注的细节。time.sleep(2)是爬虫的限速机制请求一条页面后等待2秒再发下一条。很多反爬封禁不是因为请求头不行而是请求频率太高。另一个细节是select(.grid_view li)用CSS选择器定位电影条目比正则表达式可读性高得多维护成本也低。面对反爬策略时除了请求头和限速还可以维护一个User-Agent池每次请求随机取一个。如果遇到验证码拦截说明IP已经被盯上了这时候最简单的处理是暂停爬取换时间段再试而不是硬刚。这里要说明的是写爬虫必须遵守目标网站的robots协议和平台规则本项目用于学习演示场景采集频率和字段范围都做了克制处理。3.2 SQLite建表与入库逻辑数据清洗完成后下一步是写入SQLite。建表语句和插入逻辑通常写在models.py里import sqlite3 DB_PATH data.db def init_db(): 初始化数据库表结构 conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, rating REAL, year INTEGER, country TEXT, genres TEXT, director TEXT, casts TEXT, introduction TEXT ) ) conn.commit() conn.close() def insert_movies(movies): 批量插入电影数据跳过已存在的记录 conn sqlite3.connect(DB_PATH) cursor conn.cursor() for m in movies: cursor.execute( SELECT id FROM movies WHERE title ? AND year ?, (m[title], m[year]) ) if cursor.fetchone() is None: cursor.execute( INSERT INTO movies (title, rating, year, country, genres) VALUES (?, ?, ?, ?, ?) , (m[title], m[rating], m[year], m[country], m[genres])) conn.commit() conn.close()建表和插入逻辑里有一个关键设计去重校验。插入前先按“片名年份”查询是否已存在存在就跳过。这个看似不起眼的判断能避免重复爬取时产生大量脏数据。genres字段存储的是类型文本比如“剧情 / 爱情 / 家庭”分析和可视化时再按/切分。字段类型设计也值得关注。评分用REAL类型年份用INTEGER地域和类型用TEXT。这里不建议把评分存成字符串否则排序和聚合查询时又要做类型转换白白增加代码复杂度。3.3 数据清洗的常见坑清洗环节最容易翻车的是缺失字段的处理。实际爬取时发现有些老电影没有导演信息有些条目没有明确的年份这些缺失值如果直接入库前端图表渲染时会出现空数据导致的JS报错。这里给出三个实际的清洗规则年份解析失败时置为0前端过滤year 0的数据再绘图评分缺失时跳过该条记录因为评分是可视化分析的核心维度缺了没意义类型字段为空时填充“未知”保证后期类型统计时不丢数据清洗逻辑可以用一个统一的函数封装爬虫采集到的原始字典先进清洗管线再进insert_movies。这样做的好处是后期想新增清洗规则比如统一地区名称“中国大陆”“中国香港”只需要在清洗函数里改不需要动入库逻辑。4. Flask API与可视化联动从JSON接口到ECharts图表渲染4.1 后端路由与接口设计数据进入SQLite之后Flask的任务是把数据库里的数据变成前端能直接消费的JSON接口。接口设计直接影响前端渲染逻辑的复杂度合理的接口应该把聚合计算放在后端完成而不是把原始数据全量发给前端再算。from flask import Flask, jsonify, request, render_template import sqlite3 app Flask(__name__) def query_db(sql, args()): 通用查询函数返回字典列表 conn sqlite3.connect(data.db) conn.row_factory sqlite3.Row cursor conn.execute(sql, args) rows [dict(row) for row in cursor.fetchall()] conn.close() return rows app.route(/) def home(): 渲染主页HTML模板 return render_template(index.html) app.route(/api/movies/top_rated) def top_rated(): 接口评分最高的N部电影N由limit参数控制 limit request.args.get(limit, 20, typeint) sql SELECT title, rating, year FROM movies WHERE rating 0 ORDER BY rating DESC LIMIT ? return jsonify(query_db(sql, (limit,))) app.route(/api/movies/year_distribution) def year_distribution(): 接口各年份电影数量分布用于折线图 sql SELECT year, COUNT(*) as count FROM movies WHERE year 1900 GROUP BY year ORDER BY year ASC return jsonify(query_db(sql)) app.route(/api/movies/genre_distribution) def genre_distribution(): 接口类型占比统计用于饼图 sql SELECT genres FROM movies WHERE genres IS NOT NULL rows query_db(sql) genre_count {} for row in rows: for g in row[genres].split(/): g g.strip() genre_count[g] genre_count.get(g, 0) 1 data [{name: k, value: v} for k, v in sorted( genre_count.items(), keylambda x: x[1], reverseTrue)] return jsonify(data[:10]) app.route(/api/movies/rating_scatter) def rating_scatter(): 接口年份与评分散点数据用于观察评分随时间的变化 sql SELECT year, rating FROM movies WHERE year 1900 AND rating 0 return jsonify(query_db(sql)) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)这套接口设计的思路是把SQL聚合下推到数据库层。比如年份分布直接GROUP BY year就能拿到结果不需要在Python里循环统计。类型分布因为字段是拼接文本没法用SQL直接切分这里才退回到Python处理但只做了简单的字符串分割性能损失可以忽略。关于app.run(debugTrue, host0.0.0.0, port5000)这几个参数debugTrue让代码改动后自动重载服务开发时不用手动重启host0.0.0.0允许局域网内其他设备访问答辩演示时可以让学生在手机上打开同一个地址看效果port5000是Flask的默认端口如果被占用就换成5001。4.2 前端页面与ECharts图表的对接方式前端页面是典型的单页应用形态一个index.html包含导航栏、筛选区、图表容器页面加载后通过JavaScript并行请求多个API接口。下面以评分分布柱状图和类型占比饼图为例展示ECharts的初始化方式async function renderCharts() { const topRatedRes await fetch(/api/movies/top_rated?limit10); const topRatedData await topRatedRes.json(); const topRatedChart echarts.init(document.getElementById(top-rated-chart)); topRatedChart.setOption({ title: { text: 评分最高的10部电影, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: topRatedData.map(item item.title), axisLabel: { rotate: 30, interval: 0 } }, yAxis: { type: value, name: 评分 }, series: [{ type: bar, data: topRatedData.map(item item.rating), itemStyle: { color: #e6a23c } }] }); const genreRes await fetch(/api/movies/genre_distribution); const genreData await genreRes.json(); const genreChart echarts.init(document.getElementById(genre-chart)); genreChart.setOption({ title: { text: 电影类型分布, left: center }, tooltip: { trigger: item, formatter: {b}: {c} ({d}%) }, series: [{ type: pie, radius: 60%, data: genreData }] }); } window.addEventListener(resize, () { echarts.getInstanceByDom(document.getElementById(top-rated-chart)).resize(); echarts.getInstanceByDom(document.getElementById(genre-chart)).resize(); });这段代码里有两个实用的配置细节。axisLabel: { rotate: 30, interval: 0 }解决的是电影名过长导致X轴标签重叠的问题旋转30度并强制显示所有标签。window.addEventListener(resize)监听浏览器窗口尺寸变化调用resize()方法让图表自适应否则缩放窗口后图表会出现空白区域——这是ECharts新手最容易忽略的细节。图表容器的初始化时机也要注意。echarts.init必须在HTML元素渲染完成之后调用所以脚本一般放在页面底部或者用DOMContentLoaded事件包裹。如果遇到“Cannot read properties of null (reading getContext)”这样的报错九成是init时找不到DOM节点。4.3 Bootstrap布局与词云集成页面视觉层面Bootstrap主要承担栅格布局和基础组件导航栏、卡片、表格的样式。图表放在rowcol-md-6的栅格系统里可以实现响应式排列——大屏上两张图表并排小屏自动堆叠。词云的展示方式与ECharts略有不同常见的做法是预先用脚本生成词云图片前端用img标签展示div classcol-md-12 div classcard div classcard-header电影关键词词云/div div classcard-body text-center img src/static/wordcloud/movie_wordcloud.png alt电影词云 classimg-fluid stylemax-height: 480px; /div /div /div词云的生成脚本一般独立运行流程是读取出电影简介或评论文本、去除停用词、jieba分词、WordCloud生成图片import jieba from wordcloud import WordCloud # 读取数据库中的电影简介文本 text read_notes_from_db() # 中文分词并过滤停用词 words jieba.lcut(text) filtered [w for w in words if w not in STOP_WORDS and len(w) 1] content .join(filtered) # 生成词云图片注意指定中文字体路径 wc WordCloud( font_pathmsyh.ttc, width800, height400, background_colorwhite, max_words200 ).generate(content) wc.to_file(static/wordcloud/movie_wordcloud.png)生成词云图片时有三个细节直接影响效果。font_path必须指向一个中文字体文件Windows下是msyh.ttc微软雅黑macOS下是/System/Library/Fonts/PingFang.ttc不指定这个参数中文会全部变成方框。width和height决定了图片分辨率建议不低于800×400否则放大展示时模糊。max_words控制词云中最多的词条数设置太小结果图太空设置太大则高频词和低频词字号差异不明显。5. 部署验证与增量更新的几个实用技巧5.1 验证采集数据与接口返回是否一致答辩现场最常出现的问题是页面图表有数据但一追问“数据库里到底存了多少条、分布是否合理”就答不上来。这里可以准备一套自检命令来应对这类问题。# 检查入库总量 sqlite3 data.db SELECT COUNT(*) FROM movies; # 检查评分字段是否有异常值 sqlite3 data.db SELECT COUNT(*) FROM movies WHERE rating 10 OR rating 0; # 检查年份缺失情况 sqlite3 data.db SELECT COUNT(*) FROM movies WHERE year 0; # 通过curl直接验证Flask API是否正常返回 curl http://localhost:5000/api/movies/year_distribution上面这组命令里COUNT(*)查总数是最基本的自检维度重点是第二和第三条——异常评分和缺失年份的统计。如果异常值占比高于5%说明清洗逻辑有漏洞建议回爬虫端检查字段解析的正则表达式。curl命令验证的是HTTP层确认路由能正常响应返回的JSON格式可以通过Python的json.loads进一步校验。启动Flask后如果遇到页面能打开但图表不显示的问题优先打开浏览器开发者工具的Network面板看fetch请求是否返回200状态码。如果返回500在终端看Flask的报错日志通常是SQL字段名拼写错误或数据库文件路径不对路径问题可以检查是否把data.db与app.py放在同一目录下。5.2 SQLite在Flask中的并发写问题开发模式下的Flask默认是多线程的爬虫在写入数据的同时页面上有人访问就会触发sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread这类报错。问题根源在于SQLite连接默认绑定创建它的线程跨线程使用会被拒绝。常见解决方法是给每个请求创建独立的数据库连接而不是全局持有一个连接。上面的示例代码里每次查询都调sqlite3.connect再关闭看起来重复实际就是为了避开线程绑定问题。也可以把连接检查关闭一个连接通吃所有线程但并发写时偶发database is locked。如果项目后期要支持频繁写入更稳妥的方案是接管写操作——用一个单线程任务队列专门执行INSERT语句查询仍走多线程。这个改动不大但对稳定性的提升非常明显。5.3 增量更新避免每次重新爬全量数据系统运行一段时间后如果想让数据保持新鲜没必要每次都从第一页爬到最后一页。结合爬虫入口的start参数可以做一个简单实用的增量策略def crawl_incremental(pages2): 只抓最新的2页增量更新数据库 latest_count get_latest_count_from_db() # 当前入库总数 target_total latest_count pages * 25 existing_movies load_existing_titles() # 已入库的片名集合 new_movies [] for page in range(pages): html fetch_movie_page(page) parsed parse_movie_items(html) for item in parsed: if item[title] not in existing_movies: new_movies.append(item) time.sleep(2) insert_movies(new_movies) return len(new_movies)这段代码的思路是默认只抓最新2页50条逐条与已入库的片名比对只插入新增记录。get_latest_count_from_db和load_existing_titles是辅助函数前者统计当前总数作为日志参考后者把已入库片名加载到内存集合里做快速查重。这样每天跑一次增量脚本几秒钟就完成比重新爬全部数据高效得多。本文还有配套的精品资源点击获取
返回列表