资讯动态

基于Python+Flask的豆瓣电影爬虫可视化系统实战

发布时间:2026/10/8 16:32:49 来源:尧图企业网站定制
简介这是一份面向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. 基于Pythonflask的豆瓣电影爬虫可视化系统毕业设计为什么选它基于Pythonflask的豆瓣电影爬虫采集与分析可视化系统简单说就是一条从豆瓣网页抓电影数据存进数据库再用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;q0.9, Referer: https://movie.douban.com/top250 } def fetch_page(url, retry3): for i in range(retry): try: resp requests.get(url, headersheaders, timeout10) 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-requestsAccept-Language 也尽量带上豆瓣会参考它做内容协商。第二重试逻辑里遇到 403 不要立刻重试先 sleep 5-8 秒这是最粗粒度的反爬规避本质是模拟人的访问节奏。第三timeout 必须设置否则某个请求卡死会让整个采集脚本停在那里这是血泪经验。拿到列表页 HTML 后用 xpath 解析出每个电影的详情链接和标题。这里有个高频坑豆瓣的 HTML 结构里标题在span classtitle里但英文名/副标题也在同类节点需要用 text() 来过滤。def parse_list(html): tree etree.HTML(html) items tree.xpath(//div[classhd]) movie_links [] for item in items: a item.xpath(.//a/href) title item.xpath(.//span[classtitle]/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[classtitle]/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(//*[propertyv:average]/text()) votes tree.xpath(//*[propertyv:votes]/text()) # 类型 genres tree.xpath(//*[propertyv:genre]/text()) # 年份 year tree.xpath(//*[classyear]/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解析时优先用豆瓣微数据标记的属性选择器比如propertyv:average。这类标记是给搜索引擎或浏览器扩展用的结构相对稳定。反过来很多教程用/html/body/...这种绝对路径一旦页面改版就整体翻车不推荐。年份节点classyear返回的是(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, indexFalse, encodingutf-8-sig) df.to_sql(movies, sqlite3.connect(douban.db), if_existsreplace, indexFalse)注意这里用encodingutf-8-sig导出 CSV是为了避免 Excel 打开乱码写入 SQLite 用的是to_sqlpandas 会自动建表字段类型也会自动推断。但这种简单的整表替换策略只适合全量重采后面要增量更新时需要给 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(host0.0.0.0, port5000, debugFalse)这里面有几个关键决策。debugFalse是必须的尤其当项目要部署给老师演示时debug 模式会把源码暴露在错误页安全上很不好看。host0.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.connectSQL 本身几乎不用改。顺便提一句把 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?limit20min_votes5000答辩时演示参数改动很方便。类型占比的查询稍微复杂因为一部电影可能有多个类型存在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_existsreplace, indexFalse)到这里后端数据链路已经完整。下一步就把这些 JSON 接到前端可视化页面上。4. 可视化大屏与图表ECharts 接入 Flask 模板4.1 前端框架选择原生模板 ECharts 够用可视化部分不需要上 Vue、ReactFlask 自带的 Jinja2 模板加 ECharts 的 CDN 就够了。原因有两点一是毕设页面只有一两个组件之间没有复杂状态共享用 Vue 反而要处理构建工具二是 ECharts 是纯前端图表库数据通过 fetch 拿 JSON 填进去和 Flask 模板天然配合。百度可视化大屏的很多风格其实用 ECharts 就能实现背景渐变、大标题、多图表栅格布局这些和框架无关。如果离线演示最好把 echarts.min.js 下载到static/js/目录避免答辩现场没有外网。CDN 在公网演示没问题但在学校机房局域网里容易出幺蛾子这是个翻车高发点。下面是一个模板骨架示例。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title豆瓣电影数据分析可视化/title script src{{ url_for(static, filenamejs/echarts.min.js) }}/script style body { background: #0f1423; margin: 0; } .chart { width: 100%; height: 400px; } /style /head body div idratingChart classchart/div div idgenreChart classchart/div div idtopChart classchart/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(orientrecords))注意pd.read_sql_query支持params元组传参能有效防止 SQL 注入比直接字符串拼接稳妥。当然这里查询参数是整数风险低但习惯要养好。返回orientrecords得到的是一个列表套字典前端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_pages10): 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 fhttps://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 与封 IPUser-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, encodingunicode)打印前 2000 个字符看 class 名还在不在。改 xpath 时优先依赖id、property、class这类稳定属性避免使用//div[1]/div[2]/div[3]这种位置路径。改完记得把列表页和详情页各跑一次别只用一个页面验证。这里补充一个经验豆瓣的列表页偶尔会出现审核未通过或已删除的条目导致详情页 404。解析详情页时先判断status_code 200再解析如果 404 就跳过并记录日志。很多新手解析到一半崩溃就是因为没有处理 404 响应。5.3 Flask 接口返回慢N1 查询与 JSON 序列化现象页面加载要 2 秒以上打开接口直接看也是慢吞吞但数据库里数据才几千条。原因常见的是两种。一是接口里循环查库比如在循环里执行一条 SQL 查询每部电影的演员几千条电影就几千次查询SQLite 每次查询都有开销这就是 N1 问题。二是jsonify序列化大量嵌套字典时Python 本身的 json 库处理慢但这只在数据量特别大时才明显。解决把循环内的查询合并成一条 SQL用IN条件或 JOIN 一次性取出来。另外接口返回给前端的数据尽量精简只返回图表需要的字段不要把整条电影记录包括简介都塞进去。我在top接口里只返回 id、title、rating、votes简介前端用不到就不返回。把 SQLite 查询改成 pandas 批量读取可以减少约一半时间。还有一个容易被忽略的点Flask 开发服务器的debugTrue会显著降低响应速度正式演示时务必debugFalse。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 服务确认host0.0.0.0和静态资源路径正常第四把 ECharts 改成离线模式断网后图表仍能渲染第五准备一个只包含 100 条数据的小数据库用于演示时快速重置避免现场等待采集。这个“小数据备份”是我的后悔药现场万一数据被弄乱一键还原。有条件的话再加三个加分项。一是给系统加一个简单的趋势时间轴展示不同年份电影评分的均值变化这个只需要在上面的 SQL 上按年份分组很快就能出一个折线图。二是把爬虫耗时和抓取条数写到日志文件答辩时展示采集过程截图会非常有说服力。三是做一个“重采”按钮在前端页面上触发后端采集脚本让数据量可见地增长这个功能要限制只能手动触发不要做成自动循环爬取。这三个扩展都能在一天内完成但对“完整度”的评价提升非常明显。我最后想说的是所有踩过的坑其实都在帮你积累一手经验。刚开始跑豆瓣采集时我也因为 403 频繁想放弃后来发现把间隔调大就解决了第一次因为text()拿不到内容排查了半小时才发现是 xpath 没写/text()。这类问题不值钱但当你把它们写进论文的“问题与解决”章节就是评委眼里的工程能力。希望这篇笔记能让你少走一点弯路尽快把项目做成真正能演示、能讲清楚、能得高分的作品。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