什麼是Python爬蟲?教你輕鬆爬取歌詞網站

人工智慧大師之路

什麼是Python爬蟲?教你輕鬆爬取歌詞網站

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 spider

如何輕鬆地在網路上蒐集你想要的資料?

相信讀者有時會遇到需要自己蒐集資料的情況吧,或是你想要利用機器學習作某方面的研究。

此時第一步就是要有符合你研究需求的資料,

但是通常這種情形都很難在網路上找到可以直接下載的資料。

常常就是你想要研究的問題沒有符合你需求現成的資料

而有現成資料的問題又都被別人研究過了或甚至你沒興趣研究

那究竟該怎麼辦呢? 我想,現在Python正熱門的時代,最容易想到的就是爬蟲了吧?

只不過,如果對爬蟲完全沒概念或是Python的初學者究竟該怎麼辦呢?

別擔心,以下文章我將與你介紹爬蟲的基本概念、分享有哪些技巧可以使用,

以及在最後示範如何實際爬取歌詞網站,讓你再也不會覺得爬蟲很困難。

 

Python爬蟲基本概念

 

沒有通用的爬蟲程式,只有通用的爬蟲架構

 

在了解爬蟲之前,請先建立一個基本概念,

那就是爬蟲程式是需要客製化的,每隻爬蟲都不盡相同,

某個網站的爬蟲能解決的問題不一定適用於其他網站的爬蟲,

所以應該要把重心放在學會爬蟲的精髓,這樣才能舉一反三。

畢竟每個網站存放資料的位置、方式都不一樣,甚至如果網站有一些更動爬蟲也可能失效,

所以,大多的情況都只能依照該網站的格式自己刻一個爬蟲出來。

 

適量使用,避免被列入黑名單

 

爬蟲是一個位於灰色地帶的存在,畢竟是免費獲取別人辛苦整理好的資料,

在使用上就應該低調、酌量使用,不要讓爬蟲程式無止境地對網站發出請求,

否則現在很多網站都有反爬蟲機制,有些強度高的會直接封鎖你的網路IP、列入黑名單,

有些甚至是永久封鎖,所以雖然爬蟲爬取的速度可以非常快,

但也不要一次爬太多,要設置休息時間讓網站休息

 

觀察固定規律

 

製作爬蟲最關鍵的部分就是網站存放資訊的方式要有規則可循,而且盡量要沒有難以觀察到的例外,

所以培養敏銳的觀察能力是很重要的,在探索規則時不免要Get your hands dirty!

試著問自己你想要拿的資訊能否藉由一些固定的語法、規則獲取,

最後最重要的就是測試看看有沒有什麼漏掉的例外。

 

Python爬蟲重要套件

 

Python爬蟲套件

 

Requests

 

這是Python爬蟲界最簡單易懂也最有名的套件之一,

基本上大部分的爬蟲功能都能在這個套件被一次滿足。

它的主要功能是HTTP請求,可以代替人手動進入網頁。

還沒有安裝過的人可以先在cmd環境內輸入以下安裝指令。

pip install requests

 

BeautifulSoup

 

這個套件是requests的好搭檔,經常會看到它們一起出現。

基本上它們兩個聯手起來就能完成許多爬蟲工作了。

它的主要功能是解析HTML,將請求到的網頁轉成HTML以利爬取我們感興趣的資訊。

還沒有安裝過的人可以先在cmd環境內輸入以下安裝指令。

pip install bs4

 

time

 

這個套件不是必需品,因為它的存在是源於怕被網站封鎖的恐懼。

如果你要爬的網站完全不會擋人的話,就不需要使用。

它的功能在於讓我們的爬蟲程式每爬完一部份就休息一下 (休息時間可以依網站情況自訂),

這麼一來,網站就會認為我們的爬蟲程式表現得像人類,同時也減輕了網站的負擔。

 

random

 

這個套件不是必需品,因為它的存在是源於怕被網站封鎖的恐懼。

如果你要爬的網站完全不會擋人的話,就不需要使用。

它的功能在於隨機生成特定範圍的數字作為爬蟲程式不固定的休息時間,

