20241227曹鹏泰《python程序设计》实验四报告

20241227曹鹏泰 《Python程序设计》实验四报告
课程:《Python程序设计》
班级: 2412
姓名: 曹鹏泰
学号:20241227
实验教师:王志强
实验日期:2025年5月14日
必修/选修:公选课

一.实验任务:
1.用爬虫学着爬取了酷狗音乐上排行榜前100的音乐及其相关信息;(由于酷狗音乐的反爬功能强大,所以没有获取到歌曲链接)
2.还尝试着生成了歌手上榜歌曲数量柱状图和歌曲时长分布直方图(用于分析歌曲时长的分布情况,并标记平均时长);
3.编写一个系统,其中包括五个功能:
===== 酷狗音乐数据分析与推荐系统 =====
1). 爬取酷狗Top100音乐数据
2). 查看歌手排行榜可视化
3). 查看歌曲时长分布可视化
4). 心情音乐推荐
5). 退出系统
(但是我自己真的是新手小白,也是刚接触python这门课,所以设计的程序功能并不强大,也就简单的几个功能)

二.实验内容:
1.实验环境的搭建:(工欲善其事,必先利其器)

在用爬虫爬取数据时,我发现如果python解释器里面如果没有相应的模块,那么这个程序就会报错。

所以需要提前下载好所需要的模块,如程序需要安装 requests、beautifulsoup4、pandas 和 matplotlib 库......



如果下载了两个及以上的python版本,有可能会出现

那么就需要我们采用下面方法:(完成下述操作后,回到代码中,“未解析的引用” 报错一般就会消除)

如果不行,则需要检查 IDE 配置(以 PyCharm 为例)
1)打开 PyCharm,进入你的项目。
2)点击 File -> Settings(Windows、Linux)或 PyCharm -> Settings(Mac)。
在弹出的设置窗口中,找到 Project: 你的项目名 -> Python Interpreter 。(这里的Interpreter是解释器的意思)
3)在 Python Interpreter 页面的包列表中,查看是否有 pyecharts 。如果没有,点击右上角的 + 号,在搜索框中输入 pyecharts ,然后点击 Install Package 进行安装;如果有,确认安装的版本是否符合你的需求,也可以尝试重新安装(先点击 - 号卸载,再点击 + 号安装 )。

2.实验代码:

import requests
import json
import time
import random
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import pandas as pd
from bs4 import BeautifulSoup
import re

# 设置成中文的字体
try:
    font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf")  # Windows系统默认黑体
except:
    # 如果找不到指定字体,可以使用matplotlib支持的其他中文字体
    font = FontProperties(family=["SimHei", "WenQuanYi Micro Hei", "Heiti TC"])

