1. 为什么我们要爬取豆瓣电影Top250?

每次看到豆瓣电影Top250的榜单,你是不是也和我一样,心里会冒出很多问号?比如,评分最高的电影是哪部?评价人数最多的电影是不是评分也最高?哪个国家的电影在榜单里最受欢迎?这些电影都是什么类型的?以前,要回答这些问题,我们只能一部一部地手动记录,或者凭感觉猜测,既费时又不准确。

现在好了,我们有了Python这个“万能工具”。今天,我就带你一起,用Python写一个程序,自动把豆瓣电影Top250的数据“搬”到我们自己的电脑里。这可不是简单的复制粘贴,而是让程序像一个小机器人,自动浏览网页、识别信息、整理成表格。更酷的是,我们不仅要拿到数据,还要用图表把它“画”出来,让数据自己“说话”,看看里面藏着哪些我们没注意到的秘密。

整个过程,我会假设你是一个刚接触Python不久的朋友,可能对“爬虫”这个词有点陌生,甚至有点怕。别担心,我会把每一步都掰开揉碎了讲,从安装软件到运行代码,再到画出漂亮的图表,保证你能跟着做下来。你不需要是编程高手,只要有好奇心,会复制粘贴代码,就能完成这个有趣的项目。最终,你不仅能得到一份专属的电影数据分析报告,还能掌握一套非常实用的数据获取和处理技能,以后想分析其他网站的数据,思路也是一样的。

2. 动手前的准备工作:搭建你的工具箱

工欲善其事,必先利其器。在开始写代码之前,我们需要把“战场”布置好。这个过程很简单,就像给新手机安装几个必备的APP。

2.1 安装Python:获得你的“超能力”

Python是我们的核心工具,它就像一把瑞士军刀,功能强大又容易上手。首先,你需要确认电脑上是否已经安装了Python。打开你的命令行工具(Windows上是“命令提示符”或PowerShell,Mac或Linux上是“终端”),输入 python --version 或者 python3 --version 并回车。

如果屏幕上显示类似 Python 3.8.10 这样的版本号,并且数字大于等于3.6,那么恭喜你,可以直接进入下一步。如果提示“找不到命令”,那就需要去安装了。我强烈推荐去Python的官方网站下载安装包,记得勾选“Add Python to PATH”这个选项(Windows用户尤其要注意),这样系统才能在任何地方找到Python命令。安装过程就像安装普通软件一样,一路“下一步”即可。

2.2 安装必备的“武器库”

有了Python,我们还需要几个专门的库来帮忙。你可以把它们想象成游戏里的不同技能:requests 负责和网站服务器“对话”,获取网页原始代码;BeautifulSoup4 是个“解析大师”,能把杂乱无章的HTML代码整理成我们看得懂的结构;pandas 是“数据处理专家”,能轻松地对数据进行排序、筛选和计算;matplotlibseaborn 则是两位“画家”,负责把枯燥的数字变成直观的图表。

安装它们只需要一行命令。再次打开你的命令行工具,输入下面的命令并回车:

pip install requests beautifulsoup4 pandas matplotlib seaborn openpyxl

这里多了一个 openpyxl,它是为了让我们能更好地生成和读写Excel文件。输入命令后,你会看到屏幕上开始滚动下载和安装的信息,稍等片刻,直到出现“Successfully installed”的字样,就表示所有“武器”都装备完毕了。如果遇到网络慢或者报错,可以尝试在命令后面加上 -i https://pypi.tuna.tsinghua.edu.cn/simple 来使用国内的镜像源加速下载。

3. 深入敌后:分析网页结构与爬取策略

