news 2026/6/11 2:54:51

豆瓣电影TOP250与全量数据采集+MySQL存储+Flask后台+PyEcharts动态图表

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
豆瓣电影TOP250与全量数据采集+MySQL存储+Flask后台+PyEcharts动态图表

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

简介:直接可运行的豆瓣电影数据分析系统,自动抓取豆瓣电影TOP250榜单及全站电影链接(spider_url.py),再深度提取影片详情(spider_info.py),清洗后存入MySQL(含tb_film.sql、tb_movies.sql等建表脚本)。Flask后端(app.py)提供统一接口,前端基于layuiAdmin搭建,所有图表均用PyEcharts实时渲染:上映时间分布直方图、评分趋势折线图、评论数时序变化图、电影类型占比饼图、产地分布热力图。配套完整页面:登录页、首页、类型分析页、404页,以及come.png、welcome.png等静态资源。支持按片名模糊搜索(film_search.py),数据导出为CSV,适配课程设计、毕设部署或自学练手,requirements.txt已列明全部依赖。
我做过不下十套豆瓣数据采集分析系统,从本科课程设计到带学生做毕设,这套方案是我反复打磨三年、在三所高校的Python数据分析课上验证过的“教学级工业平衡点”——既不因过度简化而失去工程完整性,也不因堆砌技术而让新手望而却步。它不是炫技的玩具,而是真正能跑通“采集→清洗→存储→服务→可视化”全链路的最小可行产品(MVP)。关键词里五个词,每一个我都亲手调过至少27次:豆瓣爬虫要绕过反爬又不激进;电影数据分析不是简单count和avg,得体现时间维度与类型交叉;Flask后台必须轻量但接口健壮;PyEcharts可视化不能只画图,得支持前端交互下钻;MySQL存储不是dump完就完事,表结构设计直接决定后续分析效率。下面我就以一个真实部署过14次、修复过86个细节问题的从业者身份,带你把这套系统从“能跑”变成“稳跑”,再变成“可扩展”。


1. 整体架构设计与选型逻辑拆解

1.1 为什么是“TOP250 + 全量链接”双轨采集?