和time一起使用的話,網站就會認為我們的爬蟲程式表現得更像人類,同時也減輕了網站的負擔。

 

Python爬蟲重要零件

 

header

 

這是爬蟲的必備零件,原因是它可以幫助我們的爬蟲程式偽裝成瀏覽器訪問至該網頁。

如果沒有它的話,被我們爬取的網站很容易就會發現有爬蟲來爬它的東西,

所以就很容易被網站封鎖、拒絕訪問。

這邊介紹一個最常見的header,

User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36

其實還有很多種,不過只要使用這個基本就沒什麼問題了。

 

tdqm_notebook

 

這雖然不是爬蟲的必備零件,但是我猜你不會想錯過它,

它可以幫助我們計時,以進度條隨時更新的方式呈現,讓你輕鬆掌握目前爬蟲狀況、估算時間。

如果沒有它的話,我們就只會看到我們的python爬蟲程式有在跑,但完全不知道還要跑多久、現在跑到哪裡。

所以,還沒有安裝過的人趕快在cmd環境內輸入以下指令安裝一下吧。

pip install tdqm

 

try & except

 

這個基本上也是必備零件,因為爬蟲偶爾會因為各種奇怪的理由突然報錯,

至於原因很可能是網址或網站的內部問題,

如果沒有使用的話,只要遇到一個小錯誤你的爬蟲就會馬上停下來。

因為一般而言我們都希望執行爬蟲完就可以不用理它,等它爬完預計範圍再回來收集資料就好,

所以,加了try & except後就算爬蟲程式遇到錯誤它也會跳過錯誤、繼續爬下一個部分。

你也就能安穩的去做其他事了喔。

 

實際爬取歌詞網站案例

 

AZLyrics

AZLyrics

 

本次的案例是使用知名、架構簡單的歌詞網站AZLyrics,進入後可以發現它歌詞資料存放的方式也清楚易懂。

基本上是一個巢狀結構,每個頁面都是一個html,是屬於相當適合爬蟲自動化爬取的型式。

 

以下就是爬取歌詞的三部曲,以下教學就依循著這些步驟一步一步進行,

強烈建議閱讀以下文章時配合程式碼操作你會更明白我想表達的意思喔

 

1.先依照歌手名稱的第一個字母選擇A~Z或# (數字開頭的),點進去後出現的是開頭字母或數字頁面。

2.在選好的開頭字母或數字頁面內點選任一歌手名稱的連結後可進入該歌手所有歌曲名稱的頁面。

3.進入該歌手所有歌曲名稱的頁面後,再點選任一歌曲名稱的連結即可進入歌詞的頁面啦。

 

首先,我們先觀察HTML網址的規律,看看是否有規則可循。

 

進入首頁後如果點A會進入以下網址,

https://www.azlyrics.com/a.html,

再進入第一個A開頭的歌手則會進入以下網址,

https://www.azlyrics.com/a/a1.html,

最後進入這位歌手的第一首歌的頁面,

https://www.azlyrics.com/lyrics/a1/foreverinlove.html。

 

有觀察到什麼規則嗎?

網址通則是不是可以寫成

https://www.azlyrics.com/ + 歌手名稱第一個字母或數字 + .html

依此類推,其他兩種的規律則是

https://www.azlyrics.com/ + 歌手名稱第一個字母或數字 + / + 歌手名稱 + .html

https://www.azlyrics.com/lyrics/ + 歌手名稱 + 歌曲名稱 + .html

如果到這裡都沒問題的話,就可以開始爬蟲了喔。

 

依照A~Z和1~9爬取歌手名稱

 

import requests
import time
import random
from bs4 import BeautifulSoup

pagename = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 
            'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '19']

peoplename = []
people = []

headers = {'User-Agent': 'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'}

 

首先,我們先引入上面說的重要套件,以及宣告一些變數,

將headers以字典的值存放在User-Agent的鍵上,

pagename則是藉由上面觀察到AZLyrics HTML網址的規律而預先打好所有字母和數字,

people則是用來存放歌手名稱第一個字母或數字 + / + 歌手名稱 + .html字串的list

(例如 : ‘a/a1.html’)