写爬虫程序,有点像玩一个“找东西”的游戏。我们得先搞清楚,想要的东西藏在网页的哪个角落,有什么规律。直接打开浏览器,访问豆瓣电影Top250的页面(https://movie.douban.com/top250),然后按下F12键打开“开发者工具”,这是我们观察网页内部结构的“透视镜”。

3.1 找到数据的藏身之处

在开发者工具里,点击左上角那个鼠标箭头的图标,然后去页面上点击任意一部电影的区域。你会发现,右侧的代码区域会自动高亮显示对应的一部分HTML代码。仔细看,每一部电影的信息都被包裹在一个 <div class="item">...</div> 的标签里。这就是我们的目标单元!我们写程序就是要先找到所有的 class="item" 的div标签,然后再逐个从里面提取详细信息。

接下来,我们看看具体信息在哪。比如电影名称,它通常在一个 <span class="title"> 标签里;评分在 <span class="rating_num"> 里面;评价人数则在 class="star" 的div下的最后一个span标签里。导演、年份、国家等信息,则集中在一个 <p class=""> 的段落里,它们之间用斜杠 / 或换行符分隔。通过这样观察,我们就知道了该命令程序去“挖”哪些标签。

3.2 设计高效的爬取路线

豆瓣Top250一共有10页,每页25部电影。观察地址栏你会发现一个规律:第一页网址是 ?start=0,第二页是 ?start=25,第三页是 ?start=50……这个 start 参数就像页码指示器,每次增加25。所以,我们写一个循环,让 start 的值从0开始,每次加25,一直加到225,就能遍历所有10页了。

这里有个非常重要的细节:礼貌爬取。我们不能像轰炸机一样一秒内请求几十次页面,那样很容易被网站识别为恶意攻击,从而封锁我们的IP。因此,在爬取每一页之后,我们需要让程序“休息”一下,比如睡上1到2秒钟。这既是对网站服务器的尊重,也能保证我们稳定地拿到数据。我一般用 time.sleep(1) 来让程序暂停1秒,实测下来这个频率比较稳妥,很少会被反爬虫机制拦截。

4. 编写爬虫核心代码:一步步把数据抓回来

分析清楚了,现在开始动手写代码。我会把代码分成几个功能明确的函数,这样结构清晰,也方便你理解和修改。

4.1 打造单页数据抓取函数

我们先写一个函数,专门负责处理一页25部电影的数据抓取。这个函数是整个爬虫的“心脏”。

import requests
from bs4 import BeautifulSoup
import time

def get_movie_data(page_start):
    """
    爬取豆瓣电影Top250单页数据
    :param page_start: URL中的start参数,如0, 25, 50...
    :return: 包含当前页25部电影信息的列表,每部电影是一个子列表
    """
    # 1. 设置请求头,伪装成普通浏览器访问
    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"
    }

    # 2. 构造当前页的完整URL
    url = f"https://movie.douban.com/top250?start={page_start}&filter="

    # 3. 发送网络请求,获取网页内容
    # 这里增加了异常处理,网络不稳定时程序不会直接崩溃
    try:
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()  # 如果状态码不是200,会抛出异常
        response.encoding = 'utf-8'  # 设置编码,防止中文乱码
    except requests.RequestException as e:
        print(f"请求页面失败 (start={page_start}): {e}")
        return []  # 返回空列表,不影响其他页的抓取

    # 4. 使用BeautifulSoup解析HTML
    soup = BeautifulSoup(response.text, 'html.parser')

    # 5. 找到当前页所有电影项目
    movie_items = soup.find_all('div', class_='item')
    page_data = []  # 用来存储这一页的所有电影数据

    # 6. 遍历每一个电影项目,提取详细信息
    for item in movie_items:
        try:
            # 电影中文名(注意,有些电影可能有多个名字,这里取第一个)
            name = item.find('span', class_='title').text.strip()

            # 评分
            score = item.find('span', class_='rating_num').text.strip()

            # 评价人数,需要处理“xx万”这种格式
            comment_tag = item.find('div', class_='star').find_all('span')[-1]
            comment_text = comment_tag.text.replace('人评价', '').strip()
            if '万' in comment_text:
                comment_num = int(float(comment_text.replace('万', '')) * 10000)
            else:
                comment_num = int(comment_text)

            # 导演、主演、年份、地区、类型等信息都在一个p标签里
            bd = item.find('div', class_='bd')
            info_paragraph = bd.find('p', class_='').text.strip()

            # 这个段落信息比较乱,我们用换行符分割成多行处理
            info_lines = info_paragraph.split('\n')
            info_lines = [line.strip() for line in info_lines if line.strip()]  # 清理空行和空格

            # 通常第一行是导演和主演信息
            director_actor_line = info_lines[0].replace('\xa0', ' ')  # 替换掉网页中的特殊空格
            # 分割导演和主演
            if '主演:' in director_actor_line:
                director_part, actor_part = director_actor_line.split('主演:', 1)
                director = director_part.replace('导演:', '').strip()
                actor = actor_part.strip()
            else:
                # 有些电影可能没有列出主演
                director = director_actor_line.replace('导演:', '').strip()
                actor = '暂无'

            # 通常第二行是年份、地区、类型,用 / 分割
            if len(info_lines) > 1:
                details = info_lines[1].split('/')
                details = [d.strip() for d in details]
                # 确保有足够的信息,有些条目可能不完整
                year = details[0] if len(details) > 0 else '未知'
                area = details[1] if len(details) > 1 else '未知'
                genre = details[2] if len(details) > 2 else '未知'
            else:
                year, area, genre = '未知', '未知', '未知'

            # 把一部电影的所有信息打包成一个列表,添加到当前页数据中
            page_data.append([name, score, comment_num, director, actor, year, area, genre])

        except AttributeError as e:
            # 如果某个标签找不到,跳过这部电影,打印错误但不中断程序
            print(f"解析一部电影时出错,已跳过。错误信息: {e}")
            continue

    return page_data