class KugouMusicCrawler:
    def __init__(self):
        self.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',
            'Referer': 'https://www.kugou.com/yy/rank/home/1-8888.html',
            'Accept': 'application/json, text/plain, */*'
        }
        self.song_list = []

    def get_song_info(self, rank_url):
        """获取酷狗音乐排行榜中的歌曲信息"""
        try:
            response = requests.get(rank_url, headers=self.headers)
            response.raise_for_status()  # 检查请求是否成功

            # 使用BeautifulSoup解析页面内容
            soup = BeautifulSoup(response.text, 'html.parser')

            # 查找歌曲列表容器
            song_containers = soup.select('.pc_temp_songlist > ul > li')

            for song in song_containers:
                # 提取歌曲排名
                rank = song.select_one('.pc_temp_num').get_text(strip=True)

                # 提取歌曲名称和歌手
                info = song.select_one('.pc_temp_songname')
                song_name = info.get_text(strip=True).split('-')[-1].strip()
                singer = info.get_text(strip=True).split('-')[0].strip()

                # 提取时长
                duration = song.select_one('.pc_temp_time').get_text(strip=True)

                # 提取歌曲ID(用于获取播放链接)
                song_id_match = re.search(r'href=".*?id=(\d+)"', str(info))
                song_id = song_id_match.group(1) if song_id_match else None

                # 获取歌曲播放链接
                play_url = self.get_song_url(song_id) if song_id else None

                self.song_list.append
              ({
                    'rank': rank,
                    'song_name': song_name,
                    'singer': singer,
                    'duration': duration,
                    'play_url': play_url
                })

            print(f"成功获取 {len(song_containers)} 首歌曲信息") #生成成功爬取到的提示
            time.sleep(random.uniform(1, 3))#避免请求过于频繁,使得对服务器的压力过大!还有,网易云音乐 API 可能限制高频请求,所以建议单次爬取间隔不低于 1 秒

        except requests.exceptions.RequestException as e:
            print(f"请求错误: {e}")
        except Exception as e:
            print(f"处理页面时出错: {e}")  #报错处理

    def get_song_url(self, song_id):    #在这里由于第三方API可能存在不稳定的情况,且酷狗官方API需授权,需要通过官网获取
        """获取歌曲的播放链接"""
        try:
            url = f"https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash={song_id}"
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()

            data = response.json()
            if data.get('status') == 1 and 'data' in data:
                return data['data'].get('play_url')
            return None
        except:
            return None

    def crawl_top_100(self):
        """爬取酷狗音乐Top100"""
        # 酷狗音乐排行榜有多个页面,每个页面20首歌
        for page in range(1, 6):  # 前5页共100首歌
            rank_url = f"https://www.kugou.com/yy/rank/home/{page}-8888.html"
            print(f"正在爬取第 {page} 页: {rank_url}")
            self.get_song_info(rank_url)

        print(f"成功爬取 {len(self.song_list)} 首歌曲信息")
        return self.song_list

    def visualize_singers(self):
        """可视化歌手上榜歌曲数量"""
        if not self.song_list:
            print("没有数据可可视化")         
            return

        # 转换为DataFrame
        df = pd.DataFrame(self.song_list)

        # 统计每个歌手的上榜歌曲数量
        singer_counts = df['singer'].value_counts().head(10)

        # 创建图表
        plt.figure(figsize=(12, 8))
        bars = plt.bar(singer_counts.index, singer_counts.values, color='skyblue')

        # 添加数据标签
        for bar in bars:
            height = bar.get_height()
            plt.text(bar.get_x() + bar.get_width() / 2., height + 0.5,
                     f'{height}', ha='center', va='bottom', fontproperties=font)

        # 设置图表标题和坐标轴标签
        plt.title('酷狗音乐Top100歌手上榜歌曲数量', fontproperties=font, fontsize=15)
        plt.xlabel('歌手', fontproperties=font, fontsize=12)
        plt.ylabel('歌曲数量', fontproperties=font, fontsize=12)

        # 设置x轴标签旋转角度和字体
        plt.xticks(rotation=45, ha='right', fontproperties=font)

        # 显示网格线
        plt.grid(axis='y', linestyle='--', alpha=0.7)

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

    def visualize_song_durations(self):
        """可视化歌曲时长分布"""
        if not self.song_list:
            print("没有数据可以可视化") 
            return

        # 转换为DataFrame
        df = pd.DataFrame(self.song_list)

        # 将时长转换成为秒为单位,便于统计
        df['duration_seconds'] = df['duration'].apply(lambda x: int(x.split(':')[0]) * 60 + int(x.split(':')[1]))

        # 创建直方图
        plt.figure(figsize=(12, 8))
        n, bins, patches = plt.hist(df['duration_seconds'], bins=20, color='lightgreen', edgecolor='black')

        # 计算平均的时长
        mean_duration = df['duration_seconds'].mean()
        plt.axvline(mean_duration, color='red', linestyle='dashed', linewidth=2,
                    label=f'平均时长: {mean_duration:.1f}秒')

        # 设置图表标题和坐标轴标签
        plt.title('酷狗音乐Top100歌曲时长分布', fontproperties=font, fontsize=15)
        plt.xlabel('歌曲时长(秒)', fontproperties=font, fontsize=12)
        plt.ylabel('歌曲数量', fontproperties=font, fontsize=12)
        plt.legend(prop=font)

        # 显示网格线
        plt.grid(axis='y', linestyle='--', alpha=0.7)
        plt.tight_layout()
        plt.savefig('song_duration.png', dpi=300, bbox_inches='tight')
        plt.show()

    def save_to_json(self, filename='kugou_top100.json'):    #解决了老师在示范代码中只能打印输出结果,不能保存的缺点。
        """将爬取的歌曲信息保存为JSON文件"""
        if self.song_list:
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(self.song_list, f, ensure_ascii=False, indent=4)
            print(f"数据已保存到 {filename}")

以下分别是优化前后的代码

#!!!未优化的代码,无法打印爬取得到的数据
if __name__ == "__main__":
    crawler = KugouMusicCrawler()
    top_100_songs = crawler.crawl_top_100()
    
    if top_100_songs:
        crawler.save_to_json()
        crawler.visualize_singers()
        crawler.visualize_song_durations()
    else:
        print("未获取到任何数据,请检查爬虫逻辑或网站结构是否变化")    #代码的报错提示,可能原因:酷狗对HTML页面做了严格反爬,直接请求会返回空内容或验证码页面。