url1 = 'http://www.azlyrics.com/' + 'a' + '.html'
html = requests.get(url1, headers=headers)
soup1 = BeautifulSoup(html1.text, 'html.parser')

 

接著就是Requests和BeautifulSoup聯手的時機了,

首先利用requests.get( )讓程式代替我們向網站提出HTTP請求,

並且加入headers以偽裝我們的真實身分,

再使用BeautifulSoup解析爬取下來的HTML。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 html1

html1結果

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 soup1

soup1結果

 

由上圖我們可以看到html1回傳的結果表示網站同意我們的request,

soup1則是解析後的HTML。

 

href1 = soup1.select('a[href]')

for link in href1:
    temp1 = link.get('href')
    peoplename.append(temp1)
        
    for r in range(len(peoplename)):
        if peoplename[r][0:5] == '//www':
            pass
        else:
            people.append(peoplename[r])

 

接下來則是要使用定位器的語法,

soup1.select(‘a[href]’),告訴程式我們只要整份HTML中有出現網址的部分。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 href1

 

只不過,由上圖可知,有些href1裡的連結並不是我們要的,

所以還得把我們想要的連結挑出來,存放在people裡面。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 href get

 

link.get(“href”)的語法可以幫助我們提取存放在href1裡的任一個字串,

觀察href1圖中不難發現,href1中我們不要的部分開頭都有//www,

所以只要寫一個for迴圈過濾掉即可。

 

最後,我們檢查一下people看看裡面的東西是不是都是我們要的了。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 people

 

依照剛才得到的歌手名稱爬取該歌手的所有歌曲名稱

 

songname = []

url2 = 'https://www.azlyrics.com/' + 'a/adamlambert.html'
html2 = requests.get(url2, headers=headers)
soup2 = BeautifulSoup(html2.text, 'html.parser')    
href2 = soup2.select('a[href]')

for link in href2:
    temp2 = link.get('href')
    songname.append(temp2)

 

和前一個步驟差不多的語法,只是我選擇開頭a裡的Adam Lambert作為歌手名稱發出HTTP請求,

songname則是用來存取/lyrics/ + 歌手名稱 + 歌曲名稱 + .html的list

(例如 : ‘/lyrics/adamlambert/climb.html’)

執行上面的程式碼後,可以發現songname裡仍然有許多我們不要的資訊,

例如有很多這個歌手無關的網址,此時敏銳的觀察力就派上用場了。

仔細看的話應該能發現songname裡前31個和最後8個網址都是HTML中固定、我們不要的資訊,

我們可以利用list取值的功能將不要的部分截掉,並且每個字串也消除掉前兩個字元。

songname = songname[31:]
songname = songname[:-8]
for i in range(len(songname)):
    songname[i] = songname[i][2:]

 

經過上面程式碼的處理後,最後我們終於得到沒有雜訊的songname了。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 songname

 

依照剛才得到的歌手、歌曲名稱爬取歌曲的各種資訊

 

終於到最後一步了,一開始的語法跟前面幾乎一樣,只是我們先列出總共要爬取的資訊,

一共有歌手正式名稱歌曲正式名稱歌曲曲風歌詞這四項資訊,所以分別宣告一個list用來存放。

另外,因為我選擇爬取songname裡的第12首歌 : Whataya want from me,

因此以下程式碼才寫songname[12],你也可以改成其他的字串。

比較特別的是這次我們是用select(‘script’),原因是透過觀察能發現這些資訊都被放在script裡。

至於要如何觀察呢?其實你只要在這首歌的歌詞網頁點右鍵裡的檢查網頁原始碼,

你就能清楚地發現為什麼這些資訊藏在script了。

artist_name = []
song_name = []
lyrics = []
genre = []

url3 = 'https://www.azlyrics.com' + songname[12]
html3 = requests.get(url3, headers=headers)
soup3 = BeautifulSoup(html3.text, 'html.parser')
script = soup3.select('script')

 

以下就是經過數次的Trial and error後整理出無例外的爬取資訊規則,

特別的小技巧就是利用split,例如genre和lyrics所在位置都被兩段說明文字夾住,

