爬取http://www.yymp3.com網站歌曲相關信息,包括歌曲名字、做者相關信息、歌曲的音頻數據、歌曲的歌詞數據。html
隨便打開一首歌曲的詳情頁:前端
歌曲的名字、做者相關信息能夠經過解析html獲得,這些信息在html中可以搜索獲得,那麼歌曲的音頻數據的下載連接如何獲得呢?python
要在網頁中播放音頻,首先要有一個audio標籤,已經加載完畢的網頁的內存DOM模型中會有一個audio標籤掛載着,使用Chrome的開發者工具,切換到Elements選項卡,搜索audio標籤:json
第一個想法就是馬上試下在頁面中搜索一下http://ting666.yymp3.com:86/new27/tiandan/3.mp3看看能不能搜索到,先冷靜想一下,前端開發的時候對於url地址通常都是會有一個變量存放baseUrl,而後使用其它的相對路徑拼接出完整的url,這樣作是爲了方便在測試環境和開發環境切換,好比測試環境是test.foo.com,開發環境是www.foo.com,那麼測試經過上線的時候只須要修改一個變量值就能夠了,這樣比較方便。因此正確的搜索方式是隻拿相對路徑搜索或者只拿文件名在html中搜索:數組
有點尷尬,網頁中沒有搜索到,是由於網頁中沒有攜帶音頻播放地址嗎?再冷靜想一下,想到前端技術棧那一籮筐亂七八糟的兼容性問題,因此基本能夠確定在播放的時候確定會檢測當前的瀏覽器環境選擇不一樣的播放格式,那麼頗有多是返回的地址是一個player/foo.wma這種格式的,而後通過檢測環境後發現使用mp3更合適,因此直接修改了擴展名爲player/foo.mp3,不妨將擴展名去掉只使用new27/tiandan/3搜索試一下:瀏覽器
此次果真搜到了,看上面這個變量是將歌曲的相關信息都賦給了一個變量,那麼後面必定有一個地方使用到了這個變量,在html頁面中搜索了一下:服務器
看來不是在當前頁面中使用的,那麼必定是在這個變量聲明以後引入的js中使用到的,找了一下,找到了這個js文件:http://img.yymp3.com/jplay/jplayer.ready.js,在這個文件的第一行就有如何處理音頻播放地址的邏輯:app
try{var firstplay="http://ting666.yymp3.com:86/"+$song_data[0].split("|")[4].toLowerCase().replace(".wma",".mp3");}catch(e){var firstplay='';}
將$song_data[0]="268462|迷途之光|10888|田丹|new27/tiandan/3.wma|23353||";按照| split取下標爲4的,將.wma格式的換爲.mp3格式的,做爲播放地址,這個替換有點奇怪,不太清楚是由於什麼緣由。dom
接下來就是搞懂$song_data這個變量按照| split以後數組中每一個元素的意思。函數
這是歌曲的詳情頁url:
http://www.yymp3.com/Play/23353/268462.htm
通過對比能夠獲得0下標存放的是歌曲的id,那麼23353是個什麼鬼呢?注意到網站有一個專輯功能,隨便找一個專輯:
http://www.yymp3.com/Album/23304.htm
上面的23304就是專輯的id,而後隨便進入專輯下的某個歌曲的詳情頁:
http://www.yymp3.com/Play/23304/268053.htm
由此能夠證實,歌曲詳情頁的格式是:
http://www.yymp3.com/Play/{專輯id}/{歌曲id}.htm
即5下標的數字是此歌曲所屬的專輯id。
還有一個沒搞懂的2下標的數字,歌曲id有了,專輯id有了,貌似還差個做者id,仍是上面那首歌:
http://www.yymp3.com/Play/23304/268053.htm
查看其源代碼中:
$song_data[0]="268053|失語|10845|王思遠|new27/wansiyuan2/1.wma|23304||";
而後打開做者詳情頁:
http://www.yymp3.com/singer/10845.htm
由此能夠肯定,下標爲2的是做者id。
至此$song_data[0]中的全部列表示的含義都已被推測出:
268053|失語|10845|王思遠|new27/wansiyuan2/1.wma|23304||"; 歌曲id | 歌曲名字 | 做者id | 做者名字 | 歌曲音頻播放地址 | 歌曲所屬專輯id
爬取歌曲信息與普通音頻類爬蟲不一樣的是歌曲還須要額外的抓取歌詞信息,歌詞使用的格式是lrc,關於lrc的更多知識能夠看這裏:lrc詳解。
那麼歌曲的lrc數據如何獲得呢?
回到歌曲詳情頁,在播放頁上可以看到歌詞在滾動,說明這一頁一定有獲得lrc的方式,在html找了下沒有,那麼比較可能的方式就是請求的js而後將此歌曲的id傳入,好,來根據歌曲的id在network下搜索:
搜索出來四個請求,第一個是doc類型,是html的請求,前面已經肯定其中沒有lrc格式的歌詞了,第二個看了下是個訪問統計信息,也沒什麼做用,第三個是個人卡巴斯基檢測,也沒啥用,第四個是最可疑的,把完整的請求路徑拿出來看一下:
http://www.yymp3.com/lrc/27/268462.js
路徑中帶着lrc三個字,又傳遞了歌曲的id,八成就是請求去請求歌詞的,看下它的返回內容是什麼:
$song_Lrc[268462] = "0,0,1000,2000,3000,4000,5000,6000,7000,16000,18000,21000,29000,35000,43000,47000,50000,57000,61000,64000,72000,74000,79000,81000,86000,90000,93000,104000,107000,110000,114000,118000,120000,124000,128000,132000,136000,139000,146000,149000,153000,160000,162000,166000,168000,173000,177000,180000,186000,187000,189000,194000,196000,201000,205000,208000,213000,217000,220000,224000,230000,234000,237000[/]迷途之光[n]田丹 - 迷途之光[n]做詞:田丹、弓強子[n]做曲:田丹[n]編曲:程天禹[n]吉他:胡閣 [n]混音:顧瀟予[n]母帶:全相彥@OKMastering[n]製做人:程天禹[n]不想了[n]不要再想着[n]不要再等了 ye[n]咱們 再說過之後[n]就不要騙了 嗚哦[n]愛像坐過山車[n]通過一路顛簸[n]路過迷泊[n]走過聖地亞哥[n]徘徊海的顏色[n]我不懂[n]也許背離的[n]很像愛情的自由選擇[n]也許放棄[n]是謊話安排不甘寂寞[n]就算默數愛的真正意義[n]也無從繼續[n]來得實際不如再說一句[n]結果[n]還不是猜想[n]有什麼好說[n]Ye[n]咱們[n]在說過之後[n]就不要騙了[n]wo[n]愛像坐過山車[n]通過一路顛簸[n]路過迷泊[n]走過聖地亞哥[n]徘徊海的顏色[n]我不懂[n]也許背離的[n]像愛情的自由選擇[n]也許放棄是[n]謊話安排不甘寂寞[n]就算默許愛的真正意義[n]也無從繼續[n]來得實際不如再說一句[n]哦[n]也許背離的[n]像愛情的自由選擇[n]也許放棄是[n]謊話安排不甘寂寞[n]就算默許愛的真正意義[n]也無從繼續[n]來得實際不如再說一句[n]哦哦哦哦[n]哦哦哦哦哦哦哦[n]哦哦哦哦哦哦哦[n]哦哦哦哦哦哦哦[n]哦哦哦哦哦哦哦[n]哦哦哦哦哦哦哦[n]哦哦哦哦哦哦哦[n]";
看上去亂七八糟的,應該只是爲了方便程序解析才返回這個格式的,雖然我感受搞成這個格式程序解析起來一點也不方便,並且人看起來也一點也不方便...那麼接下來的事情也能夠腦補出來了,這個請求的返回值聲明瞭一個變量,那麼必定有一個地方是使用了這個變量的,只須要找到使用這個變量的地方分析其使用規則便可解析出lrc格式的歌詞來:
這個調用棧是從下往上的,整理一下加載歌詞的邏輯,首先初始化了一個播放器:
var pu = new PlayerUtils();optlist(0);pu.utils(0,0,3);
在初始化的時候會加載歌詞數據:
this.downloadlrc(song_u[0]);
而後看下是怎麼去請求歌詞的:
this.downloadlrc = function(t) { var tfolder = ""; var fdata = t / 10000 + 1; fdata = fdata.toString(); tfolder = fdata; if (fdata.indexOf(".") != -1) { tfolder = fdata.split(".")[0]; } if (!$song_Lrc[t]) { this.led('', '', '', '正在載入歌詞...', '', '', ''); this.ledColor(4); $download(_url + 'lrc/' + tfolder + '/' + t + '.js'); } lrctimea = 8888888; };
這段代碼大概就是檢查當前歌曲的歌詞數據是否已經被加載,若是沒有加載的話就取服務器將對應的歌詞數據拉取一下,這裏有個比較奇怪的地方:
var fdata = t / 10000 + 1; fdata = fdata.toString(); tfolder = fdata;
爲何要除以10000呢?事情到這裏就比較有意思了,如今咱們站在站長的角度來思考一下,若是我有幾十萬歌曲的lrc數據我應該如何存儲呢?由上面的這段代碼我猜想站長應該是將這些歌詞數據放在磁盤上,以小文件的形式存儲,而後每10000個小文件新建一個文件夾避免一個文件夾下存放過多。
分析到這裏只是爲了知足一下個人好奇心,其實還有更好的方式獲取歌詞數據,在歌曲詳情頁有個連接:
在Chrome的控制檯直接輸入showword回車:
返回值打印的是函數體,直接單擊便可跳到對應js文件的對應位置:
function showword(obj) { var wordmulu = ""; var mudata = obj / 10000; mudata = mudata.toString(); wordmulu = mudata; if (mudata.indexOf(".") != -1) { wordmulu = mudata.split(".")[0]; } epen2("/Songword/" + wordmulu + "/" + obj + ".htm"); }
上面的這個反卻是沒有+1,直接對10000整除便可,由此能夠肯定獲取lrc數據的規則,
http://www.yymp3.com/Songword/{歌曲id // 10000}/{歌曲id}.htm
寫此篇文章的目的只是爲了訓練下分析能力,並非爲了爬取全站數據,因此僅僅是對上面分析寫了個簡單的實現,輸入歌曲詳情頁,返回歌曲的相關信息:
#! /usr/bin/python3 # -*- coding: utf-8 -*- """ 音樂mp3爬蟲 http://www.yymp3.com/ """ import json import logging import re import time import requests from bs4 import BeautifulSoup, NavigableString logging.basicConfig(level=logging.INFO, format='%(asctime)s %(filename)s : %(levelname)s %(message)s') logger = logging.getLogger(__name__) last_request_time = 0 REQUEST_MIN_INTERVAL = 1 def download_html(url): rate_limiter() logger.info('download url ' + url) headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36'} response = requests.get(url, headers=headers) return response.content def rate_limiter(): """ 用來對請求限度,以避免請求過快對網站產生較大壓力影響其正常運行 :return: """ global last_request_time interval_seconds = time.time() - last_request_time if interval_seconds < REQUEST_MIN_INTERVAL: time.sleep(REQUEST_MIN_INTERVAL - interval_seconds) last_request_time = time.time() def parse_music_info(music_detail_page_url): """ 解析音樂的相關信息 :param music_detail_page_url: :return: """ html = download_html(music_detail_page_url) music_info_array = re.search('song_data\[0\]="([^"]+)"', html.decode('UTF-8')).group(1).split('|') return { "music_name": music_info_array[1], "music_id": music_info_array[0], "music_audio_link": 'http://ting666.yymp3.com:86/' + music_info_array[4].replace('.wma', '.mp3'), "lrc": get_lrc_by_music_id(music_info_array[0]), "author_name": music_info_array[3], "author_id": music_info_array[2], "album_id": music_info_array[5] } def get_lrc_by_music_id(music_id): """ 根據歌曲id抓取lrc格式的歌詞 :param music_id: :return: """ lrc_page_url = 'http://www.yymp3.com/Songword/%d/%s.htm' % (int(music_id) // 10000, music_id) html = download_html(lrc_page_url) dom = BeautifulSoup(html) lrc_box = dom.select_one('#lrc') return lrc_box_to_text(lrc_box) def lrc_box_to_text(lrc_box): """ 使用bs4的text沒有把br解析成換行符,仍是手動實現一下這個功能吧 :param lrc_box: :return: """ lrc_lines = [] for e in lrc_box.children: # 只有非空白的文本節點才被認爲是有效的歌詞 if type(e) == NavigableString and not str(e).isspace(): lrc_lines.append(str(e).strip()) return '\n'.join(lrc_lines) if __name__ == '__main__': music_info = parse_music_info('http://www.yymp3.com/Play/15042/191056.htm') print(json.dumps(music_info))
抓取結果:
{ "music_name":"鄉戀", "music_id":"191056", "music_audio_link":"http://ting666.yymp3.com:86/new17/Gongyue10/12.mp3", "lrc":"[00:40.11]你的身影 [00:45.10]你的歌聲 [00:50.19]永遠印在 [00:54.22]個人心中 [00:59.26]昨天雖已消逝 [01:03.87]分別難相逢 [01:08.22]怎能忘記 [01:13.04]你的一片深情 [01:18.09]昨天雖已消逝 [01:22.19]分別難相逢 [01:27.29]怎能忘記 [01:31.19]你的一片深情 [02:33.91]個人情愛 [02:38.17]個人好夢 [02:43.01]永遠留在 [02:46.95]你的懷中 [02:52.14]明天就要來臨 [02:57.07]卻可貴和你相逢 [03:01.23]只有風兒 [03:05.29]送去個人一片深情 [03:11.00]明天就要來臨 [03:16.10]卻可貴和你相逢 [03:19.98]只有風兒 [03:24.68]送去個人深情 [[03:50.18]", "author_name":"龔玥", "author_id":"2974", "album_id":"15042" }
.