简介:本资源是一套完整的Python爬虫与数据可视化分析期末大作业项目代码,面向计算机类专业本科生及Python初学者,解决课程设计与期末考核中从数据采集到图表呈现的全流程实践需求。压缩包共39个文件,包含3个核心Python脚本(爬取、存储、可视化)、24个HTML可视化报告页面、4个Excel原始与清洗后数据集、1个SQL建表语句及环境配置文件(yaml、requirements.txt),整体6.68MB,结构清晰、模块分离明确。已有3403人学习下载,项目代码注释详尽,覆盖requests+BeautifulSoup基础爬虫、pandas数据清洗、matplotlib/plotly图表生成等关键环节,附带chromedriver与 stealth.min.js 防反爬适配组件,开箱即用;同时支持二次开发,可快速拓展至招聘平台(如51job)、技术栈(PyTorch/TensorFlow)等多源数据分析场景。
1. 项目缘起:从“交作业”到“拿高分”的实战跨越
又到了期末,看着课程群里老师发布的“爬虫数据可视化大作业”要求,你是不是也感到一阵头大?要求听起来很明确:用Python爬取数据,然后做可视化分析,最后提交一份报告和代码。但真动起手来,问题就来了:爬什么网站才不会被封?数据怎么清洗才干净?可视化图表怎么做才能既有深度又好看?代码怎么组织才能让老师觉得你“会编程”而不仅仅是“会复制粘贴”?这几乎是每个学Python数据分析、爬虫课程的同学都会遇到的经典困境。我当年也是这么过来的,从最初对着要求文档发呆,到后来摸索出一套能稳定拿高分的项目构建方法,中间踩过的坑、总结的经验,今天一次性分享给你。
这个项目,表面上是完成一次课程作业,本质上是一次完整的、小型的“数据工程”实战演练。它考察的绝不仅仅是你会不会用requests和matplotlib,而是你解决一个真实数据问题的系统性能力:需求分析、技术选型、工程实现、结果呈现。很多同学把作业做成了“Demo拼接”,爬虫、分析、可视化各干各的,代码混乱,报告空洞,自然分数平平。而高分的秘诀,在于将这三个环节有机地串联成一个有逻辑、有故事、有深度的数据分析项目。
接下来,我将以一个完整的、可复现的高分项目为例,手把手带你走通全流程。我们会选择一个有代表性、数据规整、且分析维度丰富的网站作为数据源(比如豆瓣电影Top250),从零开始,构建一个结构清晰、健壮性高、可视化专业的项目。你会看到,如何用不到200行核心代码,做出让老师眼前一亮的大作业。
2. 谋定而后动:高分项目的顶层设计与技术选型
在写第一行代码之前,花半小时做好设计,能省去后面几天的调试和重构时间。一个高分项目,必然有一个清晰的架构。
2.1 项目目标与数据源选择
首先,我们需要一个明确的、可衡量的项目目标。例如:“分析豆瓣电影Top250榜单,探究高分电影的共性特征(如年份、地区、类型分布),并可视化呈现其评分与评价人数之间的关系。” 这个目标具体、有分析点,而不是泛泛的“爬取并展示数据”。
数据源的选择至关重要。豆瓣电影Top250是一个经典选择,原因如下:
- 数据质量高:字段规整(片名、评分、评价人数、导演、主演、年份、地区、类型、简介等),非常适合做多维分析。
- 反爬措施温和:对于学习型爬虫,其反爬策略(如请求头校验、频率限制)是很好的练习对象,既不会太简单,也不会复杂到让人绝望。
- 分析维度丰富:可以从评分分布、年代变迁、地区/类型统计、口碑与热度关系等多个角度进行挖掘,为可视化提供充足素材。
- 静态页面:数据直接渲染在HTML中,无需处理复杂的JavaScript动态加载,降低了入门门槛。
注意:任何爬虫行为都应遵守网站的
robots.txt协议,并设置合理的请求间隔(如每次请求间隔3-5秒),避免对目标服务器造成压力。本项目仅用于学习交流。
2.2 技术栈与工具清单
一个清爽、专业的技术栈是给老师好印象的开始。我们选择成熟、主流且易于展示的库。
- 爬虫层:
requests: 用于发送HTTP请求,获取网页源代码。比urllib更简洁易用。BeautifulSoup4 (bs4): 用于解析HTML,提取结构化数据。对于豆瓣这种静态页面,它比lxml的学习曲线更平缓。time/random: 用于在请求间插入随机延时,模拟人类操作,规避反爬。
- 数据处理层:
pandas: 数据分析的核心。将爬取的列表数据转化为DataFrame,方便进行清洗、筛选、分组、聚合等操作。这是体现你数据分析能力的关键。re(正则表达式): 辅助清洗数据中的杂音(如提取纯数字、去除多余空格等)。
- 可视化层:
matplotlib: 绘图基础库,高度自定义,适合绘制精确的统计图表。seaborn: 基于matplotlib,默认样式更美观,统计图表集成度高(如箱线图、分布图),能极大提升出图效率和质量。wordcloud: 生成词云,用于展示电影类型、导演等文本字段的频次分布,视觉冲击力强。
- 项目组织:
- 使用函数和类对代码进行模块化封装,而不是写成一个冗长的脚本。
- 使用配置文件(如
config.py)或常量来管理URL、请求头、文件路径等。 - 使用
Jupyter Notebook或清晰的.py文件+注释来呈现你的思考过程。
2.3 项目目录结构规划
一个清晰的目录结构,能直观反映你的工程化思维。建议如下:
douban_movie_analysis/ ├── README.md # 项目说明文档(很重要!) ├── requirements.txt # 依赖包列表 ├── config.py # 配置文件(URL、请求头等) ├── spider.py # 爬虫模块 ├── data_processor.py # 数据处理与清洗模块 ├── visualizer.py # 可视化模块 ├── main.py # 主程序,串联整个流程 ├── data/ # 数据目录 │ ├── raw_movies.csv # 原始爬取数据 │ └── cleaned_movies.csv # 清洗后数据 ├── output/ # 输出目录 │ ├── figures/ # 存放所有生成的图片 │ └── report.md # 分析报告(Markdown格式) └── utils/ # 工具函数目录(可选) └── logger.py # 日志记录在README.md中,清晰地写明项目目标、如何运行、各文件作用,这本身就是专业性的体现。
3. 爬虫实战:稳健、优雅地获取数据
爬虫是项目的基石。一个健壮的爬虫,不仅要能拿到数据,还要能应对网络异常、反爬策略,并且代码可读、易维护。
3.1 请求与解析:细节决定成败
我们首先分析豆瓣Top250的页面规律:每页25条电影,共10页。URL模式为:https://movie.douban.com/top250?start={page*25}。
核心代码实现与详解:
# config.py BASE_URL = 'https://movie.douban.com/top250' HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9', } DELAY = 5 # 基础延迟秒数 # spider.py import requests from bs4 import BeautifulSoup import time import random import pandas as pd from config import BASE_URL, HEADERS, DELAY def get_page(url): """发送请求,获取页面内容""" try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 # 豆瓣有时会返回非UTF-8编码,指定编码更稳妥 resp.encoding = resp.apparent_encoding or 'utf-8' return resp.text except requests.exceptions.RequestException as e: print(f"请求 {url} 失败: {e}") return None def parse_page(html): """解析单个页面,提取电影信息""" if not html: return [] soup = BeautifulSoup(html, 'html.parser') movie_items = soup.find_all('div', class_='item') movies_on_page = [] for item in movie_items: # 使用更健壮的查找方式,避免因个别标签缺失导致崩溃 title_elem = item.find('span', class_='title') title = title_elem.text.strip() if title_elem else 'N/A' # 评分和评价人数在同一<p>里,需要拆分 rating_elem = item.find('span', class_='rating_num') rating = float(rating_elem.text.strip()) if rating_elem else 0.0 # 使用正则表达式提取评价人数中的数字 import re comment_elem = item.find('div', class_='star').find_all('span')[-1] comment_text = comment_elem.text if comment_elem else '' comment_num = int(re.search(r'\d+', comment_text).group()) if re.search(r'\d+', comment_text) else 0 # 其他信息在`<p class="">`里,包含导演、主演、年份、地区、类型 info_elem = item.find('p', class_='') if info_elem: info_text = info_elem.text.strip() # 简单的分割处理,更复杂的可以用正则或更精细的解析 infos = info_text.split('\n') year_region_genre = infos[-1].strip() if len(infos) > 1 else '' # 进一步拆分年份、地区、类型(这里假设格式为"2006 / 美国 / 犯罪 剧情") parts = [p.strip() for p in year_region_genre.split('/')] year = parts[0] if len(parts) > 0 else 'N/A' region = parts[1] if len(parts) > 1 else 'N/A' genres = parts[2] if len(parts) > 2 else 'N/A' else: year = region = genres = 'N/A' movie_info = { 'title': title, 'rating': rating, 'comment_num': comment_num, 'year': year, 'region': region, 'genres': genres } movies_on_page.append(movie_info) return movies_on_page def crawl_all_pages(): """爬取所有页面""" all_movies = [] for page in range(10): # 0-9页 start = page * 25 url = f'{BASE_URL}?start={start}' print(f'正在爬取: {url}') html = get_page(url) movies = parse_page(html) all_movies.extend(movies) # 关键:随机延迟,避免请求过快 sleep_time = DELAY + random.uniform(0, 2) time.sleep(sleep_time) return all_movies if __name__ == '__main__': movies_data = crawl_all_pages() df = pd.DataFrame(movies_data) # 保存原始数据 df.to_csv('./data/raw_movies.csv', index=False, encoding='utf-8-sig') print(f'爬取完成,共{len(df)}条数据,已保存至 raw_movies.csv')避坑经验与技巧:
- 请求头(User-Agent):务必设置一个常见的浏览器UA,这是绕过基础反爬的第一步。直接从浏览器开发者工具(F12 -> Network)里复制一个即可。
- 异常处理:网络请求充满不确定性,必须用
try...except包裹,并记录日志。resp.raise_for_status()能帮你快速发现403、404等问题。 - 编码问题:中文网站常出现乱码。优先使用
resp.apparent_encoding(requests推测的编码),并备选utf-8。 - 解析容错:网页结构可能微调,或个别条目信息缺失。使用
if elem: ... else 'N/A'的模式,保证程序不会因为一个标签找不到而崩溃。 - 遵守爬虫礼仪:
time.sleep是必须的。固定延时容易被识别,加入随机因子(random.uniform(0, 2))更模拟人类行为。这是本项目能稳定运行的核心之一。
3.2 数据持久化与增量爬取思路
将数据保存为CSV是最简单通用的方式。utf-8-sig编码能确保Excel打开时中文不乱码。
对于真正的“企业级”项目,会考虑增量爬取:只爬取新增或更新的数据。一个简单的思路是,在数据库中记录每条数据的爬取时间或版本号,下次爬虫运行时,通过对比last_updated字段或数据哈希值,来决定是否需要更新。虽然本次大作业不强制要求,但在报告里提一下这个思路,能展示你的前瞻性思考。
4. 数据清洗与预处理:从“原始”到“可用”
爬下来的数据通常是“脏”的,直接用于分析会产生偏差。数据清洗是体现你数据分析功底的关键环节。
4.1 常见数据问题与处理
加载原始数据后,我们系统性地进行检查和清洗。
# data_processor.py import pandas as pd import numpy as np import re def load_and_clean_data(filepath='./data/raw_movies.csv'): df = pd.read_csv(filepath, encoding='utf-8-sig') print("数据概览:") print(df.info()) print("\n前5行数据:") print(df.head()) # 1. 处理缺失值 print(f"\n缺失值统计:\n{df.isnull().sum()}") # 对于关键数值字段(如评分),用中位数填充;对于文本字段,用‘Unknown’填充 if df['rating'].isnull().any(): df['rating'].fillna(df['rating'].median(), inplace=True) df.fillna('Unknown', inplace=True) # 2. 数据类型转换与提取 # 年份字段:提取数字部分,并转为整数 df['year'] = df['year'].apply(lambda x: int(re.search(r'\d+', str(x)).group()) if re.search(r'\d+', str(x)) else np.nan) # 地区字段:可能包含多个地区,如“美国 / 英国”,我们取第一个作为主要地区 df['main_region'] = df['region'].apply(lambda x: str(x).split(' / ')[0].strip()) # 类型字段:拆分为列表,便于后续分析 df['genre_list'] = df['genres'].apply(lambda x: [g.strip() for g in str(x).split() if g.strip()]) # 3. 处理异常值 # 评分应在0-10之间 rating_outliers = df[(df['rating'] < 0) | (df['rating'] > 10)] if not rating_outliers.empty: print(f"发现异常评分数据 {len(rating_outliers)} 条,将进行修正") # 这里可以根据业务逻辑修正,例如用上下限截断或删除 df['rating'] = df['rating'].clip(0, 10) # 评价人数应为非负整数 df['comment_num'] = pd.to_numeric(df['comment_num'], errors='coerce').fillna(0).astype(int) # 4. 创建衍生特征(Feature Engineering) # 这是加分项!例如,创建“热度”指标(评分*评价人数的对数,以平衡绝对数量和分数) df['popularity_score'] = df['rating'] * np.log1p(df['comment_num']) # log1p防止comment_num为0 print("\n清洗后数据概览:") print(df[['title', 'year', 'main_region', 'rating', 'comment_num', 'popularity_score']].head()) return df if __name__ == '__main__': cleaned_df = load_and_clean_data() cleaned_df.to_csv('./data/cleaned_movies.csv', index=False, encoding='utf-8-sig') print('数据清洗完成,已保存至 cleaned_movies.csv')核心操作解析:
- 缺失值处理:
fillna()是基本操作。对于数值型,用中位数(median)比均值(mean)更抗干扰。对于分类型,用‘Unknown’标记比直接删除更能保留数据规模。 - 文本字段提取:使用
apply()配合lambda函数和正则表达式re,是处理非结构化文本的利器。例如从“2006 / 美国 / 犯罪 剧情”中精准提取年份。 - 异常值检测与处理:通过逻辑判断(如评分范围)找出异常值。
clip()函数可以方便地将超出范围的值截断到边界。 - 特征工程:创建
popularity_score这样的衍生特征,是数据分析的进阶技能。它综合了评分(质量)和评价人数(热度),比单纯看评分更有信息量。在报告里解释这个特征的构建逻辑,能极大提升作业深度。
4.2 数据探索性分析(EDA)
在正式可视化前,用Pandas进行快速的EDA,能帮你确定分析方向和可视化重点。
# 在data_processor.py中增加一个EDA函数 def exploratory_data_analysis(df): print("=== 探索性数据分析 (EDA) ===") # 1. 描述性统计 print("\n1. 数值型字段描述性统计:") print(df[['rating', 'comment_num', 'year', 'popularity_score']].describe()) # 2. 分类字段分布 print("\n2. 电影主要地区分布 Top10:") print(df['main_region'].value_counts().head(10)) print("\n3. 电影类型频次统计 (展开列表后):") # 将genre_list展开为多行 all_genres = df['genre_list'].explode() print(all_genres.value_counts().head(15)) # 3. 相关性分析 print("\n4. 评分、评价人数、热度分数的相关性矩阵:") corr_matrix = df[['rating', 'comment_num', 'popularity_score']].corr() print(corr_matrix) # 可以发现,评分和评价人数相关性很弱,但热度分数与两者都有一定关联,符合设计预期。运行EDA后,你可能会发现:“美国电影占比极高”、“剧情片是绝对主流”、“评分与评价人数几乎不相关”等洞察,这些都将成为你可视化报告的核心论点。
5. 可视化呈现:用图表讲一个好故事
数据可视化不是图表的堆砌,而是用视觉语言讲述数据背后的故事。我们要选择最合适的图表类型来回答具体问题。
5.1 基础统计图表:分布、趋势与对比
我们使用seaborn配合matplotlib,其默认样式更现代。
# visualizer.py import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import numpy as np from wordcloud import WordCloud import jieba # 用于中文分词,处理电影类型等 # 设置中文字体和seaborn样式 plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 sns.set_style("whitegrid") def plot_rating_distribution(df, save_path='./output/figures/rating_dist.png'): """评分分布直方图与密度曲线""" plt.figure(figsize=(10, 6)) # 直方图 sns.histplot(df['rating'], bins=20, kde=False, color='skyblue', alpha=0.7, stat='percent') # 密度曲线 sns.kdeplot(df['rating'], color='red', linewidth=2) plt.axvline(df['rating'].mean(), color='green', linestyle='--', label=f'平均分: {df["rating"].mean():.2f}') plt.axvline(df['rating'].median(), color='orange', linestyle='--', label=f'中位数: {df["rating"].median():.2f}') plt.xlabel('电影评分') plt.ylabel('百分比 (%)') plt.title('豆瓣Top250电影评分分布') plt.legend() plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() print(f"评分分布图已保存至: {save_path}") def plot_movies_by_year(df, save_path='./output/figures/movies_by_year.png'): """电影数量随年份的变化(折线图)""" # 按年份分组计数 year_counts = df['year'].value_counts().sort_index() # 过滤掉NaN和过早的年份 year_counts = year_counts[year_counts.index > 1950] plt.figure(figsize=(14, 6)) plt.plot(year_counts.index, year_counts.values, marker='o', linestyle='-', linewidth=2, markersize=5) # 标注峰值点 peak_year = year_counts.idxmax() peak_count = year_counts.max() plt.annotate(f'峰值: {peak_year}年\n({peak_count}部)', xy=(peak_year, peak_count), xytext=(peak_year-15, peak_count+5), arrowprops=dict(facecolor='black', shrink=0.05)) plt.xlabel('上映年份') plt.ylabel('电影数量') plt.title('豆瓣Top250电影数量随年份变化趋势') plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def plot_region_and_genre(df, save_path1='./output/figures/region_top10.png', save_path2='./output/figures/genre_top15.png'): """地区与类型分布(条形图)""" fig, axes = plt.subplots(1, 2, figsize=(16, 6)) # 地区分布 Top10 region_top10 = df['main_region'].value_counts().head(10) sns.barplot(x=region_top10.values, y=region_top10.index, ax=axes[0], palette='viridis') axes[0].set_xlabel('电影数量') axes[0].set_title('Top250电影主要地区分布 (Top10)') for i, v in enumerate(region_top10.values): axes[0].text(v + 0.5, i, str(v), va='center') # 类型分布 Top15 (需展开genre_list) all_genres = df['genre_list'].explode() genre_top15 = all_genres.value_counts().head(15) sns.barplot(x=genre_top15.values, y=genre_top15.index, ax=axes[1], palette='rocket') axes[1].set_xlabel('出现频次') axes[1].set_title('电影类型出现频次 (Top15)') for i, v in enumerate(genre_top15.values): axes[1].text(v + 0.5, i, str(v), va='center') plt.tight_layout() plt.savefig(save_path1, dpi=300, bbox_inches='tight') # 注意,这里保存的是第一个子图,实际应分别保存或保存整个figure # 更佳实践:分别保存或使用plt.savefig保存整个fig plt.show()图表设计心得:
- 一图一议:每张图回答一个明确的问题。图1回答“评分集中在哪个区间?”,图2回答“哪个年代的好电影多?”,图3回答“哪些地区和类型更受青睐?”。
- 美化与标注:
seaborn的palette调色板能让图表更美观。使用annotate添加关键标注(如峰值点),使用text在条形图上显示具体数值,能让读者一目了然。 - 保存格式:
dpi=300确保图片高清,bbox_inches='tight'自动裁剪白边。
5.2 进阶关系与文本可视化
基础图表展现分布,进阶图表揭示关系。
def plot_rating_vs_comments(df, save_path='./output/figures/rating_vs_comments.png'): """评分与评价人数的关系(散点图)""" plt.figure(figsize=(10, 8)) # 使用热度分数作为散点大小,形成三维视觉 scatter = plt.scatter(df['rating'], df['comment_num'], c=df['popularity_score'], cmap='YlOrRd', s=df['popularity_score']*2, alpha=0.6, edgecolors='grey') plt.colorbar(scatter, label='热度分数') plt.xlabel('评分') plt.ylabel('评价人数 (对数尺度)') plt.yscale('log') # 评价人数差异巨大,使用对数坐标更清晰 plt.title('电影评分 vs. 评价人数(气泡大小与颜色代表热度)') # 添加趋势线 z = np.polyfit(df['rating'], np.log1p(df['comment_num']), 1) p = np.poly1d(z) plt.plot(df['rating'].sort_values(), np.exp(p(df['rating'].sort_values()))-1, 'b--', linewidth=2, label='趋势线') plt.legend() plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() print("从散点图可以看出,评分和评价人数并无强线性关系。高分电影不一定评价人数多,反之亦然。") def generate_genre_wordcloud(df, save_path='./output/figures/genre_wordcloud.png'): """生成电影类型词云""" # 合并所有类型为一个长字符串 all_genres_text = ' '.join(df['genres'].dropna().astype(str)) # 简单清洗,去除分隔符 all_genres_text = all_genres_text.replace('/', ' ').replace(' ', ' ') wordcloud = WordCloud( width=800, height=400, background_color='white', font_path='msyh.ttc', # 指定中文字体路径 max_words=100, contour_width=1, contour_color='steelblue' ).generate(all_genres_text) plt.figure(figsize=(12, 6)) plt.imshow(wordcloud, interpolation='bilinear') plt.axis('off') plt.title('豆瓣Top250电影类型词云', fontsize=16) plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show()进阶分析解读:
- 散点图与趋势线:这张图是分析的核心。它直观地揭示了“口碑”(评分)与“热度”(评价人数)的分离现象。许多经典老片评分极高但评价人数不多,而一些大众商业片评价人数巨量但评分中庸。趋势线平坦,印证了EDA中相关性弱的结论。
- 词云:词云是一种高效的文本数据可视化方式,能快速形成视觉焦点。
剧情、爱情、喜剧等高频词会突出显示,直观反映榜单偏好。
5.3 集成与报告生成
最后,我们创建一个主程序,串联整个流程,并生成一份简明的Markdown报告。
# main.py from spider import crawl_all_pages from data_processor import load_and_clean_data, exploratory_data_analysis from visualizer import (plot_rating_distribution, plot_movies_by_year, plot_region_and_genre, plot_rating_vs_comments, generate_genre_wordcloud) import pandas as pd import os def main(): print("豆瓣电影Top250数据分析项目启动...") # 步骤1:爬虫(如果已有数据文件可跳过) data_file = './data/cleaned_movies.csv' if not os.path.exists(data_file): print("未找到清洗后数据,开始爬取...") raw_data = crawl_all_pages() # 假设spider.py返回的是df # 这里需要根据你的spider.py实际返回值调整 # 示例:假设spider.crawl_all_pages()已直接保存CSV,我们直接加载 pass else: print("找到已清洗数据,直接加载...") # 步骤2:加载与清洗数据 df = load_and_clean_data('./data/raw_movies.csv') # 或直接加载cleaned # 步骤3:探索性分析 exploratory_data_analysis(df) # 步骤4:创建输出目录 os.makedirs('./output/figures', exist_ok=True) # 步骤5:生成所有可视化图表 print("\n开始生成可视化图表...") plot_rating_distribution(df) plot_movies_by_year(df) plot_region_and_genre(df) # 注意这个函数会生成两张子图,保存逻辑需调整 plot_rating_vs_comments(df) generate_genre_wordcloud(df) # 步骤6:生成简易分析报告 generate_report(df) print("\n项目执行完毕!所有图表和分析报告已保存在 ./output/ 目录下。") def generate_report(df): """生成Markdown格式的分析报告""" report_content = f""" # 豆瓣电影Top250数据分析报告 ## 项目概述 本报告基于对豆瓣电影Top250榜单数据的爬取、清洗与可视化分析,旨在揭示高分电影的共性特征与分布规律。 ## 数据概览 * **数据总量**:{len(df)} 部电影 * **评分范围**:{df['rating'].min():.1f} - {df['rating'].max():.1f} (平均分: {df['rating'].mean():.2f}) * **时间跨度**:{int(df['year'].min())} 年 - {int(df['year'].max())} 年 * **主要数据字段**:片名、评分、评价人数、上映年份、地区、类型。 ## 核心发现 ### 1. 评分分布集中,头部效应明显 电影评分主要集中在 **{df['rating'].median():.1f}** 分以上,呈明显的左偏分布。超过90%的电影评分在8.5分以上,说明榜单筛选严格,真正意义上的“高分电影”。 ### 2. 经典电影年代集中涌现 电影数量在 **1990-2010年** 间达到高峰,其中 **{df['year'].mode().iloc[0]}** 年入围电影最多。这表明该时期是全球电影创作的一个黄金时代。 ### 3. 地区与类型分布高度集中 * **地区**:**美国**电影以绝对优势占据主导 ({df['main_region'].value_counts().get('美国', 0)}部),其次是**中国大陆**、**日本**、**英国**等。 * **类型**:**剧情**片是绝对主流,其次是**爱情**、**喜剧**、**犯罪**、**动画**。一部电影通常包含多个类型标签。 ### 4. 口碑与热度关联性弱 通过评分与评价人数的散点图分析发现,两者**不存在强相关性**。许多影史经典(高评分)因年代或题材原因,评价人数相对较少;而部分大众商业片(高评价人数)评分则处于中上水平。这体现了专业评价与大众流行度之间的差异。 ### 5. 综合热度指标 本项目构建了“热度分数”(评分 * log(评价人数))这一综合指标。分析发现,该指标能较好地平衡电影的艺术价值与大众接受度,排名靠前的电影通常是**既叫好又叫座**的典范之作(如《肖申克的救赎》、《霸王别姬》)。 ## 结论 豆瓣Top250榜单反映了影迷群体对电影艺术性、思想深度的普遍追求。榜单以剧情片为主,时间上集中于上世纪末至本世纪初,地域上以美国电影文化输出为主导。高分电影的核心在于其经受住了时间的考验,获得了跨越时代和文化的认可。 ## 附录:可视化图表 所有生成图表已保存在 `./output/figures/` 目录下。 """ with open('./output/report.md', 'w', encoding='utf-8') as f: f.write(report_content) print("分析报告已生成: ./output/report.md") if __name__ == '__main__': main()6. 项目打包、答辩与高分要点
代码和报告都完成后,如何提交才能获得最高分?
6.1 代码质量与工程化
- 模块化与注释:确保你的
spider.py,processor.py,visualizer.py,main.py分工明确。关键函数上方用"""文档字符串"""说明其功能、参数和返回值。复杂的逻辑行内要有注释。 - 健壮性:你的爬虫有异常处理吗?有延时吗?数据清洗函数能处理缺失和异常吗?在报告里简要说明这些设计,能体现你的工程思维。
- 依赖管理:
requirements.txt文件必不可少。使用pip freeze > requirements.txt生成。老师可以一键pip install -r requirements.txt复现你的环境。 - 可复现性:确保
main.py能一键运行,从爬虫(或读取本地数据)、清洗、分析到出图、生成报告,全流程自动化。这是加分项。
6.2 报告撰写与呈现
- 结构清晰:模仿上面
generate_report函数输出的结构。有概述、有数据、有发现(分点)、有结论。 - 图文并茂:在报告里直接嵌入关键图表(Markdown语法:
),并配上一两句精炼的解读。不要只说“如图所示”,要说“从图X可以看出,评分呈现……分布,这说明……”。 - 深度洞察:不要停留在描述图表(“美国电影最多”),要尝试解释(“这反映了美国在全球电影产业中的文化输出主导地位”)。结合你构建的
popularity_score等衍生指标进行分析。 - 提及难点与解决方案:在报告最后或README中,可以加一个“遇到的问题与解决”部分,简短说明你遇到了什么反爬问题、数据清洗的难点,以及你是怎么解决的。这能展示你解决问题的能力。
6.3 答辩准备(如果需要进行演示)
- 讲一个好故事:不要平铺直叙地讲代码。用“我们好奇……于是爬取了数据……清洗后发现……通过可视化验证了……最终得出结论……”这样的叙事线来串联。
- 突出重点:重点展示你的设计思路(为什么选这个数据源、为什么这样设计可视化)、技术亮点(如何应对反爬、如何进行特征工程)、和核心发现(那一两个最有趣的结论)。
- 准备Q&A:提前想想老师可能会问什么。“如果数据量增大10倍,你的程序需要如何优化?”(答:引入数据库、分布式爬虫框架如Scrapy)。“你的热度分数公式为什么这样设计?有没有其他设计方式?”(答:解释公式的权衡,可以提及其他如加权平均等思路)。
把这个项目当作一个真正的产品来打磨,从代码、数据、可视化到文档,每一个细节都体现着你的专业度和用心程度。当你交上去的是一份结构清晰、代码健壮、图表精美、分析深入、文档齐全的“项目包”时,高分自然水到渠成。这份作业的价值,远不止一个分数,它更是你数据分析能力的一次完整演练和有力证明。
本文还有配套的精品资源,点击获取