☰
Python爬虫与数据分析实战:复现Taylor Swift《Midnights》Billboard榜单数据
2026/10/12 14:00:06 网站建设 项目流程

最近在分析音乐榜单数据时,发现了一个非常有意思的现象:泰勒·斯威夫特(Taylor Swift)的专辑《Midnights》在美国公告牌百强单曲榜(Billboard Hot 100)上的表现堪称“统治级”。这张专辑发行后,一度有十首歌曲同时进入Hot 100前十名,几乎“霸榜”。对于从事数据分析、爬虫开发,或者对流行文化数据感兴趣的朋友来说,这背后不仅是一个娱乐新闻,更是一个绝佳的数据分析实战案例。

本文将带你从零开始,完整复现《Midnights》专辑歌曲在Billboard Hot 100榜单上的周榜推移数据分析项目。我们将涵盖数据采集、清洗、分析、可视化到结论解读的全流程,使用Python作为主要工具。无论你是想学习如何从网站抓取结构化数据,还是希望掌握时间序列数据的可视化技巧,这篇文章都能提供一套可直接复用的代码和清晰的思路。

1. 项目背景与目标

公告牌百强单曲榜(Billboard Hot 100)是美国乃至全球最具权威性的单曲排行榜之一,其排名综合了实体销量、数字下载、流媒体播放量以及电台点播量等多个维度数据,每周更新一次。因此,追踪一首歌在Hot 100上的排名变化,是衡量其商业成功和流行热度的重要指标。

泰勒·斯威夫特的《Midnights》专辑于2022年10月21日发行,随即创造了历史记录:在发行后的首周,专辑内的歌曲包揽了Hot 100榜单的前十名。这不仅是她个人的里程碑,也是公告牌历史上首次由一位艺术家实现此壮举。

我们的项目目标如下:

  1. 数据获取:自动化采集《Midnights》专辑中所有进入过Hot 100的歌曲,自专辑发行日起至今(或指定时间段内)的每周排名数据。
  2. 数据处理与存储:将采集到的杂乱数据清洗、整理成结构化的格式(如CSV、数据库),便于分析。
  3. 可视化分析:绘制每首歌曲的排名随时间(周次)变化的折线图,直观展示其“周榜推移”轨迹。
  4. 深度洞察:通过数据分析,回答诸如“哪首歌在榜时间最长?”、“哪首歌的排名下滑最快?”、“歌曲排名变化与哪些事件(如MV发布、现场表演)相关?”等问题。

通过这个项目,你将掌握一套处理时序排名数据的通用方法论,并可轻松迁移到分析其他歌手、专辑或任何榜单数据上。

2. 环境准备与工具说明

在开始编码前,请确保你的开发环境已就绪。本项目主要使用Python,以下是核心库及其作用:

  • Python 3.8+:基础编程语言环境。
  • requests:用于向目标网站发送HTTP请求,获取网页HTML内容。
  • BeautifulSoup4 (bs4):用于解析HTML文档,从中提取我们需要的结构化数据(如排名、歌曲名、周次)。
  • pandas:数据处理的核心库,用于数据清洗、整合、分析和存储为CSV文件。
  • matplotlib / seaborn:数据可视化库,用于绘制专业、美观的排名推移折线图。
  • sqlite3 (Python内置):可选,用于将数据存储到轻量级数据库中进行更复杂查询。

安装依赖库:打开你的终端或命令提示符,使用pip命令安装所需库。

pip install requests beautifulsoup4 pandas matplotlib seaborn

项目结构建议:创建一个清晰的项目文件夹,便于管理代码和数据。

midnights_billboard_analysis/ ├── data/ # 存放原始和清洗后的数据 │ ├── raw_html/ # 可存放下载的原始HTML页面(可选) │ └── processed/ # 存放清洗后的CSV文件 ├── src/ # 存放源代码 │ ├── scraper.py # 爬虫脚本,负责数据采集 │ ├── cleaner.py # 数据清洗脚本 │ ├── analyzer.py # 数据分析与可视化脚本 │ └── config.py # 配置文件(如URL、请求头) └── main.py # 主程序入口,协调整个流程

