目錄
- 宣告
- Hello,酷狗!
- 創建一個Scrapy專案
- spider模塊
- 分析前端界面
- 注意
- items模塊
- pipeline模塊
- 處理音頻檔案自定義下載路徑
- 自定義下載圖片路徑
- 異步存入到資料庫
- settings.py
- 除錯
- 運行
- 原始碼
宣告
文章僅供學習交流使用,切勿他用,如有侵權,請聯系本人處理,
scrapy之前了解過,但是過一段時間又忘記,于是打算爬一個網站,順便記錄下,以便后續能夠快速回憶,以下是自己的一些理解,如果有不對的地方,還請各位看官指教,本來想爬取echo音樂的(喜歡而已),但是好像echo音樂掛了有一段時間了,無奈,找個酷狗啪啪啪,??????爬爬爬...
先上成果圖



Hello,酷狗!
話不多說,首先看看酷狗首頁長啥樣,
分類很多,我們沒必要全站爬取,hold不住,簡單爬一爬,就從熱門歌手下手吧,
擼起袖子加油干,奧利給~
創建一個Scrapy專案
#創建一個scrapy專案
scrapy startproject KugouMusicSpider
#創建一個爬蟲,我用酷狗首頁作為入口,也可以直接從目標網址入手
scrapy genspider kugou_music_spider www.kugou.com
spider模塊
spider模塊主要有兩個作用:
- yield組裝Item物體,engine會將item交由pipeline處理
- yield新的Request請求,engine會將request交由scheduler處理
由原始碼可以看出,我們的spider會從start_requests方法中通過start_urls開始爬取,并會將結果交由parse方法處理,當然默認去重dont_filter是為True的,
所以我們可以復寫start_requests方法自行處理,也可直接從默認的parse方法中等待start_url的結果response進行處理,

弄清楚spider的作用后,我們鬼刀一開,就可以操作了,
def start_requests(self):
for url in self.start_urls:
yield Request(url, dont_filter=True, callback=self.parse_index)
def parse_index(self, response):
"""
根據酷狗首頁獲取'更多'歌手連接(即歌手首頁)
:param response:
:return:
"""
singer_index_url = response.xpath('//div[@id="tabMenu"]//a[@]/@href').extract_first()
singer_index_url = parse.urljoin(response.url, singer_index_url)
yield Request(
url=singer_index_url,
callback=self.parse_singer_index,
dont_filter=True
)
拿到歌手首頁后,我們獲取前面18為歌手,主要是html剛好在一個標簽內,其他的不想搞了,否則太多了,
def parse_singer_index(self, response):
"""
決議歌手首頁,只爬取前18位歌手資料
:param response:
:return:
"""
head_singers = response.xpath('//ul[@id="list_head"]/li')
for singer_info in head_singers:
singer_url = singer_info.xpath('./a/@href').extract_first()
match_re = re.match(".*?(\d+).*", singer_url)
if match_re:
author_id = match_re.group(1)
name = singer_info.xpath('./a/@title').extract_first()
pic_url = singer_info.xpath('./a/img/@_src').extract_first()
singer_item = SingerItem()
singer_item.update({
"name": name,
"author_id": author_id,
"index_url": singer_url,
"pic_url": pic_url
})
yield Request(
url=singer_url,
callback=self.parse_singer_detail,
meta={"singer_item": singer_item},
dont_filter=True
)
接著我們請求每一個歌手的詳情界面,獲取音樂串列
def parse_singer_detail(self, response):
"""
決議歌手詳情頁面,提取歌曲
:param response:
:return:
"""
singer_item = response.meta.get('singer_item')
brief = response.xpath('//div[@]/p/text()').extract_first()
singer_item.update({
"brief": brief
})
yield singer_item
musics = response.xpath('//ul[@id="song_container"]/li')
for music in musics:
music_hash = music.xpath('./a/input/@value').extract_first()
match_re = re.match(".*\|(.*)\|.*", music_hash)
if match_re:
music_hash = match_re.group(1)
if music_hash:
url = 'https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash={}'.format(music_hash)
yield Request(
url=url,
headers=self.headers,
cookies=self.cookies,
callback=self.parse_music_info,
dont_filter=True
)
到此,歌手資訊我們要這些差不多夠了,那如何爬取下載音樂的問題呢?其實,上面代碼中可以看到我除了yield出去一個singer_item,還yield出去一個請求,實不相瞞,這個請求就是獲取歌曲資訊的,我們由前端頁面來分析下為何會有如此的騷操作,
分析前端界面
可以看出,當我們在歌手詳情界面的時候,比如周董這個界面,
打開F12,我們點擊歌曲,它會進行打開一個新標簽進行播放,
我們接著在播放頁面打開F12,清除快取重繪(command+R)
不難可以看到這樣一個url請求
https://wwwapi.kugou.com/yy/index.php?r=play/getdata&callback=jQuery191016332021391396312_1610098389715&hash=0A62227CAAB66F54D43EC084B4BDD81F&dfid=339APl2z0YhL137NsD3cEyfR&mid=ce2e5886bdbb858e1d6dced7a863772f&platid=4&album_id=960399&_=1610098389716
她的回傳是如此的優美,漂亮,落落大方,(哈哈哈哈,皮一下就很開心,)
這不正是我們想找的東西嗎?play_url,她是MP3格式的,我們試著打開這個play_url鏈接
https://webfs.yun.kugou.com/202101081726/d8118a93c70849d1c597affdef32cf61/part/0/960083/G185/M0B/0C/13/-Q0DAF5Nfl-AGwcXADaWAFwMkd8892.mp3
完美的音頻檔案,
但是,有個問題,不論是上述的url請求還是play_url,都有我們看不懂的引數,目測是加密用的,并且mp3格式的鏈接應該是有時效性的
一籌莫展之際,抱著試試看的態度,將上述url留下我們能認識的引數
hash,這個我們認識,在歌手詳情界面每首歌的標簽里我們見過

