python3爬蟲應用--爬取網易雲音樂(兩種辦法)


一、需求

  好久沒有碰爬蟲了,竟不知道從何入手。偶然看到一篇知乎的評論(https://www.zhihu.com/question/20799742/answer/99491808),一時興起就也照葫蘆畫瓢般嘗試做一做。本文主要是通過網頁的歌名搜索,然后獲取到頁面上的搜索結果,最后自行選擇下載搜索結果中的哪條歌曲。

 

二、應用

  在這個過程中,有很多坑,但還好撐過去了。過程中主要用到的東西有 fiddler抓包查看日志、瀏覽器JS的分析、python ASE的加密、request包 的簡單應用、jsonpath包的運用、python基礎的列表、字典、格式化的簡單運用。

 

三、說明

  1、本文主要參考引用了以下博文,感謝大神們的思路和過程:

  https://www.zhihu.com/question/36081767

  https://www.cnblogs.com/nienie/p/8511999.html

  https://www.zhihu.com/question/20799742/answer/99491808

 

  https://www.cnblogs.com/mxk123/p/11832247.html(CSS選擇器學習)

  https://www.cnblogs.com/songzhenhua/p/10260992.html  (也是CSS選擇器學習)

  https://www.cnblogs.com/yuluoxingkong/p/10019246.html  (可以借鑒無界面話的瀏覽器模式)

 

  2、本文僅供娛樂和學習,切勿用於商業用途,如有發現,概不負責!

 

四、正文

  

 

上圖就是我們的目標頁面,很簡單的實現搜索和下載音樂的功能。  

根據搜索地址(https://music.163.com/#/search/m/?s=難得有情人&type=1),本文主要的思路有:

思路一

1、使用beautifulsoup 把整個網頁load下來,看HTML中是否有歌曲的ID,我們主要是用ID來進行下載,有個歌曲外鏈的下載地址(http://music.163.com/song/media/outer/url?id=id.mp3

),我也不知道從哪里來的,可以根據ID來下載歌曲。現在看下網頁的HTML源碼,對着歌名右鍵——檢查,可以看到歌名、歌曲的id、歌手等信息,然后使用beautifulsoup 進行獲取就可以了。但是現在關鍵信息都放在了iframe里面,暫時不知道怎么抓取。本文就先不深入闡述,有時間再研究一下。(補:網上搜了一下可以使用webdriver來實現跨iframe抓取數據,因此也就使用該種方法了,不多說直接放代碼,代碼比第二種少了一些,不過速度較慢)

 

------------------------------------補上思路一的代碼,效果和思路二是一樣的----------------------------------------

import requests                        # 用於獲取網頁內容的模塊
from bs4 import BeautifulSoup        # 用於解析網頁源代碼的模塊
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def handle_hmtl(search_name):
    chrome_options = Options()
    chrome_options.add_argument('--headless')
    driver = webdriver.Chrome('chromedriver', chrome_options=chrome_options)
    link = "https://music.163.com/#/search/m/?s=" + search_name + "&type=1"  # 要搜索的鏈接
    driver.get(link)
    iframe_elemnt = driver.find_element_by_id("g_iframe")  # 因為直接獲取不到iframe的內容,因此使用web_driver
    driver.switch_to.frame(iframe_elemnt)  # 關鍵步驟,跳轉到iframe里面,就可以獲取HTML內容
    soup = BeautifulSoup(driver.page_source, "html.parser")  # 通過 BeautifulSoup 模塊解析網頁,具體請參考官方文檔。
    L = []  # 存儲結果的列表
    nu = 0
    for value in soup.select(
            "div[class='srchsongst'] div[class^='item f-cb h-flag']"):  # 獲取到關鍵class:srchsongst下面的所有元素,結果是一個列表,使用的是CSS的方式
        D = {'num': 'null', 'name': 'null', 'id': 'null', 'singer': 'null', 'song_sheet': 'null'}  # 初始化字典
        D['num'] = nu  # 用來計算num
        D['name'] = value.b.attrs['title']  # 歌名
        D['id'] = value.a.attrs['data-res-id']  # 歌曲ID
        D['singer'] = '/'.join([i.string for i in value.select('a[href^="/artist?i"]')])  # 歌唱者
        D['song_sheet'] = value.select('a[class^="s-fc3"]')[0].attrs['title']  # 專輯
        L.append(D)
        nu += 1
    return L


def load_song(num, result):
    """
    result 是一個列表
    num 是一個str
    """
    if isinstance(int(num), int):
        num = int(num)
        if num >= 0 and num <= len(result):
            song_id = result[num]['id']
            song_down_link = "http://music.163.com/song/media/outer/url?id=" + result[num]['id'] + ".mp3"  # 根據歌曲的 ID 號拼接出下載的鏈接。歌曲直鏈獲取的方法參考文前的注釋部分。
            print("歌曲正在下載...")
            response = requests.get(song_down_link, headers=headers).content  # 親測必須要加 headers 信息,不然獲取不了。
            f = open(result[num]['name'] + ".mp3", 'wb')  # 以二進制的形式寫入文件中
            f.write(response)
            f.close()
            print("下載完成.\n\r")
        else:
            print("你輸入的數字不在歌曲列表范圍,請重新輸入")
    else:
        print("請輸入正確的歌曲序號")


if __name__ == '__main__':
    headers={    # 偽造瀏覽器頭部,不然獲取不到網易雲音樂的頁面源代碼。
        'User-Agent':'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36',
        'Referer':'http://93.174.95.27',
    }
    search_name = input("請輸入你想要在網易雲音樂中搜索的單曲:")
    result = handle_hmtl(search_name)
    print("%3s %-35s %-20s %-20s " % ("序號", "  歌名", "歌手", "專輯"))
    for i in range(len(result)):
        print("%3s %-35s %-20s %-20s " % (
        result[i]["num"], result[i]["name"], result[i]["singer"], result[i]["song_sheet"]))
    num = input("請輸入你想要下載歌曲的序號/please input the num you want to download:")
    load_song(num, result)  # 下載歌曲

 

 

思路二      

2、第二種思路就是參考博文中,直接獲取接口返回的內容,查看接口是否返回歌曲的關鍵信息,同樣右鍵檢查一下,歌詞的關鍵信息是這個接口(https://music.163.com/weapi/cloudsearch/get/web?csrf_token=)返回的,而請求參數是:params和encSecKey,看那么長的一串,肯定是經過加密的了。

  

 

從參考博文中可以得知,該參數是經過JS加密得到的,至於如何知道是經過JS加密得到的,我也不太清楚。從瀏覽器的initiator項可以這個地址使用了叫core_xxx的JS文件,因此,我們點擊JS項,查看該JS文件,果然有該文件。如果沒有加載出來該JS文件,那需要在F12的開發者工具的“Disable cashe”勾選上(因為本地緩存了之后瀏覽器就不加載了)。通過preview來查看JS的代碼,我們可以搜索一下我們想要的那兩個關鍵字段encSecKey, 搜索后發現有一個關鍵的函數 window.asrsea,而這個函數又是一個d 的函數。

 

因此我們需要知道window.asrsea 的4個參數,JSON.stringify(i1x)、bxX3x(["流淚", "強"])、bxX3x(RW5b.md)、bxX3x(["愛心", "女孩", "驚恐", "大笑"])。這里我們也使用fiddler抓包,然后分別打印出這幾個參數的值。【注:這里有個踩坑點,就是參照的博文中,打印的是i1x 而不是JSON.stringify(i1x) 這個。但是打印出i1x之后會有個風險就是使用轉義字符的時候容易出錯,因此建議打印出全部】,可以看出,JSON.stringify(i1x) 這個參數打印出了好多個值,這里只能一一試錯,我暫時沒有好的辦法定位到哪個是目標接口調用的。同樣的辦法可以找出那4個參數:

 

 

 

   first_param = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"' + search_name + r'","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}'
    second_param = "010001"
    third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
    forth_param = "0CoJUm6Qyw8W8jud"

 

fiddler抓包再說一下,如果出現使用fiddler抓包但是原網頁顯示不全的話,那就需要安裝好fiddler的證書之后再試。

 

知道了這4個參數之后,可以通過JS文件中的內容獲取到加密的方式,就是ASE加密,模式是CBC模式,偏移量為vi ;知道了這些參數,就可以使用python的ASE加密進行加密,代碼如下:【注:這里有個坑:參考的博文中可能使用的ASE的版本不一致導致了加密算法報錯,不加.encoding('utf-8') 的話會出現二進制的報錯,另外需要先轉成轉碼變成二進制之后再進行16位補齊】,這樣就可以得到params 值,為什么說encSecKey 是常量呢,可以觀察到encSecKey 的值是d 函數中的h.encSecKey,這里我們給定一個i 之后,就可以得到固定的encSecKey值。知道了這兩個參數之后,就可以獲取到完整的返回值了。

 


def
AES_encrypt(text, key, iv): text = text.encode('utf-8') pad = 16 - len(text) % 16 text = text + ( pad * chr(pad)).encode('utf-8') # 需要轉成二進制,且可以被16整除 key = key.encode('utf-8') iv = iv.encode('utf-8') encryptor = AES.new(key, AES.MODE_CBC, iv) encrypt_text = encryptor.encrypt(text) # .encode('utf-8') encrypt_text = base64.b64encode(encrypt_text) return encrypt_text.decode('utf-8')
def get_params():  # 獲取params 參數的函數
    iv = "0102030405060708"
    first_key = forth_param
    second_key = 16 * 'F'
    h_encText = AES_encrypt(first_param, first_key, iv)
    h_encText = AES_encrypt(h_encText, second_key, iv)
    return h_encText

 

得到的json報文之后,可以使用jsonpath 包來進行處理,提取我們需要的值,詳細的Jsonpath的規則可參考(https://www.cnblogs.com/aoyihuashao/p/8665873.html):

def handle_json(ressult_str):
    """通過request返回的json結果,對結果進行處理"""
    ressult_str = ressult_b.decode('utf-8')  # 結果轉碼為str類型
    json_text = json.loads(ressult_str)  # 加載為json格式
    i = 0
    L = []
    for i in range(len(jsonpath(json_text, '$..songs[*].id'))):  # 根據id獲取列表條數
        D = {'num': 'null', 'name': 'null', 'id': 'null', 'singer':'null', 'song_sheet':'null'}  # 初始化字典
        D['num'] = i
        D['name'] = '/'.join(jsonpath(json_text, "$..songs["+str(i)+"].name"))  # 獲取名稱
        D['id'] = str(jsonpath(json_text, "$..songs["+str(i)+"].id")[0])  # 獲取ID且獲取第一個ID值並轉化為str類型
        D['singer'] = '/'.join(jsonpath(json_text, "$..songs["+str(i)+"].ar[*].name"))  # 獲取歌手列表
        al_list = jsonpath(json_text, "$..songs["+str(i)+"].al.name")  # 獲取專輯列表
        al = '/'.join(al_list)  # 將獲取的專輯列表合並
        D['song_sheet'] = "" + al + ""
        L.append(D)
    return L

 

下面附上全部代碼(只為實現功能,還有些bug需要優化):

# coding = utf-8
#from bs4 import BeautifulSoup        # 用於解析網頁源代碼的模塊
from binascii import b2a_hex, a2b_hex
from jsonpath import jsonpath
from Crypto.Cipher import AES
import requests                        # 用於獲取網頁內容的模塊
import base64
import json


def get_params():  # 獲取params 參數的函數
    iv = "0102030405060708"
    first_key = forth_param
    second_key = 16 * 'F'
    h_encText = AES_encrypt(first_param, first_key, iv)
    h_encText = AES_encrypt(h_encText, second_key, iv)
    return h_encText


def get_encSecKey():
    encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
    return encSecKey


def AES_encrypt(text, key, iv):
    text = text.encode('utf-8')
    pad = 16 - len(text) % 16
    text = text + ( pad * chr(pad)).encode('utf-8')  # 需要轉成二進制,且可以被16整除
    key = key.encode('utf-8')
    iv = iv.encode('utf-8')
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    encrypt_text = encryptor.encrypt(text)  # .encode('utf-8')
    encrypt_text = base64.b64encode(encrypt_text)
    return encrypt_text.decode('utf-8')


def get_json(url, params, encSecKey):
    data = {
        "params": params,
        "encSecKey": encSecKey
    }
    response = requests.post(url, headers=headers, data=data)
    return response.content


def handle_json(ressult_str):
    """通過request返回的json結果,對結果進行處理"""
    ressult_str = ressult_b.decode('utf-8')  # 結果轉碼為str類型
    json_text = json.loads(ressult_str)  # 加載為json格式
    i = 0
    L = []
    for i in range(len(jsonpath(json_text, '$..songs[*].id'))):  # 根據id獲取列表條數
        D = {'num': 'null', 'name': 'null', 'id': 'null', 'singer':'null', 'song_sheet':'null'}  # 初始化字典
        D['num'] = i
        D['name'] = '/'.join(jsonpath(json_text, "$..songs["+str(i)+"].name"))  # 獲取名稱
        D['id'] = str(jsonpath(json_text, "$..songs["+str(i)+"].id")[0])  # 獲取ID且獲取第一個ID值並轉化為str類型
        D['singer'] = '/'.join(jsonpath(json_text, "$..songs["+str(i)+"].ar[*].name"))  # 獲取歌手列表
        al_list = jsonpath(json_text, "$..songs["+str(i)+"].al.name")  # 獲取專輯列表
        al = '/'.join(al_list)  # 將獲取的專輯列表合並
        D['song_sheet'] = "" + al + ""
        L.append(D)
    return L


def load_song(num, result):
    if isinstance(int(num), int):
     num = int(num)
if num >= 0 and num <= len(result): song_id = ressult[num]['id'] song_down_link = "http://music.163.com/song/media/outer/url?id=" + ressult[num]['id'] + ".mp3" # 根據歌曲的 ID 號拼接出下載的鏈接。歌曲直鏈獲取的方法參考文前的注釋部分。 print("歌曲正在下載...") response = requests.get(song_down_link, headers=headers).content # 親測必須要加 headers 信息,不然獲取不了。 f = open(ressult[num]['name'] + ".mp3", 'wb') # 以二進制的形式寫入文件中 f.write(response) f.close() print("下載完成.\n\r") else: print("你輸入的數字不在歌曲列表范圍,請重新輸入") else: print("請輸入正確的歌曲序號") if __name__ == "__main__": search_name = input("請輸入你想要在網易雲音樂中搜索的單曲:") headers = { 'Cookie': 'appver=1.5.0.75771;', 'Referer': 'http://music.163.com/' } first_param = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"' + search_name + r'","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}' second_param = "010001" third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" forth_param = "0CoJUm6Qyw8W8jud" url = "https://music.163.com/weapi/cloudsearch/get/web?csrf_token=" params = get_params() encSeckey = get_encSecKey() ressult_b = get_json(url, params, encSeckey) ressult = handle_json(ressult_b) # 過濾出需要的數據,存入到result中 print("%3s %-35s %-20s %-20s " %("序號", " 歌名", "歌手", "專輯") ) for i in range(len(ressult)): print("%3s %-35s %-20s %-20s " %(ressult[i]["num"], ressult[i]["name"], ressult[i]["singer"], ressult[i]["song_sheet"])) num = input("請輸入你想要下載歌曲的序號/please input the num you want to download:") load_song(num, ressult) # 下載歌曲

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM