1. 程式人生 > >爬蟲筆記之刷小怪練級:yymp3爬蟲(音樂類爬蟲)

爬蟲筆記之刷小怪練級:yymp3爬蟲(音樂類爬蟲)

lan resp tle 想法 stp 傳遞 header 壓力 idt

一、目標

爬取http://www.yymp3.com網站歌曲相關信息,包括歌曲名字、作者相關信息、歌曲的音頻數據、歌曲的歌詞數據。

二、分析

2.1 歌曲信息、歌曲音頻數據下載地址的獲取

隨便打開一首歌曲的詳情頁:

技術分享圖片

歌曲的名字、作者相關信息可以通過解析html得到,這些信息在html中能夠搜索得到,那麽歌曲的音頻數據的下載鏈接如何得到呢?

要在網頁中播放音頻,首先要有一個audio標簽,已經加載完畢的網頁的內存DOM模型中會有一個audio標簽掛載著,使用Chrome的開發者工具,切換到Elements選項卡,搜索audio標簽:

技術分享圖片

第一個想法就是立刻試下在頁面中搜索一下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,在這個文件的第一行就有如何處理音頻播放地址的邏輯:

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格式的,作為播放地址,這個替換有點奇怪,不太清楚是因為什麽原因。

接下來就是搞懂$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

2.2 lrc數據的獲取

爬取歌曲信息與普通音頻類爬蟲不同的是歌曲還需要額外的抓取歌詞信息,歌詞使用的格式是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"
}

.

爬蟲筆記之刷小怪練級:yymp3爬蟲(音樂類爬蟲)