3. 核心步骤一:数据采集(爬虫开发)

数据源是分析的基础。我们需要找到可靠、稳定地提供Billboard Hot 100历史周榜数据的网站。请注意,直接爬取Billboard官网可能违反其服务条款且会遇到反爬机制。因此,我们可以选择一些聚合了历史榜单数据的第三方音乐数据网站,并在爬取时务必遵守robots.txt协议,设置合理的请求间隔,避免对目标服务器造成压力。

这里我们以一个假设的、结构清晰的榜单数据网站为例,讲解爬虫思路。在实际操作中,你需要根据目标网站的实际HTML结构调整解析代码。

步骤拆解:

  1. 确定目标URL和参数:找到能通过歌手名(Taylor Swift)、专辑名(Midnights)或歌曲名筛选,并展示每周排名历史的页面。观察URL规律,例如https://example-chart-site.com/history?artist=Taylor+Swift&song=Anti-Hero。
  2. 模拟浏览器请求:使用requests库发送GET请求。务必设置User-Agent等请求头,模拟真实浏览器访问。
  3. 解析HTML内容:使用BeautifulSoup解析返回的HTML,定位到包含排名数据的表格(<table>)或列表(<ul>/<ol>)。
  4. 提取结构化数据:遍历表格的行(<tr>),从每个单元格(<td>)中提取日期(周)、排名、歌曲名等信息。
  5. 处理分页与循环:如果数据有多页,需要找到“下一页”的链接或构造翻页参数,循环抓取。
  6. 数据暂存:将每页提取的数据临时存入一个列表(list of dictionaries)或pandas DataFrame。
  7. 增加延迟:在请求之间使用time.sleep()添加随机延迟(如2-5秒),体现良好的爬虫礼仪。

示例代码框架 (src/scraper.py):

import requests from bs4 import BeautifulSoup import pandas as pd import time import random def fetch_song_chart_history(song_name): """ 获取指定歌曲的榜单历史数据 :param song_name: 歌曲名称 :return: 包含周次、排名等信息的DataFrame """ # 1. 构造请求URL和头部(示例,需替换为真实URL) base_url = "https://example-chart-site.com/history" params = { 'artist': 'Taylor Swift', 'song': song_name, 'chart': 'hot-100' } 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' } all_data = [] page = 1 has_next_page = True while has_next_page: params['page'] = page print(f"正在抓取 {song_name} 第 {page} 页...") try: response = requests.get(base_url, params=params, headers=headers, timeout=10) response.raise_for_status() # 检查请求是否成功 except requests.RequestException as e: print(f"请求失败: {e}") break soup = BeautifulSoup(response.text, 'html.parser') # 2. 解析页面,定位数据表格 # 假设数据在一个id为`chart-history`的table中 chart_table = soup.find('table', {'id': 'chart-history'}) if not chart_table: print("未找到榜单数据表格,可能页面结构已变化或数据已抓取完毕。") break # 3. 提取表格行数据 for row in chart_table.find('tbody').find_all('tr'): cols = row.find_all('td') if len(cols) >= 3: # 假设至少有日期、排名、歌曲名列 week_date = cols[0].text.strip() rank = cols[1].text.strip() # 歌曲名已知,可从参数获取,这里可能还有其他信息如播放量等 song = song_name peak = cols[3].text.strip() if len(cols) > 3 else 'N/A' # 假设第4列是峰值排名 weeks_on_chart = cols[4].text.strip() if len(cols) > 4 else 'N/A' # 假设第5列是在榜周数 all_data.append({ 'song': song, 'week': week_date, 'rank': int(rank) if rank.isdigit() else None, 'peak_rank': peak, 'weeks_on_chart': weeks_on_chart }) # 4. 检查是否存在下一页(示例:查找带有‘next’类名的链接) next_button = soup.find('a', class_='next') has_next_page = bool(next_button) page += 1 # 5. 礼貌性延迟,避免请求过快 time.sleep(random.uniform(2, 4)) # 将列表转换为DataFrame df = pd.DataFrame(all_data) if not df.empty: df['week'] = pd.to_datetime(df['week']) # 转换日期格式 df.sort_values('week', inplace=True) # 按周次排序 return df def main(): # 《Midnights》专辑进入过热百的歌曲列表(示例) midnights_songs = [ "Anti-Hero", "Lavender Haze", "Maroon", "Snow On The Beach", "You‘re On Your Own, Kid", "Midnight Rain", "Question...?", "Vigilante Shit", "Bejeweled", "Labyrinth", "Karma", "Sweet Nothing", "Mastermind" # 可根据实际情况增减 ] all_songs_data = pd.DataFrame() for song in midnights_songs: print(f"\n开始处理歌曲: {song}") song_df = fetch_song_chart_history(song) if not song_df.empty: all_songs_data = pd.concat([all_songs_data, song_df], ignore_index=True) # 每首歌抓取后稍作长时间休息 time.sleep(random.uniform(5, 8)) # 保存所有歌曲的原始数据 if not all_songs_data.empty: all_songs_data.to_csv('../data/raw/midnights_hot100_history_raw.csv', index=False, encoding='utf-8-sig') print(f"\n所有数据已保存至 CSV 文件,共 {len(all_songs_data)} 条记录。") else: print("未能抓取到任何数据。") if __name__ == '__main__': main()