所以就能利用這兩段說明文字作為分割點,切成這段文字以前的區塊和這段文字以後的區塊,

先取上方文字以後的區塊,再取下方文字以前的區塊、再使用list取值定位到它們的所在地,

就能成功去蕪存菁、得到夾在裡面的資訊了,split是不是很方便呢?

而且重點是除非網站管理員改動這兩段文字,不然這麼做就不會出錯,也不會有例外。

# artist name
artist_name.append(script[1].text.split('\r\n')[1][:-2][14:])

# song name
song_name.append(script[1].text.split('\r\n')[2][:-2][12:])

# genre
genre_up_part = 'cf_page_song = SongName;'
genre_down_part = '<script src="//cdn.clickfuse.com/publishers/azlyrics/single.min.js"></script>'
      
gen = str(soup3)
gen = gen.split(genre_up_part)[1]
gen = gen.split(genre_down_part)[0]
gen = gen[19:][:-14]
        
genre.append(gen)
        
# lyrics
lyrics_up_part = '<!-- Usage of azlyrics.com content by any third-party lyrics provider is prohibited by our licensing agreement. Sorry about that. -->'
lyrics_down_part = '<!-- MxM banner -->'

ly = str(soup3)
ly = ly.split(lyrics_up_part)[1]
ly = ly.split(lyrics_down_part)[0]
ly = ly.replace('<br>','').replace('</br>','').replace('<br/>','').replace('</div>','').replace('</i>','').replace('<i>','').strip()
        
lyrics.append(ly)

 

最後查看一下是否4項資訊都順利被我們爬進各自的串列了。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 all info

 

完美 ! 

 

經過這一整個探索後,你只要稍微改動以上程式碼的某些片段,

就能一次爬取特定一位歌手、或是所有開頭為a的歌手所有歌詞的資訊,

你甚至也可以將以上的程式碼都組裝起來寫成一個function,

例如說輸入歌手名稱後這個function就會自動把該歌手的所有歌詞資訊都爬完,

並且以進度條的方式告訴你現在的進度,至於如何使用請看下方有我的示範程式。

 

Python爬蟲示範程式

 

我使用的爬蟲程式比較不同,不針對任何特定歌手爬取,

而是盡可能全面地爬取這個網站裡的所有歌詞,

所以我的作法變成是先依照上述前兩步的方式爬取資訊,而這些資訊是用來操作第三步的準備作業。

 

第一步先爬取歌詞網站所有歌手的名字後並所有名字存起來,

再使用第二步的程式碼爬取Artist Name和Song Name,最後統一存成DataFrame,

而之後爬的時候我就是用這個DataFrame裡的artist name和song name

組成html網址、並依照DataFrame順序爬取。

 

因此,你會看到tqdm_notebook(range(385800, 390000, 1)),

這裡面的數字就是代表DataFrame裡的順序,表示我指定要爬DataFrame裡的第385800到390000的row,

而讀完list.csv後的music就是我存放先前資訊的DataFrame,有artist和song兩個欄位。

而for裡面你就可以看到用到許多上面提到的各個重要零件,

例如tqdm_notebook、try & except、time.sleep(25 + random.uniform(0,10)),

至於它們的功能在上面都提過了就不再贅述。

import pandas as pd
import numpy as np
import requests
from bs4 import BeautifulSoup
import time
import random
from tqdm import tqdm_notebook

SEED=42

headers = {'User-Agent': 'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'}

artist_name = []
song_name = []
lyrics = []
genre = []
failed = []

music = pd.read_csv('list.csv')