优化后的代码:
# 在原代码的基础上,修改main函数部分,直接调用酷狗的JSON接口,绕过HTML反爬
if __name__ == "__main__":
    crawler = KugouMusicCrawler()
    top_100_songs = crawler.crawl_top_100()

    # 新增:打印爬取的歌曲信息
    print("\n===== 酷狗音乐Top100排行榜 =====")
    for idx, song in enumerate(top_100_songs, 1):
        print(f"{idx}. {song['singer']} - {song['song_name']} ({song['duration']})")
        if song['play_url']:
            print(f"播放链接: {song['play_url']}")
        else:
            print("播放链接: 未获取到")
        print("-" * 40)

    # 原有功能保持不变
    if top_100_songs:
        crawler.save_to_json()
        crawler.visualize_singers()
        crawler.visualize_song_durations()
    else:
        print("未获取到任何数据,请检查爬虫逻辑或网站结构是否变化")

3.运行结果:
至此,实验的基本代码部分已经完成,以下是实验截图:



这里只能获取到歌曲的排名,名字,歌手,时间,以及可视化的图,对于歌曲的链接和歌词均爬取失败。

4.实验的进一步优化:
我希望可以根据爬取到的数据结果,粗略地做出一个可以根据心情推荐歌曲的系统。当时也是抱着试试看的心态(毕竟这个python爬虫程序的爬取效果并不理想)

import random
import json
from collections import defaultdict
class MusicRecommender:
    def __init__(self, data_file='kugou_top100.json'):
        self.song_data = self.load_data(data_file)
        self.mood_keywords = self.create_mood_keywords()
        
    def load_data(self, filename):
        """加载歌曲数据"""
        try:
            with open(filename, 'r', encoding='utf-8') as f:
                return json.load(f)
        except FileNotFoundError:
            print(f"错误:找不到数据文件 {filename}")
            return []
        except json.JSONDecodeError:
            print("错误:数据文件格式不正确")
            return []

首先,系统从先前的JSON文件中加载歌曲数据。

    def create_mood_keywords(self):
        """创建心情关键词与歌曲的映射关系"""
        mood_map = defaultdict(list)
        
        # 为每首歌添加一些可能的心情标签(实际应用中可以通过歌词分析获得更准确的标签)
        for song in self.song_data:
            # 随机分配一些心情标签(实际应用中应该基于歌曲分析)
            moods = []
            if random.random() < 0.3:
                moods.append('快乐')
            if random.random() < 0.3:
                moods.append('悲伤')
            if random.random() < 0.3:
                moods.append('放松')
            if random.random() < 0.3:
                moods.append('励志')
            if random.random() < 0.3:
                moods.append('浪漫')
            if random.random() < 0.3:
                moods.append('怀旧')
            
            # 如果没有分配任何心情,默认添加一个
            if not moods:
                moods.append(random.choice(['快乐', '悲伤', '放松']))
            for mood in moods:
                mood_map[mood].append(song)
        return mood_map

这里想用一种映射的关系,反映心情与歌曲之间的联系。(可是先前的运行结果中没有爬取到歌词,所以就无法正确预测和映射,所以这里的映射关系都是随机的,我在这里只是模拟一个映射,真正需要通过分析歌词来判断歌曲的情感基调)

    def get_mood_input(self):
        """获取用户心情输入"""
        print("\n===== 音乐心情推荐系统 =====")
        print("请选择你当前的心情:")
        print("1. 快乐")
        print("2. 悲伤")
        print("3. 放松")
        print("4. 励志")
        print("5. 浪漫")
        print("6. 怀旧")
        print("7. 随机推荐")
        print("0. 退出")
        
        while True:
            choice = input("\n请输入对应的数字(0-7): ")
            if choice == '0':
                return None
            elif choice == '1':
                return '快乐'
            elif choice == '2':
                return '悲伤'
            elif choice == '3':
                return '放松'
            elif choice == '4':
                return '励志'
            elif choice == '5':
                return '浪漫'
            elif choice == '6':
                return '怀旧'
            elif choice == '7':
                return random.choice(['快乐', '悲伤', '放松', '励志', '浪漫', '怀旧'])
            else:
                print("输入无效,请重新输入!")

