简介:这是一份面向Python毕业设计、期末大作业及课程设计场景的完整项目源码,基于Python与Flask框架实现豆瓣电影数据的爬取、存储、分析与可视化,适合具备一定Python基础、希望快速搭建Web数据应用的学生参考。压缩包仅6.25MB,共110个文件,核心包含5个Python脚本、前端HTML/CSS/JS文件以及Bootstrap、AOS等可视化组件,另有数据库db与xls文件,可直接支撑从爬虫采集到页面展示的完整链路。项目中已配置好运行环境,源码经过本地编译可运行,评审分达98分,难度适中,并经过助教审定,可放心用于项目演示、二次开发或学习Flask+爬虫的实战流程。目前已有241人学习下载,文件类型覆盖py、html、css、js、db、xls等,目录结构清晰,方便检索与复用。
1. 基于Python+flask的豆瓣电影爬虫可视化系统:毕业设计为什么选它
基于Python+flask的豆瓣电影爬虫采集与分析可视化系统,简单说就是一条从豆瓣网页抓电影数据,存进数据库,再用flask提供接口,前端用ECharts把评分、类型、年份做成可视化大屏的完整链路。适合毕业设计、课程项目,也适合想快速拥有一套“数据采集→分析→展示”全流程作品的人。它不涉及复杂的分布式爬虫,也不用微服务,一个人用半天就能跑通骨架,但足以覆盖数据获取、清洗、存储、接口、可视化这些最容易被答辩老师追问的考点。下面我按实际开发顺序,把它拆成能直接照做的六个部分。
2. 先把地基打牢:爬虫采集层的选型与豆瓣反爬边界
2.1 requests + xpath 还是 Scrapy?毕设规模下的选型理由
常见的选择是requests加lxml的xpath,而不是Scrapy。理由有三:第一,毕设规模通常只需要采集一千到一万条电影数据,requests单线程加time.sleep已经够用,Scrapy的下载中间件、管道、selector这些概念学习成本高,答辩时容易把自己绕进去。第二,requests配合lxml,调试非常直接,用浏览器控制台复制xpath,写出来就能跑;Scrapy的xpath跑在Twisted异步环境里,报错定位相对绕。第三,后续要交到flask里做分析,requests采集的数据直接转pandas DataFrame最顺手,Scrapy的item dict还要多一层转换。我一般会先问自己一个问题:这个项目的数据量需要分布式爬虫吗?不需要。那就别为了“看起来很厉害”而引入Scrapy。当然,如果老师明确要求爬虫框架,Scrapy也能用,但下面的核心思路完全一致。
结论:requests + lxml + xpath 是这个场景里最可靠的最小方案。用到的库是 requests、lxml、time、random、sqlite3 或 pandas。若以后要改成Scrapy,代码结构也不冲突,把解析部分抽出来就行。
2.2 豆瓣电影列表页与详情页的最小可爬方案
豆瓣本身有公开的榜单页,比如豆瓣电影TOP250,这个页面结构稳定,适合练手。常见做法是先抓列表页拿电影详情页链接,再进入详情页抓评分、导演、演员、类型、简介。第一步是请求列表页。
import requests from lxml import etree import time import random 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", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://movie.douban.com/top250" } def fetch_page(url, retry=3): for i in range(retry): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text elif resp.status_code == 403: print("403, 触发反爬,等待后重试", url) time.sleep(random.uniform(5, 8)) except Exception as e: print("请求异常", e) time.sleep(2) return None这段代码有三个关键点。第一,headers 里的 User-Agent 必须是完整的浏览器标识,很多 403 都是因为默认 UA 是 python-requests;Accept-Language 也尽量带上,豆瓣会参考它做内容协商。第二,重试逻辑里遇到 403 不要立刻重试,先 sleep 5-8 秒,这是最粗粒度的反爬规避,本质是模拟人的访问节奏。第三,timeout 必须设置,否则某个请求卡死会让整个采集脚本停在那里,这是血泪经验。
拿到列表页 HTML 后,用 xpath 解析出每个电影的详情链接和标题。这里有个高频坑:豆瓣的 HTML 结构里,标题在<span class="title">里,但英文名/副标题也在同类节点,需要用 text() 来过滤。
def parse_list(html): tree = etree.HTML(html) items = tree.xpath('//div[@class="hd"]') movie_links = [] for item in items: a = item.xpath('.//a/@href') title = item.xpath('.//span[@class="title"]/text()') if a and title: # 只取第一个 title,避免中文名后面的空格和副标题混进来 clean_title = title[0].strip() movie_links.append({"title": clean_title, "url": a[0]}) return movie_links这里用到了 xpath 的 text() 函数。注意.//a/@href拿的是链接列表,.//span[@class="title"]/text()拿到的可能是['肖申克的救赎', ' / The Shawshank Redemption']这样两个值,直接取第一个就是中文名。网上很多教程把text()写成text,结果拿不到文本,这是 python xpath 最常见的一个低级坑,报错不会明显,只会返回空列表。
进入详情页后的解析,核心是取评分、评价人数、类型、导演、年份等字段。详情页 url 形如https://movie.douban.com/subject/1292052/。
def parse_detail(html, movie): tree = etree.HTML(html) # 评分 rating = tree.xpath('//*[@property="v:average"]/text()') votes = tree.xpath('//*[@property="v:votes"]/text()') # 类型 genres = tree.xpath('//*[@property="v:genre"]/text()') # 年份 year = tree.xpath('//*[@class="year"]/text()') movie["rating"] = float(rating[0]) if rating else None movie["votes"] = int(votes[0]) if votes else None movie["genres"] = [g.strip() for g in genres] if genres else [] movie["year"] = year[0].strip("()") if year else None return movie解析时优先用豆瓣微数据标记的属性选择器,比如property="v:average"。这类标记是给搜索引擎或浏览器扩展用的,结构相对稳定。反过来,很多教程用/html/body/...这种绝对路径,一旦页面改版就整体翻车,不推荐。年份节点class="year"返回的是(1994)带括号,记得 strip 掉。
2.3 数据字段设计:为后面的分析和可视化预留空间
采集只是第一步,数据字段直接决定后面的可视化能做多深。我建议至少预留这些字段:电影id(详情页url里的数字)、标题、年份、评分、评价人数、导演、演员(前三位)、类型、地区、语言、简介、封面url、采集时间。这些字段能支撑绝大多数分析:按评分的直方图、按年份的评分趋势、按类型的数量占比、按评价人数的TOP榜、演员和导演的词云。
存储上,最省事的是先用列表存 dict,再转 pandas.DataFrame,导出成 CSV 做备份,同时写入 SQLite 供 Flask 查询。不要一上来就上 MySQL,毕设答辩时老师问“为什么用 SQLite”,你可以答:单机数据量在十万条以内,SQLite 零配置、文件可拷贝、支持 SQL,对原型系统足够。如果老师偏好 MySQL,代码改成 pymysql 也不复杂。
import pandas as pd import sqlite3 def save_to_db(movies): df = pd.DataFrame(movies) df.to_csv("douban_movies.csv", index=False, encoding="utf-8-sig") df.to_sql("movies", sqlite3.connect("douban.db"), if_exists="replace", index=False)注意这里用encoding="utf-8-sig"导出 CSV,是为了避免 Excel 打开乱码;写入 SQLite 用的是to_sql,pandas 会自动建表,字段类型也会自动推断。但这种简单的整表替换策略只适合全量重采,后面要增量更新时,需要给 movies 表带一个 movie_id 唯一键,并用INSERT OR REPLACE或先查再插的方式处理。
到这里,爬虫层已经能跑通。下一步是把这些数据通过 Flask 开放成接口,让前端的可视化图表有数据可依。
3. Flask 做数据服务层:把爬虫结果变成接口
3.1 Flask 应用骨架与蓝图划分
Flask 相比 FastAPI 在这个项目里的优势是零额外概念、模板渲染顺手,和前端同端口部署最简单。FastAPI 自带 OpenAPI 文档固然好,但毕设项目里 Flask 的render_template+ 路由 + JSON接口 的组合已经足够,答辩时不会因为“用了新框架”加分,反而可能因为不熟悉异步和类型标注被问倒。所以我的选择是 Flask。
常见的项目结构是把爬虫、数据访问、接口、模板分开。目录可以是这样:
douban_analysis/ ├── app.py # Flask 入口 ├── spider/ │ ├── douban.py # 爬虫采集逻辑 │ └── parser.py # xpath 解析 ├── data/ │ ├── douban.db # SQLite 数据库 │ └── douban_movies.csv ├── stats/ │ └── analyzer.py # 数据分析聚合 └── templates/ └── index.html # 可视化页面app.py 里创建 Flask 实例,再把爬虫、接口、页面路由分别注册成蓝图。蓝图的好处是代码不堆在一起,后续加接口不用动主文件。下面的代码是入口的最小骨架。
from flask import Flask, render_template, jsonify from spider.douban import run_spider from stats.analyzer import get_rating_stats, get_genre_stats app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/api/summary") def summary(): return jsonify(get_rating_stats()) @app.route("/api/genres") def genres(): return jsonify(get_genre_stats()) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)这里面有几个关键决策。debug=False是必须的,尤其当项目要部署给老师演示时,debug 模式会把源码暴露在错误页,安全上很不好看。host="0.0.0.0"是为了能通过局域网 IP 在别的电脑上演示,不用每次把电脑搬到答辩现场。jsonify返回 JSON 时,默认的ensure_ascii在 Flask 里是 True,所以中文会变成\uXXXX转义,浏览器解析没问题,但调试时不直观,可以在 app 配置里加一句app.config["JSON_AS_ASCII"] = False。
3.2 用 SQLite 还是 MySQL?可视化系统的存储取舍
如果你只是做可视化展示,SQLite 完全够用。但前提是你得处理好并发写和路径问题。SQLite 在 Flask 默认线程模式下,多个请求同时读没问题,写的时候会锁库。不过我们这个系统只有采集脚本会写,浏览器端只读,所以冲突概率很小。
真正的坑是 SQLite 的路径。很多新手直接把数据库文件放在项目根目录,然后 Flask 里写sqlite3.connect("douban.db")。如果启动 Flask 的工作目录不同,会找不到文件。我一般会在app.py里用绝对路径:
import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "data", "douban.db") def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn设置row_factory = sqlite3.Row之后,查询结果可以像字典一样取值,直接扔给 jsonify 也不用手动转 dict。这个细节能省掉大量序列化代码。
如果老师问“为什么不选 MySQL”,你可以说:这是单机原型,MySQL 的安装、账号、端口配置对学生演示环境不友好;SQLite 支持完整的增删改查和聚合查询,迁移到 MySQL 时只需把sqlite3.connect换成pymysql.connect,SQL 本身几乎不用改。顺便提一句,把 SQL 写成标准 ANSI 风格,才能在迁移时少踩坑,比如不要用INSERT OR REPLACE,改用ON CONFLICT或先查后插。
3.3 提供 JSON 接口:按评分、年份、类型聚合查询
可视化页面需要的数据不是原始电影列表,而是聚合结果。比如评分分布的直方图、年份与评分的关系、类型的占比。这些用 SQL 的 GROUP BY 就能算出来,不要把数据全 pull 到 Python 里再循环统计,那样慢且显得不专业。
下面这个查询是按十分制评分取整后统计数量,用来画评分分布图。
SELECT CAST(rating AS INT) AS bucket, COUNT(*) AS cnt FROM movies WHERE rating IS NOT NULL GROUP BY bucket ORDER BY bucket;在 analyzer.py 里,用 pandas 读 SQL 再转成图表需要的 JSON 结构。pandas 的优势是分组聚合代码短,且能顺手处理空值。
import pandas as pd def get_rating_distribution(): df = pd.read_sql_query(""" SELECT CAST(rating AS INT) AS bucket, COUNT(*) AS cnt FROM movies WHERE rating IS NOT NULL GROUP BY bucket ORDER BY bucket """, DB_PATH) return { "buckets": df["bucket"].tolist(), "counts": df["cnt"].tolist() }接口返回给前端的数据结构很关键。我建议统一返回{ "buckets": [...], "counts": [...] }这种平行数组结构,ECharts 的 xAxis 和 series 可以直接映射,不需要前端做二次处理。另一个常用接口是电影TOP榜单,取评分最高且评价人数不少于某个阈值的电影,避免只有 3 个人评分的小众片霸榜。这个阈值建议放在查询参数里,比如/api/top?limit=20&min_votes=5000,答辩时演示参数改动很方便。
类型占比的查询稍微复杂,因为一部电影可能有多个类型,存在genres字段里存的是[剧情, 爱情]这种以逗号分隔的文本。SQLite 里没有 split 函数,我会在导入时将每条电影的每个类型展开成一行存入movie_genres表,再在 analyzer 里查询。这样避免了前端和服务端做字符串切分,而且后面做类型筛选也很方便。下面这一段是我常用的建表写入逻辑:
genre_rows = [] for m in movies: for g in m["genres"]: genre_rows.append({"movie_id": m["id"], "genre": g}) pd.DataFrame(genre_rows).to_sql("movie_genres", conn, if_exists="replace", index=False)到这里,后端数据链路已经完整。下一步就把这些 JSON 接到前端可视化页面上。
4. 可视化大屏与图表:ECharts 接入 Flask 模板
4.1 前端框架选择:原生模板 + ECharts 够用
可视化部分不需要上 Vue、React,Flask 自带的 Jinja2 模板加 ECharts 的 CDN 就够了。原因有两点:一是毕设页面只有一两个,组件之间没有复杂状态共享,用 Vue 反而要处理构建工具;二是 ECharts 是纯前端图表库,数据通过 fetch 拿 JSON 填进去,和 Flask 模板天然配合。百度可视化大屏的很多风格其实用 ECharts 就能实现,背景渐变、大标题、多图表栅格布局,这些和框架无关。
如果离线演示,最好把 echarts.min.js 下载到static/js/目录,避免答辩现场没有外网。CDN 在公网演示没问题,但在学校机房局域网里容易出幺蛾子,这是个翻车高发点。下面是一个模板骨架示例。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>豆瓣电影数据分析可视化</title> <script src="{{ url_for('static', filename='js/echarts.min.js') }}"></script> <style> body { background: #0f1423; margin: 0; } .chart { width: 100%; height: 400px; } </style> </head> <body> <div id="ratingChart" class="chart"></div> <div id="genreChart" class="chart"></div> <div id="topChart" class="chart"></div> </body> </html>下面接 JavaScript 部分,注意要用fetch('/api/...')而不是直接url_for写死地址,否则改端口又得改前端。
async function loadRatingChart() { const res = await fetch('/api/rating_distribution'); const data = await res.json(); const chart = echarts.init(document.getElementById('ratingChart')); chart.setOption({ title: { text: '评分分布' }, tooltip: {}, xAxis: { type: 'category', data: data.buckets }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.counts }] }); } loadRatingChart();这里的 data 来自 Flask 接口,平行数组直接映射。注意echarts.init必须在 DOM 元素已经渲染之后调用,如果脚本放在 head 里又没有等 DOMContentLoaded,图表容器宽度会是 0,出现“白屏”的假象。常见的做法是把<script>放到 body 底部,或者用window.onload包起来。
4.2 豆瓣电影评分分布、TOP 榜单与类型占比三个核心图表
评分分布用柱状图,能快速看出豆瓣电影常见的“高分集中在中上区间”形态。TOP 榜单用横向条形图,便于展示片名和评分。类型占比用饼图或玫瑰图,展示剧情、喜剧、动作等类型的数量权重。这三个图表正好覆盖了数据分布、排序和组成三种分析视角。
TOP 榜接口在 Flask 里可以这样写:
@app.route("/api/top") def top_movies(): limit = int(request.args.get("limit", 20)) min_votes = int(request.args.get("min_votes", 5000)) df = pd.read_sql_query(""" SELECT title, rating, votes FROM movies WHERE votes >= ? ORDER BY rating DESC, votes DESC LIMIT ? """, DB_PATH, params=(min_votes, limit)) return jsonify(df.to_dict(orient="records"))注意pd.read_sql_query支持params元组传参,能有效防止 SQL 注入,比直接字符串拼接稳妥。当然这里查询参数是整数,风险低,但习惯要养好。返回orient="records"得到的是一个列表套字典,前端series.data可以直接用data.map(item => item.title)取出来。这种“接口返回什么、前端怎么消费”的模式,尽量在开发时固定下来,后面的功能堆叠会快很多。
玫瑰图展示类型占比时,要注意类型字段是展开后的多条记录,饼图直接按 genre 分组计数即可。但如果一个电影有三个类型,它在饼图里会算三次,这本身是合理的——表达的是“所有类型标签的数量分布”,而不是“电影数量分布”。答辩时如果有人问,解释清楚这个口径就行,不说反而被当成 bug。
4.3 定时采集与增量更新:让数据自己“长”出来
可视化系统的演示效果很大程度上依赖数据量。只采集 250 部电影,图表形状很单薄;采集 2000 部,评分分布才有层次。常见做法是写一个循环,分页遍历多个榜单或按分类标签采集。为了控制请求频率,建议每秒钟最多一个请求,爬取 2000 部大概需要 40 到 50 分钟,这个时长可以接受。不要把间隔设成 0.1 秒,否则大概率采到一半就 403。
增量更新的思路很简单:每次采集前,先从数据库里查已有 movie_id 集合;详情页抓回来后,如果 movie_id 已存在,就跳过。这样可以反复运行脚本而不会重复插入。下面是一个带增量控制的采集函数:
def collect_movies(max_pages=10): conn = sqlite3.connect(DB_PATH) existing = set(pd.read_sql_query("SELECT movie_id FROM movies", conn)["movie_id"]) for page in range(max_pages): list_url = f"https://movie.douban.com/top250?start={page * 25}" html = fetch_page(list_url) if not html: continue for link in parse_list(html): movie_id = link["url"].split("/")[-2] if movie_id in existing: continue detail_html = fetch_page(link["url"]) movie = parse_detail(detail_html, link) movie["movie_id"] = movie_id save_one(conn, movie) existing.add(movie_id) time.sleep(random.uniform(1.2, 2.5))这里有个细节:movie_id从 URL 最后一段取,比如1292052,这是字符串,不要转成 int,防止前导零问题(虽然豆瓣目前是纯数字)。save_one用INSERT OR IGNORE配合 movie_id 唯一索引,实现天然去重。随机睡眠 1.2 到 2.5 秒比固定 2 秒更接近人的行为,从结果上看不容易被识别成脚本,这是很多爬虫工具箱背后的通用思路,不是玄学,是统计特征。
数据量到位后,可视化才开始有价值。下面进入整篇最容易出问题的一章,也是我做过很多次毕设辅导后总结的一手排错经验。
5. 避坑手册:豆瓣爬虫与可视化系统的常见问题排查
5.1 403 与封 IP:User-Agent、Cookie 与请求间隔的玄学
现象:爬了几十页后突然连续返回 403,或者脚本停下来报HTTP Error 403。原因:短时间内请求频率过高,触发了豆瓣的风控;也可能是没有带完整 Headers,被识别为机器人。
解决:第一步先检查 headers,是否包含完整的 User-Agent、Accept、Accept-Language、Referer。第二步把请求间隔调大,从 1 秒调到 3 秒以上。第三步如果仍然 403,检查是不是需要登录态的 Cookie——有些榜单内容对未登录用户可见,但如果触发风控,带上从浏览器复制的 Cookie 往往能立刻恢复。我在本地跑通时,通常用浏览器登录豆瓣后,从开发者工具里复制 Cookie 字符串放进 headers。注意 Cookie 会过期,演示前需要重新复制,这个做法只用于本地学习,不用于大规模采集。
很多人把 403 当成“封 IP”的最终判决,其实大部分 403 都是请求头不规范或频率问题。如果你用电信/联通的家用宽带,一般没有固定 IP 封禁;校园网出口 IP 是共享的,更容易被误伤,所以调低频率比换 IP 更有效。另外,不要真的去研究 IP 代理池,那是分布式爬虫和生产级系统的范畴,毕业论文里写“本项目使用单机采集,尊重目标网站访问频率”,比堆砌高风险技术反而更稳妥。
5.2 榜单结构改版导致 xpath 失效
现象:昨天还能跑通的解析代码,今天parse_list返回空列表,或者parse_detail里 rating[0] 报 IndexError。
原因:目标网站的 HTML 结构调整了,可能是 class 名变了、标签层级变了,也可能是页面从静态渲染改成了前端动态加载。豆瓣电影列表页总体稳定,但详情页偶尔会调整某些属性。
解决:不要死守原来的 xpath,第一时间去浏览器里打开对应页面,右键“检查”看实际 DOM。常用的排查方法是在代码里打印 HTML 片段,确认拿到的内容是不是包含目标字段。我经常用etree.tostring(tree, encoding="unicode")打印前 2000 个字符,看 class 名还在不在。改 xpath 时,优先依赖@id、@property、@class这类稳定属性,避免使用//div[1]/div[2]/div[3]这种位置路径。改完记得把列表页和详情页各跑一次,别只用一个页面验证。
这里补充一个经验:豆瓣的列表页偶尔会出现审核未通过或已删除的条目,导致详情页 404。解析详情页时,先判断status_code == 200,再解析;如果 404 就跳过并记录日志。很多新手解析到一半崩溃就是因为没有处理 404 响应。
5.3 Flask 接口返回慢:N+1 查询与 JSON 序列化
现象:页面加载要 2 秒以上,打开接口直接看也是慢吞吞,但数据库里数据才几千条。
原因:常见的是两种。一是接口里循环查库,比如在循环里执行一条 SQL 查询每部电影的演员,几千条电影就几千次查询,SQLite 每次查询都有开销,这就是 N+1 问题。二是jsonify序列化大量嵌套字典时,Python 本身的 json 库处理慢,但这只在数据量特别大时才明显。
解决:把循环内的查询合并成一条 SQL,用IN条件或 JOIN 一次性取出来。另外,接口返回给前端的数据尽量精简,只返回图表需要的字段,不要把整条电影记录(包括简介)都塞进去。我在top接口里只返回 id、title、rating、votes,简介前端用不到就不返回。把 SQLite 查询改成 pandas 批量读取可以减少约一半时间。还有一个容易被忽略的点:Flask 开发服务器的debug=True会显著降低响应速度,正式演示时务必debug=False。
5.4 前端图表空白:数据格式与 ECharts 的坑
现象:接口返回正常,浏览器 network 里能看到 JSON,但图表区域空白,或者显示异常。
原因:常见三个。一是echarts.init时容器不可见或宽度为 0,比如表格在弹窗里、Tab 页里,初始化发生在元素隐藏时。二是数据格式不匹配,xAxis 需要字符串数组,你传的是对象数组。三是图表容器继承了父级的 height 为 0,只设 width 不设 height。ECharts 对高度非常敏感,容器必须显式给高度。
解决:最稳的做法是给每个.chart容器设置height: 400px或height: 60vh;在页面加载后统一初始化图表,而不是在数据请求前初始化。如果要在 Tab 切换后显示图表,切到该 Tab 时调用chart.resize()。另外要在setOption之前用console.log(data)确认数据结构,我见过最多的翻车是接口返回的 buckets/counts 字段名和前端取的不一致,一个叫buckets,一个取bucket,静默失败。
const chart = echarts.init(document.getElementById('genreChart')); window.addEventListener('resize', () => chart.resize());这段代码能避免窗口缩放后图表变形。如果图表放在 grid 布局里,resize尤为重要。还有一个细节:fetch请求失败时不会抛出异常,需要检查res.ok,否则接口 500 时前端只会拿到一个错误页文本,res.json()会抛错,导致后面图表全部不渲染。最好在加载图表前统一加一个错误提示。
5.5 毕业设计答辩时常见追问与应答角度
老师一般会问四个方向:数据怎么来的、反爬怎么做的、数据量多少、可视化指标怎么选的。数据来源就说“使用公开的豆瓣电影榜单页面,按照每分钟 20 到 30 条的频率采集,只做学习分析”。反爬这块,不要说“破解了验证码”,而要说“通过控制请求频率、完善请求头模拟浏览器行为,同时优先采集允许公开访问的内容”。数据量最好能报出具体数字,比如采集了 3 年共 5000 部电影,统计出去重后不同类型分布。可视化指标的选择要说得出理由:评分分布反映榜单整体口碑水平,类型占比反映电影市场构成,TOP 榜帮助用户快速找片。不要只讲图表类型,老师更愿意听到分析意图。
如果老师问“为什么不用现成的豆瓣 API”,你可以说“项目目标是完整走一遍数据采集到可视化的流程,掌握爬虫与清洗能力;公开 API 的开放范围和稳定性不可控,不能满足自定义分析字段”。这样回答既诚实又不会显得不懂现状。另一个高频追问是“如何解决增量采集”,把第 4.3 节的设计讲出来,用 movie_id 去重 + 定时任务即可,如果时间允许可以加 APScheduler 定时触发。
这一系列避坑经验,基本覆盖了我自己在这个项目上从零到一遇到过的绝大多数问题。最后再分享一个让这个项目从“能跑”变成“高分”的收尾技巧。
6. 把系统变成“高分项目”:验证方法、演示技巧与后续扩展
系统做完,不能只跑通就交。我用一个 20 分钟的质量检查单做自检:第一,重新安装干净环境跑一遍pip install -r requirements.txt,确认没有漏依赖;第二,清空数据库,从头跑一遍采集脚本,验证增量逻辑和去重逻辑;第三,在局域网另一台电脑上访问 Flask 服务,确认host=0.0.0.0和静态资源路径正常;第四,把 ECharts 改成离线模式,断网后图表仍能渲染;第五,准备一个只包含 100 条数据的小数据库,用于演示时快速重置,避免现场等待采集。这个“小数据备份”是我的后悔药,现场万一数据被弄乱,一键还原。
有条件的话,再加三个加分项。一是给系统加一个简单的趋势时间轴,展示不同年份电影评分的均值变化,这个只需要在上面的 SQL 上按年份分组,很快就能出一个折线图。二是把爬虫耗时和抓取条数写到日志文件,答辩时展示采集过程截图,会非常有说服力。三是做一个“重采”按钮,在前端页面上触发后端采集脚本,让数据量可见地增长,这个功能要限制只能手动触发,不要做成自动循环爬取。这三个扩展都能在一天内完成,但对“完整度”的评价提升非常明显。
我最后想说的是,所有踩过的坑其实都在帮你积累一手经验。刚开始跑豆瓣采集时,我也因为 403 频繁想放弃,后来发现把间隔调大就解决了;第一次因为text()拿不到内容,排查了半小时,才发现是 xpath 没写/text()。这类问题不值钱,但当你把它们写进论文的“问题与解决”章节,就是评委眼里的工程能力。希望这篇笔记能让你少走一点弯路,尽快把项目做成真正能演示、能讲清楚、能得高分的作品。希望帮到你。
本文还有配套的精品资源,点击获取