本文同時發(fā)布至我的個人博客,點擊進入我的個人博客閱讀。本博客供技術(shù)交流與經(jīng)驗分享,可自由轉(zhuǎn)載。轉(zhuǎn)載請附帶原文鏈接,感謝!
項目背景

AllMusic 是一個關(guān)于音樂的元數(shù)據(jù)資料庫,在1991年由流行文化維護者Michael Erlewine與數(shù)學家兼哲學博士Vladimir Bogdanov創(chuàng)立,目的是成為音樂消費者的導覽。AllMusic New Releases 是 AllMusic 為用戶提供的一項內(nèi)容推薦服務(wù),以周為頻次向用戶推薦本周的新音樂/新專輯,甚至你可以通過郵件的形式來訂閱該內(nèi)容。
對于中重度音樂愛好者或習慣聆聽新音樂的人群,AllMusic New Releases 提供了很優(yōu)秀的推薦服務(wù)。作為一個嚴謹專業(yè)音樂資料庫,AllMusic 提供十分專業(yè)且全面的音樂信息。于此同時,AllMusic 有一個由若干專家樂評人組成的內(nèi)容團隊,每周推薦的都是一些比較具有音樂性或話題性的專輯,同時也提供十分專業(yè)的樂評
作為一名 AllMusic 的用戶,我對其中的內(nèi)容質(zhì)量十分滿意,但是使用過程中還是有一些不好的體驗:
- 由于服務(wù)器架設(shè)在國外,雖然沒有被墻,但是網(wǎng)頁加載十分緩慢。
- AllMusic 在去年接入廣告服務(wù),需要安裝對應(yīng)的廣告插件才能正常訪問。
學習了 Python 的基本爬蟲技術(shù)后,我決定嘗試一下通過爬蟲技術(shù)來規(guī)避這個問題?;舅悸肥牵号廊∽罱?0周的 AllMusic New Releases 的內(nèi)容,獲取專輯圖片(地址)、藝術(shù)家、專輯名、風格、廠牌、評分等基本信息,并以文本形式存儲于本地,下次需要查看時可以直接查看本地文件。
功能實現(xiàn)
一個原始的爬蟲實現(xiàn)可以分為:抓取頁面 —> 信息提取 —> 格式化輸出/存儲,同時,由于我們需要處理10個頁面,所以引入線程池來實現(xiàn)多線程爬蟲能一定程度地優(yōu)化爬蟲性能。有了基本的方向之后就可以開始編寫程序,這里我們使用最原始的步進式編程策略來完成。
(一)抓取單個頁面
Python 中關(guān)于實現(xiàn)頁面抓取的一般有 urllib 與 requests, 這里我們選擇 API 更加簡潔的requests 。
def getOnePage(url, headers):
try:
rp = requests.get(url=url, headers=headers)
if rp.status_code == 200:
return rp.text
return None
except RequestException as e:
print('Request Exception')
return None
getOnePage()主體上是一個try...except...結(jié)構(gòu),調(diào)用requests.get()獲取指定 url 的 html 代碼,并以字符串的形式返回;若獲取失敗則獲取函數(shù)拋出的RequestException異常,同時要注意 Allmusic 會檢查 get 方法的請求頭,所以我們需要傳入headers請求頭參數(shù)。
(二)信息析取
這里我們需要爬取兩方面的信息:一是我們需要獲取的New Releases 的內(nèi)容;二是需要從網(wǎng)頁中獲取日期信息來構(gòu)成url(當然也可以直接通過算法計算,Allmusic 的更新日期是每周的周五)。
析取 New Releases 中的內(nèi)容

