python歌词下载器
import requests # 导入requests库,用于发送HTTP请求
from bs4 import BeautifulSoup # 导入BeautifulSoup库,用于解析HTML
import re # 导入re库,用于正则表达式操作
import os # 导入os库,用于操作系统相关功能
class LyricDownloader:
"""
歌词下载器类,用于从不同网站搜索和下载歌词。
"""
def __init__(self, search_engine="wangyiyun"): # 初始化方法,search_engine默认为网易云
"""
初始化 LyricDownloader 对象。
Args:
search_engine: 指定搜索引擎,可以是 "baidu", "wangyiyun", 或 "qqmusic"。
"""
self.search_engine = search_engine # 设置搜索引擎
self.headers = { # 设置HTTP请求头,模拟浏览器
"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"
}
def search_lyrics(self, song_name, artist=None):
"""
根据歌曲名称和歌手(可选)搜索歌词。
Args:
song_name: 歌曲名称。
artist: 歌手名称(可选)。
Returns:
一个包含搜索结果的列表,每个结果是一个字典,包含标题、URL和来源。
如果没有找到结果,返回None。
"""
if self.search_engine == "baidu": # 如果搜索引擎是百度
return self._search_baidu(song_name, artist) # 调用百度搜索方法
elif self.search_engine == "wangyiyun": # 如果搜索引擎是网易云
return self._search_wangyiyun(song_name, artist) # 调用网易云搜索方法
elif self.search_engine == "qqmusic": # 如果搜索引擎是QQ音乐
return self._search_qqmusic(song_name, artist) # 调用QQ音乐搜索方法
else:
raise ValueError(f"不支持的搜索引擎: {self.search_engine}") # 如果搜索引擎不支持,抛出异常
def _search_baidu(self, song_name, artist=None):
"""在百度搜索歌词 (私有方法)."""
return None # 百度搜索很难直接获得lrc.
def _search_wangyiyun(self, song_name, artist=None):
"""
在网易云音乐上搜索歌词(私有方法)。
Args:
song_name: 歌曲名称。
artist: 歌手名称(可选)。
Returns:
一个包含搜索结果的列表。
"""
query = f"{song_name}" # 构建搜索查询字符串
if artist: # 如果提供了歌手名称
query = f"{artist} {song_name}" # 将歌手名称添加到查询字符串
# 使用网易云音乐API(非官方)
try:
search_url = "https://music.163.com/api/search/get/web?csrf_token=" # 搜索API的基础URL
data = { # 构建POST请求的数据
"s": query, # 搜索关键词
"type": 1, # 搜索类型(1表示歌曲)
"offset": 0, # 偏移量(用于分页)
"total": "true", # 是否返回总数
"limit": 10, # 限制结果数量
}
response = requests.post(search_url, headers=self.headers, data=data) # 发送POST请求
response.raise_for_status() # 检查HTTP状态码
search_data = response.json() # 将响应内容解析为JSON
results = [] # 初始化搜索结果列表
if "result" in search_data and "songs" in search_data["result"]: # 检查JSON结构
for song in search_data["result"]["songs"]: # 遍历歌曲列表
song_id = song["id"] # 获取歌曲ID
song_title = song["name"] # 获取歌曲名称
artist_name = ", ".join([artist["name"] for artist in song["artists"]]) # 获取歌手名称(可能有多个歌手)
results.append({
"title": f"{song_title} - {artist_name}", # 标题
"url": f"https://music.163.com/#/song?id={song_id}", # 歌曲URL
"id": song_id, # 歌曲ID
"source": "wangyiyun" # 来源
})
return results # 返回搜索结果
except requests.exceptions.RequestException as e: # 捕获网络请求异常
print(f"网易云音乐搜索出错: {e}")
return None # 发生错误时返回None
except (KeyError, TypeError) as e: # 捕获JSON解析错误
print(f"解析网易云音乐响应出错: {e}")
return None
def _search_qqmusic(self, song_name, artist=None):
"""
在QQ音乐上搜索歌词(私有方法)。
Args:
song_name: 歌曲名称。
artist: 歌手名称(可选)。
Returns:
一个包含搜索结果的列表。
"""
query = f"{song_name}" # 构建搜索查询字符串
if artist: # 如果提供了歌手名称
query = f"{artist} {song_name}" #将歌手添加到搜索字符串中
try:
# 构建搜索URL
search_url = f"https://c.y.qq.com/soso/fcgi-bin/client_search_cp?p=1&n=10&w={query}&format=json"
response = requests.get(search_url, headers=self.headers) # 发送GET请求
response.raise_for_status() # 检查HTTP状态码
search_data = response.json() # 将响应内容解析为JSON
results = [] # 初始化搜索结果列表
if "data" in search_data and "song" in search_data["data"] and "list" in search_data["data"]["song"]: # 检查JSON结构
for song in search_data["data"]["song"]["list"]: # 遍历歌曲列表
song_title = song["songname"] # 获取歌曲名称
artist_name = ", ".join([singer["name"] for singer in song["singer"]]) # 获取歌手名称
song_mid = song["songmid"] # 获取歌曲的songmid
results.append({
"title": f"{song_title} - {artist_name}", # 标题
"url": f"https://y.qq.com/n/ryqq/songDetail/{song_mid}", # 歌曲URL
"id": song_mid, # songmid
"source": "qqmusic" # 来源
})
return results # 返回搜索结果
except requests.exceptions.RequestException as e: # 捕获网络请求异常
print(f"QQ音乐搜索出错: {e}")
return None # 发生错误时返回None
except (KeyError, TypeError) as e: #捕获JSON解析错误
print(f"解析QQ音乐响应出错: {e}")
return None
def get_lyrics_from_url(self, url, source):
"""
根据URL和来源获取歌词。
Args:
url: 歌词页面的URL。
source: 歌词来源("baidu", "wangyiyun", "qqmusic")。
Returns:
歌词文本(字符串),如果获取失败则返回None。
"""
if source == "baidu": # 如果来源是百度
return self._get_lyrics_from_baidu_result(url) # 调用百度歌词获取方法
elif source == "wangyiyun": # 如果来源是网易云
return self._get_lyrics_from_wangyiyun(url) # 调用网易云歌词获取方法
elif source == "qqmusic": # 如果来源是QQ音乐
return self._get_lyrics_from_qqmusic(url) # 调用QQ音乐歌词获取方法
else:
print(f"不支持的歌词来源: {source}") # 如果来源不支持
return None # 返回None
def _get_lyrics_from_baidu_result(self, url):
"""从百度搜索结果提取歌词(私有方法)"""
return None # 百度搜索很难直接获得lrc.
def _get_lyrics_from_wangyiyun(self, url):
"""从网易云音乐获取歌词(私有方法)。
Args:
url: 包含歌曲id的网易云歌曲 URL
Returns:
歌词文本,如果获取失败则返回None。
"""
# 从URL中提取歌曲ID
match = re.search(r"id=(\d+)", url)
if not match:
print("无效的网易云音乐URL。无法提取歌曲ID。")
return None
song_id = match.group(1) # 获取歌曲ID
try:
lyric_url = f"https://music.163.com/api/song/lyric?id={song_id}&lv=1&kv=1&tv=-1" # 歌词API的URL
response = requests.get(lyric_url, headers=self.headers) # 发送GET请求
response.raise_for_status() # 检查HTTP状态码
lyric_data = response.json() # 将响应内容解析为JSON
if "lrc" in lyric_data and "lyric" in lyric_data["lrc"]: # 检查JSON结构
lyrics = lyric_data["lrc"]["lyric"] # 获取歌词文本
# !!! 保留时间标签 !!!
# lyrics = re.sub(r'\[.*?\]', '', lyrics).strip() # 注释掉这一行
return lyrics.strip() #直接返回, 不做处理
else:
print(f"在网易云上找不到歌曲ID为 {song_id} 的歌词。")
return None # 没有找到歌词时返回None
except requests.exceptions.RequestException as e: # 捕获网络请求异常
print(f"从网易云音乐获取歌词出错: {e}")
return None
except (KeyError, TypeError) as e: # 捕获JSON解析错误
print(f"解析网易云音乐歌词响应出错: {e}")
return None
def _get_lyrics_from_qqmusic(self, url):
"""
从QQ音乐获取歌词(私有方法)
Args:
url: 包含songmid的QQ音乐歌曲url
Returns:
歌词文本,如果获取失败则返回None。
"""
# 从URL中提取songmid
match = re.search(r"songDetail/(\w+)", url)
if not match:
print("无效的QQ音乐URL。无法提取songmid。")
return None
song_mid = match.group(1)
try:
# 构建歌词URL(这是一个API端点)
lyric_url = f"https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid={song_mid}&format=json&nobase64=1" # 歌词API的URL
response = requests.get(lyric_url, headers=self.headers) # 发送GET请求
response.raise_for_status() # 检查HTTP状态码
lyric_data = response.json() # 将响应内容解析为JSON
if "lyric" in lyric_data: # 检查JSON结构
lyrics = lyric_data["lyric"] # 获取歌词文本
# !!! 保留时间标签, 但可能需要进一步处理 !!!
# lyrics = re.sub(r'\[.*?\]', '', lyrics).strip() # 注释掉这一行!
lyrics = re.sub(r' ', ' ', lyrics) # 替换 为空格
lyrics = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), lyrics) # 转换-等HTML实体
return lyrics.strip()
else:
print(f"在QQ音乐上找不到songmid为 {song_mid} 的歌词。")
return None # 没有找到歌词时返回None
except requests.exceptions.RequestException as e: # 捕获网络请求异常
print(f"从QQ音乐获取歌词出错: {e}")
return None
except (KeyError, TypeError) as e: # 捕获JSON解析错误
print(f"解析QQ音乐歌词响应出错: {e}")
return None
def save_lyrics(self, lyrics, song_name, artist=None, directory="lyrics"):
"""
将歌词保存到 .lrc 文件。
Args:
lyrics: 歌词文本。
song_name: 歌曲名称。
artist: 歌手名称(可选)。
directory: 保存文件的目录(默认为"lyrics")。
"""
if not lyrics: # 如果没有歌词
print("没有要保存的歌词。")
return # 直接返回
if '.lrc' in directory:
file_path = directory
else:
if not os.path.exists(directory): # 如果目录不存在
os.makedirs(directory) # 创建目录
file_name = f"{song_name}" # 构建文件名
if artist: # 如果提供了歌手名称
file_name = f"{artist} - {song_name}" # 将歌手名称添加到文件名
file_name = file_name.replace("/", "_").replace("\\", "_").replace(":", "_").replace("*", "_").replace("?", "_").replace('"', "_").replace("<", "_").replace(">", "_").replace("|", "_") # 移除文件名中的非法字符
# !!! 将文件扩展名改为 .lrc !!!
file_path = os.path.join(directory, f"{file_name}.lrc")
try:
with open(file_path, "w", encoding="utf-8") as f: # 以UTF-8编码打开文件
f.write(lyrics) # 写入歌词
print(f"歌词已保存到 {file_path}") # 提示保存成功
except Exception as e: # 捕获文件写入错误
print(f"保存歌词到文件时出错: {e}")
def main():
"""
主函数,用于与用户交互,获取输入,调用下载器。
"""
downloader = LyricDownloader() # 创建LyricDownloader实例,默认使用网易云
main_dir = 'Z:\share\jellyfin\media\music' # 遍历目录
directory=r'F:\gi\lrc' # 保存目录
gc_dic = {}
# # 1.方法输入歌词和歌手
# song_name = input("请输入歌曲名称: ") # 提示用户输入歌曲名称
# artist = input("请输入歌手名称(可选,按Enter键跳过): ") # 提示用户输入歌手名称
# gc_dic[song_name] = {"artist": artist, 'save_dir': directory}
# 2.方法2 遍历
for root, dirs, files in os.walk(main_dir):
for file in files:
# print(file)
if file.endswith('.mp3') or file.endswith('.flac') or file.endswith('.ape') or file.endswith('.wav') or file.endswith('.m4a') or file.endswith('.wma') :
# print(os.path.join(root, file))
# print(os.path.join(root, file))
files = os.listdir(root)
gc = file.split('.')[0] + '.lrc'
# print(gc, dirs)
if gc in files:
# print(gc)
print(file + ',✅')
else:
if '-' in file:
song_name = file.split('-')[1].split('.')[0].strip()
artist = file.split('-')[0].strip()
print(f'{song_name} - {artist}')
else:
song_name = file.split('.')[0].strip()
artist = ''
directory = os.path.join(root, gc)
gc_dic[song_name] = {"artist": artist, 'save_dir': directory}
print("需要下载歌词,歌曲名:", song_name, "歌手:", artist)
# 3.方法3 遍历字典
for song_name in gc_dic:
# print('gc_dic[song_name]=', gc_dic[song_name])
artist = gc_dic[song_name]['artist']
directory = gc_dic[song_name]['save_dir']
search_results = downloader.search_lyrics(song_name, artist if artist else None) # 搜索歌词
if not search_results: # 如果没有找到搜索结果
print("没有找到搜索结果。")
continue # 直接返回
print("\n[%s %s]搜索结果:" % (song_name, artist)) # 显示搜索结果
for i, result in enumerate(search_results): # 遍历搜索结果
print(f"{i+1}. {result['title']} ({result['source']})") # 打印搜索结果
while True: # 循环获取用户输入
try:
choice = int(input("请输入要下载歌词的结果编号(输入0退出): ")) # 提示用户选择
if 0 <= choice <= len(search_results): # 检查输入是否有效
break # 输入有效,跳出循环
else:
print("无效的选择。请输入列表中的数字。") # 输入无效,提示用户重新输入
except ValueError:
print("无效的输入。请输入一个数字。") #输入无效,提示用户重新输入
if choice == 0: # 如果用户选择退出
continue # 直接返回
selected_result = search_results[choice - 1] # 获取用户选择的结果
lyrics = downloader.get_lyrics_from_url(selected_result['url'], selected_result['source']) # 获取歌词
if lyrics: # 如果成功获取歌词
downloader.save_lyrics(lyrics, song_name, artist if artist else None, directory=directory) # 保存歌词
if __name__ == "__main__": # 如果脚本是直接运行的(而不是作为模块导入的)
main() # 调用main函数
这段代码实现了一个名为 LyricDownloader
的 Python 类,用于从多个音乐网站(目前支持网易云音乐和 QQ 音乐)搜索和下载歌词。它允许用户通过歌曲名称和可选的歌手名称来搜索歌词,并能将歌词保存为 .lrc
文件。
代码结构与解析
-
导入模块
import requests from bs4 import BeautifulSoup import re import os
requests
: 用于发送 HTTP 请求,从网站获取数据。BeautifulSoup
: 用于解析 HTML 和 XML 文档,方便从网页中提取信息。re
: 用于正则表达式操作,处理文本和提取特定模式的内容(如歌曲 ID)。os
: 用于与操作系统交互,如创建目录、处理文件路径等。
-
LyricDownloader
类-
__init__(self, search_engine="wangyiyun")
:- 初始化方法,设置默认搜索引擎为网易云音乐。
search_engine
: 指定要使用的搜索引擎("baidu", "wangyiyun", "qqmusic")。headers
: 设置 HTTP 请求头,模拟浏览器访问,避免被网站的反爬虫机制阻止。
-
search_lyrics(self, song_name, artist=None)
:- 根据歌曲名称和歌手(可选)搜索歌词。
- 根据
self.search_engine
的值,调用相应的私有方法(_search_baidu
,_search_wangyiyun
,_search_qqmusic
)进行搜索。 - 如果搜索引擎不支持,抛出
ValueError
异常。
-
_search_baidu(self, song_name, artist=None)
:- 在百度上搜索歌词(目前返回
None
,因为百度搜索结果不易直接获取 .lrc 文件)。
- 在百度上搜索歌词(目前返回
-
_search_wangyiyun(self, song_name, artist=None)
:- 在网易云音乐上搜索歌词。
- 使用网易云音乐的非官方 API 进行搜索。
- 构建 POST 请求,发送到搜索 API。
- 解析 JSON 响应,提取歌曲信息(标题、URL、ID、来源)。
- 返回搜索结果列表。
- 处理可能出现的请求异常和 JSON 解析错误。
-
_search_qqmusic(self, song_name, artist=None)
:- 在 QQ 音乐上搜索歌词。
- 构建搜索 URL,发送 GET 请求。
- 解析 JSON 响应,提取歌曲信息。
- 返回搜索结果列表。
- 处理可能出现的请求异常和 JSON 解析错误。
-
get_lyrics_from_url(self, url, source)
:- 根据 URL 和来源获取歌词。
- 根据
source
的值,调用相应的私有方法(_get_lyrics_from_baidu_result
,_get_lyrics_from_wangyiyun
,_get_lyrics_from_qqmusic
)获取歌词。 - 如果来源不支持,打印错误信息并返回
None
。
-
_get_lyrics_from_baidu_result(self, url)
:- 从百度搜索结果中提取歌词(目前返回
None
)。
- 从百度搜索结果中提取歌词(目前返回
-
_get_lyrics_from_wangyiyun(self, url)
:- 从网易云音乐获取歌词。
- 从 URL 中提取歌曲 ID。
- 使用网易云音乐的歌词 API 获取歌词。
- 解析 JSON 响应,提取歌词文本。
- 关键修改: 保留歌词中的时间标签(没有使用
re.sub
删除)。 - 返回歌词文本。
- 处理可能出现的请求异常和 JSON 解析错误。
-
_get_lyrics_from_qqmusic(self, url)
:- 从 QQ 音乐获取歌词。
- 从 URL 中提取 songmid。
- 使用 QQ 音乐的歌词 API 获取歌词。
- 解析 JSON 响应,提取歌词文本。
- 关键修改: 保留歌词中的时间标签,并进行了一些额外的文本处理(替换特殊字符、转换 HTML 实体)。
- 返回歌词文本。
- 处理可能出现的请求异常和 JSON 解析错误。
-
save_lyrics(self, lyrics, song_name, artist=None, directory="lyrics")
:- 将歌词保存到 .lrc 文件。
- 如果
directory
参数是.lrc
结尾,则直接将directory
作为保存路径。 - 如果
directory
参数不是.lrc
结尾,如果目录不存在,则创建目录。 - 构建文件名(包含歌曲名称和歌手名称,并处理文件名中的非法字符)。
- 关键修改: 将文件扩展名设置为
.lrc
。 - 以 UTF-8 编码将歌词写入文件。
- 处理可能出现的文件写入错误。
-
-
main()
函数-
创建
LyricDownloader
实例。 -
设置主目录
main_dir
(用于遍历音乐文件)和保存目录directory
。 -
初始化一个空字典
gc_dic
-
提供三种方式来获取歌曲和歌手信息:
- (已注释)方法1: 通过
input()
函数提示用户输入歌曲名称和歌手名称。 - 方法2: 遍历
main_dir
目录下的音乐文件(支持多种格式:.mp3, .flac, .ape, .wav, .m4a, .wma)。- 检查与音乐文件同名的
.lrc
文件是否已存在。如果存在,则输出 ✅;否则,根据文件名推断出歌曲名和歌手,并把这些数据加入到gc_dic
字典
- 检查与音乐文件同名的
- 方法3:遍历
gc_dic
字典.
- (已注释)方法1: 通过
-
循环遍历
gc_dic
字典的每一首歌曲:- 调用
search_lyrics
方法搜索歌词。 - 如果找不到歌词,打印提示信息并继续下一首歌曲。
- 显示搜索结果,让用户选择要下载的歌词。
- 获取用户选择的歌词。
- 如果成功获取歌词,调用
save_lyrics
方法保存歌词。
- 调用
-
-
if __name__ == "__main__":
- 这个条件判断确保
main()
函数只在脚本直接运行时执行,而在作为模块导入时不执行。
- 这个条件判断确保
关键改进与解释
-
保留时间标签: 在
_get_lyrics_from_wangyiyun
和_get_lyrics_from_qqmusic
方法中,注释掉了原本用于删除时间标签的代码 (re.sub(r'\[.*?\]', '', lyrics).strip()
)。这使得下载的歌词保留了原始的时间信息,符合.lrc
文件的标准格式。 -
QQ 音乐歌词处理: 在
_get_lyrics_from_qqmusic
方法中,添加了以下处理:lyrics = re.sub(r' ', ' ', lyrics)
: 将特殊的 " " 字符替换为空格。lyrics = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), lyrics)
: 将 HTML 实体(如{
)转换为对应的 Unicode 字符。
-
文件名和路径处理:
- 在save_lyrics 函数中:如果
directory
参数是.lrc
结尾,则直接将directory
作为保存路径。 - 如果
directory
参数不是.lrc
结尾,创建目录,对文件名进行过滤,保证文件名的合法性。
- 在save_lyrics 函数中:如果
-
遍历本地文件:
main()
函数中现在包含了一个遍历指定目录(main_dir
)下音乐文件的功能。它会检查每个音乐文件对应的.lrc
文件是否已存在,并根据文件名自动提取歌曲和歌手信息,进行下载。 -
字典管理:使用字典
gc_dic
来存储待下载歌词的信息,避免重复下载。
文章结构建议
-
引言:
- 介绍歌词下载的常见需求。
- 简要介绍
.lrc
歌词文件格式及其重要性。 - 引出本文要介绍的 Python 歌词下载器项目。
-
项目概述:
- 介绍
LyricDownloader
类的功能和特点。 - 列出支持的音乐网站。
- 强调项目的核心优势(如保留时间标签、自动处理文件名、遍历本地文件等)。
- 介绍
-
代码详解:
- 按照代码结构,逐步解析每个模块、类和方法的功能。
- 重点解释关键代码段(如 API 请求、JSON 解析、正则表达式、文件操作等)。
- 突出关键改进(如保留时间标签、QQ 音乐歌词处理等)。
-
使用方法:
- 详细说明如何使用
LyricDownloader
类。 - 提供示例代码,展示如何搜索、选择和保存歌词。
- 解释
main()
函数中的交互逻辑和遍历文件功能。
- 详细说明如何使用
-
扩展与改进:
- 讨论项目的潜在改进方向,如:
- 支持更多音乐网站。
- 添加 GUI 界面。
- 实现更智能的搜索(如模糊匹配)。
- 自动匹配歌曲文件和歌词文件。
- 添加代理支持,应对更复杂的网络环境。
- 鼓励读者参与项目的开发和完善。
- 讨论项目的潜在改进方向,如:
-
总结:
- 回顾项目的主要功能和特点。
- 强调项目的实用性和学习价值。
- 提供项目源代码的获取方式(如 GitHub 链接)。
注意:
- 代码中使用了网易云音乐和 QQ 音乐的非官方 API。这些 API 可能会随时变化,导致代码失效。
- 频繁的 API 请求可能会触发网站的反爬虫机制。建议在使用时添加适当的延时,或使用代理。
- 代码的错误处理还可以进一步完善,例如更详细地记录错误日志。
本文作者: 永生
本文链接: https://www.yys.zone/detail/?id=385
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
评论列表 (0 条评论)