for i in tqdm_notebook(range(385800, 390000, 1)):
    try:
            
        url = 'https://www.azlyrics.com/lyrics/' + music.iloc[i].artist + '/' + music.iloc[i].song + '.html'

        html = requests.get(url, headers=headers)#, proxies={'http': proxy})
        soup = BeautifulSoup(html.text, 'html.parser') 

        script = soup.select('script')
 
        # artist name & song name
        artist_name.append(script[1].text.split('\r\n')[1][:-2][14:])
        song_name.append(script[1].text.split('\r\n')[2][:-2][12:])
        
        
        # genre
        genre_up_part = 'cf_page_song = SongName;'
        genre_down_part = '<script src="//cdn.clickfuse.com/publishers/azlyrics/single.min.js"></script>'
      
        gen = str(soup)
        gen = gen.split(genre_up_part)[1]
        gen = gen.split(genre_down_part)[0]
        gen = gen[19:][:-14]
        
        genre.append(gen)
        
        # lyrics
        lyrics_up_part = '<!-- Usage of azlyrics.com content by any third-party lyrics provider is prohibited by our licensing agreement. Sorry about that. -->'
        lyrics_down_part = '<!-- MxM banner -->'

        ly = str(soup)
        ly = ly.split(lyrics_up_part)[1]
        ly = ly.split(lyrics_down_part)[0]
        ly = ly.replace('<br>','').replace('</br>','').replace('<br/>','').replace('</div>','').replace('</i>','').replace('<i>','').strip()
        
        lyrics.append(ly)

        # sleep
        time.sleep(25 + random.uniform(0, 10))
        
    except Exception as e:
        print("Error occurred \n" + str(e))
        print('failed song {}: {} by {}'.format(i, music.iloc[i].song, music.iloc[i].artist))
        failed.append([i, music.iloc[i].song, music.iloc[i].artist])
df = pd.DataFrame({
    "ArtistName":artist_name,
    "SongName":song_name,
    "Genre":genre,
    "Lyrics":lyrics,
})
df

 

爬完後你就能透過上面的程式碼直接一鍵把它們從list轉成DataFrame,

爬蟲經過數小時的辛苦成果就會出現在你眼前了。

貼心提醒,跑完後第一件事一定要記得先把它存起來喔。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 output

DataFrame最後結果

 

注意事項

 

1.第一級警告

現在AZLyrics已經有反爬蟲裝置了,但這不代表你無法使用爬蟲,只是你無法一天24小時無限制地爬。

如果你是第一次使用爬蟲,只要沒有數小時地連續爬,只爬一小部分、只爬一下子的話,

一般而言,都不會被第一級警告。就算不幸發生下圖的狀況,

只要打勾我不是機器人你就可以重新回網站。

只不過這表示你已經被盯上了,如果在短時間內又被警告的話,

就會進入第二級警告的畫面,短時間內將完全無法進入網站。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 block

AZLyrics 第一級警告

 

2.第二級警告

如果進入網站時是這個畫面的話,就表示你得休息個一兩天不能使用爬蟲了,

休息一兩天後你再進入網站的話一般來說它就會變回第一級警告的畫面,

此時,打勾我不是機器人後就能再度順利回到網站。

也就是說,如果你真的有大量需求的話,

一天可以連續爬數個小時,爬到遇見第二級警告後就休息一天,

然後隔天再繼續,維持著這個循環是目前最好的作法。

 

什麼是Python爬蟲?教你輕鬆爬取歌詞網站 deny

AZLyrics 第二級警告

 

3.永久封鎖

如果你經常被第二級警告的話,就有可能被AZLyrics永久封鎖,再也無法使用該網路的IP訪問。

畫面就會和第二級警告的圖一樣,只是很可能過了很久這個畫面都不會改變。

然而,如果真的發生這種情況也還是有其他替代方案,

例如 : 換一個全新沒爬過的網路,或是在原有被鎖的網路下使用VPN,

如此一來就等於又多一個IP可以使用了喔。

 

結論

 

你了解爬蟲的秘密了嗎?

 

這篇文章利用基本的python語法成功地拼湊了出一支能實際運作的Python爬蟲,

並且詳細解說了如何從無到有建立一個爬蟲程式

往後如果你有自己利用爬蟲蒐集資料的需求時,

你就不用在從頭開始撰寫程式了。

只要將這個原型架構改造成符合其他類似網站的規格,

就能輕鬆地再生出一支爬蟲程式了喔。

 

Call To Action

 

歡迎任何能使我或是這篇文章改進的意見,所以如果有問題或想法都歡迎在下面告訴我喔 !

另外,如果你覺得這篇文章有幫助到你、很符合你的需求的話,記得幫我拍手以及分享喔 !

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *