蝦米音樂爬蟲


蝦米音樂爬蟲

https://www.xiami.com/ 這是本次我爬取的網站

這是前后端分析的網站,這種類型的web其實很好的,只要找對了API,成功發生請求,那么想要的數據就直接獲取到了

這里就拿綠色這首歌為例子:https://www.xiami.com/song/mTrNQf7d590,分析她的評論的APi接口

  1. 請求分析,主要知道APi的地址,請求的參數,請求的方式,還有就是請求頭需要攜帶什么參數。

    這是本次訪問攜帶的參數數據

    _q: {"objectId":"mTrNQf7d590","objectType":"song","pagingVO":{"page":1,"pageSize":20}}
    _s: a05126e10a02e9702e790e47c27d2002
    
    開始分析吧(_s的值和你們的不一樣,這是正常的)
    _q:一個明文,數據都能找到 objectId就是url中的一個數據,不難拿到,objectType這里是固定為song的,pagingVO就是頁數和每一頁多少條數據,_q就是很容易的。
    _s:它是一個32為的隨機字符串,第一時間應該想到md5生成的32位的數據,_s很有可能就是md5加密過后的,至於加密數據是什么?以后台開發來說,md5它是無法可逆的,后端拿到這個md5加密串不能反解密出來,所以只能根據前端傳來的數據,將數據通過md5加密,再和前端傳來的md5字符串進行比較,比較成功的說明這次請求是通過web網頁來的(理想狀況,排除別人知道你的加密方法)。不同的歌曲,不同的評論頁數頁數_s是不一樣的,那么這個md5加密的數據肯定有標識(指不一樣的數據)。很多后台會拿時間戳作為加密數據之一,去生成md5加密字符串,不過前端要將這個時間戳傳到后台。這里呢,並沒有傳時間戳到后台,那么猜測加密的數據很有可能就是_q的值
    

    你單單訪問一次:https://www.xiami.com/song/mTrNQf7d590,是很難找出加密規律的,我們再刷新一次頁面,還是找到剛剛評論的api,看看請求的參數有何不同。很確定(前提你沒有做其他操作),找到的請求參數和第一次的請求參數是一樣的

    _q: {"objectId":"mTrNQf7d590","objectType":"song","pagingVO":{"page":1,"pageSize":20}}
    _s: a05126e10a02e9702e790e47c27d2002
    

    不管你刷新多少次,請求參數是一樣的,時間是一直再變的,_s的值依舊沒有變化,所以直接排除時間戳作為加密數據的可能,_q的值一直沒變,_s的值也沒有變化,那么_q的值是_s的加密參數的可能性變大。

    上面我們一直看的是本首歌曲的第一頁的評論信息,我們試試看第二頁評論的信息,看看請求參數有何變化,翻到頁面最下面,點擊更多評論,我們繼續找到評論的接口

    _q: {"objectId":"mTrNQf7d590","objectType":"song","pagingVO":{"page":2,"pageSize":20}}
    _s: f82616e410172d3e76b9b48561a0d257
    

    _q的值發生了變化,_s的值相應的發生了變化,我們可以確定的是_q就是_s的加密數據之一(可能還有其他的加密數據)

  2. _s的加密方式破解

    找js加密的位置是有一定難度的,你訪問一個web頁面,請求的js文件有很多,單個js文件的內容有大,還有就是js文件又是混淆的,以a,b,c,d,e...這些名字命名,你都不知道代表什么意思。

    我找位置的方式就是:先看本次請求是哪個js文件,js文件的哪個位置發送的,再順着往上慢慢找。這只是一個思路。

    還有一種就是思路就是,通過search搜索關鍵字,通過關鍵字的位置,找到加密位置。

    這里,最終我通過搜索關鍵字_q,找到了加密位置。位置找到了,通過chrome的開發者工具進行js斷點調試。我就講下這段js大概。

    _q的值需要我們自己按照需要自己生成相應的字符串
    _s的指就是這行代碼:a.split("_")[0] + "_xmMain_" + e._url + "_" + t
    a:就是cookie中xm_sg_tk對應的value值
    e._url:比如評論請求的url地址為 https://www.xiami.com/api/comment/getCommentList
    	   那么e._url就是/api/comment/getCommentList
    t:就是_q的值
    最終將它他們拼接成字符串,最終通過md5的方式加密,這就是_s的值
    

    注意:

    ​ 關於上面的a,也就是cookie是xm_sg_tk的value的獲取。在你發送Api請求之前,先去請求下主頁,拿到對應cookie的值。

    _q很明顯就是json字符串,通過json.dumps字典生成,但是注意,字典是無序的,json.dumps出來的字符串順序會不一樣。

  3. 成功請求之后,就會拿到響應的數據

這里我就分析了評論這一個APi,其他Api的加密方式也是一樣。只是_q的值和對應的url地址不一樣。需要爬取其他數據相應改下就是。

import requests, pprint
from fake_useragent import UserAgent
from hashlib import md5


