豆瓣电影 Top250 爬虫与数据可视化大屏:开发演进日志

本文档记录了“豆瓣电影 Top250 数据大屏项目”从最初的简单爬虫脚本,经过数次迭代与重构,最终演变为全自动 Web 交互式数据大屏的完整开发过程。

引入

大一的我因为还没有学习爬虫和数据分析,在听说有这个项目后,于是我决定先自学一下,我用一天肝了两个实战项目

1.小红书评论爬取

我的实战项目参考:zheco68-collab/-: 小红书爬虫实战

2.携程景点评论爬取

我的实战项目参考:zheco68-collab/-_DrissionPage: 基于DrissionPage的携程景点评论实战爬取

经过两个实战项目的沉淀,我大致懂了爬虫的逻辑, 1.通过模拟浏览器或者真实浏览器对真人操作经行模拟 2.分析接口字段和json 3.用数据结构对接口数据进行存储 4.保存到csv或者要使用的文件中,值得一提的是下面我会将爬出的数据保存到html中

阶段一:基础数据抓取与持久化 (CSV)

目标:获取豆瓣 Top250 的基础数据,并保存为本地可读文件。 过程

  1. 初始代码分析:项目最初基于 requestsBeautifulSoup,只能将数据打印在控制台。

    """
     获取时光影评电影评分排行榜
    """
    import requests
    from bs4 import BeautifulSoup
    from datetime import datetime, timedelta
    ​
    ​
    # 获得指定开始排行的电影url
    def get_url(root_url,start):
        return root_url+"?start="+str(start)+"&filter="
    ​
    def get_review(page_url):
        movies_list=[]
        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'
        }
        response=requests.get(page_url, headers=headers)
        soup=BeautifulSoup(response.text,"lxml")
        soup=soup.find('ol','grid_view')
        for tag_li in soup.find_all('li'):
            dict={}
            dict['rank']=tag_li.find('em').string
            dict['name']=tag_li.find_all('span','title')[0].string
            dict['score']=tag_li.find('span','rating_num').string
            if(tag_li.find('span','inq')):
                dict['desc']=tag_li.find('span','inq').string
            movies_list.append(dict)
        return movies_list
    ​
    if __name__ == "__main__":
        root_url="https://movie.douban.com/top250"
        start=0
        while(start<250):
            movies_list=get_review(get_url(root_url,start))
            for movie_dict in movies_list:
                print('电影排名:'+movie_dict['rank'])
                print('电影名称:'+movie_dict.get('name'))
                print('电影评分:'+movie_dict.get('score'))
                print('电影评词:'+movie_dict.get('desc','无评词'))
                print('------------------------------------------------------')
            start+=25

  2. 引入 CSV 模块:为了进行后续分析,增加了 csv 模块,将抓取的数据写入本地文件。

  3. 解决乱码问题:在写入文件时,特意指定了 encoding='utf-8-sig',确保在使用 Microsoft Excel 打开 CSV 文件时不会出现中文乱码。

阶段二:引入 Pandas 与基础静态图表 (Matplotlib)

目标:对抓取到的数据进行简单的统计分析,并生成直观的图表。 过程

  1. 扩展数据维度:为了让图表不至于单调,在爬虫逻辑中增加了对“上映年份 (Year)”的提取。

  2. 生成静态图表:引入 pandas 进行数据清洗和聚合,使用 matplotlib 绘制了“评分分布柱状图”和“年代排行条形图”。

  3. 踩坑与修复 (KeyError):在此阶段遇到了著名的 KeyError: 'year' 报错。原因是旧版 CSV 缺失该列,且中英文表头读取不一致。通过统一内部使用英文表头(写入和读取对齐)并加入容错逻辑,彻底解决了历史遗留数据导致的崩溃问题。

  4. 中文字体适配:加入了针对 Windows/macOS/Linux 的跨平台中文字体兼容代码,防止 Matplotlib 图表中的中文变成“方块”。

    """
     获取豆瓣电影 Top250,保存为 CSV,并进行数据分析图表生成
    """
    import requests
    from bs4 import BeautifulSoup
    import csv
    import pandas as pd
    import matplotlib.pyplot as plt
    import platform
    import os
    ​
    # --- 1. 爬虫与数据采集部分 ---
    ​
    def get_url(root_url, start):
        return f"{root_url}?start={start}&filter="
    ​
    def get_review(page_url):
        movies_list = []
        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'
        }
        response = requests.get(page_url, headers=headers)
        soup = BeautifulSoup(response.text, "lxml")
        soup = soup.find('ol', 'grid_view')
        
        if not soup:
            return movies_list
    ​
        for tag_li in soup.find_all('li'):
            movie = {}
            movie['rank'] = tag_li.find('em').string
            movie['name'] = tag_li.find_all('span', 'title')[0].string
            movie['score'] = tag_li.find('span', 'rating_num').string
            
            # 尝试抓取年份
            bd_p = tag_li.find('div', 'bd').find('p')
            if bd_p:
                texts = bd_p.text.strip().split('\n')
                if len(texts) >= 2:
                    # 豆瓣的年份信息通常在第二行,用 '/' 分隔
                    movie['year'] = texts[1].split('/')[0].strip()
                else:
                    movie['year'] = '未知'
            else:
                movie['year'] = '未知'
                
            if tag_li.find('span', 'inq'):
                movie['desc'] = tag_li.find('span', 'inq').string
            else:
                movie['desc'] = '无评词'
                
            movies_list.append(movie)
        return movies_list
    ​
    def scrape_and_save(csv_filename):
        root_url = "https://movie.douban.com/top250"
        start = 0
        
        print("开始爬取数据...")
        with open(csv_filename, mode='w', encoding='utf-8-sig', newline='') as f:
            fieldnames = ['rank', 'name', 'year', 'score', 'desc']
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            # 修复:写入英文表头,使得 Pandas 能够通过 'year' 和 'score' 正确读取列
            writer.writeheader()
    ​
            while start < 250:
                movies_list = get_review(get_url(root_url, start))
                for movie_dict in movies_list:
                    writer.writerow(movie_dict)
                start += 25
                print(f"已获取 {start} 条数据...")
                
        print(f"爬取完成!数据已保存至 {csv_filename}\n")
    ​
    ​
    # --- 2. 数据分析与图表生成部分 ---
    ​
    def set_chinese_font():
        """设置 matplotlib 的中文字体,防止图表中的中文显示为方块"""
        system = platform.system()
        if system == 'Windows':
            plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # Windows系统
        elif system == 'Darwin':
            plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC'] # macOS系统
        else:
            plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei'] # Linux系统
        plt.rcParams['axes.unicode_minus'] = False # 正常显示负号
    ​
    def analyze_and_plot(csv_filename):
        print("开始进行数据分析并生成多维图表...")
        
        # 1. 读取 CSV 数据
        try:
            df = pd.read_csv(csv_filename)
        except FileNotFoundError:
            print("未找到 CSV 文件,请先执行爬虫获取数据!")
            return
    ​
        # 修复:增加兼容性处理,如果读取到包含“年份”的中文字段,自动重命名为英文字段
        if '年份' in df.columns:
            df.rename(columns={'排名': 'rank', '电影名称': 'name', '年份': 'year', '评分': 'score', '评词': 'desc'}, inplace=True)
    ​
        # 检查数据中是否包含 'year' 列,防止读取了旧版 CSV 导致崩溃
        if 'year' not in df.columns:
            print("\n 错误:在 CSV 文件中没有找到 'year' 列!")
            return
    ​
        # 清洗数据:提取年份中的纯数字(去掉可能的地区后缀),并将评分转为浮点数
        df['year'] = df['year'].astype(str).str.extract(r'(\d{4})')
        df['year'] = pd.to_numeric(df['year'], errors='coerce') # 转为数字以方便区间划分
        df['score'] = pd.to_numeric(df['score'])
        
        # 过滤掉无法转换为数字的无效年份数据
        df = df.dropna(subset=['year'])
        
        set_chinese_font()
        
        # 应用内置的绘图风格作为基底
        plt.style.use('ggplot')
        
        # 创建一个 2行2列 的画板,设置画板背景颜色和更宽敞的尺寸
        fig, axes = plt.subplots(2, 2, figsize=(20, 14), facecolor='#F4F6F9')
        # 添加全局大标题
        fig.suptitle('豆瓣电影 Top250 多维度数据分析报告', fontsize=28, fontweight='bold', color='#2C3E50', y=0.98)
    ​
        # ---------------------------------------------------------
        # 图表 1: 豆瓣Top250电影评分分布 (柱状图 - 左上)
        # ---------------------------------------------------------
        ax1 = axes[0, 0]
        score_counts = df['score'].value_counts().sort_index() 
        colors_left = plt.cm.Blues([0.4 + 0.6 * i / len(score_counts) for i in range(len(score_counts))])
        
        bars1 = ax1.bar(score_counts.index.astype(str), score_counts.values, 
                           color=colors_left, width=0.6, zorder=3, edgecolor='none')
                           
        ax1.set_title('豆瓣 Top250:电影评分分布', fontsize=18, color='#34495E', pad=15, fontweight='bold')
        ax1.set_xlabel('电影评分', fontsize=14, color='#7F8C8D', fontweight='bold')
        ax1.set_ylabel('电影数量(部)', fontsize=14, color='#7F8C8D', fontweight='bold')
        ax1.tick_params(axis='x', rotation=45, colors='#34495E', labelsize=12)
        ax1.tick_params(axis='y', colors='#34495E', labelsize=12)
        
        ax1.set_facecolor('#FFFFFF')
        ax1.grid(axis='y', linestyle='--', alpha=0.5, color='#BDC3C7', zorder=0)
        ax1.grid(axis='x', visible=False) 
        
        for spine in ax1.spines.values():
            spine.set_visible(False)
            
        for bar in bars1:
            height = bar.get_height()
            ax1.annotate(f'{height}',
                        xy=(bar.get_x() + bar.get_width() / 2, height),
                        xytext=(0, 4), 
                        textcoords="offset points",
                        ha='center', va='bottom',
                        fontsize=12, color='#2C3E50', fontweight='bold')
    ​
        # ---------------------------------------------------------
        # 图表 2: 豆瓣Top250电影上榜最多的年份 Top15 (水平条形图 - 右上)
        # ---------------------------------------------------------
        ax2 = axes[0, 1]
        year_counts = df['year'].value_counts().head(15).sort_values() 
        colors_right = plt.cm.Oranges([0.8 - 0.4 * i / len(year_counts) for i in range(len(year_counts))])
        
        bars2 = ax2.barh(year_counts.index.astype(int).astype(str), year_counts.values, 
                             color=colors_right, height=0.6, zorder=3, edgecolor='none')
                             
        ax2.set_title('豆瓣 Top250:上榜数量最多的年份 Top15', fontsize=18, color='#34495E', pad=15, fontweight='bold')
        ax2.set_xlabel('电影数量(部)', fontsize=14, color='#7F8C8D', fontweight='bold')
        ax2.set_ylabel('上映年份', fontsize=14, color='#7F8C8D', fontweight='bold')
        ax2.tick_params(axis='both', colors='#34495E', labelsize=12)
        
        ax2.set_facecolor('#FFFFFF')
        ax2.grid(axis='x', linestyle='--', alpha=0.5, color='#BDC3C7', zorder=0)
        ax2.grid(axis='y', visible=False)
        
        for spine in ax2.spines.values():
            spine.set_visible(False)
        
        for bar in bars2:
            width = bar.get_width()
            ax2.text(width + 0.3, bar.get_y() + bar.get_height()/2, 
                         f'{int(width)}', 
                         va='center', fontsize=12, color='#2C3E50', fontweight='bold')
    ​
        # ---------------------------------------------------------
        # 图表 3: 电影所属年代分布占比 (甜甜圈环形图 - 左下)
        # ---------------------------------------------------------
        ax3 = axes[1, 0]
        
        # 划分年代区间
        bins = [1900, 1980, 1990, 2000, 2010, 2030]
        labels = ['80年代前', '80年代', '90年代', '00年代', '10年代及以后']
        df['decade'] = pd.cut(df['year'], bins=bins, labels=labels, right=False)
        decade_counts = df['decade'].value_counts()
        
        # 配色方案,使用柔和的色调
        pie_colors = ['#FF9999', '#66B2FF', '#99FF99', '#FFCC99', '#c2c2f0']
        
        # 绘制饼图
        wedges, texts, autotexts = ax3.pie(
            decade_counts.values, 
            labels=decade_counts.index, 
            autopct='%1.1f%%',
            startangle=140, 
            colors=pie_colors,
            pctdistance=0.85, # 调整百分比数字的位置
            textprops=dict(color="w", weight="bold", fontsize=12),
            wedgeprops=dict(width=0.4, edgecolor='w') # 设置 width 实现环形图效果
        )
        
        # 调整外部标签的样式
        for text in texts:
            text.set_color('#34495E')
            text.set_fontsize(13)
            text.set_weight('bold')
    ​
        ax3.set_title('豆瓣 Top250:经典电影年代分布', fontsize=18, color='#34495E', pad=15, fontweight='bold')
        ax3.set_facecolor('#FFFFFF')
        
        # ---------------------------------------------------------
        # 图表 4: 评分随时间变化的平滑趋势 (折线分布图 - 右下)
        # ---------------------------------------------------------
        ax4 = axes[1, 1]
        
        # 按年份分组,计算平均评分和每年上榜数量
        year_group = df.groupby('year').agg({'score': ['mean', 'count']}).reset_index()
        year_group.columns = ['year', 'avg_score', 'count']
        
        # 过滤掉数量太少的年份(比如只有1部电影的年份),避免平均分被个例带偏,使趋势更具代表性
        valid_years = year_group[year_group['count'] >= 2].sort_values('year')
        
        # 绘制平滑折线图(带有数据标记点)
        ax4.plot(valid_years['year'], valid_years['avg_score'], 
                 color='#8E44AD', marker='o', markersize=6, linestyle='-', linewidth=2, zorder=3)
                 
        # 填充折线图下方面积,增加视觉层次感
        ax4.fill_between(valid_years['year'], valid_years['avg_score'], 8.0, 
                         color='#8E44AD', alpha=0.1, zorder=2)
    ​
        ax4.set_title('豆瓣 Top250:高上榜率年份的平均评分趋势', fontsize=18, color='#34495E', pad=15, fontweight='bold')
        ax4.set_xlabel('上映年份 (仅统计上榜2部及以上的年份)', fontsize=14, color='#7F8C8D', fontweight='bold')
        ax4.set_ylabel('平均评分', fontsize=14, color='#7F8C8D', fontweight='bold')
        
        # 设置 y 轴范围,让波动看起来更合理
        ax4.set_ylim(8.0, 10.0) 
        ax4.tick_params(axis='both', colors='#34495E', labelsize=12)
        
        ax4.set_facecolor('#FFFFFF')
        ax4.grid(axis='both', linestyle='--', alpha=0.5, color='#BDC3C7', zorder=0)
        
        for spine in ax4.spines.values():
            spine.set_visible(False)
    ​
        # ---------------------------------------------------------
        # 调整布局与保存
        # ---------------------------------------------------------
        plt.tight_layout(rect=[0.02, 0.02, 0.98, 0.95]) # 调整外边距
        
        output_img = 'douban_analysis_charts_multidimensional.png'
        plt.savefig(output_img, dpi=300, bbox_inches='tight', facecolor=fig.get_facecolor())
        print(f"多维图表已生成并保存为精美图片:{output_img}")
        
        plt.show()
    ​
    if __name__ == "__main__":
        csv_file = "douban_top250.csv"
        
        # 执行步骤1:爬取并保存 CSV
        scrape_and_save(csv_file)
        
        # 执行步骤2:读取 CSV 并生成数据图表
        analyze_and_plot(csv_file)

