玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發

一、分析說明

現在的音樂類網站僅提供歌曲在線免費試聽,如果下載歌曲,往往要收取版權費用,但通過爬蟲可繞開這類收費問題,可以直接下載我們所需要的歌曲。

以 QQ 音樂為爬取對象,爬取範圍是全站的歌曲信息,爬取方式是在歌手列表下獲取每一位歌手的全部歌曲。由於爬取的數量較大,還會使用異步編程實現分佈式爬蟲開發,提高爬蟲效率。

整個爬蟲項目按功能分為爬蟲規則和數據入庫,分別對應文件 music.py 和 music_db.py。

爬蟲規則大致如下:

在歌手列表(https://y.qq.com/portal/singer_list.html)中按照字母類別對歌手進行分類,遍歷每個分類下的每位歌手頁面,然後獲取每位歌手頁面下的全部歌曲信息。根據該設計方案列出遍歷次數:

  1. 遍歷每個歌手的歌曲頁數。
  2. 遍歷每個字母分類的每頁歌手信息。
  3. 遍歷每個字母分類的歌手總頁數。
  4. 遍歷 26 個字母分類的歌手列表。

在功能上至少需要實現 4 次遍歷,但實際開發中往往比這個次數要多。統計遍歷次數,主要能讓開發者對項目開發有整體的設計邏輯。項目開發使用模塊化設計思想,對整個項目模塊的劃分如下:

  1. 歌曲下載。
  2. 歌手信息和歌曲信息。
  3. 字母分類下的歌手列表。
  4. 全站歌手列表。

二、歌曲下載

下載歌曲前,先要找到歌曲的相關信息,才能夠確定歌曲的下載鏈接。以 QQ 音樂中的某一首歌曲為例進行介紹(歌曲鏈接),如圖所示。

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


我們在網頁裡點擊歌曲播放,在新的頁面打開谷歌開發者工具,點擊 Netword 的 Media 選項卡即可找到歌曲播放文件,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


歌曲播放文件有很多,但是真正能播放就只有一個,分析 URL 發現,只有一個文件名與其他的不一樣的,如圖上的 C400003OUlho2HcRHC.m4a,我們將其 URL 複製到瀏覽器的地址欄訪問,發現歌曲是可以播放,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


從整個歌曲文件的 URL 分析可知,這是一個 GET 請求並設有各種請求參數,如下所示:

http://dl.stream.qqmusic.qq.com/C400003OUlho2HcRHC.m4a?guid=7475275330&vkey=6438F3731278CD2AD8C1A5C3C25723DFD7386607DD790C429B2BF8A2AC75FE4652E348DA94EAC480DFA393AB66FAC6ECE244E48B928D6B10&uin=6153&fromtag=66

那麼,要實現歌曲下載,首先需要找到歌曲文件的 URL 請求參數。我們複製某個請求參數,並在其他請求信息裡查找這個請求參數,以參數 vkey 的值為例,在每個請求信息的響應內容裡查找,最終在 JS 選項卡下找到該參數,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


從圖上發現,purl 的值是歌曲文件 URL 的構成部分,我們只需再加上一個域名即可得到完整的歌曲文件 URL。對於域名的選擇,QQ 音樂提供了五個域名,每個域名都可以獲取歌曲文件,這是一種集群的管理方式。在圖上可以找到具體的域名,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


我們再分析這個請求的地址,發現 URL 很長,並且有複雜的請求參數。請求參數分為三大類型:

  1. 整個參數可以直接去掉。
  2. 參數值固定不變。
  3. 參數值從其他請求信息獲取。

根據參數類型進行分析,將 URL 放到瀏覽器的地址欄訪問,逐一刪除某些參數查看響應內容是否發生改變,若沒改變,則參數是可以直接去除。此外也發現有兩個參數的來源尚不明確,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


對於尚不明確的參數 guid 和 songmid,參數 songmid 從命名角度看來,這是歌曲的唯一標識值,每首歌曲的 songmid 都是固定的。而參數 guid 是來自 Cookies,這是一種常見的反爬蟲機制,並且每個請求都不是有 Cookies。我們將歌曲下載定為函數 download,並設置參數 guid、songmid 和 cookie_dict,分別代表請求參數 guid 和 songmid 和用戶的 Cookies 信息,具體代碼如下所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


對於 Cookies 信息的獲取,我們需要使用 selenium 來實現,並且要分別訪問兩次 QQ 音樂才能生成 Cookies。第一次訪問 QQ 音樂的網站首頁,第二次訪問一個 ajax 請求,該請求會生成 Cookies 信息,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


生成的 Cookies 信息還需要將其轉換成字典格式,因此,Cookies 的獲取過程如下所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


現在將函數 getCookies 和 download 結合使用就能實現單首歌曲下載。在 music.py 編寫以下代碼並運行即可下載歌曲。注意:函數參數 songmid 的獲取會在下一章節講述。

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


三、歌手和歌曲信息

現在已經實現單首歌曲下載,只要調用函數 download 並傳入不同的參數 songmid 即可下載不同的歌曲,在本章節,我們通過歌手頁面來獲取不同歌曲的 songmid。以周杰倫為例,打開歌手頁面 並在開發者工具查找歌曲信息,最終在 JS 選項卡里找到歌曲信息,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


分析圖上請求的 URL,發現某些參數存在一定的規律,比如 singermid 是歌手的唯一標識值,每個歌手都有一個 singermid;begin 是頁數,每一頁有 30 首歌曲,每次翻頁的時候,begin 以 30 進行遞增,如第一頁為 0,第二頁為 30,第三頁為 60……以此類推;其餘的參數都是固定不變,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


本章所實現的代碼主要針對圖上的請求 URL 而進行的。首先獲取歌手的總歌曲數量,然後根據總歌曲數來計算頁數,最後遍歷每一頁來獲取每首歌曲信息以及歌曲的 songmid 進行歌曲下載。實現代碼如下:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


函數 get_singer_songs() 用於爬取歌手的全部歌曲,代碼說明如下:

  1. 參數 singermid 代表歌手的唯一值,只需要傳入不同歌手的 singermid,就能爬取不同歌手的全部歌曲。
  2. 代碼有兩個相同變量 url,第一個用於動態設置歌手的 singermid,獲取歌曲總數和歌手姓名;第二個用於動態設置頁數,獲取當前歌手每一頁的歌曲信息。
  3. 下載歌曲調用已實現的 download() 函數;入庫處理是調用入庫函數 insert_data(),該函數會在後續章節講解。

四、分類歌手列表

現在已實現獲取單個歌手的全部歌曲信息,只要在此功能的基礎上遍歷輸入不同歌手的 singermid,就能獲取不同歌手的歌曲信息。從 Chrome 開發者工具對歌手列表的分析得知,歌手頁數有 297 頁,每頁 80 位歌手,全站的歌手共有 23760 位。

根據項目設計,將循環次數按字母分類劃分。在歌手列表頁上使用字母 A~Z 對歌手進行分類篩選,利用這個分類功能可以將全部歌手分為兩層循環:

  1. 第一層是循環每一個字母分類
  2. 第二層是循環每個分類下的總頁數

拆分兩層循環主要為異步編程提供切入點,具體實現方式會在後面的章節講解。首先在網頁上單擊分類“A”,可在開發者工具的JS標籤看到相應的請求信息,如圖所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


分別點擊不同的字母分類以及頁數,分析每個請求參數,發現請求參數 index、sin 和 cur_page 是有變化規律:

  1. index 代表字母分類 A,從 1 開始,2 代表字母 B,以此類推。
  2. sin 是根據頁數計算歌手數量,如第一頁為 0,每頁 80 位歌手,第二頁為 80,第三頁為 160,以此類推。
  3. cur_page 代表當前頁數,從 1 開始,每頁以 1 遞增,如第二頁為 2。

根據上述分析,本章的功能代碼如下所示:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


函數 get_genre_singer 是獲取單個字母分類的歌手列表,函數參數的說明如下:

  1. index 代表字母的數字,如 1 代表 A,2 代表 B,以此類推。
  2. page_list 代表當前字母分類的總頁數。
  3. cookie_dict 代表函數 getCookies 的返回值,即用戶的 Cookies 信息。

五、全站歌手列表

現在得到函數 get_genre_singer,我們只需將傳入不同的函數參數 index 和 page_list 即可實現 26 個英文字母分類的歌手列表。在此基礎上遍歷 26 個字母即可實現,將這個遍歷定義在函數 get_all_singer,具體的代碼如下:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


上述代碼是整個項目程序的運行入口,程序運行執行函數的順序如下:

  1. get_all_singer():循環 26 個字母,構建參數並調用函數get_genre_singer()。
  2. get_genre_singer(index, page_list, cookie_dict):遍歷當前分類總頁數,獲取每頁每位歌手的歌曲信息。
  3. get_singer_songs(singermid, cookie_dict):實現歌手的歌曲入庫和下載。
  4. download(guid, songmid, cookie_dict):下載歌曲。
  5. getCookies():使用 Selenium 獲取用戶的 Cookies
  6. insert_data(song_dict):入庫處理。

每個函數之間通過層層的調用來實現整個網站的歌曲下載和信息入庫,每次函數調用都會傳入不同的函數參數,使得函數之間存在一定的關聯。

六、數據存儲

在爬蟲邏輯功能實現過程中發現數據入庫的函數 insert_data(),該函數主要存放在 music_db.py 中,本節使用 SQLAlchemy 實現數據入庫。

從爬蟲規則分析,入庫的數據有歌名、所屬專輯、時長、歌曲 mid(下載歌曲文件以歌曲 mid 命名)和歌手姓名。針對所爬取的數據及性質,數據庫命名如下:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


根據數據庫的命名,SQLAlchemy 映射數據庫代碼如下:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


函數 insert_data() 主要對傳遞的參數 song_dict 進行入庫處理,參數 song_dict 為字典格式。函數運行會創建新的數據庫連接,創建新數據庫連接主要是為異步編程做準備。

上述代碼存放在 music_db.py 文件中,在 music.py 中只需導入 music_db.py 的 insert_data() 函數即可實現數據入庫。

七、併發庫 concurrent.futures

Python 標準庫為我們提供了 threading 和 multiprocessing 模塊編寫相應的多線程 / 多進程代碼。從 Python 3.2 開始,標準庫為我們提供了 concurrent.futures 模塊,它提供了 ThreadPoolExecutor 和 ProcessPoolExecutor 兩個類,實現了對 threading 和 multiprocessing 更高級的抽象,對編寫線程池 / 進程池提供了直接的支持。

下面通過簡單的例子講解如何使用 concurrent.futures,代碼如下:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


在上述代碼中,創建了進程 ProcessPoolExecutor 和線程 ThreadPoolExecutor,其中在每個進程中又創建了兩個線程。

下面簡單講述一下 concurrent.futures 屬性和方法。

Executor:Executor 是一個抽象類,它不能被直接使用。為具體的異步執行定義了基本的方法:ThreadPoolExecutor 和 ProcessPoolExecutor 繼承了 Executor,分別被用來創建線程池和進程池的代碼。

創建進程和線程之後,Executor 提供了 submit() 和 map() 方法對其操作。submit() 和 map() 最大的區別是參數類型,map() 的參數必須是列表、元組和迭代器的數據類型。

Future:可以理解為一個在未來完成的操作,這是異步編程的基礎。通常情況下,我們執行 IO 操作和訪問 URL 時,在等待結果返回之前會產生阻塞,CPU 不能做其他事情,而 Future 的引入幫助我們在等待的這段時間可以完成其他的操作。

八、分佈式爬蟲

我們已經知道,爬取全站歌曲信息是按照字母 A~Z 依次循環爬取的,這是在單進程單線程的情況下運行的。如果將這 26 次循環分為 26 個進程同時執行,每個進程只需執行對應的字母分類,假設執行一個分類的時間相同,那麼多進程併發的效率是單進程的 26 倍。

除了運用多進程之外,項目代碼大部分是 IO 密集型的,那麼在每個進程下使用多線程也可以提高每個進程的運行效率。我們知道歌手列表頁是通過兩層循環實現的,第一層是循環每個分類字母,現將每個分類字母作為一個單獨進程處理;第二層是循環每個分類的歌手總頁數,可將這個循環使用多線程處理。假設每個進程使用 10 條線程(線程數可自行設定,具體看實際需求),那麼每個進程的效率也相對提高 10 倍。

分佈式策略考慮的因素有網站服務器負載量、網速快慢、硬件配置和數據庫最大連接量。舉個例子,爬取某個網站 1000 萬數據,從數據量分析,當然進程和線程越多,爬取的速度越快。但往往忽略了網站服務器的併發量,假設設定 10 個進程,每個進程 200 條線程,每秒併發量為 200×10=2000,若網站服務器併發量遠遠低於該併發量,在請求網站的時候,就會出現卡死的情況,導致請求超時(即使對超時做了相應處理),無形之中增加等待時間。除此之外,進程和線程越多,對運行程序的系統的壓力越大,若涉及數據入庫,還要考慮併發數是否超出數據庫連接數。

根據上述分佈式策略,在 music_db.py 中分別添加函數 myThread 和 myProcess,分別代碼多線程和多進程:

玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


玩轉 Python 網絡爬蟲:QQ 音樂全站爬蟲開發


代碼中定義了 myProcess() 和 myThread() 方法函數,分別實現多進程和多線程。

  • 多進程 myProcess() 函數:主要是循環字母 A~Z 以及 #,將每個字母獨立創建一個進程,每個進程執行的方法函數是 myThread()。
  • 多線程 myThread() 函數:首先根據傳入函數參數獲取當前分類的歌手總頁數,然後根據得到的總頁數和設定的線程數計算每條線程應執行的頁數,最後遍歷設定線程數,讓每條線程執行相應的頁數。例如總頁數 100 頁、10 條線程,每條線程應執行 10 頁,第一條線程執行 0~10 頁,第二條線程執行 10~20 頁,以此類推。線程調用的方法函數是 get_genre_singer()。

在實現分佈式爬蟲的時候,必須注意的是:

  1. 全局變量不能放在 if __name__ == '__main__' 中,因為使用多進程的時候,新開的進程不會在此獲取數據。
  2. 使用 SQLalchemy 入庫最好重新創建一個數據庫連接,如果多個線程和進程共同使用一個連接,就會出現異常。
  3. 分佈式策略最好在程序代碼的最外層實現。例如在項目中,get_singer_songs() 方法函數裡有兩個循環,不建議在此使用分佈式處理,在代碼底層實現分佈式不是不可行,只是代碼變動太大,而且考慮的因素較多,代碼維護相對較難。

總結

本 Chat 以 QQ 音樂為爬取對象,爬取範圍是全站的歌曲信息,爬取方式在歌手列表獲取每一位歌手的全部歌曲。如果爬取的數量較大,就使用異步編程實現分佈式爬蟲開發,可提高爬蟲效率。讀者應重點掌握以下內容:

1. 項目實現的功能

  1. 歌曲下載 download(guid, songmid, cookie_dict):爬蟲最底層的功能,也是爬蟲最核心的功能。
  2. 歌手和歌曲信息 get_singer_songs(singermid, cookie_dict):將歌手的歌曲信息入庫和歌曲下載。
  3. 分類歌手列表 get_genre_singer(index, page_list, cookie_dict):獲取單一字母分類的全部歌手和歌曲信息。
  4. 全站歌手列表 get_all_singer():獲取全站歌手和歌曲信息。
  5. 用戶 Cookies 信息 getCookies():使用 Selenium 獲取用戶的Cookies。
  6. 數據存儲 insert_data(song_dict):將爬取的歌手和歌曲信息入庫處理。
  7. 多進程 myProcess():每個字母分類創建一個單獨進程運行。
  8. 多線程 myThread(genre):每個進程使用多線程爬取數據。

2. 分佈式策略考慮的因素

分佈式策略考慮的因素有網站服務器負載量、網速快慢、硬件配置和數據庫最大連接量。舉個例子,比如爬取某個網站 1000 萬數據,從數據量分析,當然進程和線程越多,爬取的速度越快。但往往忽略了網站服務器的併發量,假設設定 10 個進程,每個進程 200 條線程,每秒併發量為 200×10=2000,若網站服務器併發量遠遠低於該併發量,在請求網站的時候,就會出現卡死的情況,導致請求超時(即使對超時做了相應處理),無形之中增加等待時間。除此之外,進程和線程越多,對程序運行的系統的壓力越大,若涉及數據入庫,還要考慮併發數是否超出數據庫連接數。

3. 實現分佈式爬蟲的注意事項

  1. 全局變量不能放在 if __name__ == '__main__' 中,因為使用多進程的時候,新開的進程不會在此獲取數據。
  2. 使用 SQLalchemy 入庫最好重新創建一個數據庫連接,如果多個線程和進程共同使用一個連接,就會拋出異常。
  3. 分佈式策略最好在程序代碼的最外層實現。例如在項目中,get_singer_songs() 方法函數裡有兩個循環,不建議在此使用分佈式處理,在代碼底層實現分佈式不是不可行,只是代碼變動太大,而且考慮的因素較多,代碼維護相對較難。

最後,本文教程是來自本人著作的《玩轉 Python 網絡爬蟲》裡面的實戰教程。如有問題以及建議,請留下您寶貴的意見,謝謝!!

相關推薦

推薦中...