class XiaMi:
    ua = UserAgent()
    DOMAIN = "https://www.xiami.com"

    # 各個API接口地址
    # 每日音樂推薦
    APIDailySongs = "/api/recommend/getDailySongs"
    # 排行榜音樂
    APIBillboardDetail = "/api/billboard/getBillboardDetail"
    # 所有排行榜
    APIBillboardALL = "/api/billboard/getBillboards"
    # 歌曲詳情信息
    APISongDetails = "/api/song/getPlayInfo"

    def __init__(self):
        self.session = requests.Session()
        self.headers = {
            "user-agent": self.ua.random
        }
        self.session.get(self.DOMAIN)

    def _get_api_url(self, api):
        return self.DOMAIN + api

    # 獲取每日推薦的30首歌曲
    def get_daily_songs(self):
        url = self._get_api_url(self.APIDailySongs)
        params = {
            "_s": self._get_params__s(self.APIDailySongs)
        }
        result = self.session.get(url=url, params=params).json()
        self._dispose(result)

    # 獲取蝦米音樂的音樂排行榜
    def get_billboard_song(self, billboard_id: int = 0):
        '''
        :param billboard_id: 各類型的排行榜
        :return: 排行榜音樂數據
        '''
        if not hasattr(self, "billboard_dict"):
            self._get_billboard_dict_map()

        assert hasattr(self, "billboard_dict"), "billboard_dict獲取失敗"
        pprint.pprint(self.billboard_dict)
        if billboard_id == 0:
            billboard_id = input("輸入對應ID,獲取排行榜信息")
        assert billboard_id in self.billboard_dict, "billboard_id錯誤"

        url = self._get_api_url(self.APIBillboardDetail)
        _q = '{\"billboardId\":\"%s\"}' % billboard_id
        params = {
            "_q": _q,
            "_s": self._get_params__s(self.APIBillboardDetail, _q)
        }
        result = self.session.get(url=url, params=params).json()
        self._dispose(result)

    # 生成一個排行榜對應的字典映射
    def _get_billboard_dict_map(self):
        billboard_dict = {}
        billboards_info = self.get_billboard_all()
        try:
            if billboards_info["code"] == "SUCCESS":
                xiamiBillboards_list = billboards_info["result"]["data"]["xiamiBillboards"]
                for xiamiBillboards in xiamiBillboards_list:
                    for xiamiBillboard in xiamiBillboards:
                        id = xiamiBillboard["billboardId"]
                        name = xiamiBillboard["name"]
                        billboard_dict[id] = name
                self.billboard_dict = billboard_dict
        except Exception:
            pass

    # 獲取所有的排行榜信息
    def get_billboard_all(self):
        url = self._get_api_url(self.APIBillboardALL)
        params = {
            "_s": self._get_params__s(self.APIBillboardALL)
        }
        result = self.session.get(url=url, params=params).json()
        self._dispose(result)

    # 獲取歌曲詳情信息
    def get_song_details(self, *song_ids) -> dict:
        '''
        :param song_ids: 歌曲的id,可以為多個
        :return: 歌曲的詳情信息
        '''
        assert len(song_ids) != 0, "參數不能為空"

        for song_id in song_ids:
            if not isinstance(song_id, int):
                raise Exception("每個參數必須為整型")

        url = self._get_api_url(self.APISongDetails)
        _q = "{\"songIds\":%s}" % list(song_ids)
        params = {
            "_q": _q,
            "_s": self._get_params__s(self.APISongDetails, _q)
        }
        result = self.session.get(url=url, params=params).json()
        return self._dispose(result)

    # 獲取歌曲的下載地址
    def get_song_download_url(self, *song_ids):
        download_url_dict = {}
        song_details = self.get_song_details(*song_ids)
        songPlayInfos = song_details["result"]["data"]["songPlayInfos"]
        for songPlayInfo in songPlayInfos:
            song_download_url = songPlayInfo["playInfos"][0]["listenFile"] or songPlayInfo["playInfos"][1]["listenFile"]
            song_id = songPlayInfo["songId"]
            download_url_dict[song_id] = song_download_url

        print("歌曲下載地址為:", download_url_dict)

    # 處理爬蟲獲取到的數據,這里我就輸出值
    def _dispose(self, data):
        pprint.pprint(data)
        return data

    # 獲取加密字符串_s
    def _get_params__s(self, api: str, _q: str = "") -> str:
        '''
        :param api: URL的地址
        :param _q:  需要加密的參數
        :return: 加密字符串
        '''
        xm_sg_tk = self._get_xm_sg_tk()
        data = xm_sg_tk + "_xmMain_" + api + "_" + _q
        return md5(bytes(data, encoding="utf-8")).hexdigest()

    # 獲取xm_sg_tk的值,用於對數據加密的參數
    def _get_xm_sg_tk(self) -> str:
        xm_sg_tk = self.session.cookies.get("xm_sg_tk", None)
        assert xm_sg_tk is not None, "xm_sg_tk獲取失敗"
        return xm_sg_tk.split("_")[0]

    def test(self):
        # self.get_daily_songs()
        # self._get_xm_sg_tk()
        # self.get_billboard_song(332)
        # self.get_billboard_all()
        # self.get_song_details(1813243760)
        # self.get_song_download_url(1813243760)

        pass


if __name__ == '__main__':
    xm = XiaMi()
    xm.test()


免責聲明!

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



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