試著請求
https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash=0A62227CAAB66F54D43EC084B4BDD81F
可以獲取到歌曲的部分資訊,其中包含album_id

剛剛我們url鏈接里也看到過這個引數,再加上,試試
https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash=0A62227CAAB66F54D43EC084B4BDD81F&album_id=960399

大哥,搞定~
此時,音樂的資訊我們也基本差不多了,
def parse_music_info(self, response):
"""
獲取歌曲album_id
:param response:
:return:
"""
res = json.loads(response.text)
status = res.get('status')
err_code = res.get('err_code')
if status == 1 and err_code == 0:
get_detail = response.meta.get('get_detail')
data = https://www.cnblogs.com/silence4allen/archive/2021/01/10/res.get('data', {})
if data:
if get_detail:
music_item = MusicItem()
music_item.update({
"hash": data.get('hash'),
"name": data.get('song_name'),
"lyrics": data.get('lyrics'),
"play_url": data.get('play_url'),
"audio_id": data.get('audio_id'),
"author_id": data.get('author_id'),
"author_name": data.get('author_name'),
"audio_name": data.get('audio_name'),
"album_name": data.get('album_name'),
"album_id": data.get('album_id'),
"img_url": data.get('img'),
"have_mv": data.get('have_mv'),
"video_id": data.get('video_id')
})
yield music_item
else:
album_id = data.get('album_id')
if album_id is not None:
url = '{}&album_id={}'.format(response.url, album_id)
yield Request(
url=url,
headers=self.headers,
cookies=self.cookies,
callback=self.parse_music_info,
meta={'get_detail': True},
dont_filter=True
)
將music資訊yield出去交由pipeline處理,
注意
- 此處yield出去的Request需要帶上headers和cookies,并且cookies不能放在headers中,否則拿不到資料
- 但是,cookies放在headers中,可以通過requests包發送請求,也可以拿到資料
- 再但是,requests是同步的方式,會阻塞,所以不建議
items模塊
我們已經在spider中用過了,主要用來定義待處理的物體,此處有singer和music兩個item
pipeline模塊
我們定義的pipeline模塊需要對item進行持久化處理,此處是下載音樂到本地以及存盤資訊到資料庫,
- pipelines模塊大都需要配置settings.py檔案,勿忘記,
- 處理檔案和圖片可以自己繼承FilesPipeline或者ImagesPipeline,自定義下載路徑及名稱
- 處理資料庫也建議使用異步的方式插入
處理音頻檔案自定義下載路徑
class KugouMusicPipeline(FilesPipeline):
"""
下載音頻檔案
"""
def get_media_requests(self, item, info):
if isinstance(item, MusicItem):
url = item['play_url']
yield Request(url)
def file_path(self, request, response=None, info=None, *, item=None):
# media_guid = hashlib.sha1(to_bytes(request.url)).hexdigest()
author_name = item['author_name']
media_guid = item['name']
media_ext = os.path.splitext(request.url)[1]
if media_ext not in mimetypes.types_map:
media_ext = ''
media_type = mimetypes.guess_type(request.url)[0]
if media_type:
media_ext = mimetypes.guess_extension(media_type)
return f'{author_name}/{media_guid}{media_ext}'
def item_completed(self, results, item, info):
if isinstance(item, MusicItem):
file_paths = [x['path'] for ok, x in results if ok]
if file_paths:
item['play_path'] = file_paths[0]
return item
自定義下載圖片路徑
class KugouImagePipeline(ImagesPipeline):
"""
處理檔案下載路徑,并將路徑資訊存入item
"""
def get_media_requests(self, item, info):
if isinstance(item, SingerItem):
url = item['pic_url']
yield Request(url)
def file_path(self, request, response=None, info=None, *, item=None):
if isinstance(item, SingerItem):
name = item['name']
image_guid = hashlib.sha1(to_bytes(request.url)).hexdigest()
return f'{name}/{image_guid}.jpg'
def item_completed(self, results, item, info):
if isinstance(item, SingerItem):
file_paths = [x['path'] for ok, x in results if ok]
if file_paths:
item['pic_path'] = file_paths[0]
return item
兩者差不多,主要是復寫三個方法用于不同功能
- get_media_requests:用來指明下載的url
- file_path:用來指明下載路徑
- item_completed:用來更新到item中,保存下載的檔案路徑,跟隨item存入資料庫
異步存入到資料庫
class MysqlTwistedPipeline(object):
"""
異步方式插入資料庫
"""
def __init__(self, db_pool):
self.db_pool = db_pool
@classmethod
def from_settings(cls, settings):
from MySQLdb.cursors import DictCursor
db_params = dict(
host=settings['MYSQL_HOST'],
db=settings['MYSQL_DBNAME'],
user=settings['MYSQL_USER'],
passwd=settings['MYSQL_PASSWORD'],
charset='utf8',
cursorclass=DictCursor,
use_unicode=True
)
db_pool = adbapi.ConnectionPool('MySQLdb', **db_params)
return cls(db_pool)
def process_item(self, item, spider):
query = self.db_pool.runInteraction(self.do_insert, item)
query.addErrback(self.handle_error, item, spider)
def handle_error(self, failure, item, spider):
print(failure)
def do_insert(self, cursor, item):
params = list()
cur_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if isinstance(item, SingerItem):
insert_sql = """
insert into singer
(name,author_id,index_url,pic_url,pic_path,brief,first_create_time,update_time)
values
(%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
author_id=VALUES(author_id),
index_url=VALUES(index_url),
pic_url=VALUES(pic_url),
brief=VALUES(brief),
update_time=VALUES(update_time);
"""
params.append(item.get('name', ''))
params.append(item.get('author_id', ''))
params.append(item.get('index_url', ''))
params.append(item.get('pic_url', ''))
params.append(item.get('pic_path', ''))
params.append(item.get('brief', ''))
params.append(cur_time)
params.append(cur_time)
cursor.execute(insert_sql, tuple(params))
elif isinstance(item, MusicItem):
pass
insert_sql = """
insert into music
(hash,name,lyrics,play_url,play_path,audio_id,author_id,author_name,audio_name,album_name,album_id,img_url,have_mv,video_id,first_create_time,update_time)
values
(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
hash=VALUES(hash),
play_url=VALUES(play_url),
img_url=VALUES(img_url),
update_time=VALUES(update_time);
"""
params.append(item.get('hash', ''))
params.append(item.get('name', ''))
params.append(item.get('lyrics', ''))
params.append(item.get('play_url', ''))
params.append(item.get('play_path', ''))
params.append(item.get('audio_id', ''))
params.append(item.get('author_id', ''))
params.append(item.get('author_name', ''))
params.append(item.get('audio_name', ''))
params.append(item.get('album_name', ''))
params.append(item.get('album_id', ''))
params.append(item.get('img_url', ''))
params.append(item.get('have_mv', ''))
params.append(item.get('video_id', ''))
params.append(cur_time)
params.append(cur_time)
cursor.execute(insert_sql, tuple(params))
在setting.py中配置pipelines注意先后順序,數字越小,越先處理

settings.py
配置資料庫引數和圖片、檔案的下載路徑
除錯
- 專案下可以新建main.py檔案,使用
execute(["scrapy", "crawl", "kugou_music_spider"])避免命令開啟爬蟲無法debug的問題 - 除錯的時候先一位歌手一首歌曲的除錯,避免啥你懂的
運行
奧利給~
scrapy crawl kugou_music_spider
測驗了下,運行結果如下

原始碼
代碼托管于github,傳送門,截止發文,有效,
部分資料可參考
scrapy官方檔案
scrapy_redis 分布式爬取酷狗音樂
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/247061.html
標籤:其他
上一篇:20200110-正則運算式
下一篇:go協程全域變數和區域變數
