教你如何使Python爬取酷我在線音樂(lè)
前言
寫(xiě)這篇博客的初衷是加深自己對(duì)網(wǎng)絡(luò)請(qǐng)求發(fā)送和響應(yīng)的理解,僅供學(xué)習(xí)使用,請(qǐng)勿用于非法用途!文明爬蟲(chóng),從我做起。下面進(jìn)入正題。
獲取歌曲信息列表
在酷我的搜索框中輸入關(guān)鍵詞 aiko,回車(chē)之后可以看到所有和 aiko 相關(guān)的歌曲。打開(kāi)開(kāi)發(fā)者模式,在網(wǎng)絡(luò)面板下按下 ctrl + f,搜索 二人,可以找到響應(yīng)結(jié)果中包含 二人 的請(qǐng)求,這個(gè)請(qǐng)求就是用來(lái)獲取歌曲信息列表的。

請(qǐng)求參數(shù)分析
請(qǐng)求的具體格式如下圖所示,可以看到請(qǐng)求路徑為 http://www.kuwo.cn/api/www/search/searchMusicBykeyWord,請(qǐng)求參數(shù)包括:
key: 搜索關(guān)鍵詞,此處為aikopn: 頁(yè)碼,page number的縮寫(xiě),此處為1rn: 每頁(yè)條目數(shù),應(yīng)該是row number的縮寫(xiě),默認(rèn)為30httpsStatus:https 的狀態(tài)?感覺(jué)沒(méi)啥大用,看了源代碼里面是直接寫(xiě)死t.url = t.url + "?reqId=".concat(n, "&httpsStatus=1")reqId:請(qǐng)求標(biāo)識(shí),刷新頁(yè)面之后值會(huì)發(fā)生改變,不知道有啥用,待會(huì)兒模擬請(qǐng)求的時(shí)候試著不帶上他會(huì)怎么樣

打開(kāi) Apifox(當(dāng)然 postman 也行),新建一個(gè)接口,把請(qǐng)求路徑和參數(shù)設(shè)置為下圖所示的樣子,為了讓響應(yīng)結(jié)果簡(jiǎn)短點(diǎn),這里把每頁(yè)的條目數(shù)設(shè)置為 1 而非默認(rèn)的 30:

在沒(méi)有設(shè)置額外請(qǐng)求頭的情況下發(fā)個(gè)請(qǐng)求試試,發(fā)現(xiàn) 403 Forbidden 了,emmmmm,應(yīng)該是防盜鏈所致:

可以看到瀏覽器發(fā)出的請(qǐng)求的請(qǐng)求頭中有設(shè)置 Referer 字段,把它加上,應(yīng)該不會(huì)再報(bào)錯(cuò)了吧:

這次狀態(tài)碼為 200,但是沒(méi)有收到任何數(shù)據(jù),success 為 false 說(shuō)明請(qǐng)求失敗了,message 指明了失敗原因是缺少 CSRF token。問(wèn)題不大,接著把瀏覽器發(fā)出的請(qǐng)求中的 csrf 加到 Apifox 請(qǐng)求頭中,再發(fā)請(qǐng)求,還是報(bào)錯(cuò) CSRF token Invalid!。算了,還是老老實(shí)實(shí)把 Cookie 也加上吧,但也不是全部加上,只加 kw_token=CCISYM2HV96 部分,因?yàn)?Cookie 里面只有這個(gè)字段和 token 有關(guān)系且它的值和 csrf 相同。
在源代碼面板按下 ctrl + shift + f,搜索一下 csrf,可以看到 csrf 本來(lái)就是來(lái)自 Object(h.b)("kw_token"),這個(gè)函數(shù)用來(lái)取出 document.cookie 中的 kw_token 字段值。至于 Cookie 中的 kw_token 怎么計(jì)算得到的,那就是服務(wù)器的事情了,咱們只管 CV 操作即可。

