
简介基于Python的豆瓣电影Top250数据分析与可视化项目源码及文档说明面向计算机相关专业大作业、学生课程设计与需要实战练习的学习者。项目难度适中源码经过导师指导与本地严格调试可稳定运行并获项目评审98分。压缩包内含2000个文件约80.74MB其中以1830个py脚本为主要代码文件另有pdf文档、txt说明文件、html/css/js前端页面、少量c与cpp扩展文件构成完整的学习资源包。文件类型基本覆盖爬虫采集、数据清洗、统计分析、可视化展示及前端交互等环节同时配有文档说明目录结构清晰便于按模块学习与复用。目前已有943人浏览学习适合作为课程设计、毕业设计或Python数据分析项目的参考能帮助读者掌握豆瓣Top250从数据抓取到可视化呈现的全流程兼具高分作业模板与实战练习价值。1. 豆瓣电影Top250这个数据集值得你动手完整跑一遍第一次看到这份基于Python爬取豆瓣电影Top250数据分析与可视化的源码包时我第一反应是“又是个爬虫Demo”但真正把源码和文档过了一遍之后我发现它并不是那种贴个requests请求就交差的玩具项目。它把爬取、清洗、存储、分析、可视化完整串成了一条链路数据量不大不小刚好250条适合用来练手完整的数据分析流程。无论你是正在做课程设计的在校生还是想找个练手项目熟悉Python数据分析的从业者这套源码都能直接运行评审分98分不是虚的起码代码逻辑完整、文档齐全能省掉你大量从零造轮子的时间。下文我会按“爬取 → 清洗 → 可视化 → 排坑 → 进阶”的顺序把它拆开讲清楚。2. 爬虫抓取层的拆解五步把页面数据变成结构化文本2.1 请求头与UA池为什么不能直接裸爬Top250页面的反爬强度不算高但如果你什么都不带直接用requests.get去访问大概率会拿到418或403服务端会拒绝陌生客户端的访问。常见做法是维护一个UA池每次请求随机挑一个模拟浏览器环境。import requests from fake_useragent import UserAgent ua UserAgent() headers { User-Agent: ua.random, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } session requests.Session() session.headers.update(headers)这段代码里有几个关键点。fake_useragent库负责随机生成不同浏览器版本的UA字符串避免每次请求都带着同一个指纹Session保持了TCP连接复用和Cookie传递减少了反复握手的开销也降低了被站点风控盯上的概率。Accept-Language尽量写成中文优先豆瓣会根据语言字段返回对应编码的页面如果缺失可能拿到默认繁体或英文界面后续解析xpath时文本格式会不一样。2.2 翻页遍历start参数的正确姿势Top250一共10页每页25条URL里的start参数控制偏移量从0开始步长25。这里不建议用循环拼接字符串直接把start定义成参数列表方便调试时单独拉某一页。base_url https://movie.douban.com/top250 page_data [] for page in range(10): params {start: page * 25, filter: } resp session.get(base_url, paramsparams, timeout10) resp.raise_for_status() resp.encoding utf-8 html resp.text # 这里把parse_html函数传进去稍后详解 items parse_html(html) page_data.extend(items) print(f第{page 1}页抓取完成累计{len(page_data)}条) # 频率控制避免一次性打满服务器 time.sleep(2)两个容易被忽略的参数timeout10设了单次请求超时上限避免某个请求卡死导致整个脚本挂起time.sleep(2)是在每页之间强行插入停顿虽然拖慢了整体速度但对目标站点更友好也降低了IP被临时限制的风险。如果你只想抓前两页做测试把range(10)改成range(2)即可。2.3 xpath解析字段从li节点取八类字段的路径解析部分使用的是lxml库的xpath比BeautifulSoup在复杂节点嵌套时更稳定。Top250每个电影条目都包在li标签里的div.item结构中核心字段全部能一层层取到。from lxml import etree def parse_html(html_doc): tree etree.HTML(html_doc) results [] for li in tree.xpath(//div[classitem]): title_node li.xpath(.//span[classtitle][1]/text()) title title_node[0] if title_node else rating_node li.xpath(.//span[classrating_num]/text()) rating float(rating_node[0]) if rating_node else 0.0 people_node li.xpath(.//div[classstar]/span[4]/text()) comment_people int(people_node[0].replace(人评价, )) if people_node else 0 quote_node li.xpath(.//p[classquote]/span/text()) quote quote_node[0].strip() if quote_node else detail_node li.xpath(.//div[classbd]/p[1]//text()) detail_text .join(detail_node).strip().replace(\n, ) link_node li.xpath(./div[classpic]/a/href) link link_node[0] if link_node else results.append({ title: title, rating: rating, comment_people: comment_people, quote: quote, detail: detail_text, link: link, }) return results这段代码最值得学习的是detail字段的处理方式。豆瓣电影条目里导演、主演、年份、地区、类型都挤在同一个p标签里并且夹杂着空格和换行直接取text()会得到一堆碎片。这里把//text()把所有子文本节点全部取出再用join合并后续清洗阶段再统一拆分。注意people_node里的评价人数是“xxxx人评价”的字符串取完要replace掉中文后缀再转int不处理的话后面做数值统计会直接类型报错。2.4 落盘与日志csv模块和重试机制数据清洗前的原始数据直接存成CSV用Python内置的csv模块而不是手动拼逗号。手动拼接遇到电影名里有逗号的情况会错列csv.writer会自动处理字段包裹这是最省心的写法。import csv def save_to_csv(rows, filenametop250_raw.csv): fieldnames [title, rating, comment_people, quote, detail, link] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(rows)encodingutf-8-sig是给Excel用户准备的普通utf-8文件用Excel打开中文会乱码带BOM的utf-8-sig打开就能正常显示。写完CSV后建议立刻用pd.read_csv读回来看一眼行数确认250条都在别等清洗阶段才发现少页。如果你在Windows上运行文件路径里的斜杠要注意转义或者直接使用相对路径。3. 清洗与入库把爬下来的脏文本变成可统计的DataFrame3.1 读CSV和类型检查先看dtypes再看值爬下来的数据虽然结构完整但所有字段都是字符串格式评分列虽然能转float但评价人数列里还有中文残留detail字段更是一团混着导演、年份、国别、类型的混合文本。第一步先把CSV读进来检查每列的类型。import pandas as pd df pd.read_csv(top250_raw.csv) print(df.dtypes) print(df.head(3)) print(df.isnull().sum())这段代码会暴露三个典型问题comment_people是object类型因为爬取时没做转换quote字段存在空值——不是所有电影都有一句话短评detail列是完整字符串没法直接做分组统计。df.isnull().sum()一定要跑能提前知道每个字段缺多少值判断后续策略。3.2 拆分导演、年份、地区、类型正则与split的配合detail字段的原始格式类似“导演: 某某 主演: 某某 / 某某 2020 / 中国大陆 / 剧情 / 爱情”分隔规则是斜杠但导演和主演之间还有空格。最稳妥的办法是先按“/”split再对每一段做二次清洗。import re def parse_detail(text): parts [p.strip() for p in text.split(/)] director_line parts[0] # 常见格式导演: 某某 主演: ... director director_line.replace(导演:, ).strip() if 导演: in director_line else year region genres [] for p in parts: if re.search(r(19|20)\d{2}, p): year_match re.search(r(19|20)\d{2}, p) year year_match.group(0) elif p in [中国大陆, 中国香港, 中国台湾, 美国, 日本, 韩国, 法国, 英国]: region p else: genres.append(p) return director, year, region, /.join(genres) df[director], df[year], df[region], df[genre] zip(*df[detail].apply(parse_detail))这里用正则(19|20)\d{2}匹配年份是因为豆瓣早期电影里年份可能出现在导演、出品地区之后位置不固定正则比硬编码位置更可靠。zip(*df[detail].apply(...))这个写法一次性把四列拆出来比apply返回Series再逐个赋值快得多。genres字段最终用“/”拼接而不是存列表是为了后续直接做分组统计方便。3.3 缺失与重复值处理策略什么字段值得dropna拿到清洗结果后统计一下缺失情况再决定去留。Top250数据整体质量很高但quote字段有缺失director字段偶尔因为解析失败变成空串。df[year] pd.to_numeric(df[year], errorscoerce) df df.dropna(subset[rating, title]) df df[df[year].notna()] df df.drop_duplicates(subset[title]) print(df.shape)逻辑上遵循“稀缺字段缺失不影响主流程”。dropna(subset[rating, title])只删核心字段为空的行quote缺失的行保留year列通过pd.to_numeric转换解析不了的变NaN再统一丢掉drop_duplicates(subset[title])去掉重复电影名——Top250本身不应该有重复但如果你手动在原URL上加了多余参数个别条目可能重复抓取。处理完的行数大概率还是250如果少于250就要回头看爬虫逻辑。3.4 SQLite入库幂等写入与查询验证清洗完的数据落SQLite比存回CSV更适合做后续查询。代码里注意做幂等处理每次运行先删掉旧表再重建保证重复跑脚本不会出现重复数据。import sqlite3 conn sqlite3.connect(top250.db) df.to_sql(movies, conn, if_existsreplace, indexFalse) verify_df pd.read_sql_query(SELECT COUNT(*) as cnt FROM movies, conn) print(入库行数:, verify_df[cnt].iloc[0]) conn.close()if_existsreplace是关键的幂等参数每次运行都会先drop旧表再写入不留重复数据。入库后立刻用一个COUNT查询验证行数和清洗阶段的行数对得上才继续往下走。如果对SQL不熟至少学会这条验证命令比盲信DataFrame里看到的数字可靠得多。4. 可视化与分析评分分布、年份趋势和风格词云4.1 图表库选型pyecharts出交互图matplotlib出统计图这份资源的可视化部分同时用了两个库分工很清晰。pyecharts负责生成可交互的HTML图表便于嵌入项目报告展示matplotlib负责生成静态统计图适合放在论文或文档里。pyecharts的API是新版写法老教程里Bar().add()的用法已经废弃新版必须用add_xaxis和add_yaxis这个差异容易让第一次跑的人报错。from pyecharts.charts import Bar from pyecharts import options as opts year_count df[year].value_counts().sort_index() bar Bar() bar.add_xaxis([str(y) for y in year_count.index]) bar.add_yaxis(每年上榜电影数, [int(v) for v in year_count.values]) bar.set_global_opts(title_optsopts.TitleOpts(title豆瓣Top250电影年份分布)) bar.render(year_distribution.html)注意bar.add_xaxis里要把年份转成字符串pyecharts的x轴不接受int类型列表否则直接抛类型错误。render生成的是独立HTML文件浏览器打开就能交互还可以在set_global_opts里加datazoom_opts方便年份跨度大时拖动查看细节。4.2 评分与评论数的分布直方图与散点图评分和评价人数是这份数据里最核心的两个数值字段评分集中在7.5到9.5之间评价人数跨度从几千到几百万分布非常偏斜。直方图用来观察评分分布形态散点图用来检查评分和评论数是否存在相关性。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 2, figsize(14, 5)) axes[0].hist(df[rating], bins20, edgecolorblack, alpha0.8) axes[0].set_title(评分分布) axes[0].set_xlabel(评分) axes[0].set_ylabel(电影数量) axes[1].scatter(df[rating], df[comment_people], alpha0.5) axes[1].set_title(评分与评论人数散点图) axes[1].set_xlabel(评分) axes[1].set_ylabel(评论人数) axes[1].set_yscale(log) plt.tight_layout() plt.savefig(rating_analysis.png, dpi150)两行rcParams必须放在所有绘图代码之前否则中文标签全是方框。axes[1].set_yscale(log)把评论人数转成对数刻度因为原始数据从几千到数百万线性坐标下低评分的点会被压扁看不清。savefig的dpi150足够放进报告里打印内存占用也不大。4.3 年份与评分趋势识别不同时代的作品特征把年份作为横轴、评分作为纵轴可以直观看出各年代作品的评分水平。这里用rolling均值做平滑否则单年样本太少折线会像心电图一样乱跳。trend df.groupby(year)[rating].mean().reset_index() trend[rolling_avg] trend[rating].rolling(window5, min_periods1).mean() plt.figure(figsize(12, 5)) plt.plot(trend[year], trend[rating], alpha0.3, linestyle--, label年度平均评分) plt.plot(trend[year], trend[rolling_avg], linewidth2, colorred, label5年滑动平均) plt.xlabel(年份) plt.ylabel(平均评分) plt.legend() plt.title(Top250电影年份-评分趋势) plt.savefig(year_trend.png, dpi150)rolling(window5, min_periods1)是滑动窗口平均window5表示每5年取一次均值min_periods1保证早期年份数据不够5条时也能输出。从这张图能明显看出早期入选Top250的电影评分整体偏高近年入选门槛越来越高这个发现可以直接写进报告的分析章节。4.4 电影类型词云与导演频次让报告有“读图感”词云是这类课程设计报告里最讨喜的配图但直接用原始文本生成的结果往往乱七八糟。类型词云需要先把多类型字段拆开再用jieba分词过滤停用词不然“电影”“剧情”这类高频泛词会占据大面积词云。from wordcloud import WordCloud import jieba genre_text .join(df[genre].dropna().tolist()) word_list jieba.cut(genre_text) stopwords {电影, 一部, 一个, 以及, 关于} filtered .join([w for w in word_list if w not in stopwords and len(w.strip()) 1]) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height400, background_colorwhite, max_words80, ).generate(filtered) wc.to_file(genre_wordcloud.png)font_path必须指定一个系统中真实存在的中文字体Windows一般用simhei.ttfmacOS用户要改成/System/Library/Fonts/PingFang.ttcLinux需要先安装中文字体包否则词云全是乱码方块。len(w.strip()) 1过滤掉单字stopwords里的词按你的数据情况自行调整跑一次看效果再迭代这一步基本上靠感觉调。5. 复现中的常见问题排查六条真实踩坑记录5.1 爬取下来count是249不是250现象CSV写入后总行数少一条或者某页抓到23条。 原因页面偶尔会有动态加载延迟lxml解析时节点还没渲染完少抓了当页一两个条目。 解决先确认print里每页的条数日志哪一页少了就单独重抓那一页不要重跑全量。在循环里对每页结果做长度校验长度不等于25的时候sleep三秒再重试一次。5.2 encoding设为utf-8后Excel打开中文乱码现象CSV文件在记事本里看正常但Excel打开全是乱码。 原因普通utf-8编码不带BOMExcel默认用系统本地编码如GBK猜测文件格式猜错就显示乱码。 解决写入CSV时用utf-8-sig代替utf-8这是血泪经验第一次跑这个项目就被这问题坑了十几分钟。5.3 xpath路径写对但取到空列表现象调试时li节点能取到但子节点的xpath路径返回[]。 原因豆瓣页面层级中部分字段嵌套在其他属性里例如span[4]的索引位置可能因页面结构微调而变化。 解决不要依赖太长的绝对路径用li.xpath(.//span[classrating_num]/text())这类相对路径。如果还是取不到先li.xpath(.//span)打印所有span的class和text再决定用哪个定位。5.4 高频请求后IP被临时限制现象抓完两页后突然返回418之后所有请求都失败。 原因请求频率太高触发了站点的反爬策略。 解决把time.sleep(2)改成time.sleep(3)并加上随机抖动time.sleep(random.uniform(2, 4))。这个项目只是250条数据没必要追求高并发慢一点反而稳。5.5 pyecharts运行报错AttributeError: Bar object has no attribute add现象代码在旧版pyecharts上能跑升级最新版后报错。 原因pyecharts v1.0以上版本彻底移除了旧版add()链式调用方法。 解决统一使用add_xaxis/add_yaxisset_global_opts的新版API。检查项目环境里安装的pyecharts版本建议锁在1.2.0以上。5.6 matplotlib绘图出现乱码和负号方块现象图表的标题和坐标轴中文全部显示成方块坐标轴负号也变成异常字符。 原因matplotlib默认字体不包含中文字符集且默认unicode_minus为True。 解决绘图前固定写两行配置plt.rcParams[font.sans-serif][SimHei]和plt.rcParams[axes.unicode_minus]False。Linux环境下SimHei不存在要先安装fonts-wqy-microhei或直接指定系统已有的字体路径。6. 从复现到改造把Top250数据链路迁移到自己的数据集跑通这份资源之后最有价值的动作是把整套数据链路迁移到其他业务场景。核心逻辑其实只有四件事请求页面、解析节点、清洗入库、可视化输出。我把这套结构抽成了一个模板遇到任何需要采集分析的新需求只换URL和解析规则就能复用。def generic_crawl_and_analyze( url_list, parse_func, sleep_interval2, ): all_rows [] for url in url_list: resp session.get(url, timeout10) rows parse_func(resp.text) all_rows.extend(rows) time.sleep(sleep_interval) df pd.DataFrame(all_rows) df clean(df) df.to_sql(target_table, conn, if_existsreplace, indexFalse) return df以豆瓣的新片榜单为例改写思路是把parse_func里的xpath常量抽成参数年份、评分字段保持不变增加票价、上映日期等新字段。如果你要爬的是其他站点需要把fake_useragent换成对方接受的请求头解析逻辑重新写一遍但清洗和可视化层基本可以原封不动搬过去。我一般会把每页字段的解析函数单独写成一个文件换站点只改这个文件其他部分不动。实际验证改造是否成功我会在迁移后做三个检查一是爬取条数与原网站列表页数量一致二是随机抽三条数据打开详情页对一下评分和评论数三是把df.dtypes打印出来确认新字段类型不是object。这三步走完基本能确定新链路是可靠的。这套资源的价值在于它构建了一个完整可扩展的分析脚手架。第一次完整跑下来之后从那以后我每次拿到新数据集都会强制自己走一遍“解析→清洗→入库→可视化”的完整链路遇到再简单的爬虫项目也不跳步。这条流程已经成了我的固定习惯帮我在后续好几个模拟项目中少走了不少弯路。如果你也在做数据分析类的大作业直接下载这套源码对照着跑比从零开始省力得多希望帮到你。本文还有配套的精品资源点击获取