这个函数里我加了 try...except 异常处理。因为网页结构可能会微调,或者某部电影信息缺失,有了异常处理,即使遇到一点小问题,程序也不会整个崩溃,只会跳过有问题的电影继续运行, robustness(健壮性)大大增强。

4.2 循环抓取所有页面

单页抓取函数写好之后,抓取全部10页就非常简单了,只需要一个循环。

def crawl_all_movies():
    """
    爬取所有10页,共250部电影的数据
    :return: 包含250部电影数据的二维列表
    """
    all_data = []
    total_pages = 10

    for page in range(total_pages):
        start = page * 25  # 计算每一页的start参数
        print(f'正在爬取第 {page + 1} 页 (start={start})...')
        one_page_data = get_movie_data(start)
        all_data.extend(one_page_data)  # 将本页数据添加到总列表中
        time.sleep(1.5)  # 每爬完一页,礼貌地等待1.5秒

    print(f'所有页面爬取完成!共获取 {len(all_data)} 条电影数据。')
    return all_data

运行 crawl_all_movies() 函数,你会看到控制台一页一页地打印进度。等待一两分钟,所有数据就静静地躺在 all_data 这个变量里了。这个过程是不是有点像在看一个机器人帮你勤奋地收集资料?

5. 数据清洗与保存:让数据变得规整可用

爬取下来的原始数据可能有些小瑕疵,比如前后空格、格式不统一等。直接使用可能会影响后续分析,所以我们先做个简单的“大扫除”,然后保存到文件里。

5.1 使用Pandas进行数据清洗

Pandas是处理表格数据的利器。我们先把列表数据转换成Pandas的DataFrame,它看起来就像一个Excel表格,操作起来非常方便。

import pandas as pd