很多人一上来就想爬全站电影,结果被封IP、被验证码卡死,最后连TOP250都拿不全。我坚持先做TOP250再扩全量,不是保守,而是基于豆瓣反爬机制的实测判断。豆瓣对榜单页(如https://movie.douban.com/top250?start=0)的防护明显弱于详情页(如https://movie.douban.com/subject/1292052/),前者基本无JavaScript渲染、无动态token、无行为指纹校验;后者则大量依赖AJAX加载短评、影人、相似影片等模块,且存在X-Requested-With头校验、Referer白名单、请求频率熔断(实测超过3次/秒即返回403)。所以我的采集策略是分层推进:

  • 第一阶段(spider_url.py):只抓取TOP250页面的250条<a href="/subject/1292052/">链接,并额外采集豆瓣“电影分类导航页”(如https://movie.douban.com/tag/#/?tags=%E7%88%B1%E6%83%85)中高频出现的标签页(爱情、剧情、喜剧、动作等共28个),每个标签页翻10页(每页20部),生成约5600条种子URL。这些URL全部写入豆瓣电影链接.csv,作为第二阶段的输入源。

  • 第二阶段(spider_info.py):对CSV中的每条URL发起GET请求,但绝不并发猛攻。我用concurrent.futures.ThreadPoolExecutor(max_workers=3)控制线程数,配合time.sleep(random.uniform(1.8, 3.2))随机延时——这个区间是我实测出来的安全窗口:小于1.5秒大概率触发429,大于4秒效率太低。更重要的是,我在headers里固定了User-Agent(用Chrome最新版UA)、Accept-Language: zh-CN,zh;q=0.9,并强制携带Referer:所有详情页请求的Referer都设为对应分类页(如从“爱情”标签页来的,Referer就是https://movie.douban.com/tag/#/?tags=%E7%88%B1%E6%83%85)。豆瓣后端会校验这个Referer,缺失或错误直接返回403。这个细节,90%的教程都漏掉。

提示:spider_url.py里有个隐藏技巧——它会自动过滤掉/celebrity/(影人页)和/tv/(电视剧页)链接,只保留/subject/开头的纯电影页。这是靠正则r'/subject/\d+/'实现的,避免后续解析器误入歧途。

1.2 MySQL表结构为何设计为三张主表?

看目录里的SQL文件:tb_film.sqltb_movies.sqltb_film_top250.sql,有人会问“为啥不一张表搞定?”——因为数据来源、更新频率、字段粒度完全不同,强行合并会导致冗余爆炸和查询低效。

  • tb_film基础信息宽表:存所有爬到的电影ID(film_id)、片名(title)、副标题(sub_title)、年份(year)、评分(score)、评论数(comment_count)、简介(intro)、海报URL(poster_url)。它的主键是film_id(即豆瓣subject ID),字符型,长度设为12(足够覆盖当前最大ID)。这里有个关键设计:score字段用DECIMAL(2,1)而非FLOAT,避免浮点精度误差(比如9.6存成9.5999999);comment_countBIGINT UNSIGNED,因为热门电影评论超百万,INT会溢出。

  • tb_movies关系型事实表:它不存片名、评分等描述性字段,而是专注记录“电影-类型-地区-语言”的多对多关系。结构是(film_id, type_name, type_category)三元组,其中type_category取值为'genre'(类型)、'area'(地区)、'language'(语言)。这样设计的好处是:查“所有犯罪片”只需WHERE type_name='犯罪' AND type_category='genre';查“日本出品的动画片”就是JOIN两次。如果把类型、地区全塞进tb_film,字段会膨胀到30+列,且无法灵活增删类型(比如新增“赛博朋克”子类型就得加字段)。

  • tb_film_top250榜单快照表:只存TOP250的film_id、排名(rank)、上榜日期(snapshot_date)。它用ON DUPLICATE KEY UPDATE机制实现增量更新——每次爬完TOP250,执行INSERT INTO tb_film_top250 VALUES (1292052, 1, '2024-06-15') ON DUPLICATE KEY UPDATE rank=VALUES(rank), snapshot_date=VALUES(snapshot_date)。这样既能追踪某部电影排名变化(比如《肖申克的救赎》常年第1,但《阿甘正传》在2023年曾跌出前3),又避免重复插入。

注意:三张表之间通过film_id关联,但不建外键约束。这是刻意为之——MySQL外键会拖慢大批量INSERT性能(尤其spider_info.py单次入库常达2000+行),而业务逻辑层(Python代码)已确保数据一致性。牺牲一点ACID换百倍写入速度,在分析型系统里是合理trade-off。

1.3 Flask后台为何不做RESTful API,而用模板直出?

看到app.py里全是render_template('index.html', data=xxx),有人会觉得“不够现代”。但这是针对教学场景的精准选择。学生第一次接触Web开发,如果让他们同时理解JWT鉴权、CORS跨域、Axios异步请求、Vue响应式绑定,学习曲线会陡峭到放弃。而render_template模式,把数据处理(Python)和页面渲染(Jinja2)放在同一进程,调试时print一行就能看到变量值,报错堆栈清晰指向具体行号。更重要的是,PyEcharts的图表对象(Bar()Line()等)可以直接.render_embed()生成HTML片段,嵌入Jinja2模板,无需前后端分离的JSON序列化/反序列化。app.py/genres路由的代码只有12行:

@app.route('/genres') def genres_page(): # 从MySQL查出各类型电影数量 cursor.execute("SELECT type_name, COUNT(*) as cnt FROM tb_movies WHERE type_category='genre' GROUP BY type_name ORDER BY cnt DESC LIMIT 15") genre_data = cursor.fetchall() # 用PyEcharts画饼图 pie = Pie() pie.add("", [list(d) for d in genre_data], radius=["40%", "70%"]) pie.set_global_opts(title_opts=opts.TitleOpts(title="电影类型分布")) return render_template('genres.html', chart=pie.render_embed())

这段代码,学生抄一遍就能懂:查库→转数据→画图→塞进模板。比教他们写fetch('/api/genres').then(...)document.getElementById().innerHTML=直观十倍。

1.4 PyEcharts为何不接ECharts原生,而用Python端渲染?

PyEcharts本质是ECharts的Python封装,但它解决了两个致命痛点:一是免JS环境配置,学生不用装Node.js、配webpack;二是Python数据无缝对接。比如timeline_comment.py要画“近十年电影评论数变化”,原始数据是MySQL里按年份聚合的[(2014, 12500), (2015, 18900), ...],用PyEcharts只需:

line = Line() line.add_xaxis([str(y) for y in years]) line.add_yaxis("评论总数", comment_counts) line.set_global_opts( title_opts=opts.TitleOpts(title="豆瓣电影年度评论数趋势"), xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False), yaxis_opts=opts.AxisOpts(type_="value", splitline_opts=opts.SplitLineOpts(is_show=True)) )

如果用原生ECharts,你得把Python列表json.dumps()成字符串,再在HTML里用JSON.parse()还原,中间还可能遇到中文编码、单双引号转义等问题。而PyEcharts的.render_embed()直接输出<div id="xxx">...</div><script>...</script>,复制粘贴就能用。对于入门项目,省下的2小时调试时间,够学生多跑三轮数据清洗。


2. 核心模块细节解析与实操要点

2.1 爬虫模块:spider_url.py与spider_info.py的协同机制

spider_url.pyspider_info.py不是孤立脚本,而是一个生产者-消费者流水线。spider_url.py负责“找种子”,spider_info.py负责“种庄稼”,二者通过CSV文件解耦——这种设计让调试极其方便:你可以单独运行spider_url.py生成新种子,再用旧种子测试spider_info.py的解析逻辑,互不影响。

spider_url.py的核心是get_movie_urls_from_tag(tag_name, pages=10)函数。它访问豆瓣标签页时,实际请求的是https://movie.douban.com/j/search_subjects?type=movie&tag={tag}&page_limit=20&page_start={start}这个AJAX接口(注意不是HTML页面)。豆瓣把标签页的电影列表藏在JSON接口里,返回结构是:

{ "subjects": [ {"id": "1292052", "url": "https://movie.douban.com/subject/1292052/", "title": "肖申克的救赎", "cover": "https://imgX.douban.com/view/photo/m/public/p480747492.jpg", "rate": "9.7", "is_new": false}, ... ] }

这个接口没有Referer校验,且频率限制宽松(实测5次/秒无压力),比解析HTML快3倍。我用requests.get()直接调它,提取subject['id']拼成/subject/{id}/链接,写入CSV。关键细节page_start参数不是页码,而是偏移量(0, 20, 40…),所以循环里要for start in range(0, pages*20, 20)

spider_info.py的解析难点在于豆瓣详情页的DOM结构不稳定。比如“类型”字段,有时在<span property="v:genre">里,有时在<span class="pl">类型:</span>后的<span>里;“上映日期”可能有多个(大陆、台湾、香港、北美),需取第一个含年份的。我的解决方案是多策略 fallback 解析

def extract_genres(soup): # 策略1:优先找property="v:genre"的span genres = soup.select('span[property="v:genre"]') if genres: return [g.get_text(strip=True) for g in genres] # 策略2:找class="pl"且文本含"类型"的span,然后取其下一个兄弟span pl_span = soup.find('span', class_='pl', string=re.compile(r'类型')) if pl_span and pl_span.next_sibling: next_span = pl_span.next_sibling.find_next('span') if next_span: return [t.strip() for t in next_span.get_text(strip=True).split('/') if t.strip()] return []

这种写法牺牲了一点性能(多几次DOM查找),但换来极高的鲁棒性——即使豆瓣明天改版,只要核心字段还在页面上,大概率能fallback成功。

实操心得:spider_info.py里所有time.sleep()都放在try...except之外。我见过太多人把sleep包在异常处理里,导致网络失败时疯狂重试不休眠,1分钟内触发豆瓣风控。正确姿势是:
python for url in urls: try: response = requests.get(url, headers=headers, timeout=10) # 解析逻辑... except Exception as e: print(f"Failed {url}: {e}") finally: time.sleep(random.uniform(1.8, 3.2)) # 每次请求后必休眠

2.2 数据清洗:为什么清洗逻辑不在爬虫里,而在独立模块?

看目录里没有clean_data.py,但spider_info.py里有大量strip()replace()、正则清洗。这看似矛盾,实则是分层清洗策略:爬虫内做“保活清洗”,独立模块做“分析清洗”

  • “保活清洗”指保证数据能入库:去掉字符串首尾空格、将'暂无评分'转为NULL、把'2023-03-15(中国大陆)'用正则r'(\d{4})-\d{2}-\d{2}'抽年份。这些必须在爬虫里做,否则INSERT INTO会因类型不匹配失败。

  • “分析清洗”指为可视化准备的数据变形:比如showtime.py要画上映时间分布直方图,需要把年份归到10年一个桶(1990-1999, 2000-2009…),这个逻辑放在select_showtime.py里,而不是爬虫里。因为归桶规则可能随分析需求变(老师要求按年代,学生想按世纪),如果硬编码在爬虫里,每次改都要重爬。

select_showtime.py的SQL是这样的:

SELECT CASE WHEN year BETWEEN 1920 AND 1929 THEN '1920s' WHEN year BETWEEN 1930 AND 1939 THEN '1930s' -- ... 省略中间 WHEN year BETWEEN 2020 AND 2029 THEN '2020s' ELSE 'Unknown' END AS decade, COUNT(*) as count FROM tb_film WHERE year IS NOT NULL AND year > 1910 GROUP BY decade ORDER BY MIN(year);

用CASE WHEN做分桶,比Python端pd.cut()更高效(数据在数据库里,不用拉到内存)。这个SQL被showtime.py调用,结果喂给PyEcharts的Bar()。整个链条:MySQL计算 → Python取结果 → PyEcharts画图,数据不动,只有结果流动。

2.3 Flask路由设计:如何避免“一个页面一个路由”的面条代码?

app.py里有/,/genres,/areas,/search等路由,但没看到/score,/timeline——因为score.pytimeline_score.py独立的图表生成模块,它们不暴露HTTP接口,只提供generate_score_chart()函数。app.py统一用一个/chart路由接收参数:

@app.route('/chart') def render_chart(): chart_type = request.args.get('type', 'score') # 默认画评分图 if chart_type == 'score': chart = score.generate_score_chart() elif chart_type == 'timeline': chart = timeline_score.generate_timeline_chart() elif chart_type == 'genres': chart = genres.generate_genre_pie() else: chart = "<p>Chart not found</p>" return render_template('chart_base.html', chart=chart)

chart_base.html是一个通用模板,只包含{{ chart|safe }}。这样做的好处是:新增一种图表(比如加个导演热度图),只需写director.py提供generate_director_chart(),再在/chart路由里加一个elif分支,完全不用动HTML文件和URL结构。对比“每个图表一个HTML文件”的做法,维护成本降低70%。

注意:/chart路由必须加@app.route('/chart', methods=['GET'])明确声明方法,否则Flask默认只响应GET,而某些前端操作(如搜索后刷新图表)可能意外发POST,导致405错误。这个细节,我在帮学生debug时修过11次。

2.4 PyEcharts图表定制:如何让饼图点击下钻到类型页?

genres.html里的饼图不只是静态展示,点击某个扇形(如“剧情”),页面应跳转到/search?keyword=剧情,显示所有剧情片。这需要PyEcharts的JavaScript事件绑定。但PyEcharts的Python API不直接支持事件,得用set_series_opts()注入JS代码:

pie = Pie() pie.add("", genre_data, radius=["40%", "70%"]) pie.set_series_opts( label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)"), # 关键:绑定点击事件 emphasis_itemstyle_opts=opts.ItemStyleOpts(), # 注入JS:点击时跳转 tooltip_opts=opts.TooltipOpts(trigger_on="click"), # 这里用add_js_funcs注入全局JS函数 ) pie.add_js_funcs(""" chart.on('click', function(params) { window.location.href = '/search?keyword=' + encodeURIComponent(params.name); }); """)

add_js_funcs()把JS代码注入到PyEcharts生成的HTML里,chart.on('click')监听点击,params.name就是扇形名称(如“剧情”),encodeURIComponent处理中文编码。这个技巧让静态图表有了应用逻辑,是PyEcharts进阶用法,但代码只有4行,学生容易掌握。


3. 完整实操流程与核心环节实现

3.1 环境准备与依赖安装:requirements.txt的深意

requirements.txt内容看着普通,但每一行都有讲究:

requests==2.31.0 beautifulsoup4==4.12.2 pymysql==1.1.0 flask==2.2.5 pyecharts==2.0.2 jinja2==3.1.2
  • requests==2.31.0:固定版本是因为豆瓣在2023年升级了TLS协议,旧版requests(<2.28)不支持TLS 1.3,会报SSLError: HTTPSConnectionPool。我试过2.32,但和pyecharts==2.0.2有兼容问题,2.31.0是黄金组合。

  • pymysql==1.1.0:不用mysql-connector-python,因为后者编译安装在Windows上常失败;pymysql纯Python,pip install秒装。版本1.1.0修复了execute()批量插入时的NoneType错误(spider_info.pyexecutemany()会触发)。

  • pyecharts==2.0.2:这是最后一个支持render_embed()的2.x版本。3.x版强制要求render()输出HTML文件,破坏了render_template流。很多教程推荐最新版,但会卡住学生。

安装命令必须用pip install -r requirements.txt --force-reinstall--force-reinstall确保覆盖旧版本。我见过学生用pip install -r requirements.txt,结果机器上已有pyecharts==1.9.0,pip认为满足要求就不装,导致.render_embed()报错。

3.2 MySQL建库与初始化:三步走策略

不要直接mysql -u root -p < tb_film.sql,因为SQL文件里没有CREATE DATABASE语句,且表引擎、字符集可能不匹配。标准流程是:

第一步:创建数据库与用户

CREATE DATABASE douban_film CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'douban_user'@'localhost' IDENTIFIED BY 'douban123'; GRANT ALL PRIVILEGES ON douban_film.* TO 'douban_user'@'localhost'; FLUSH PRIVILEGES;

utf8mb4是必须的——豆瓣片名有emoji(如《寄生虫》海报常带🩸符号),utf8不支持4字节UTF-8字符,会存成??

第二步:导入建表SQL

mysql -u douban_user -p'douban123' douban_film < tb_film.sql mysql -u douban_user -p'douban123' douban_film < tb_movies.sql mysql -u douban_user -p'douban123' douban_film < tb_film_top250.sql

注意:三个SQL文件要按顺序导入,因为tb_movies的外键依赖tb_filmfilm_id(虽然没建外键,但逻辑上如此)。

第三步:修改app.py数据库配置
打开app.py,找到:

db_config = { 'host': 'localhost', 'user': 'douban_user', 'password': 'douban123', 'database': 'douban_film', 'charset': 'utf8mb4' }

password改成你设的密码。切记不要用root用户,这是安全底线。我让学生交作业时,发现32%的人直接写'root', '',一旦服务器暴露,后果严重。

3.3 数据采集全流程:从零开始跑通一次

假设你刚初始化好MySQL,现在要采集TOP250并生成首页图表。按顺序执行:

① 采集TOP250链接

python spider_url.py --top250

--top250参数告诉脚本只爬https://movie.douban.com/top250,生成豆瓣电影TOP250链接.csv(250行)。耗时约40秒。

② 采集TOP250详情

python spider_info.py --input "豆瓣电影TOP250链接.csv" --output "top250_data.csv"

--input指定种子文件,--output指定解析结果CSV(用于调试)。这一步会连MySQL,把250部电影信息INSERT进tb_filmtb_movies。耗时约15分钟(受网络影响)。

③ 更新TOP250榜单快照

python list_data.py --update-top250

list_data.py读取豆瓣电影TOP250链接.csv,解析出排名和ID,写入tb_film_top250。这一步秒级完成。

④ 启动Flask服务

python app.py

终端显示* Running on http://127.0.0.1:5000,浏览器打开http://localhost:5000,首页图表即刻渲染。

踩坑实录:spider_info.py首次运行时,如果MySQL里tb_film为空,INSERT IGNORE INTO tb_film (...) VALUES (...)会因film_id主键冲突失败(因为IGNORE只对重复主键生效,但初始表无数据,冲突发生在其他唯一索引?)。解决方案是在spider_info.py开头加一句:
python cursor.execute("TRUNCATE TABLE tb_film; TRUNCATE TABLE tb_movies;")
清空表再插。这个逻辑后来被我移到app.py/init路由里,供管理员一键重置。

3.4 可视化图表生成原理:以上映时间分布图(showtime.py)为例

showtime.py不是简单画个柱状图,而是实现了动态时间范围选择。首页图表默认显示1920-2029,但用户可在index.html的下拉框选“近10年”、“近20年”,触发AJAX请求/chart?type=showtime&range=10y

showtime.pygenerate_showtime_chart(range_years=10)函数核心是:

# 计算起始年份 end_year = datetime.now().year start_year = end_year - range_years # 查询SQL(参数化防止SQL注入) sql = """ SELECT YEAR(showtime) as year, COUNT(*) as count FROM tb_film WHERE showtime IS NOT NULL AND showtime >= %s AND showtime <= %s GROUP BY YEAR(showtime) ORDER BY year """ cursor.execute(sql, (f"{start_year}-01-01", f"{end_year}-12-31")) data = cursor.fetchall() # 生成Bar图,x轴补全空年份(如2021年无电影,也要显示0) years = list(range(start_year, end_year + 1)) counts = [0] * len(years) for y, c in data: if start_year <= y <= end_year: idx = y - start_year counts[idx] = c bar = Bar() bar.add_xaxis([str(y) for y in years]) bar.add_yaxis("上映数量", counts) # ... 设置样式

关键点有三:一是用%s参数化查询,杜绝SQL注入;二是手动补全年份数组,避免图表出现“断档”(如2015年没电影,x轴直接跳到2016,视觉不连续);三是YEAR(showtime)函数要求showtime字段是DATE类型,所以spider_info.py里存日期时,会把'2023-03-15'转成datetime.date(2023,3,15)再INSERT,而不是存字符串。


4. 常见问题与排查技巧实录

4.1 爬虫常见问题速查表

现象可能原因排查命令/步骤解决方案
spider_url.pyConnectionError: Max retries exceededDNS解析失败或豆瓣域名被污染ping movie.douban.comnslookup movie.douban.com换DNS(如8.8.8.8),或加--timeout 15参数
spider_info.py抓到的评分全是None豆瓣改了<strong class="ll rating_num">结构curl -s "https://movie.douban.com/subject/1292052/" \| grep "rating_num"更新spider_info.pyselect('strong.rating_num')select('strong.ll.rating_num')
MySQL报Incorrect string value: '\xF0\x9F\x92\xA9'表字符集不是utf8mb4SHOW CREATE TABLE tb_film;ALTER TABLE tb_film CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Flask启动报ModuleNotFoundError: No module named 'pyecharts'虚拟环境未激活或pip安装错环境which pythonpip list \| grep pyechartssource venv/bin/activate(Linux/Mac)或venv\Scripts\activate.bat(Win)

4.2 图表不显示的五大原因及修复

PyEcharts图表空白是最高频问题,按发生概率排序:

  1. render_embed()返回的HTML被Jinja2转义{{ chart }}会把<转成&lt;,导致HTML失效。修复:用{{ chart\|safe }}safe过滤器告诉Jinja2这是可信HTML。

  2. 静态资源路径错误pyecharts生成的图表依赖echarts.min.js,默认从CDN加载。如果网络不通(如公司内网屏蔽外网),图表白屏。修复:在app.py里加全局配置:
    python from pyecharts.globals import CurrentConfig CurrentConfig.ONLINE_HOST = "https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/"
    或下载echarts.min.jsstatic/js/,设CurrentConfig.ONLINE_HOST = "/static/js/"

  3. 时间字段为NULL导致图表崩溃timeline_comment.pycomment_count时,若某年份无数据,cursor.fetchall()返回空列表,Line().add_xaxis([])会报错。修复:加空值保护:
    python if not years: years, counts = ['2020'], [0] # 默认显示一年

  4. 中文乱码(图表里显示□□):PyEcharts默认字体不支持中文。修复:全局设置字体:
    python line.set_global_opts( title_opts=opts.TitleOpts(title="评论趋势", subtitle_textstyle_opts=opts.TextStyleOpts(font_family="Microsoft YaHei")), legend_opts=opts.LegendOpts(textstyle_opts=opts.TextStyleOpts(font_family="Microsoft YaHei")), )

  5. LayuiAdmin框架CSS覆盖PyEcharts样式layui.css.layui-card .layui-card-headerpadding: 15px 20px;,挤压图表容器。修复:在genres.html里加自定义CSS:
    ```html

{{ chart|safe }}

```

4.3 性能瓶颈与优化技巧

当数据量超5000部电影,首页加载变慢,根本原因是app.py每次请求都重新查库、重新画图。优化分三级:

  • 一级(立即生效):给MySQL加索引。在tb_filmyearscore字段建索引:
    sql ALTER TABLE tb_film ADD INDEX idx_year (year); ALTER TABLE tb_film ADD INDEX idx_score (score);
    查询速度从1.2秒降到0.03秒。

  • 二级(推荐):用Flask-Caching缓存图表HTML。在app.py顶部:
    python from flask_caching import Cache cache = Cache(app, config={'CACHE_TYPE': 'simple'}) @app.route('/chart') @cache.cached(timeout=300, query_string=True) # 缓存5分钟,参数不同则不同缓存 def render_chart(): ...
    首次访问慢,后续相同参数请求直接返回缓存。

  • 三级(进阶):用Redis做分布式缓存。当部署多实例Flask时,simple缓存失效,需换redis后端。requirements.txtredis==4.6.0,配置改为:
    python cache = Cache(app, config={ 'CACHE_TYPE': 'redis', 'CACHE_REDIS_URL': 'redis://localhost:6379/0' })

最后分享一个小技巧:film_search.py的模糊搜索用LIKE '%keyword%'很慢,换成全文索引:
sql ALTER TABLE tb_film ADD FULLTEXT(title, intro); SELECT * FROM tb_film WHERE MATCH(title, intro) AGAINST('肖申克' IN NATURAL LANGUAGE MODE);
搜索10万条记录,响应时间从800ms降到60ms。


这套豆瓣电影分析系统,我把它比作一辆“教学用自行车”——没有碳纤维车架,但每个螺丝的位置、每根辐条的张力,都经过反复校准。它不追求技术栈的华丽,而专注解决真实问题:怎么让一个刚学完Python基础的学生,在三天内,从零跑通一个有数据、有界面、有图表的完整项目?答案就藏在spider_url.pytime.sleep()里,在app.pyrender_template()里,在showtime.pyYEAR(showtime)里。当你亲手把豆瓣电影TOP250链接.csv拖进spider_info.py,看着终端里一行行INSERT SUCCESS滚动,最后在浏览器里看到那张带着交互的评分趋势图时,那种“我造出来了”的踏实感,是任何框架文档都给不了的。这辆车的终极目的,从来不是载你去多远的地方,而是让你看清每一颗齿轮怎么咬合,然后,自己造一辆更快的。

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

简介:直接可运行的豆瓣电影数据分析系统,自动抓取豆瓣电影TOP250榜单及全站电影链接(spider_url.py),再深度提取影片详情(spider_info.py),清洗后存入MySQL(含tb_film.sql、tb_movies.sql等建表脚本)。Flask后端(app.py)提供统一接口,前端基于layuiAdmin搭建,所有图表均用PyEcharts实时渲染:上映时间分布直方图、评分趋势折线图、评论数时序变化图、电影类型占比饼图、产地分布热力图。配套完整页面:登录页、首页、类型分析页、404页,以及come.png、welcome.png等静态资源。支持按片名模糊搜索(film_search.py),数据导出为CSV,适配课程设计、毕设部署或自学练手,requirements.txt已列明全部依赖。


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

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

Open UI5 源代码解析之1443:Table.js

源代码仓库: https://github.com/SAP/openui5 源代码位置:src\sap.ui.mdc\src\sap\ui\mdc\Table.js Table.js 深度解析:在 openui5 中的定位、机制与工程价值 文件总体定位 Table.js 是 sap.ui.mdc 里的核心控件实现之一,它不是单纯包装一个可见表格,而是承担了一个更…

作者头像 李华
网站建设 2026/6/11 2:53:53

信创生态再进一步!罗盘云与金仓数据库 KingbaseES 完成兼容互认

在国家信创战略加速落地、酒店行业数字化转型深入推进的背景下&#xff0c;核心系统的安全性、稳定性与国产化适配能力成为大型酒店集团和国资平台关注的关键。 近日&#xff0c;罗盘云一体化酒店管理系统已与金仓数据库 KingbaseES 完成兼容互认。KingbaseES 是国内领先的企业…

作者头像 李华
网站建设 2026/6/11 2:53:52

解密AI如何重塑体育分析新范式:从像素到战术洞察的革命性跨越

解密AI如何重塑体育分析新范式&#xff1a;从像素到战术洞察的革命性跨越 【免费下载链接】sports computer vision and sports 项目地址: https://gitcode.com/gh_mirrors/sp/sports 想象一下这样的场景&#xff1a;一场激烈的足球比赛中&#xff0c;教练组需要实时分析…

作者头像 李华
网站建设 2026/6/11 2:48:54

手把手教你用HC-14串口模块和STM32 HAL库,打造一个XBOX风格无线遥控器(附底盘运动学解算代码)

基于STM32与HC-14的XBOX风格无线遥控系统开发全解析在机器人开发领域&#xff0c;稳定可靠的无线控制系统往往是决定项目成败的关键因素之一。本文将深入探讨如何利用STM32微控制器和HC-14串口模块&#xff0c;构建一套完整的XBOX风格无线遥控系统&#xff0c;特别针对三轮全向…

作者头像 李华
网站建设 2026/6/11 2:48:52

从皮肤接触检测到波形生成:详解脉冲理疗仪H桥驱动与反馈电路设计

从皮肤接触检测到波形生成&#xff1a;详解脉冲理疗仪H桥驱动与反馈电路设计医疗电子设备的设计往往需要在功能性与安全性之间寻找精妙的平衡点。脉冲理疗仪作为典型的家用医疗设备&#xff0c;其核心电路设计直接关系到治疗效果与用户安全。本文将深入剖析H桥驱动电路的安全控…

作者头像 李华