準(zhǔn)備好參數(shù)和請(qǐng)求頭,重新發(fā)送請(qǐng)求,可以得到想要的數(shù)據(jù)。如果去掉 reqId 參數(shù),也可以拿到數(shù)據(jù),但是會(huì)有略微的不同,這里就不貼出來(lái)了:
{
"code": 200,
"curTime": 1649482287185,
"data": {
"total": "741",
"list": [
{
"musicrid": "MUSIC_11690555",
"barrage": "0",
"ad_type": "",
"artist": "aiko",
"mvpayinfo": {
"play": 0,
"vid": 8530326,
"down": 0
},
"nationid": "0",
"pic": "http://img4.kuwo.cn/star/starheads/500/24/88/4146545084.jpg",
"isstar": 0,
"rid": 11690555,
"duration": 362,
"score100": "42",
"ad_subtype": "0",
"content_type": "0",
"track": 1,
"hasLossless": true,
"hasmv": 1,
"releaseDate": "1970-01-01",
"album": "",
"albumid": 0,
"pay": "16515324",
"artistid": 1907,
"albumpic": "http://img4.kuwo.cn/star/starheads/500/24/88/4146545084.jpg",
"originalsongtype": 0,
"songTimeMinutes": "06:02",
"isListenFee": false,
"pic120": "http://img4.kuwo.cn/star/starheads/120/24/88/4146545084.jpg",
"name": "戀をしたのは",
"online": 1,
"payInfo": {
"play": "1100",
"nplay": "00111",
"overseas_nplay": "11111",
"local_encrypt": "1",
"limitfree": 0,
"refrain_start": 89150,
"feeType": {
"song": "1",
"vip": "1"
},
"down": "1111",
"ndown": "11111",
"download": "1111",
"cannotDownload": 0,
"overseas_ndown": "11111",
"refrain_end": 126247,
"cannotOnlinePlay": 0
},
"tme_musician_adtype": "0"
}
]
},
"msg": "success",
"profileId": "site",
"reqId": "4b55cf4b0171253c33ce1d71b999c42f",
"tId": ""
}
請(qǐng)求代碼
響應(yīng)結(jié)果的 data 字段中有很多東西,這里只提取需要的部分。在提取之前先來(lái)定義一下歌曲信息實(shí)體類(lèi),這樣在其他函數(shù)中要一首歌曲的信息時(shí)只要把實(shí)體類(lèi)的實(shí)例傳入即可。
# coding:utf-8
from copy import deepcopy
from dataclasses import dataclass
class Entity:
""" Entity abstract class """
def __setitem__(self, key, value):
self.__dict__[key] = value
def __getitem__(self, key):
return self.__dict__[key]
def get(self, key, default=None):
return self.__dict__.get(key, default)
def copy(self):
return deepcopy(self)
@dataclass
class SongInfo(Entity):
""" Song information """
file: str = None
title: str = None
singer: str = None
album: str = None
year: int = None
genre: str = None
duration: int = None
track: int = None
trackTotal: int = None
disc: int = None
discTotal: int = None
createTime: int = None
modifiedTime: int = None
上述代碼顯示定義了實(shí)體類(lèi)的基類(lèi),并且重寫(xiě)了 __getitem__ 和 __setitem__ 魔法方法,這樣我們可以像訪問(wèn)字典一樣來(lái)訪問(wèn)實(shí)體類(lèi)對(duì)象的屬性。接著讓歌曲信息實(shí)體類(lèi)繼承了實(shí)體類(lèi)基類(lèi),并且使用 @dataclass 裝飾器,這是 python 3.7 引入的新特性,使用它裝飾之后的實(shí)體類(lèi)無(wú)需實(shí)現(xiàn)構(gòu)造函數(shù)、__str__等常用函數(shù),python 會(huì)幫我們自動(dòng)生成。
在發(fā)送請(qǐng)求的過(guò)程中可能會(huì)遇到各種異常,如果在代碼里面寫(xiě) try except 語(yǔ)句會(huì)顯得很亂,這里同樣可以用裝飾器來(lái)解決這個(gè)問(wèn)題。
# coding:utf-8
from copy import deepcopy
def exceptionHandler(*default):
""" decorator for exception handling
Parameters
----------
*default:
the default value returned when an exception occurs
"""
def outer(func):
def inner(*args, **kwargs):
try:
return func(*args, **kwargs)
except BaseException as e:
print(e)
value = deepcopy(default)
if len(value) == 0:
return None
elif len(value) == 1:
return value[0]
else:
return value
return inner
return outer
下面是發(fā)送獲取歌曲信息請(qǐng)求的代碼,使用 exception_handler 裝飾了 getSongInfos 方法,這樣發(fā)生異常時(shí)會(huì)打印異常信息并返回默認(rèn)值:
# coding:utf-8
import json
from urllib import parse
from typing import List, Tuple
import requests
class KuWoMusicCrawler:
""" Crawler of KuWo Music """
def __init__(self):
super().__init__()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
'Cookie': 'kw_token=C713RK6IJ8J',
'csrf': 'C713RK6IJ8J',
'Host': 'www.kuwo.cn',
'Referer': ''
}
@exceptionHandler([], 0)
def getSongInfos(self, key_word: str, page_num=1, page_size=10) -> Tuple[List[SongInfo], int]:
key_word = parse.quote(key_word)
# configure request header
headers = self.headers.copy()
headers["Referer"] = 'http://www.kuwo.cn/search/list?key='+key_word
# send request for song information
url = f'http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key={key_word}&pn={page_num}&rn={page_size}&reqId=c06e0e50-fe7c-11eb-9998-47e7e13a7206'
response = requests.get(url, headers=headers)
response.raise_for_status()
# parse the response data
song_infos = []
data = json.loads(response.text)['data']
for info in data['list']:
song_info = SongInfo()
song_info['rid'] = info['rid']
song_info.title = info['name']
song_info.singer = info['artist']
song_info.album = info['album']
song_info.year = info['releaseDate'].split('-')[0]
song_info.track = info['track']
song_info.trackTotal = info['track']
song_info.duration = info["duration"]
song_info.genre = 'Pop'
song_info['coverPath'] = info.get('albumpic', '')
song_infos.append(song_info)
return song_infos, int(data['total'])
獲取歌曲下載鏈接
免費(fèi)歌曲
雖然我們實(shí)現(xiàn)了搜索歌曲的功能,但是沒(méi)拿到每一首歌的播放地址,也就沒(méi)辦法把歌曲下載下來(lái)。我們先來(lái)播放一首不收費(fèi)的歌曲試試??梢钥吹綖g覽器發(fā)送了一個(gè)獲取播放鏈接的請(qǐng)求,路徑為 http://www.kuwo.cn/api/v1/www/music/playUrl,有兩個(gè)需要關(guān)注的參數(shù):
mid:音樂(lè) Id,此處的值為941583,和頁(yè)面 url 中的編號(hào)一致,由于我們是通過(guò)點(diǎn)擊搜索結(jié)果頁(yè)面中二人跳轉(zhuǎn)過(guò)來(lái)的,而二人這條結(jié)果也是動(dòng)態(tài)加載出來(lái)的,超鏈接中的 Id 肯定也來(lái)自于上一節(jié)中響應(yīng)結(jié)果的某個(gè)字段。二人是第四條記錄,通過(guò)對(duì)比可以發(fā)現(xiàn)data.list[3].rid就是mid;type:音樂(lè)類(lèi)型?此處的值為music,發(fā)送請(qǐng)求的時(shí)候也設(shè)置為music即可