然后可以给不同的心情分类

    def recommend_songs(self, mood, num=5): #根据之前爬取到的结果,生成推荐歌单
        """根据心情推荐歌曲"""
        if mood not in self.mood_keywords or not self.mood_keywords[mood]:
            print(f"没有找到适合'{mood}'心情的歌曲,将为您随机推荐")
            available_songs = [song for songs in self.mood_keywords.values() for song in songs]
            if not available_songs:
                return []
            return random.sample(available_songs, min(num, len(available_songs)))
        return random.sample(self.mood_keywords[mood], min(num, len(self.mood_keywords[mood])))
    
    def display_recommendations(self, songs, mood):
        """显示推荐结果"""
        if not songs:
            print("抱歉,没有找到合适的歌曲推荐。")
            return
        print(f"\n===== 根据您的心情'{mood}',为您推荐以下歌曲 =====")
        for i, song in enumerate(songs, 1):
            print(f"{i}. {song['singer']} - {song['song_name']} ({song['duration']})")
            if song.get('play_url'):
                print(f"播放链接: {song['play_url']}")
            print("-" * 50)
    
    def run(self):
        """运行推荐系统"""
        while True:
            mood = self.get_mood_input()
            if mood is None:
                print("感谢使用音乐推荐系统,再见!")
                break
            recommended_songs = self.recommend_songs(mood)
            self.display_recommendations(recommended_songs, mood)

# 使用示例
if __name__ == "__main__":
    # 确保kugou_top100.json文件存在(由之前的爬虫生成)
    recommender = MusicRecommender()
    recommender.run()

5.实验视频
【实验结果视频】 https://www.bilibili.com/video/BV1SsTRzgEaH/?share_source=copy_web&vd_source=2a9ab265bf667b27b200719a1a9d67d5
https://gitee.com/uchiha-p/xiao-organization.git;)

三、实验问题和解决方法:
1.无法获取到歌词和链接:
大多数的音乐网站具有严格的反爬虫措施,一般不能频繁地请求,否则会触发反爬虫的网站安全机制。此外,酷狗音乐等的现代网站常使用JavaScript动态加载内容,直接请求HTML,页面往往无法获取完整数据。



为了防止轮换IP地址遭到封禁,我们就需要模拟浏览器行为,如使用 Selenium 模拟浏览器滚动,触发JS加载更多数据。

from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://music.example.com/playlist")
# 模拟滚动到页面底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)  # 等待数据加载

同时也可以添加随机延迟,(如time.sleep(random.uniform(1, 3)))。

2.在建造系统时,发现无法将前面的爬取结果调取使用:
由于之前没有考虑到将爬取结果储存,所以新建的系统就不能调用有效数据进行下一步操作,所以几个代码之间就无法联系在一起。
解决方法也很简单,我们可以将数据储存到kugou_top100.json类似的文件中(当前代码已经实现了将爬取到的数据保存为JSON和CSV文件的功能)
可以创建专用文件夹存储数据:

import os
# 创建保存目录
if not os.path.exists('data'):
    os.makedirs('data')
# 保存时指定路径
crawler.save_to_json('data/kugou_top100.json')
top_singers_table.to_csv('data/top_10_singers.csv')

当然还遇到了其他的很难的问题,比如说:怎么实现映射关系,怎样才能防止反爬......(列举出来的问题已经是最简单的两个了)

四、实验感悟:
这次试验是我做过的耗时最长的实验,在做的过程中有的时候真的很想放弃,又逼着自己努力理解那些不懂得概念,但是志强老师也给了我们充足的时间来让我们准备和学习。虽然做的很头疼,但是也让我认识到学无止境,很多的东西自己在以前都没有接触过,现在突然安排就很手足无措,这让我认识到我们不能总是在自己的舒适圈里颓废,需要不时地跳出我们的舒适圈去大胆的探索和尝试,尽管这个过程是痛苦的,但是我们在经历的同时也在升华和成长。感谢遇到志强老师这么好的老师,授人与鱼也授人与渔,帮助我真正对python这门课程感到了兴趣,也产生了继续深入学习的想法。很喜欢在结课时,志强老师说过的话:“那些看似波澜不惊的日复一日,终将在某一天看到坚持的意义。”那些我们坚持的事务终将会让我们变成更好的自己!最后再说一声:“强哥辛苦了!”

五、参考资料:
哔哩哔哩(python研究社爬虫教程),csdn(python爬虫的实现),deepseek(python的可视化图表),《零基础学python》

posted @ 2025-06-09 00:30  20241227曹鹏泰  阅读(43)  评论(0)    收藏  举报