使用Chrome的開發(fā)者工具分析我們需要爬取的網(wǎng)頁,觀察我們關(guān)心的字段內(nèi)容及其所在的標簽。這里我們使用正則表達式匹配來解析,當然你也可以選擇 BeautifulSoup、Pyquery 等網(wǎng)頁解析庫。
def parseOnePage(html):
# use regular expression to get the specified information we want. It do not work well on 'artist' file, so we
# we have to process 'artist' after it
pattern = re.compile('album-cover">.*?img src="(.*?)".*?artist">(.*?)</div>.*?title">.*?>(.*?)</a>.*?label">(.*?)</div>.*?styles">.*?>(.*?)</a>.*?allmusic-rating rating-allmusic-(\d+)">.*?headline-review">(.*?)<div.*?author">(.*?)</div>', re.S)
items = re.findall(pattern, html)
for item in items:
# process the 'artist',in order to remove the html code such as '<a>xxx</a>', I use re.split() func.
artist = item[1].strip()
artist = re.split('<.*?>', artist)
artist = ''.join(artist).strip()
yield {
'cover': item[0],
'artist': artist,
'title': item[2],
'label': item[3].strip(),
'styles': item[4],
'allmusic-rating': item[5],
'review': item[6].strip(),
'author': item[7].strip()[2:],
}
這里使用正則表達式來解決確實帶來了一定的麻煩,問題在于在匹配artist字段時由于html格式上的不統(tǒng)一給匹配語法帶來了麻煩,無法直接用一次正則匹配解決。這里最后采用的方法是“先擴大匹配范圍,然后再在后續(xù)處理中過濾不需要的內(nèi)容”這種思路。首先,第一次通過pattern規(guī)則匹配,我們獲得類似如下格式的artist字段:
...
{'artist': '<a >Jefre Cantu-Ledesma</a>'}
{'artist': 'Various Artists'}
{'artist': '<a }
...
進而,使用re.split('<.*?>', artist),過濾標簽即可獲得文本內(nèi)容:
...
{'artist': 'Jefre Cantu-Ledesma'}
{'artist': 'Various Artists'}
{'artist': 'Peacers'}
...
正則表達式的用法技巧性比較強,不停地試錯和調(diào)試然后靈活地調(diào)用方法才能比較高效地解決問題。正常匹配之后,我們可以嘗試添加如下main()函數(shù)測試單網(wǎng)頁的爬取是否正常。
def main():
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
'Host': 'www.allmusic.com'
}
url = 'http://www.allmusic.com/newreleases'
print(parseOnePage(getOnePage(url, headers)))
if __name__ == '__main__':
main()
獲取日期信息
若單網(wǎng)頁能正常爬取,那么我們可以開始考慮爬取多個網(wǎng)頁。首先分析這些網(wǎng)頁url規(guī)律:
http://www.allmusic.com/newreleases/20170818
http://www.allmusic.com/newreleases/20170811
http://www.allmusic.com/newreleases/20170804
...
不難想到,我們只要獲取所有的日期并以’YYYYMMDD‘的形式添加在基礎(chǔ)url上,就可以得到最終的url。通過一下方法,我們可以從網(wǎng)頁中獲取日期信息:
def getDate(html):
# get the most recent date and save as a 'datetime'
pattern = re.compile('week-filter">.*?value="(.*?)".*?selected">', re.S)
selecteDate = re.findall(pattern, html)[0]
selecteDatetime = datetime.strptime(selecteDate, '%Y%m%d')
date = []
# Allmusic update its information per week so we get information one time for every 7 days. The way to realize it
# is changing the end of url(such as /20170818 to 20170811)
for i in range(10):
i_timedelta = timedelta(7 * i, 0, 0)
last_datetime = selecteDatetime - i_timedelta
date.append(datetime.strftime(last_datetime, '%Y%m%d'))
return date
類似地,也是使用正則匹配的方法。另外,這里我在獲取第一個日期字符串后,將其轉(zhuǎn)為datetime對象,以使用datetime的相關(guān)方法來計算得出剩余九個需要獲取的日期。
(三)靜態(tài)本地存儲
def writeDown(content):
with open('AllmusicNewReleasesLast10Week.txt', 'a', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')
f.close()
寫入txt文件中,實現(xiàn)本地存儲。
(四)多線程爬取
修改main()函數(shù)與文件入口,將爬取10個網(wǎng)頁的線程加入線程池中,進行多線程爬取:
def main(offset):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome' +
'/60.0.3112.90 Safari/537.36',
'Host': 'www.allmusic.com'
}
url = 'http://www.allmusic.com/newreleases'
date = getDate(getOnePage(url, headers))
new_url = url + '/' + date[offset]
for i in parseOnePage(getOnePage(new_url, headers)):
print(i)
writeDown(i)
if __name__ == '__main__':
pool = Pool()
pool.map(main, [i for i in range(10)])
在控制臺輸出,引入多線程后爬取時間縮短了2-3秒左右,性能明顯提升。
項目總結(jié)
第一次寫爬蟲程序,選擇了使用 requests + 正則的實現(xiàn)方案,主要是為了鞏固基礎(chǔ)技術(shù)。正則表達式雖然強大,但是在實現(xiàn)過程中確實會遇到困難?;蛟S使用 BeautifulSoup 一個簡單的標簽選擇就可以實現(xiàn)的解析,用正則來實現(xiàn)可能會繁瑣許多,工具選擇確實對實現(xiàn)效率有很大影響。當然,熟練地使用正則表達式,也能在很多時候很巧妙地解決問題。