def clean_and_save_data(movie_list, excel_filename='douban_top250.xlsx'):
    """
    清洗数据并保存到Excel
    :param movie_list: crawl_all_movies()返回的原始数据列表
    :param excel_filename: 要保存的Excel文件名
    :return: 清洗后的DataFrame
    """
    # 定义表格的列名
    columns = ['电影名称', '评分', '评价人数', '导演', '主演', '上映年份', '国家/地区', '类型']

    # 将列表转换为DataFrame
    df = pd.DataFrame(movie_list, columns=columns)

    # 数据清洗
    print("正在进行数据清洗...")
    # 1. 确保评分是数值类型
    df['评分'] = pd.to_numeric(df['评分'], errors='coerce')
    # 2. 确保评价人数是整数类型
    df['评价人数'] = pd.to_numeric(df['评价人数'], errors='coerce').astype('Int64')
    # 3. 处理“上映年份”列,只提取数字年份(有些条目可能包含月份)
    df['上映年份'] = df['上映年份'].str.extract(r'(\d{4})').fillna('未知')
    # 4. 去除“电影名称”列可能存在的首尾空格
    df['电影名称'] = df['电影名称'].str.strip()

    # 保存到Excel
    with pd.ExcelWriter(excel_filename, engine='openpyxl') as writer:
        df.to_excel(writer, sheet_name='Top250', index=False)
        # 自动调整列宽,让Excel看起来更舒服
        worksheet = writer.sheets['Top250']
        for column in worksheet.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                try:
                    if len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
            adjusted_width = min(max_length + 2, 50)  # 设置最大宽度为50
            worksheet.column_dimensions[column_letter].width = adjusted_width

    print(f"数据已清洗并保存至文件:{excel_filename}")
    return df

# 使用示例
# all_movies = crawl_all_movies()
# df_movies = clean_and_save_data(all_movies)

清洗完成后,你的目录下会生成一个 douban_top250.xlsx 文件,用Excel或WPS打开它,一份干净整齐的电影榜单就诞生了!但这只是开始,好戏还在后头。

6. 让数据开口说话:多维度可视化分析

拿到数据表格,我们可以用Matplotlib和Seaborn来画图。图表能瞬间揭示数字背后的规律,比单纯看表格直观得多。我们来问数据几个问题,并用图表来回答。

6.1 评分分布:高分电影有多“卷”?

首先,我们好奇这250部顶尖电影的评分都集中在哪个区间。是9分以上神仙打架,还是8.5分左右扎堆?

import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体,防止图表出现乱码(根据你的系统调整字体路径)
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

def plot_rating_distribution(df):
    """
    绘制评分分布直方图与密度曲线
    """
    plt.figure(figsize=(12, 6))

    # 绘制直方图
    plt.subplot(1, 2, 1)
    plt.hist(df['评分'], bins=20, edgecolor='black', alpha=0.7, color='skyblue')
    plt.title('豆瓣Top250电影评分分布直方图', fontsize=14)
    plt.xlabel('评分', fontsize=12)
    plt.ylabel('电影数量', fontsize=12)
    plt.grid(axis='y', alpha=0.3)

    # 绘制核密度估计曲线
    plt.subplot(1, 2, 2)
    sns.kdeplot(df['评分'], fill=True, color='orange', alpha=0.6)
    plt.title('评分密度曲线', fontsize=14)
    plt.xlabel('评分', fontsize=12)
    plt.ylabel('密度', fontsize=12)
    plt.grid(alpha=0.3)

    plt.tight_layout()
    plt.savefig('rating_distribution.png', dpi=300, bbox_inches='tight')
    plt.show()

    # 打印一些统计信息
    print("评分描述性统计:")
    print(df['评分'].describe())
    print(f"\n评分高于9.0的电影有 {len(df[df['评分'] >= 9.0])} 部")

运行这个函数,你会看到两张并排的图。直方图能清晰显示在各个分数段有多少部电影,而密度曲线则平滑地展示了评分的集中趋势。从我的分析结果来看,绝大多数电影评分集中在8.5到9.2分之间,9分以上的“神作”并不多,符合“金字塔尖”的规律。

6.2 评价人数 vs 评分:叫好和叫座的关系

我们常听说“叫好不叫座”或者“票房冠军口碑一般”。在豆瓣Top250里,评分和评价人数(可以近似看作“热度”或“知名度”)有什么关系呢?我们用散点图来看看。