在 Apifox 中新建一個(gè)獲取歌曲播放地址的請(qǐng)求,如下所示,發(fā)現(xiàn)可以成功拿到播放地址:

付費(fèi)歌曲
現(xiàn)在換一首歌,比如 aiko - 橫顏,點(diǎn)擊歌曲頁(yè)面上的播放按鈕時(shí)會(huì)彈出要求在客戶端中付費(fèi)收聽(tīng)的對(duì)話框。直接發(fā)送請(qǐng)求,響應(yīng)結(jié)果會(huì)是下面這個(gè)樣子,狀態(tài)碼為 403:

其實(shí)酷我在 2021 年 9 月份的時(shí)候換過(guò)獲取播放地址的接口,那時(shí)候的請(qǐng)求接口為 http://www.kuwo.cn/url,支持以下幾個(gè)參數(shù):
format: 在線音樂(lè)的格式,可以是mp3type: 和現(xiàn)在的接口中的type參數(shù)一樣,但是值為convert_url3rid: 音樂(lè) Id,和mid一樣br: 在線音樂(lè)的比特率,越大則音質(zhì)越高,可選的有128kmp3、192kmp3和320kmp3
這個(gè)接口不管是付費(fèi)音樂(lè)還是免費(fèi)音樂(lè)都可以用。如果將現(xiàn)在這個(gè)接口的 type 參數(shù)的值換成 convert_url3,請(qǐng)求結(jié)果如下所示,說(shuō)明成功了:

