ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Python的电影票房爬取与可视化系统:从数据采集到Flask+ECharts展示

基于Python的电影票房爬取与可视化系统:从数据采集到Flask+ECharts展示 如果你正在为数据科学与大数据技术专业的毕业设计或课程设计发愁那基于Python的电影票房爬取与可视化系统这个题目十有八九出现在你的候选清单里。它看起来门槛不高——爬点票房数据画几张图表似乎一周就能搞定但真正动手后你会发现这个项目同时牵扯到HTTP请求、反爬对抗、数据清洗、数据库建模、Web后端、前端图表适配几乎把数据岗位日常的技术栈完整串了一遍。这篇文章我按自己的实操经验从需求拆解、数据源选型、爬虫实现、清洗入库、可视化落地到答辩准备把整个系统的搭建过程完整过一遍。适合正在做这个课题的在校生也适合想用Python快速搭建一个数据采集分析Demo的入门开发者。1. 项目定位拆解从标题到可落地的功能清单1.1 一句话标题背后藏着哪些模块很多同学拿到题目就直接开爬结果爬到一半发现不知道数据存哪、画完图不知道页面怎么串起来最后交付的东西像一堆零散脚本。我建议先花半天时间把标题拆开搞清楚电影票房爬取与可视化系统实际上包含四个可独立验收的模块数据采集模块从公开票房平台定时抓取电影票房数据包括单日票房、累计票房、上映天数、排片场次、上座率等字段数据存储模块把非结构化的网页数据清洗成结构化记录写入数据库保证后续可以被反复查询和分析数据分析模块对票房数据做排序、汇总、同比环比计算为可视化提供符合前端展示口径的数据集可视化展示模块通过Web页面呈现票房排行、走势、占比等信息支持筛选和交互。如果你的课题名称带了大数据技术前缀还要额外考虑数据量的积累问题。单日抓一次数据量很小但如果设计成每天定时抓取、持续运行几个月再叠加历史票房数据就有了数据从少到多、存储和查询方案需要跟着演进这条故事线这在后续论文和答辩里是很有分量的素材。1.2 分层架构与数据流向这个项目我推荐的架构是经典的四层结构数据从源头单向流动层次清晰写文档也好画图采集层Python Requests →存储层MySQL / SQLite →处理层Pandas PyMySQL →展示层Flask ECharts。采集层只负责拿数据不负责业务判断存储层只负责持久化处理层负责把数据库里的原始记录变成前端能直接消费的聚合结果展示层只做数据呈现和用户交互。每一层之间通过明确的接口衔接——采集层产出原始JSON存储层产出表记录处理层产出视图或API响应展示层消费JSON。这个分层的核心价值在于职责单一。如果爬虫代码里混着数据库写入逻辑清洗逻辑又塞在Flask路由函数里前期写起来快后期改一个字段就要动三处代码调试成本非常高。我见过不少同学的系统最终跑不起来问题不在某个环节多难而是层与层之间耦合太紧一个小异常就能拖垮整条链路。1.3 为什么选Python而不换Java或Node这不是情怀问题是效率问题。Python在这类数据采集分析项目里有三个不可替代的优势第一爬虫生态成熟。Requests处理HTTP请求、BeautifulSoup和lxml解析HTML、json处理接口响应这几个库组合起来写一个稳定爬虫的成本极低。用Java写同样功能HttpClient加上Jsoup虽然也能做但代码量至少多一倍。第二数据分析链路原生无缝。爬完数据直接用Pandas做清洗用PyMySQL写回数据库再用Flask提供API全程都是Python对象在流转没有跨语言序列化的麻烦。这也正好卡中数据科学与大数据技术专业的课程体系——Pandas和NumPy本来就是必修内容用在这个项目里属于学以致用。第三可视化方案灵活。ECharts虽然是前端框架但Python生态里有PyECharts库可以在后端直接生成图表配置也能用Flask 原生ECharts做更自由的前后端分离方案。起步门槛低进阶天花板也不低。不过Python也有它的毛病最典型的是全局解释器锁GIL导致多线程爬虫效率有限。但这个项目的数据量级完全不需要上多线程、多进程那套复杂方案单线程加合理延时就能在几分钟内完成一次全量抓取。2. 数据源选型与爬虫核心实现2.1 四个候选数据源的横向对比写爬虫第一步不是写代码是选数据源。数据源选得不对后面所有工作都会在反爬和字段不全这两个坑里反复挣扎。我对当时调研的几个公开数据源做了个对比数据源数据完整度获取难度是否适合学习项目猫眼专业版高字段丰富有反爬接口返回JSON推荐技术含量适中灯塔专业版高更新及时登录门槛高接口加密较强不推荐新手艺恩数据中偏行业报告部分数据需权限可作为辅助数据源Box Office Mojo高全球数据国内访问不稳定英文适合扩展对比分析我最终选的是猫眼专业版的榜单数据核心原因有三个一是它的榜单数据通过JSON接口返回不需要解析复杂的HTML嵌套二是字段齐全片名、类型、上映日期、当日票房、累计票房、排片场次、上座率都有三是公开的票房排行本身就是可被检索的公共信息用于课程设计和技术学习是合适的。这里要特别说一句爬虫项目一定要遵守目标网站的robots协议控制请求频率抓取的数据仅用于学习研究不要用于任何商业用途。这不是套话是每个爬虫开发者必须养成的职业习惯。你的毕业设计论文里也建议写上遵守robots协议、数据仅供学术研究这一条答辩时老师会很在意这个意识。2.2 以票房榜单接口为例的请求与解析选定数据源后打开浏览器开发者工具切到Network面板刷新榜单页面找到返回JSON数据的XHR请求。这类接口通常需要带两个关键请求头User-Agent告诉服务器你是浏览器Referer标识你从哪个页面跳转过来。缺少这两个头服务器很容易直接拒绝服务。下面是一段基础请求代码结构上可以复用到不同平台的榜单接口import requests import time import json def fetch_rank_data(date: str) - list: 抓取指定日期的票房排行数据返回解析后的列表。 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://piaofang.maoyan.com/, } # 接口路径以你在开发者工具里抓到的实际请求为准 url https://piaofang.maoyan.com/rankings/year params {date: date} resp requests.get(url, headersheaders, paramsparams, timeout10) resp.raise_for_status() data resp.json() # 不同的榜单接口返回结构不一样这里以常见的 list 字段为例 items [] for item in data.get(list, []): items.append( { film_name: item.get(movieName), release_date: item.get(releaseInfo), daily_box_office: item.get(boxDesc, ).replace(,, ), cumulative_box_office: item.get(sumBoxDesc, ).replace(,, ), show_count: item.get(showCount), avg_seat_rate: item.get(avgSeatView), } ) time.sleep(1) # 控制请求频率别给服务器造成压力 return items if __name__ __main__: result fetch_rank_data(2025-01-01) with open(raw_data.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)这段代码看起来简单但有几个细节值得注意。time.sleep(1)不是随手写的是给请求之间留出间隔模拟人工浏览节奏resp.raise_for_status()会在HTTP返回4xx、5xx时直接抛出异常避免把错误页面当正常数据解析ensure_asciiFalse保证中文字段以可读形式写入文件而不是变成\uXXXX转义序列。抓完数据先落一份原始JSON文件这一步很多人会跳过直接写数据库。但保留原始数据是数据开发的基本素养——清洗逻辑可能会改数据库表结构可能会调原始数据保留着一份任何时候都能重新跑清洗流程不用重新爬一遍。2.3 反爬应对和请求纪律反爬是这个项目最容易被问为什么的地方。本质上网站反爬和爬虫对抗的根源是服务器无法区分正常用户和自动化脚本。应对思路也从来不是绕过而是让请求看起来更像真实用户。我自己在项目里用到的三个稳妥手段请求头完整化除了User-Agent和Referer补上Accept、Accept-Language等常见头字段模拟浏览器的HTTP特征请求频率控制单次请求后延时1到3秒避免短时间高频访问触发限流。这个延时用随机值比固定值效果更好代码上可以用time.sleep(random.uniform(1, 3))失败重试与日志记录请求失败时不要无脑重试先记录状态码和失败原因间隔5秒后重试最多重试3次。如果触发了更严格的风控服务器返回验证码或直接封禁IP我的建议是停止继续抓取再从协议合规性、请求频率和请求头三个维度逐一自查。很多所谓被封其实只是请求频率太高把延时调大就能缓解。我的原则是学习项目里没有必要去做突破验证码、绕过签名这类对抗性操作一旦你做了项目论文的合规性就讲不清楚了。控制在合理范围内的基础反爬应对已经足够支撑整个毕业设计的技术深度。3. 数据清洗、去重与入库设计3.1 原始数据三大问题脏、乱、重复爬虫拿到的是半结构化数据直接可视化的结果通常惨不忍睹。我在第一次跑通全流程时就遇到过三种典型脏数据第一是单位混用。当日票房这个字段有的返回1.2亿有的返回3500万还有的返回928.4万Pandas排序时按字符串排1.2亿会排在3500万前面完全错误。必须统一转换成数值型的元为单位。第二是字段缺失。新上映电影可能没有累计票房冷门电影可能没有排片数据接口里这些字段直接返回空字符串或null。如果不清洗就入库后续SQL聚合会出现NULL传播图表上则表现为空白或0值分析结论全偏。第三是重复记录。连续几天抓同一部电影的数据如果只按片名去重会误删历史记录如果不去重同一部电影每天的数据会写成多行。去重逻辑必须建立在片名 统计日期这个业务键上。3.2 Pandas清洗流程与单位转换用Pandas做清洗是最顺手的方案流程清晰每一步都能看到中间结果。核心代码如下import pandas as pd def convert_to_number(value) - float: 把1.2亿、3500万这类字符串统一转成以元为单位的数值。 if value is None or value : return 0.0 value str(value).strip() if 亿 in value: return float(value.replace(亿, )) * 100_000_000 if 万 in value: return float(value.replace(万, )) * 10_000 return float(value) def clean_data(raw_path: str, output_path: str) - pd.DataFrame: df pd.read_json(raw_path) # 1. 日期标准化 df[stat_date] pd.to_datetime(df[stat_date]) # 2. 票房字段统一转数值 df[daily_box_office] df[daily_box_office].apply(convert_to_number) df[cumulative_box_office] df[cumulative_box_office].apply(convert_to_number) # 3. 缺失值填充 df[show_count] df[show_count].fillna(0) # 4. 按业务键去重保留最近一次抓取结果 df df.drop_duplicates(subset[film_name, stat_date], keeplast) # 5. 过滤异常数据 df df[df[daily_box_office] 0] df.to_csv(output_path, indexFalse, encodingutf-8-sig) return df这里有个容易忽略的细节文件输出用utf-8-sig编码而不是utf-8。前者会在文件开头写入BOM头让Excel直接打开CSV时中文不乱码这个经验在答辩演示Windows环境时很管用。单位转换为什么要统一到元而不是保留万因为后续可视化要做排序和堆叠数值型数据放图表y轴才能正确计算刻度保留字符串单位只会让前端图表拿到一坨没法加工的文本。3.3 表结构设计和增量更新思路清洗后的数据要落库。数据库选MySQL还是SQLite取决于你机器的环境。如果你是第一次做完整项目我建议先用SQLite零配置、单文件、Python标准库就支持如果课题明确要求大数据存储方案或生产环境模拟再上MySQL写法和SQL语句几乎一致差别主要在连接驱动。核心表设计如下CREATE TABLE movie_daily_box ( id INTEGER PRIMARY KEY AUTOINCREMENT, film_name TEXT NOT NULL, stat_date DATE NOT NULL, release_date DATE, daily_box_office REAL DEFAULT 0, cumulative_box_office REAL DEFAULT 0, show_count INTEGER DEFAULT 0, avg_seat_rate REAL, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE (film_name, stat_date) );这张表的主键是自增id但业务唯一键是(film_name, stat_date)。唯一约束保证了同一部电影同一天的记录不会重复配合INSERT OR REPLACE或INSERT ... ON DUPLICATE KEY UPDATE就能实现有则更新、无则插入的增量写入。每天抓取的时间也记录在表里crawl_time字段是给自己留的追溯依据。如果你发现某天的数据源接口异常导致数值不对可以通过crawl_time定位是哪一次抓取写进去的直接修正或删除重爬。4. 可视化系统的实现路径Flask ECharts4.1 后端接口怎么把数据交给前端可视化部分我采用Flask提供API、前端用ECharts渲染的方案。这个组合的好处是前后端职责清晰爬虫和数据处理都是Python后端接口也用Python整条链路语言统一。先设计接口的返回格式这一步直接决定前端图表好不好写。我强烈建议所有接口统一返回{ code: 0, data: [...] }结构前端统一做错误处理而不是每个接口返回不同风格的JSON。接口设计如下from flask import Flask, jsonify from flask_cors import CORS import pandas as pd app Flask(__name__) CORS(app) DB_PATH box_office.db def query_df(sql: str) - pd.DataFrame: import sqlite3 conn sqlite3.connect(DB_PATH) df pd.read_sql_query(sql, conn) conn.close() return df app.route(/api/box_office/top10) def top10(): df query_df( SELECT film_name, SUM(daily_box_office) AS total_box FROM movie_daily_box GROUP BY film_name ORDER BY total_box DESC LIMIT 10 ) return jsonify( { code: 0, data: df.to_dict(orientrecords), } ) app.route(/api/box_office/trend) def trend(): df query_df( SELECT stat_date, SUM(daily_box_office) AS daily_total FROM movie_daily_box WHERE stat_date date(now, -30 day) GROUP BY stat_date ORDER BY stat_date ) return jsonify( { code: 0, data: df.to_dict(orientrecords), } ) if __name__ __main__: app.run(debugTrue, port5000)这里用Pandas读取SQL查询结果再转成records字典省去了手动遍历游标组装JSON的麻烦代码量少一半。对毕业设计这个数据量级性能完全够用如果数据量上了百万行后续可以换成直接写原生SQL聚合、前端分页加载这是天然的论文优化方向。4.2 不同图表的选型逻辑可视化不是把数据丢进图表就完事每种图表都应该回答一个具体问题。我在做页面布局时对图表类型和业务问题的对应关系做了如下规划图表类型回答的问题对应数据柱状图哪部电影票房最高票房Top10排行折线图大盘票房随日期的走势如何近30天全市场每日总票房饼图不同类型电影的票房占比按影片类型汇总票房散点图上映天数与票房是否有相关性每部电影上映天数与累计票房以票房Top10柱状图为例ECharts接收后端返回的数据后核心配置如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 title电影票房可视化系统/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idtop10_chart stylewidth: 100%; height: 400px;/div script const chart echarts.init(document.getElementById(top10_chart)); fetch(/api/box_office/top10) .then(res res.json()) .then(res { const data res.data; chart.setOption({ title: { text: 票房Top10, left: center }, tooltip: {}, xAxis: { type: category, data: data.map(item item.film_name), axisLabel: { rotate: 30 } }, yAxis: { type: value, name: 票房(元) }, series: [{ type: bar, data: data.map(item item.total_box), itemStyle: { color: function(params) { // 第一名用醒目颜色弱化后续排名 return params.dataIndex 0 ? #d4380d : #91caff; } } }] }); }) .catch(err console.error(err)); /script /body /html注意axisLabel.rotate: 30这个配置电影名通常较长不旋转会互相遮挡这是很常见的显示细节。另外图表标题一定要写清楚答辩时老师扫一眼页面就知道这张图在表达什么而不需要你在旁边解释半天。4.3 从静态图到系统筛选、联动与定时刷新如果只是几个静态图表拼在一起这个项目的系统感是不够的。我建议至少补三个功能工作量不大但对完成度提升明显日期筛选页面顶部放一个日期选择器切换后所有图表联动刷新。实现上就是前端监听日期变化重新请求带日期参数的接口后端SQL加WHERE stat_date ?条件类型筛选下拉框按电影类型动作、喜剧、动画等过滤饼图和柱状图让不同维度的分析能自由组合定时数据刷新用APScheduler在每天固定时间触发一次爬虫任务让系统真正实现自动采集 → 自动入库 → 手动刷新页面查看最新数据的闭环。from apscheduler.schedulers.background import BackgroundScheduler def start_scheduler(): scheduler BackgroundScheduler() scheduler.add_job( funcdaily_crawl_job, triggercron, hour9, minute30, iddaily_box_office_crawl, ) scheduler.start()daily_crawl_job函数里就是把前面写的fetch_rank_data、清洗、入库三个步骤串起来。加了这个定时任务后你的系统就具备了持续运行、自动产出的数据管道特征论文里可以顺势引出轻量级数据管道设计这个技术点比单纯爬虫可视化的立意高一个层级。5. 实操中真正踩过的坑与完整排查过程5.1 数字乱码字体反爬的排查链路我第一次抓票房数字时页面显示正常的1200.5万可拿到的源码里却是一串像臤、翂这样的乱码字符。初看以为是编码问题换了各种charset都一样折腾了一个多小时。后来我打开开发者工具在Elements面板里逐个对比渲染后的数字和源码文本发现页面上通过CSS样式动态映射了字体文件真实数字被替换成了自定义字体里的字符直接抓HTML文本自然拿到的是乱码。这就是典型的字体反爬。完整的排查链路是这样的先确认不是编码问题对比多个页面和接口返回再检查字体文件Network面板过滤字体资源看到woff/ttf文件最后下载字体文件解析字符映射关系。不过如果你用JSON接口而不是HTML页面绕开这个过程就简单得多——这也是我建议优先找接口的原因反爬手段一般集中在HTML页面层后端数据接口反爬成本高相对干净。对于必须在网页层解析的场景可以用fontTools库解析字体文件建立字符到真实数字的映射表替换后再取值。这一步如果你的项目没遇到可以在论文里作为难点与解决方案讨论属于加分内容。5.2 请求被拒频率限制与断点续爬跑全量历史数据时我遇到过跑二十多条请求后突然连续返回403的情况。刚开始以为是IP被封了心里一凉后来冷静下来查日志发现状态码是403且响应体内容很短带有明显的限流特征。排查过程分三步走先把请求头补全对比浏览器真实请求的Header缺什么补什么再把请求间隔从固定0.5秒改成1到3秒随机值最后加了失败自动重试和断点续爬机制。断点续爬的意思是每次抓取前先查数据库里已有的stat_date跳过已抓取过的日期只补缺的日期。def get_missing_dates(start_date, end_date): import sqlite3 conn sqlite3.connect(DB_PATH) cursor conn.execute( SELECT DISTINCT stat_date FROM movie_daily_box WHERE stat_date BETWEEN ? AND ?, (start_date, end_date), ) existed {row[0] for row in cursor.fetchall()} conn.close() all_dates pd.date_range(start_date, end_date).strftime(%Y-%m-%d).tolist() return [d for d in all_dates if d not in existed]这个函数的价值在于断点续爬让整个采集任务变成可恢复的而不是要么成功要么重来。这套查缺补漏的思路在任何数据同步场景里都是通用技能写进简历不丢人。5.3 图表中文显示方块的踩坑经历项目在本地Windows开发时一切正常部署到实验室的Ubuntu服务器后ECharts图表里的中文全变成了方框。我当时第一反应是前端编码问题排查半天无解最后才发现是服务器系统缺少中文字体。ECharts图表内的文字渲染依赖浏览器所在的系统字体库服务器没有中文字体时图形上的文字就会显示为豆腐块。解决办法也很朴素给服务器安装中文字体或者在前端配置里显式指定fontFamily: Microsoft YaHei, PingFang SC, sans-serif让浏览器优先使用常见中文字体缺失时退回系统默认。这个坑提醒我一个经验系统开发不能只在我的电脑上能跑就完事环境差异是最容易忽视的问题。答辩演示如果临时换设备图表中文乱码会让你当场陷入被动提前在代码层面把字体配置写清楚能省掉很多临场麻烦。6. 论文撰写与答辩准备的实战建议6.1 论文结构怎么搭最稳妥毕业设计论文的评审逻辑是流程清晰、工作量饱满、有结论有验证。我建议把论文结构按下面的顺序安排每一章对应你系统的一个可验证模块摘要一句话说清系统目标一句话说清技术方案几句话说明实现效果和结果绪论背景与意义票房数据对电影行业决策的价值、国内外研究现状、本文主要工作相关技术介绍Python、Requests、Pandas、Flask、ECharts、MySQL每个技术写作用和在系统里承担的角色需求分析从用户角色出发列功能需求和非功能需求数据准确性、采集稳定性、系统响应时间系统设计总体架构图、功能模块划分、数据库ER图与表结构说明系统实现每个模块的核心代码、实现思路、关键页面截图系统测试功能测试用例表格、爬虫稳定性测试、数据准确性抽样对比总结与展望项目完成情况、不足、后续优化方向。系统测试这一章经常被学生忽略但它恰恰是最好凑工作量而且能体现工程素养的部分。你可以做一张测试用例表列出输入日期、预期返回数据条数、实际返回条数、断言结果再把爬虫连续运行7天的稳定性数据放进去这就是很有说服力的成果展示。6.2 高频答辩问题与应答思路答辩时老师的问题通常围绕为什么这么选数据怎么保证对系统有什么难点三个方向展开。我梳理了几个高频问题及应答思路给你做参考问题一为什么选这个数据源数据准确吗应答要点说明数据源是行业内公认的票房统计平台数据来自公开榜单同时强调在系统测试阶段做了抽样核对——随机选5部电影把爬取结果和网站页面人工核对准确率100%。这个回答把技术方案和数据验证都覆盖了。问题二你的系统大数据体现在哪里这是最容易翻车的问题。诚实的回答是目前数据量不大但系统架构上已经为数据增长做了准备——每天定时采集、增量入库、按日期聚合查询数据持续积累三个月后表里会有几千条核心票房记录和数万条明细可以支撑趋势分析和档期对比。再把数据管道的可持续运行能力作为重点讲比硬吹大而全更有说服力。问题三系统的创新点是什么诚实优先不要夸大。可以说一是实现了从数据采集、清洗、存储到可视化的全链路自动化定时调度让系统可持续运行二是在数据清洗环节处理了亿/万单位统一和字体反爬等实际问题三是前端图表与后端查询联动支持按日期、类型多维度筛选。把每个模块里你真正解决过的实际问题挖出来讲就是最有说服力的创新点。写到这里这套系统的关键路径已经完整走了一遍。如果你按这个路线图推进每天投入三到四个小时两周左右能把核心链路全部跑通。最后再分享一个我个人的小习惯每完成一个模块立刻截图、记录踩坑过程这些素材到最后写论文、做答辩PPT时全是现成的弹药库不用临时回忆赶工。祝你的毕设顺利收场。
返回列表