重要提示:

  • 合法性:始终尊重robots.txt,仅抓取允许公开访问的数据,用于个人学习和分析。
  • 反爬应对:真实网站可能有JavaScript渲染、验证码或API加密。对于复杂情况,可能需要使用Selenium或Playwright等浏览器自动化工具,或寻找官方/第三方提供的API接口。
  • 数据存储:上述代码将数据保存为CSV。对于大量或需要频繁查询的数据,可考虑使用SQLite或MySQL。

4. 核心步骤二:数据清洗与整理

爬取到的原始数据往往存在缺失值、格式不一致、重复记录等问题,需要清洗后才能用于分析。

常见清洗任务:

  1. 处理缺失值:检查rank等关键字段是否为NaN或None。根据情况,可以删除缺失行,或用前后周的平均值、中位数填充(对于排名数据,删除通常是更安全的选择)。
  2. 统一格式:
    • 确保week字段为统一的datetime格式。
    • 将rank字段转换为整数型(int)。
    • 处理peak_rank(可能包含‘Peak’字样或非数字字符)。
  3. 去重:由于爬虫可能因网络问题重复抓取,需根据song和week组合去除重复行。
  4. 数据增强:可以计算一些衍生字段,如:
    • rank_change: 本周排名与上周排名的差值(正值为下降,负值为上升)。
    • chart_entry_week: 该歌曲首次进入榜单的周次。
    • weeks_in_top10: 累计进入前10名的周数。

示例清洗代码 (src/cleaner.py):

import pandas as pd import numpy as np def clean_chart_data(raw_data_path, output_path): """ 清洗原始榜单数据 :param raw_data_path: 原始CSV文件路径 :param output_path: 清洗后输出路径 :return: 清洗后的DataFrame """ df = pd.read_csv(raw_data_path, encoding='utf-8-sig') print(f"原始数据形状: {df.shape}") print("原始数据预览:") print(df.head()) print("\n原始数据信息:") print(df.info()) # 1. 处理缺失值:删除排名为NaN的行 df_clean = df.dropna(subset=['rank']).copy() print(f"删除排名缺失行后: {df_clean.shape}") # 2. 统一格式 df_clean['week'] = pd.to_datetime(df_clean['week'], errors='coerce') df_clean['rank'] = pd.to_numeric(df_clean['rank'], errors='coerce').astype('Int64') # 可空整数类型 # 清理peak_rank列,提取数字(假设格式如‘#1’) df_clean['peak_rank'] = df_clean['peak_rank'].astype(str).str.extract(r'(\d+)').astype('Int64') # 3. 去重:按歌曲和周次去重,保留最后一条(假设) df_clean = df_clean.drop_duplicates(subset=['song', 'week'], keep='last') # 4. 按歌曲和周次排序,为计算变化做准备 df_clean.sort_values(['song', 'week'], inplace=True) # 5. 计算排名变化 (本周排名 - 上周排名) df_clean['rank_change'] = df_clean.groupby('song')['rank'].diff(-1) * -1 # 解释:diff(-1)是当前行减下一行(因为时间倒序?),根据你的数据排序方向调整。这里假设数据按时间升序排列。 # 更稳妥的方式:先确保按时间升序排列,然后计算 shift后的差值。 df_clean.sort_values(['song', 'week'], inplace=True) # 确保升序 df_clean['prev_rank'] = df_clean.groupby('song')['rank'].shift(1) df_clean['rank_change'] = df_clean['prev_rank'] - df_clean['rank'] # 正数表示排名上升(数字变小),负数表示下降 df_clean.drop(columns=['prev_rank'], inplace=True) # 6. 标记是否在Top 10 df_clean['in_top10'] = df_clean['rank'] <= 10 # 7. 计算每首歌的在榜周数(从数据中计算) df_clean['week_number'] = df_clean.groupby('song').cumcount() + 1 print(f"\n清洗后数据形状: {df_clean.shape}") print("清洗后数据预览:") print(df_clean.head()) # 保存清洗后的数据 df_clean.to_csv(output_path, index=False, encoding='utf-8-sig') print(f"\n清洗后的数据已保存至: {output_path}") return df_clean if __name__ == '__main__': # 使用示例路径 raw_file = '../data/raw/midnights_hot100_history_raw.csv' clean_file = '../data/processed/midnights_hot100_history_clean.csv' cleaned_df = clean_chart_data(raw_file, clean_file)

5. 核心步骤三:数据分析与可视化

数据清洗完毕后,就可以进行有趣的分析和可视化了。我们将使用pandas进行数据聚合和统计,并用matplotlib或seaborn绘制图表。

分析方向与可视化示例:

  1. 每首歌的排名推移折线图:这是最核心的视图,可以清晰看到每首歌的生命周期。
  2. 歌曲排名对比图:将多首歌的排名曲线放在同一张图中,比较其走势。
  3. 在榜时长统计:统计每首歌停留在Hot 100榜单上的总周数。
  4. Top 10停留周数:统计每首歌停留在前10名的周数。
  5. 峰值排名分布:查看所有歌曲达到的最高排名(Peak Position)。

示例分析与可视化代码 (src/analyzer.py):

import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from matplotlib.ticker import MaxNLocator import numpy as np # 设置中文字体和图表样式(可选) plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] # 解决中文显示问题 plt.rcParams['axes.unicode_minus'] = False sns.set_style("whitegrid") def plot_song_trajectory(df, song_list=None, output_path='../data/processed/chart_trajectory.png'): """ 绘制指定歌曲的排名随时间推移的折线图 :param df: 清洗后的DataFrame :param song_list: 需要绘制的歌曲列表,None则绘制所有 :param output_path: 输出图片路径 """ if song_list is not None: plot_df = df[df['song'].isin(song_list)].copy() else: plot_df = df.copy() plt.figure(figsize=(14, 8)) # 为每首歌绘制一条线 songs = plot_df['song'].unique() colors = plt.cm.tab20c(np.linspace(0, 1, len(songs))) # 使用色彩映射 for song, color in zip(songs, colors): song_data = plot_df[plot_df['song'] == song].sort_values('week') plt.plot(song_data['week'], song_data['rank'], marker='o', label=song, color=color, linewidth=2, markersize=4) plt.gca().invert_yaxis() # 排名第1在上方,更符合直观 plt.xlabel('日期 (周)', fontsize=12) plt.ylabel('Billboard Hot 100 排名', fontsize=12) plt.title('《Midnights》专辑歌曲 Billboard Hot 100 排名周榜推移', fontsize=16, fontweight='bold') plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0., title='歌曲') plt.xticks(rotation=45) plt.tight_layout() plt.savefig(output_path, dpi=300, bbox_inches='tight') plt.show() print(f"排名推移图已保存至: {output_path}") def generate_summary_statistics(df, output_path='../data/processed/summary_stats.csv'): """ 生成每首歌的汇总统计数据 :param df: 清洗后的DataFrame :param output_path: 输出CSV路径 :return: 汇总统计DataFrame """ summary_list = [] for song, group in df.groupby('song'): entry_week = group['week'].min() latest_week = group['week'].max() total_weeks = group['week'].nunique() weeks_top10 = group['in_top10'].sum() peak_rank = group['rank'].min() # 排名数字越小越好 current_rank = group.sort_values('week').iloc[-1]['rank'] if not group.empty else None best_rank_week = group.loc[group['rank'].idxmin(), 'week'] if not group.empty else None summary_list.append({ 'song': song, 'entry_date': entry_week, 'latest_date': latest_week, 'total_weeks_on_chart': total_weeks, 'weeks_in_top10': weeks_top10, 'peak_rank': peak_rank, 'current_rank': current_rank, 'best_rank_date': best_rank_week }) summary_df = pd.DataFrame(summary_list) summary_df.sort_values('peak_rank', inplace=True) # 按峰值排名排序 # 保存汇总数据 summary_df.to_csv(output_path, index=False, encoding='utf-8-sig') print(f"汇总统计数据已保存至: {output_path}") print("\n汇总统计预览:") print(summary_df.head(10).to_string()) return summary_df def plot_top10_weeks_bar(summary_df, output_path='../data/processed/top10_weeks_bar.png'): """ 绘制每首歌在Top 10周数的条形图 :param summary_df: 汇总统计DataFrame :param output_path: 输出图片路径 """ # 按Top10周数排序 plot_data = summary_df.sort_values('weeks_in_top10', ascending=False) plt.figure(figsize=(12, 6)) bars = plt.barh(plot_data['song'], plot_data['weeks_in_top10'], color=sns.color_palette("viridis", len(plot_data))) plt.xlabel('停留在 Top 10 的周数', fontsize=12) plt.title('《Midnights》歌曲在 Billboard Hot 100 Top 10 停留时长', fontsize=14, fontweight='bold') plt.gca().invert_yaxis() # 让最高的条在最上面 # 在条形末端添加数值标签 for bar in bars: width = bar.get_width() plt.text(width + 0.3, bar.get_y() + bar.get_height()/2, f'{int(width)}', ha='left', va='center') plt.tight_layout() plt.savefig(output_path, dpi=300, bbox_inches='tight') plt.show() if __name__ == '__main__': # 加载清洗后的数据 clean_file = '../data/processed/midnights_hot100_history_clean.csv' df = pd.read_csv(clean_file, parse_dates=['week']) # 1. 绘制核心歌曲的排名推移(例如主打歌) hit_songs = ["Anti-Hero", "Lavender Haze", "Karma", "Bejeweled"] plot_song_trajectory(df, song_list=hit_songs, output_path='../data/processed/hit_songs_trajectory.png') # 2. 生成并查看汇总统计 stats_df = generate_summary_statistics(df) # 3. 绘制Top 10周数条形图 plot_top10_weeks_bar(stats_df)

运行上述代码后,你将得到:

  • hit_songs_trajectory.png: 显示几首主打歌排名如何随时间变化。你可以看到“Anti-Hero”可能长期稳居高位,而其他歌曲在发行几周后排名逐渐下滑。
  • summary_stats.csv: 包含每首歌的详细统计数据表格。
  • top10_weeks_bar.png: 直观对比哪首歌在Top 10中生命力最顽强。

6. 常见问题与排查思路

在实施本项目过程中,你可能会遇到以下问题:

问题现象可能原因解决思路
爬虫脚本返回空数据或403错误1. 网站反爬虫(检查请求头、User-Agent)。
2. 目标网页结构已更新,CSS选择器失效。
3. 需要处理Cookie或Session。
1. 使用更真实的浏览器请求头,添加Referer等信息。
2. 用浏览器开发者工具重新检查元素,更新BeautifulSoup查找逻辑。
3. 使用requests.Session()保持会话,或考虑使用Selenium。
pd.to_datetime转换日期失败原始日期字符串格式与默认解析格式不匹配。指定日期格式,如pd.to_datetime(df[‘week’], format=‘%Y-%m-%d’)。或用errors=‘coerce’参数将无法解析的设为NaT,再检查原始数据。
排名折线图非常杂乱,线条交叉严重同时绘制的歌曲太多,且排名区间重叠。1. 分开展示,每次只绘制3-5首歌曲进行对比。
2. 使用子图(plt.subplots),每首歌一个子图。
3. 使用交互式图表库(如Plotly)实现鼠标悬停查看详情。
计算rank_change(排名变化)时逻辑错误数据排序方向(升序/降序)与diff()或shift()函数使用不匹配。在计算变化前,务必用df.sort_values([‘song’, ‘week’], inplace=True)确保数据按歌曲和时间升序排列。然后使用df[‘prev_rank’] = df.groupby(‘song’)[‘rank’].shift(1)获取上周排名。
图表中文显示为方框系统或Matplotlib未配置中文字体。按示例代码设置plt.rcParams[‘font.sans-serif’],或使用英文标签。在服务器环境可能需要安装中文字体。

7. 最佳实践与项目扩展建议

完成基础分析后,你可以从以下方向深化项目,这更能体现工程和数据思维:

  1. 数据源可靠性:

    • 多源验证:尝试从另一个公开数据源(如另一个音乐数据网站)抓取相同数据,进行交叉验证,确保数据的准确性。
    • 官方API:寻找Billboard或音乐数据公司(如Spotify、Last.fm)的官方API,虽然可能有调用限制或费用,但数据最权威。
  2. 自动化与部署:

    • 定时任务:使用cron(Linux)或schedule库(Python)设置每周定时任务,自动抓取最新榜单数据,更新你的数据集和图表。
    • 简单看板:使用Flask或Streamlit快速搭建一个本地Web应用,将分析图表集成到一个动态看板中,每次运行脚本后自动更新页面。
  3. 分析维度扩展:

    • 关联事件:将排名变化与真实世界事件(如音乐视频发布日、大型现场表演、社交媒体热点)在时间轴上对齐,分析外部事件对排名的影响。
    • 歌曲属性分析:如果能获取歌曲的音频特征(如节奏、能量、情绪),可以分析这些特征与榜单表现(如爬升速度、在榜时长)是否存在相关性。
    • 对比分析:将《Midnights》的数据与泰勒·斯威夫特的其他专辑(如《1989》、《folklore》)或同期其他热门专辑进行对比,从历史角度评估其成功程度。
  4. 代码优化:

    • 错误处理与日志:在爬虫脚本中添加更完善的异常处理(try…except)和日志记录(logging模块),便于长期运行和排错。
    • 配置化:将歌曲列表、目标URL、请求头等参数抽取到单独的配置文件(如config.yaml或config.py)中,提高代码可维护性。
    • 数据库存储:使用SQLAlchemy等ORM库将数据存入SQLite或PostgreSQL,便于执行更复杂的SQL查询(如“找出所有发行后连续三周排名上升的歌曲”)。

这个项目不仅让你获得一份关于《Midnights》专辑的深度榜单分析报告,更重要的是,它提供了一个从数据获取到洞察呈现的完整实战框架。你可以将这套方法应用到任何你感兴趣的榜单、时间序列数据上,无论是音乐、电影、体育还是加密货币价格。动手尝试,调整代码以适应真实的数据源,你会在解决一个个具体问题的过程中,大幅提升你的数据工程和数据分析能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询