def plot_rating_vs_comments(df):
    """
    绘制评分与评价人数的散点图,观察相关性
    """
    plt.figure(figsize=(10, 6))
    # 为了散点图更清晰,我们对评价人数取对数处理,因为人数差异可能非常大(从几万到几百万)
    plt.scatter(df['评分'], np.log10(df['评价人数']), alpha=0.6, c='green', edgecolors='w', s=50)

    # 添加趋势线
    z = np.polyfit(df['评分'], np.log10(df['评价人数']), 1)
    p = np.poly1d(z)
    plt.plot(df['评分'], p(df['评分']), "r--", linewidth=2, label='趋势线')

    plt.title('电影评分 vs 评价人数(对数)', fontsize=14)
    plt.xlabel('评分', fontsize=12)
    plt.ylabel('评价人数(对数尺度)', fontsize=12)
    plt.legend()
    plt.grid(alpha=0.3)
    plt.tight_layout()
    plt.savefig('rating_vs_popularity.png', dpi=300, bbox_inches='tight')
    plt.show()

    # 计算相关系数
    correlation = df['评分'].corr(np.log10(df['评价人数']))
    print(f"评分与评价人数(对数)的相关系数为: {correlation:.3f}")
    if correlation > 0:
        print("趋势:总体上看,评分越高的电影,评价人数也倾向于更多(正相关)。")

这个图非常有意思。你会发现,虽然整体趋势是评分高的电影评价人数也多(右上角密集),但左下角也存在一些评分相对较低(但也在8.5以上)且评价人数极少的“冷门佳作”。同时,右上角那些评分和热度都极高的,就是真正的“经典中的经典”了。

6.3 国家/地区与电影类型分析:谁才是榜单霸主?

Top250里,哪个国家贡献了最多的经典电影?哪种类型的电影最受评委青睐?我们用条形图和饼图来揭示。

def plot_country_and_genre_analysis(df):
    """
    分析电影的国家/地区和类型分布
    """
    fig, axes = plt.subplots(1, 2, figsize=(14, 6))

    # 1. 国家/地区分布(取前10)
    country_counts = df['国家/地区'].str.split(' ').explode().value_counts().head(10)
    axes[0].barh(country_counts.index, country_counts.values, color='lightcoral')
    axes[0].set_title('电影数量最多的国家/地区(Top10)', fontsize=14)
    axes[0].set_xlabel('电影数量', fontsize=12)
    for i, v in enumerate(country_counts.values):
        axes[0].text(v + 0.5, i, str(v), va='center')

    # 2. 电影类型分布(取前10)
    # 一部电影可能有多个类型,用explode方法拆分
    genre_counts = df['类型'].str.split(' ').explode().value_counts().head(10)
    axes[1].pie(genre_counts.values, labels=genre_counts.index, autopct='%1.1f%%', startangle=90)
    axes[1].set_title('电影类型分布(Top10)', fontsize=14)

    plt.tight_layout()
    plt.savefig('country_genre_analysis.png', dpi=300, bbox_inches='tight')
    plt.show()

    print("\n国家/地区分布洞察:")
    print(f"  榜单中电影涉及的国家/地区数量: {df['国家/地区'].str.split(' ').explode().nunique()}")
    print(f"  排名第一的国家是: {country_counts.index[0]},共有 {country_counts.values[0]} 部电影上榜。")

    print("\n电影类型洞察:")
    print(f"  所有电影共涉及 {df['类型'].str.split(' ').explode().nunique()} 种类型标签。")
    print(f"  最常见的类型是: {genre_counts.index[0]},出现了 {genre_counts.values[0]} 次。")

从分析结果来看,美国电影在数量上通常占据显著优势,这反映了其全球电影工业的影响力。而在类型上,“剧情”片毫无疑问是绝对的主流,这与Top250偏向艺术性和思想深度的评选标准是吻合的。“爱情”、“喜剧”、“犯罪”、“动画”等类型也紧随其后,构成了榜单的多样性。

6.4 导演作品数量排行:谁是“霸榜”之王?

最后,我们看看哪些导演是这份榜单的“常客”,他们的多部作品都获得了认可。

def plot_director_analysis(df):
    """
    分析导演上榜作品数量
    """
    # 有些电影有多个导演,这里进行拆分并计数
    director_series = df['导演'].str.split('/').explode().str.strip()
    director_counts = director_series.value_counts().head(15)  # 取前15位

    plt.figure(figsize=(10, 8))
    bars = plt.barh(director_counts.index, director_counts.values, color='mediumseagreen')
    plt.title('导演上榜作品数量排行(Top15)', fontsize=16)
    plt.xlabel('上榜电影数量', fontsize=12)
    plt.gca().invert_yaxis()  # 让最多的显示在最上面

    # 在条形末端添加数量标签
    for bar in bars:
        width = bar.get_width()
        plt.text(width + 0.1, bar.get_y() + bar.get_height()/2, f'{int(width)}', ha='left', va='center')

    plt.tight_layout()
    plt.savefig('director_ranking.png', dpi=300, bbox_inches='tight')
    plt.show()

    print("导演洞察:")
    print(f"  共有 {director_series.nunique()} 位导演的作品上榜。")
    print(f"  上榜作品最多的导演是: {director_counts.index[0]},共有 {director_counts.values[0]} 部作品。")

这个排行榜往往能揭示那些被影史铭记的大师,比如宫崎骏、克里斯托弗·诺兰、史蒂文·斯皮尔伯格等,他们常常有多部作品入选。这对于影迷来说,也是一个绝佳的“补片指南”。

7. 把分析整合成一个完整项目

到这里,爬取、清洗、分析、可视化的核心环节我们都完成了。在实际操作中,我习惯把这些功能封装在一个主函数里,并添加一些交互和错误处理,让整个脚本更友好、更健壮。

import numpy as np

def main():
    """主函数,串联整个流程"""
    print("=" * 50)
    print("豆瓣电影Top250数据爬取与分析系统")
    print("=" * 50)

    choice = input("是否重新爬取数据?(y/n, 直接回车使用现有数据): ").lower()

    if choice == 'y':
        print("开始爬取数据,请耐心等待...")
        raw_data = crawl_all_movies()
        df = clean_and_save_data(raw_data)
        print("数据爬取与保存完成!")
    else:
        try:
            # 尝试读取之前保存的Excel文件
            df = pd.read_excel('douban_top250.xlsx')
            print("已加载本地数据文件。")
        except FileNotFoundError:
            print("未找到本地数据文件,开始爬取新数据...")
            raw_data = crawl_all_movies()
            df = clean_and_save_data(raw_data)

    print(f"\n数据概览:")
    print(f"  总记录数: {len(df)}")
    print(f"  数据列: {', '.join(df.columns)}")
    print(f"  评分范围: {df['评分'].min():.1f} - {df['评分'].max():.1f}")
    print(f"  评价人数范围: {df['评价人数'].min():,} - {df['评价人数'].max():,}")

    # 开始可视化分析
    input("\n按回车键开始生成评分分布图...")
    plot_rating_distribution(df)

    input("\n按回车键开始生成评分与热度关系图...")
    plot_rating_vs_comments(df)

    input("\n按回车键开始生成国家与类型分析图...")
    plot_country_and_genre_analysis(df)

    input("\n按回车键开始生成导演排行榜...")
    plot_director_analysis(df)

    print("\n" + "=" * 50)
    print("所有分析已完成!图表已保存为PNG图片。")
    print("你可以打开 'douban_top250.xlsx' 查看详细数据。")
    print("=" * 50)

if __name__ == '__main__':
    main()

运行这个 main() 函数,程序会引导你选择是重新爬取数据还是使用已有的数据文件,然后一步步生成所有图表。整个过程就像在和一个数据分析助手对话,非常直观。

写完这个项目,我最深的体会是,技术最大的魅力在于它能将繁琐重复的工作自动化,并赋予我们洞察世界的全新视角。爬虫和数据分析本身并不复杂,关键在于耐心和细心:耐心分析网页结构,细心处理数据中的各种边界情况。这个豆瓣Top250项目是一个绝佳的起点,它涉及的请求、解析、清洗、分析、可视化,是数据科学中最核心的流程。你可以举一反三,用类似的思路去分析其他你感兴趣的榜单,比如豆瓣读书Top250、音乐Top榜,甚至是电商网站的商品评论。当你掌握了这套方法,互联网上的公开数据,就成了一座等待你挖掘的宝库。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