阶段三:视觉重构与 Web 大屏化 (ECharts)

目标:解决 Matplotlib 图表“不够精美”、缺乏交互的问题。 过程

  1. 技术栈跃迁:决定抛弃传统的静态图片生成方式,转向前端数据可视化领域的标准库 Apache ECharts

  2. 构建 HTML 模板:使用 CSS Grid 布局设计了一个暗夜/明亮风格的现代 Dashboard 框架,包含圆角卡片、柔和阴影和渐变色设计。

  3. Python 与 JS 桥接:利用 Python 的 json 模块,将 Pandas 计算好的聚合数据(如各年代电影数量、评分平均值)转换为 JSON 格式,直接注入到 HTML 模板的 JavaScript 变量中。

  4. 自动化体验:加入 webbrowser 模块,使得 Python 脚本在处理完数据、生成 HTML 后,能够自动唤起用户的默认浏览器展示大屏,实现了“一键生成并预览”的流畅体验。

    """
     获取豆瓣电影 Top250,保存为 CSV,并进行数据分析图表生成
    """
    import requests
    from bs4 import BeautifulSoup
    import csv
    import pandas as pd
    import json
    import webbrowser
    import os
    ​
    # --- 1. 爬虫与数据采集部分 ---
    ​
    def get_url(root_url, start):
        return f"{root_url}?start={start}&filter="
    ​
    def get_review(page_url):
        movies_list = []
        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'
        }
        response = requests.get(page_url, headers=headers)
        soup = BeautifulSoup(response.text, "lxml")
        soup = soup.find('ol', 'grid_view')
        
        if not soup:
            return movies_list
    ​
        for tag_li in soup.find_all('li'):
            movie = {}
            movie['rank'] = tag_li.find('em').string
            movie['name'] = tag_li.find_all('span', 'title')[0].string
            movie['score'] = tag_li.find('span', 'rating_num').string
            
            # 提取评价人数 (新增逻辑)
            star_div = tag_li.find('div', 'star')
            if star_div:
                spans = star_div.find_all('span')
                movie['votes'] = '0'
                for span in spans:
                    if '人评价' in span.text:
                        # 剥离中文,只保留纯数字字符串
                        movie['votes'] = span.text.replace('人评价', '').strip()
                        break
            else:
                movie['votes'] = '0'
    ​
            # 尝试抓取年份
            bd_p = tag_li.find('div', 'bd').find('p')
            if bd_p:
                texts = bd_p.text.strip().split('\n')
                if len(texts) >= 2:
                    # 豆瓣的年份信息通常在第二行,用 '/' 分隔
                    movie['year'] = texts[1].split('/')[0].strip()
                else:
                    movie['year'] = '未知'
            else:
                movie['year'] = '未知'
                
            if tag_li.find('span', 'inq'):
                movie['desc'] = tag_li.find('span', 'inq').string
            else:
                movie['desc'] = '无评词'
                
            movies_list.append(movie)
        return movies_list
    ​
    def scrape_and_save(csv_filename):
        root_url = "https://movie.douban.com/top250"
        start = 0
        
        print("开始爬取数据...")
        with open(csv_filename, mode='w', encoding='utf-8-sig', newline='') as f:
            # 在表头字段列表中加入 'votes'
            fieldnames = ['rank', 'name', 'year', 'score', 'votes', 'desc']
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            # 修复:写入英文表头,使得 Pandas 能够通过 'year' 和 'score' 正确读取列
            writer.writeheader()
    ​
            while start < 250:
                movies_list = get_review(get_url(root_url, start))
                for movie_dict in movies_list:
                    writer.writerow(movie_dict)
                start += 25
                print(f"已获取 {start} 条数据...")
                
        print(f"爬取完成!数据已保存至 {csv_filename}\n")
    ​
    ​
    # --- 2. 数据分析与动态大屏生成部分 ---
    ​
    def generate_html_dashboard(csv_filename):
        print("开始进行数据分析并生成互动式 HTML 大屏...")
        
        # 1. 读取并清洗数据
        try:
            df = pd.read_csv(csv_filename)
        except FileNotFoundError:
            print("未找到 CSV 文件,请先执行爬虫获取数据!")
            return
    ​
        # 兼容中英文表头 (加入 '评价人数': 'votes')
        if '年份' in df.columns:
            df.rename(columns={'排名': 'rank', '电影名称': 'name', '年份': 'year', '评分': 'score', '评价人数': 'votes', '评词': 'desc'}, inplace=True)
    ​
        if 'year' not in df.columns:
            print("\n 错误:在 CSV 文件中没有找到 'year' 列!")
            return
    ​
        # 数据清洗
        df['year'] = df['year'].astype(str).str.extract(r'(\d{4})')
        df['year'] = pd.to_numeric(df['year'], errors='coerce')
        df['score'] = pd.to_numeric(df['score'])
        df['votes'] = pd.to_numeric(df['votes'], errors='coerce').fillna(0) # 将评价人数转换为数字格式
        df = df.dropna(subset=['year'])
    ​
        # 2. 核心数据聚合计算
        # [图表1数据] 评分分布
        score_counts = df['score'].value_counts().sort_index()
        score_dist_data = {
            'scores': score_counts.index.astype(str).tolist(),
            'counts': score_counts.values.tolist()
        }
    ​
        # [图表2数据] 上榜数量最多的年份 Top15
        year_counts = df['year'].value_counts().head(15).sort_values()
        year_top_data = {
            'years': year_counts.index.astype(int).astype(str).tolist(),
            'counts': year_counts.values.tolist()
        }
    ​
        # [图表3数据] 经典电影年代分布
        bins = [1900, 1980, 1990, 2000, 2010, 2030]
        labels = ['80年代前', '80年代', '90年代', '00年代', '10年代及以后']
        df['decade'] = pd.cut(df['year'], bins=bins, labels=labels, right=False)
        decade_counts = df['decade'].value_counts()
        decade_data = [{"value": int(v), "name": str(k)} for k, v in decade_counts.items() if pd.notna(k)]
    ​
        # [图表4数据] 高产年份平均评分趋势
        year_group = df.groupby('year').agg({'score': ['mean', 'count']}).reset_index()
        year_group.columns = ['year', 'avg_score', 'count']
        valid_years = year_group[year_group['count'] >= 2].sort_values('year')
        trend_data = {
            'years': valid_years['year'].astype(int).astype(str).tolist(),
            'avgScores': valid_years['avg_score'].round(2).tolist()
        }
        
        # [图表5数据 - 新增] 评价人数最多的 Top 50 电影
        top_votes_df = df.sort_values(by='votes', ascending=False).head(50)
        votes_data = {
            'names': top_votes_df['name'].tolist(),
            'votes': top_votes_df['votes'].tolist()
        }
    ​
        # 3. 构建 HTML 模板 (包含 ECharts 和 CSS)
        html_top = """<!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>豆瓣电影 Top250 多维数据大屏</title>
        <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
        <style>
            :root { --bg-color: #f4f7f6; --card-bg: #ffffff; --text-main: #2c3e50; --text-muted: #7f8c8d; --shadow: 0 8px 20px rgba(0, 0, 0, 0.05); --border-radius: 16px; }
            * { box-sizing: border-box; margin: 0; padding: 0; }
            body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: var(--bg-color); color: var(--text-main); padding: 24px; min-height: 100vh; }
            .dashboard-header { text-align: center; margin-bottom: 30px; padding: 20px 0; }
            .dashboard-header h1 { font-size: 32px; font-weight: 800; color: #1a252f; letter-spacing: 1px; margin-bottom: 8px; }
            .dashboard-header p { font-size: 16px; color: var(--text-muted); }
            .dashboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; max-width: 1400px; margin: 0 auto; }
            .chart-card { background-color: var(--card-bg); border-radius: var(--border-radius); box-shadow: var(--shadow); padding: 20px; transition: transform 0.3s ease, box-shadow 0.3s ease; }
            .chart-card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); }
            .chart-container { width: 100%; height: 380px; }
            /* 新增:跨两列的宽图表容器 */
            .full-width { grid-column: 1 / -1; }
            .tall-container { height: 450px; } 
            @media (max-width: 992px) { .dashboard-grid { grid-template-columns: 1fr; } }
        </style>
    </head>
    <body>
        <div class="dashboard-header">
            <h1>豆瓣电影 Top250 自动化数据大屏</h1>
            <p>Generated by Python Pandas & ECharts</p>
        </div>
        <div class="dashboard-grid">
            <div class="chart-card"><div id="chart-score-dist" class="chart-container"></div></div>
            <div class="chart-card"><div id="chart-year-top" class="chart-container"></div></div>
            <div class="chart-card"><div id="chart-decade-pie" class="chart-container"></div></div>
            <div class="chart-card"><div id="chart-score-trend" class="chart-container"></div></div>
            <!-- 新增:评价人数统计图 -->
            <div class="chart-card full-width"><div id="chart-votes-bar" class="chart-container tall-container"></div></div>
        </div>
        <script>
    """
    ​
        # 使用 json.dumps 将 Python 字典安全地转换为 JavaScript 对象
        js_data = f"""
            // 这里是 Python 自动计算并注入的真实数据
            const scoreDistData = {json.dumps(score_dist_data)};
            const yearTopData = {json.dumps(year_top_data)};
            const decadeData = {json.dumps(decade_data)};
            const trendData = {json.dumps(trend_data)};
            const votesData = {json.dumps(votes_data)};
        """
    ​
        html_bottom = """
            // 初始化图表 1 (柱状图)
            const chartScoreDist = echarts.init(document.getElementById('chart-score-dist'));
            chartScoreDist.setOption({
                title: { text: '电影评分分布情况', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
                xAxis: { type: 'category', data: scoreDistData.scores, axisLabel: { color: '#7F8C8D' }, axisLine: { lineStyle: { color: '#BDC3C7' } } },
                yAxis: { type: 'value', name: '电影数量 (部)', splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } },
                series: [{
                    name: '数量', type: 'bar', data: scoreDistData.counts,
                    itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#36D1DC' }, { offset: 1, color: '#5B86E5' }]) },
                    label: { show: true, position: 'top', color: '#34495E', fontWeight: 'bold' }
                }]
            });
    ​
            // 初始化图表 2 (条形图)
            const chartYearTop = echarts.init(document.getElementById('chart-year-top'));
            chartYearTop.setOption({
                title: { text: '上榜数量最多的年份 Top15', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
                xAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } },
                yAxis: { type: 'category', data: yearTopData.years, axisLabel: { color: '#7F8C8D', fontWeight: 'bold' }, axisLine: { lineStyle: { color: '#BDC3C7' } } },
                series: [{
                    name: '上榜数量', type: 'bar', data: yearTopData.counts,
                    itemStyle: { borderRadius: [0, 6, 6, 0], color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [{ offset: 0, color: '#FF8008' }, { offset: 1, color: '#FFC837' }]) },
                    label: { show: true, position: 'right', color: '#34495E', fontWeight: 'bold' }
                }]
            });
    ​
            // 初始化图表 3 (环形图)
            const chartDecadePie = echarts.init(document.getElementById('chart-decade-pie'));
            chartDecadePie.setOption({
                title: { text: '经典电影年代分布', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c}部 ({d}%)' },
                legend: { top: 'bottom', textStyle: { color: '#7F8C8D' } },
                color: ['#FF9A9E', '#FECFEF', '#A1C4FD', '#C2E9FB', '#D4FC79'],
                series: [{
                    name: '年代占比', type: 'pie', radius: ['45%', '70%'], avoidLabelOverlap: false,
                    itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
                    label: { show: true, formatter: '{b}\\n{d}%', fontWeight: 'bold', color: '#34495E' },
                    labelLine: { show: true, smooth: 0.2, length: 10, length2: 20 },
                    data: decadeData
                }]
            });
    ​
            // 初始化图表 4 (面积折线图)
            const chartScoreTrend = echarts.init(document.getElementById('chart-score-trend'));
            chartScoreTrend.setOption({
                title: { text: '高产年份平均评分趋势', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'axis' },
                grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
                xAxis: { type: 'category', boundaryGap: false, data: trendData.years, axisLine: { lineStyle: { color: '#BDC3C7' } } },
                yAxis: { type: 'value', min: 8.5, max: 9.3, splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } },
                series: [{
                    name: '平均评分', type: 'line', data: trendData.avgScores, smooth: true, symbolSize: 8,
                    itemStyle: { color: '#8E2DE2' },
                    areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(142, 45, 226, 0.4)' }, { offset: 1, color: 'rgba(74, 0, 224, 0.05)' }]) }
                }]
            });
            
            // 初始化图表 5 (评价人数统计柱状图 - 新增)
            const chartVotesBar = echarts.init(document.getElementById('chart-votes-bar'));
            chartVotesBar.setOption({
                title: { text: '评价人数统计柱状图 (Top 50)', left: 'center', textStyle: { color: '#34495E', fontSize: 20 } },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                grid: { left: '4%', right: '4%', bottom: '25%', containLabel: true }, // 增加 bottom 空间放置倾斜字体
                xAxis: { 
                    type: 'category', 
                    name: '电影名称',
                    nameLocation: 'middle',
                    nameGap: 60,
                    data: votesData.names, 
                    axisLabel: { 
                        color: '#2c3e50', 
                        rotate: 45, // 文字倾斜 45 度
                        interval: 0, // 强制显示所有电影名称
                        fontSize: 10
                    }, 
                    axisLine: { lineStyle: { color: '#BDC3C7' } } 
                },
                yAxis: { 
                    type: 'value', 
                    name: '评价人数 / 人', 
                    splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } 
                },
                series: [{
                    name: '评价人数', 
                    type: 'bar', 
                    data: votesData.votes,
                    itemStyle: { color: '#2980b9' } // 采用跟截图接近的经典蓝色
                }]
            });
    ​
            window.addEventListener('resize', () => {
                chartScoreDist.resize(); chartYearTop.resize(); chartDecadePie.resize(); chartScoreTrend.resize(); chartVotesBar.resize();
            });
        </script>
    </body>
    </html>
    """
    ​
        # 4. 拼装完毕,写入 HTML 文件
        final_html = html_top + js_data + html_bottom
        output_html = "douban_dashboard_auto.html"
        
        with open(output_html, "w", encoding="utf-8") as f:
            f.write(final_html)
            
        print(f" 动态大屏已成功生成!文件位置:{os.path.abspath(output_html)}")
        
        # 5. 自动唤起默认浏览器打开该页面
        try:
            webbrowser.open('file://' + os.path.abspath(output_html))
        except Exception as e:
            print(f"尝试打开浏览器失败,请手动双击打开 {output_html}。错误信息:{e}")
    ​
    if __name__ == "__main__":
        csv_file = "douban_top250.csv"
        
        # 执行步骤1:爬取并保存 CSV
        scrape_and_save(csv_file)
        
        # 执行步骤2:读取 CSV 并生成交互式网页
        generate_html_dashboard(csv_file)

阶段四:精细化数据挖掘与定制化图表

目标:响应需求,增加“评价人数”维度,并复刻特定的图表样式。 过程

  1. 正则精准提取:最初直接读取包含“人评价”的文本,但由于网页结构差异容易提取失败。重构了爬虫逻辑,引入 re (正则表达式) 模块,强行从 HTML 节点中剥离出纯数字的评价人数 (votes)。

  2. 宽幅图表开发:根据参考图片(横向宽幅、X轴文字倾斜 45 度的柱状图),在 CSS Grid 中增加了跨越两列的 .full-width 容器。

  3. ECharts 深度定制:配置了 rotate: 45interval: 0 以及蓝色的 itemStyle,完美复刻了“评价人数 Top 50 统计柱状图”。

    import os
    import requests  # 网络请求模块
    from lxml import etree  # 数据解析模块
    import pandas as pd  # 数据分析模块快
    import json
    import webbrowser
    import re
    ​
    # 全局变量定义
    excelFileName = "豆瓣电影top250数据.xlsx"
    csvFileName = "豆瓣电影top250数据.csv"
    ​
    headers = {  # 请求头信息
        'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.46'
    }
    ​
    ​
    # 功能函数,获取列表的第一个元素
    def get_first_text(list):
        try:
            return list[0].strip()  # 返回第一个字符串,除去两端的空格
        except:
            return ""  # 返回空字符串
    ​
    ​
    def check() -> bool:
        """
        判断当前的目录下是否存在目标文件,存在则无需重复爬取
        """
        if os.path.exists(csvFileName) and os.path.exists(excelFileName):
            print("已经存在文件:", excelFileName, "无需重复爬取,将直接使用现有数据生成图表。")
            return True
        else:
            print("未找到数据文件,开始网络爬取...")
            return False
    ​
    ​
    def getMovieData():
        """
        使用爬虫爬取豆瓣电影数据
        """
        # 增加了一列 "年份",以支持大屏图表的年代分析
        df = pd.DataFrame(columns=["序号", "标题", "链接", "导演", "年份", "评分", "评价人数", "简介"])
    ​
        # 使用列表生成式表示10个页面的地址
        urls = ['https://movie.douban.com/top250?start={}&filter='.format(str(i * 25)) for i in range(10)]
        count = 1  # 用来计数
        for url in urls:
            res = requests.get(url=url, headers=headers)  # 发起请求
            html = etree.HTML(res.text)  # 将返回的文本加工为可以解析的html
            lis = html.xpath('//*[@id="content"]/div/div[1]/ol/li')  # 获取每个电影的li元素
            # 解析数据
            for li in lis:
                title = get_first_text(li.xpath('./div/div[2]/div[1]/a/span[1]/text()'))  # 电影标题
                src = get_first_text(li.xpath('./div/div[2]/div[1]/a/@href'))  # 电影链接
                
                # 从 p 标签提取导演和年份
                info_texts = li.xpath('./div/div[2]/div[2]/p[1]/text()')
                dictor = get_first_text([info_texts[0]]) if len(info_texts) > 0 else ""  # 导演等信息
                year_info = get_first_text([info_texts[1]]) if len(info_texts) > 1 else "" # 包含年份信息的字符串
                # 清洗年份数据,去掉可能包含的国家/地区信息
                year = year_info.split('/')[0].strip() if year_info else "未知" 
                # 进一步正则提取纯数字年份
                year_match = re.search(r'(\d{4})', year)
                year = year_match.group(1) if year_match else "未知"
                
                score = get_first_text(li.xpath('./div/div[2]/div[2]/div/span[2]/text()'))  # 评分
                comment = get_first_text(li.xpath('./div/div[2]/div[2]/div/span[4]/text()'))  # 评价人数
                summary = get_first_text(li.xpath('./div/div[2]/div[2]/p[2]/span/text()'))  # 电影简介
                print(count, title, src, dictor, year, score, comment, summary)  # 输出
    ​
                df.loc[len(df.index)] = [count, title, src, dictor, year, score, comment, summary]
    ​
                count += 1
    ​
        df.to_excel(excelFileName, sheet_name="豆瓣电影top250数据", na_rep="")
        df.to_csv(csvFileName, index=False, encoding='utf-8-sig') # 加上 index=False 和中文编码
        print("文件已经生成!")
    ​
    ​
    def generate_html_dashboard():
        """
        [新增] 基于爬取的数据生成交互式 HTML 大屏
        """
        print("开始进行数据分析并生成互动式 HTML 大屏...")
        try:
            df = pd.read_csv(csvFileName)
        except FileNotFoundError:
            print("未找到 CSV 文件,无法生成大屏!")
            return
    ​
        # 统一列名以适配大屏渲染逻辑
        df.rename(columns={'标题': 'name', '年份': 'year', '评分': 'score', '评价人数': 'votes'}, inplace=True)
    ​
        # 数据深度清洗
        df['year'] = pd.to_numeric(df['year'], errors='coerce')
        df['score'] = pd.to_numeric(df['score'])
        df['votes'] = df['votes'].astype(str).str.extract(r'(\d+)').astype(float).fillna(0) # 剥离 "xxx人评价" 提取纯数字
        df = df.dropna(subset=['year'])
    ​
        # 核心数据聚合计算
        # [图表1数据] 评分分布
        score_counts = df['score'].value_counts().sort_index()
        score_dist_data = {
            'scores': score_counts.index.astype(str).tolist(),
            'counts': score_counts.values.tolist()
        }
    ​
        # [图表2数据] 上榜数量最多的年份 Top15
        year_counts = df['year'].value_counts().head(15).sort_values()
        year_top_data = {
            'years': year_counts.index.astype(int).astype(str).tolist(),
            'counts': year_counts.values.tolist()
        }
    ​
        # [图表3数据] 经典电影年代分布
        bins = [1900, 1980, 1990, 2000, 2010, 2030]
        labels = ['80年代前', '80年代', '90年代', '00年代', '10年代及以后']
        df['decade'] = pd.cut(df['year'], bins=bins, labels=labels, right=False)
        decade_counts = df['decade'].value_counts()
        decade_data = [{"value": int(v), "name": str(k)} for k, v in decade_counts.items() if pd.notna(k)]
    ​
        # [图表4数据] 高产年份平均评分趋势
        year_group = df.groupby('year').agg({'score': ['mean', 'count']}).reset_index()
        year_group.columns = ['year', 'avg_score', 'count']
        valid_years = year_group[year_group['count'] >= 2].sort_values('year')
        trend_data = {
            'years': valid_years['year'].astype(int).astype(str).tolist(),
            'avgScores': valid_years['avg_score'].round(2).tolist()
        }
        
        # [图表5数据] 评价人数最多的 Top 50 电影
        top_votes_df = df.sort_values(by='votes', ascending=False).head(50)
        votes_data = {
            'names': top_votes_df['name'].tolist(),
            'votes': top_votes_df['votes'].tolist()
        }
    ​
        # 构建 HTML 模板 (包含 ECharts 和 CSS)
        html_top = """<!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>豆瓣电影 Top250 多维数据大屏</title>
        <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
        <style>
            :root { --bg-color: #f4f7f6; --card-bg: #ffffff; --text-main: #2c3e50; --text-muted: #7f8c8d; --shadow: 0 8px 20px rgba(0, 0, 0, 0.05); --border-radius: 16px; }
            * { box-sizing: border-box; margin: 0; padding: 0; }
            body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: var(--bg-color); color: var(--text-main); padding: 24px; min-height: 100vh; }
            .dashboard-header { text-align: center; margin-bottom: 30px; padding: 20px 0; }
            .dashboard-header h1 { font-size: 32px; font-weight: 800; color: #1a252f; letter-spacing: 1px; margin-bottom: 8px; }
            .dashboard-header p { font-size: 16px; color: var(--text-muted); }
            .dashboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; max-width: 1400px; margin: 0 auto; }
            .chart-card { background-color: var(--card-bg); border-radius: var(--border-radius); box-shadow: var(--shadow); padding: 20px; transition: transform 0.3s ease, box-shadow 0.3s ease; }
            .chart-card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); }
            .chart-container { width: 100%; height: 380px; }
            .full-width { grid-column: 1 / -1; }
            .tall-container { height: 450px; } 
            @media (max-width: 992px) { .dashboard-grid { grid-template-columns: 1fr; } }
        </style>
    </head>
    <body>
        <div class="dashboard-header">
            <h1>豆瓣电影 Top250 自动化数据大屏</h1>
            <p>Generated by Python Pandas & ECharts</p>
        </div>
        <div class="dashboard-grid">
            <div class="chart-card"><div id="chart-score-dist" class="chart-container"></div></div>
            <div class="chart-card"><div id="chart-year-top" class="chart-container"></div></div>
            <div class="chart-card"><div id="chart-decade-pie" class="chart-container"></div></div>
            <div class="chart-card"><div id="chart-score-trend" class="chart-container"></div></div>
            <div class="chart-card full-width"><div id="chart-votes-bar" class="chart-container tall-container"></div></div>
        </div>
        <script>
    """
        js_data = f"""
            const scoreDistData = {json.dumps(score_dist_data)};
            const yearTopData = {json.dumps(year_top_data)};
            const decadeData = {json.dumps(decade_data)};
            const trendData = {json.dumps(trend_data)};
            const votesData = {json.dumps(votes_data)};
        """
        html_bottom = """
            const chartScoreDist = echarts.init(document.getElementById('chart-score-dist'));
            chartScoreDist.setOption({
                title: { text: '电影评分分布情况', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
                xAxis: { type: 'category', data: scoreDistData.scores, axisLabel: { color: '#7F8C8D' }, axisLine: { lineStyle: { color: '#BDC3C7' } } },
                yAxis: { type: 'value', name: '电影数量 (部)', splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } },
                series: [{
                    name: '数量', type: 'bar', data: scoreDistData.counts,
                    itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#36D1DC' }, { offset: 1, color: '#5B86E5' }]) },
                    label: { show: true, position: 'top', color: '#34495E', fontWeight: 'bold' }
                }]
            });
    ​
            const chartYearTop = echarts.init(document.getElementById('chart-year-top'));
            chartYearTop.setOption({
                title: { text: '上榜数量最多的年份 Top15', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
                xAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } },
                yAxis: { type: 'category', data: yearTopData.years, axisLabel: { color: '#7F8C8D', fontWeight: 'bold' }, axisLine: { lineStyle: { color: '#BDC3C7' } } },
                series: [{
                    name: '上榜数量', type: 'bar', data: yearTopData.counts,
                    itemStyle: { borderRadius: [0, 6, 6, 0], color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [{ offset: 0, color: '#FF8008' }, { offset: 1, color: '#FFC837' }]) },
                    label: { show: true, position: 'right', color: '#34495E', fontWeight: 'bold' }
                }]
            });
    ​
            const chartDecadePie = echarts.init(document.getElementById('chart-decade-pie'));
            chartDecadePie.setOption({
                title: { text: '经典电影年代分布', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c}部 ({d}%)' },
                legend: { top: 'bottom', textStyle: { color: '#7F8C8D' } },
                color: ['#FF9A9E', '#FECFEF', '#A1C4FD', '#C2E9FB', '#D4FC79'],
                series: [{
                    name: '年代占比', type: 'pie', radius: ['45%', '70%'], avoidLabelOverlap: false,
                    itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
                    label: { show: true, formatter: '{b}\\n{d}%', fontWeight: 'bold', color: '#34495E' },
                    labelLine: { show: true, smooth: 0.2, length: 10, length2: 20 },
                    data: decadeData
                }]
            });
    ​
            const chartScoreTrend = echarts.init(document.getElementById('chart-score-trend'));
            chartScoreTrend.setOption({
                title: { text: '高产年份平均评分趋势', left: 'center', textStyle: { color: '#34495E' } },
                tooltip: { trigger: 'axis' },
                grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
                xAxis: { type: 'category', boundaryGap: false, data: trendData.years, axisLine: { lineStyle: { color: '#BDC3C7' } } },
                yAxis: { type: 'value', min: 8.5, max: 9.3, splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } },
                series: [{
                    name: '平均评分', type: 'line', data: trendData.avgScores, smooth: true, symbolSize: 8,
                    itemStyle: { color: '#8E2DE2' },
                    areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(142, 45, 226, 0.4)' }, { offset: 1, color: 'rgba(74, 0, 224, 0.05)' }]) }
                }]
            });
            
            const chartVotesBar = echarts.init(document.getElementById('chart-votes-bar'));
            chartVotesBar.setOption({
                title: { text: '评价人数统计柱状图 (Top 50)', left: 'center', textStyle: { color: '#34495E', fontSize: 20 } },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                grid: { left: '4%', right: '4%', bottom: '25%', containLabel: true },
                xAxis: { 
                    type: 'category', 
                    name: '电影名称',
                    nameLocation: 'middle',
                    nameGap: 60,
                    data: votesData.names, 
                    axisLabel: { 
                        color: '#2c3e50', 
                        rotate: 45, 
                        interval: 0,
                        fontSize: 10
                    }, 
                    axisLine: { lineStyle: { color: '#BDC3C7' } } 
                },
                yAxis: { 
                    type: 'value', 
                    name: '评价人数 / 人', 
                    splitLine: { lineStyle: { type: 'dashed', color: '#E0E6ED' } } 
                },
                series: [{
                    name: '评价人数', 
                    type: 'bar', 
                    data: votesData.votes,
                    itemStyle: { color: '#2980b9' } 
                }]
            });
    ​
            window.addEventListener('resize', () => {
                chartScoreDist.resize(); chartYearTop.resize(); chartDecadePie.resize(); chartScoreTrend.resize(); chartVotesBar.resize();
            });
        </script>
    </body>
    </html>
    """
    ​
        final_html = html_top + js_data + html_bottom
        output_html = "douban_dashboard_auto.html"
        
        with open(output_html, "w", encoding="utf-8") as f:
            f.write(final_html)
            
        print(f" 动态大屏已成功生成!文件位置:{os.path.abspath(output_html)}")
        
        try:
            webbrowser.open('file://' + os.path.abspath(output_html))
        except Exception as e:
            print(f"尝试打开浏览器失败,请手动双击打开 {output_html}。错误信息:{e}")
    ​
    ​
    def main():
        if not check():
            getMovieData()
        generate_html_dashboard()  # 运行新增的 HTML 动态大屏模块
    ​
    ​
    if __name__ == '__main__':
        main()

总结

通过这一系列的迭代,本项目从一个简单的终端爬虫,进化为了一个具备数据采集、数据清洗、数据聚合计算、前后端代码自动融合、最终呈现出商业级交互式数据大屏的完整自动化数据工程项目。



豆瓣电影 Top250 爬虫与数据可视化大屏:开发演进日志
http://blog.zheco68.asia/archives/dou-ban-dian-ying-top250-pa-chong-yu-shu-ju-ke-shi-hua-da-ping-kai-fa-yan-jin-ri-zhi
作者
zheco
发布于
2026年05月24日
更新于
2026年05月24日
许可协议