請(qǐng)求代碼
下面是獲取在線音樂(lè)播放鏈接的代碼,只需調(diào)用 downloadSong 函數(shù)并把爬取到的歌曲傳入就能完成歌曲的下載:
@exceptionHandler('')
def getSongUrl(self, song_info: SongInfo) -> str:
# configure request header
headers = self.headers.copy()
headers.pop('Referer')
headers.pop('csrf')
# send request for play url
url = f"http://www.kuwo.cn/api/v1/www/music/playUrl?mid={song_info['rid']}&type=convert_url3"
response = requests.get(url, headers=headers)
response.raise_for_status()
play_url = json.loads(response.text)['data']['url']
return play_url
@exceptionHandler('')
def downloadSong(self, song_info: SongInfo, save_dir: str) -> str:
# get play url
url = self.getSongUrl(song_info)
if not url:
return ''
# send request for binary data of audio
headers = self.headers.copy()
headers.pop('Referer')
headers.pop('csrf')
headers.pop('Host')
response = requests.get(url, headers=headers)
response.raise_for_status()
# save audio file
song_path = os.path.join(
save_dir, f"{song_info.singer} - {song_info.title}.mp3")
with open(song_path, 'wb') as f:
f.write(data)
return song
后記
除了獲取歌曲的詳細(xì)信息和播放地址外,我們還能拿到歌詞、歌手信息等,方法是類(lèi)似的,在我的 Groove 中提供了在線歌曲的功能,一部分接口就是來(lái)自酷我,還有一些來(lái)自酷狗和網(wǎng)易云,爬蟲(chóng)的代碼在 app/common/crawler 目錄下,喜歡的話可以給個(gè) star 哦,以上~~
以上就是教你如何使Python爬取酷我在線音樂(lè)的詳細(xì)內(nèi)容,更多關(guān)于Python爬取音樂(lè)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Pygame實(shí)現(xiàn)游戲最小系統(tǒng)功能詳解
這篇文章主要介紹了Pygame實(shí)現(xiàn)游戲最小系統(tǒng),Pygame是一個(gè)專(zhuān)門(mén)用來(lái)開(kāi)發(fā)游戲的 Python 模塊,主要為開(kāi)發(fā)、設(shè)計(jì) 2D 電子游戲而生,具有免費(fèi)、開(kāi)源,支持多種操作系統(tǒng),具有良好的跨平臺(tái)性等優(yōu)點(diǎn)2022-11-11
python計(jì)算機(jī)視覺(jué)opencv矩形輪廓頂點(diǎn)位置確定
這篇文章主要為大家介紹了python計(jì)算機(jī)視覺(jué)opencv矩形輪廓頂點(diǎn)位置確定,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Python可視化Matplotlib散點(diǎn)圖scatter()用法詳解
這篇文章主要介紹了Python可視化中Matplotlib散點(diǎn)圖scatter()的用法詳解,文中附含詳細(xì)示例代碼,有需要得朋友可以借鑒參考下,希望能夠有所幫助2021-09-09
在Python中使用Neo4j數(shù)據(jù)庫(kù)的教程
這篇文章主要介紹了在Python中使用Neo4j數(shù)據(jù)庫(kù)的教程,Neo4j是一個(gè)具有一定人氣的非關(guān)系型的數(shù)據(jù)庫(kù),需要的朋友可以參考下2015-04-04
python基礎(chǔ)之while循環(huán)、for循環(huán)詳解及舉例
所謂循環(huán)結(jié)構(gòu)就是程序中控制某條或某些指令重復(fù)執(zhí)行的結(jié)構(gòu),下面這篇文章主要給大家介紹了關(guān)于python基礎(chǔ)之while循環(huán)、for循環(huán)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04

