本文介紹使用Scrapy爬蟲框架爬取某FM音頻文件。
框架介紹
Scrapy是一個(gè)為了爬取網(wǎng)站數(shù)據(jù),提取結(jié)構(gòu)性數(shù)據(jù)而編寫的應(yīng)用框架。 可以應(yīng)用在包括數(shù)據(jù)挖掘,信息處理或存儲(chǔ)歷史數(shù)據(jù)等一系列的程序中。
安裝Scrapy
使用pip安裝
pip install Scrapy
創(chuàng)建項(xiàng)目
打開系統(tǒng)終端,cd到項(xiàng)目安裝文件夾,輸入命令:
scrapy startproject FmFiles
其中FmFiles*為項(xiàng)目名稱。
創(chuàng)建Scrapy項(xiàng)目后,用Pycharm打開項(xiàng)目,在編譯器下輸入代碼,也可以直接在終端下輸入代碼。
本文介紹使用Pycharm編寫項(xiàng)目代碼。
項(xiàng)目設(shè)置
Scrapy設(shè)定(settings)提供了定制Scrapy組件的方法。您可以控制包括核心(core),插件(extension),pipeline及spider組件。
設(shè)定為代碼提供了提取以key-value映射的配置值的的全局命名空間(namespace)。 設(shè)定可以通過(guò)下面介紹的多種機(jī)制進(jìn)行設(shè)置。
設(shè)定(settings)同時(shí)也是選擇當(dāng)前激活的Scrapy項(xiàng)目的方法(如果您有多個(gè)的話)。
進(jìn)入FmFiles子文件夾下名為settings的Python文件,本項(xiàng)目下需要覆蓋以下幾個(gè)默認(rèn)設(shè)置:
- 不遵守robots.txt文件,該文件定義了爬蟲相關(guān)協(xié)議,包括不允許爬蟲的代理、IP等信息。
ROBOTSTXT_OBEY = False
- 自定義pipeline文件所在路徑
ITEM_PIPELINES = {
'FmFiles.pipelines.FmfilesPipeline': 300,
}
- 設(shè)置爬取的文件保存路徑
FILES_STORE = '文件保存根路徑'
- 開啟媒體重定向。默認(rèn)情況下,媒體文件pipeline會(huì)忽略重定向,即向媒體文件URL請(qǐng)求的HTTP重定向?qū)⒁馕吨襟w下載被認(rèn)為是失敗的。
MEDIA_ALLOW_REDIRECTS = True
- 調(diào)整文件保留延遲天數(shù)。媒體文件pipeline會(huì)避免下載最近下載過(guò)的文件,默認(rèn)延遲90天。
FILES_EXPIRES = 120
定義Item
Item是保存爬取到的數(shù)據(jù)的容器;其使用方法和Python字典類似, 并且提供了額外保護(hù)機(jī)制來(lái)避免拼寫錯(cuò)誤導(dǎo)致的未定義字段錯(cuò)誤。
下載的圖像和文件默認(rèn)保存在指定根目錄下的full子文件夾下,且文件名默認(rèn)為URI(資源的唯一標(biāo)識(shí)符),本項(xiàng)目需修改每個(gè)文件的文件名和所在文件夾名,且爬蟲關(guān)閉后需刪除該full子文件夾。
進(jìn)入settingsPython文件,代碼如下:
import scrapy
class FmfilesItem(scrapy.Item):
# define the fields for your item here like:
# 專輯名稱
file_album = scrapy.Field()
# 專輯中文件名
file_name = scrapy.Field()
# 專輯中文件url
file_url = scrapy.Field()
pass
編寫爬蟲文件
首先創(chuàng)建爬蟲文件,進(jìn)入終端輸入命令:
scrapy genspider fmfiles ximalaya.com
其中genspider為創(chuàng)建爬蟲的scrapy命令,fmfiles是建立的爬蟲名稱(爬蟲的唯一識(shí)別字符串),ximalaya.com的爬取網(wǎng)站的限制域名。
爬蟲文件建立后,進(jìn)入spiders文件夾下的fmfiles文件夾。

導(dǎo)入模塊
import scrapy
import os
import json
from scrapy.selector import Selector
from FmFiles.items import FmfilesItem
from FmFiles.settings import FILES_STORE
定義爬蟲類相關(guān)屬性:
name = 'fmfiles'
allowed_domains = ['']
# PC端起始url
pc_url = 'http://www.ximalaya.com/'
# 移動(dòng)端起始url
mobile_url = 'http://m.ximalaya.com/'
allowed_domains是爬蟲限制域名,所有進(jìn)入爬取隊(duì)列的url必須符合這個(gè)域名,否則不爬取,該項(xiàng)目不限制。
該項(xiàng)目通過(guò)輸入手機(jī)端或電腦端音頻專輯所在url爬取該專輯下所有音頻文件,故需要PC端和移動(dòng)端的起始url以識(shí)別。
pc_url為PC端音頻專輯的起始url。
mobile_url為移動(dòng)端音頻專輯的起始url。
獲取文件外輸入的專輯url列表
該項(xiàng)目從主執(zhí)行文件中輸入PC端或移動(dòng)端的多個(gè)專輯url,建立main Python文件,輸入執(zhí)行scrapy爬蟲命令的代碼:
from scrapy.cmdline import execute
if __name__ == '__main__':
# 在此添加專輯url列表或在命令行執(zhí)行scrapy crawl fmfiles -a urls={多個(gè)專輯url,以逗號(hào)隔開}
album_urls = ['http://www.ximalaya.com/1000202/album/2667276/']
urls = ','.join(album_urls)
execute_str = 'scrapy crawl fmfiles -a urls=' + urls
execute(execute_str.split())
其中execute()執(zhí)行來(lái)自系統(tǒng)終端命令行語(yǔ)句,參數(shù)為單個(gè)命令的列表。
urls為爬蟲所需外部參數(shù)的鍵值,與爬蟲初始化器中屬性名一致。
在爬蟲文件獲取輸入?yún)?shù)值:
def __init__(self, urls=None):
super(FmfilesSpider, self).__init__()
self.urls = urls.split(',')
解析輸入?yún)?shù)
判斷urls參數(shù)是來(lái)自PC端還是移動(dòng)端的音頻專輯url,在請(qǐng)求爬取url方法中輸入代碼:
def start_requests(self):
for url in self.urls:
if url.startswith(self.mobile_url):
yield self.request_album_url(url)
elif url.startswith(self.pc_url):
yield scrapy.Request(url=url, callback=self.parse_pc)
若為移動(dòng)端專輯url,直接請(qǐng)求該url獲取各個(gè)資源url;若為PC端專輯url,還需在請(qǐng)求html中解析出相應(yīng)的移動(dòng)端專輯url。原因是移動(dòng)端url的反爬蟲措施較PC端少,更易爬取。
其中request_album_url(url)函數(shù)解析專輯url并交給scrapy請(qǐng)求該url,定義如下:
def request_album_url(self, album_url=''):
if len(album_url) == 0:
return None
album_url = album_url.strip().strip('/')
album_id = album_url.split('/')[-1]
return scrapy.Request(album_url,
meta={'aid': album_id},
callback=self.parse,
dont_filter=True)
其中parse_pc函數(shù)為請(qǐng)求PC端專輯url后的回調(diào)函數(shù),在下面講解。
解析PC端專輯url
使用XPath語(yǔ)法解析html結(jié)構(gòu)中的元素和內(nèi)容,scrapy官方關(guān)于XPath語(yǔ)法部分
def parse_pc(self, response):
# 從PC端專輯html中解析出移動(dòng)端專輯url
mobile_url = response.xpath('//head/link[contains(@rel, "alternate")]/@href').extract_first()
yield self.request_album_url(mobile_url)
解析移動(dòng)端專輯主頁(yè)url
def parse(self, response):
# 專輯名稱
album_name = response.xpath('//article/div/div/h2/text()').extract_first().strip()
# self.album_name = album_name
filepath = FILES_STORE + album_name
if not os.path.exists(filepath):
os.mkdir(filepath)
meta = response.meta
yield self.json_formrequest(aname=album_name,
aid=meta['aid'])
其中json_formrequest函數(shù)提交資源文件json表單,表單參數(shù)為json所在url、專輯id、資源頁(yè)數(shù)(一頁(yè)20個(gè)文件)。
def json_formrequest(self, aname='', aid=0, page=1):
moreurl = '/album/more_tracks'
# album_id = album_url.split('/')[-1]
page = str(page)
formrequest = scrapy.FormRequest(url='http://m.ximalaya.com' + moreurl,
formdata={'url': moreurl,
'aid': str(aid),
'page': str(page)},
meta={'aname': str(aname),
'aid': str(aid),
'page': str(page)},
method='GET',
callback=self.parse_json,
dont_filter=True)
return formrequest
解析資源json文件并保存到item
def parse_json(self, response):
jsondata = json.loads(response.text)
if jsondata['res'] is False:
return None
next_page = jsondata['next_page']
selector = Selector(text=jsondata['html'])
file_nodes = selector.xpath('//li[@class="item-block"]')
if file_nodes is None:
return None
meta = response.meta
for file_node in file_nodes:
file_name = file_node.xpath('a[1]/h4/text()').extract_first().strip()
file_url = file_node.xpath('a[2]/@sound_url').extract_first().strip()
item = FmfilesItem()
item['file_album'] = meta['aname']
item['file_name'] = file_name + '.' + file_url.split('.')[-1]
item['file_url'] = file_url
yield item
if int(next_page) == 0:
return None
if int(next_page) == (int(meta['page']) + 1):
yield self.json_formrequest(aname=meta['aname'],
aid=meta['aid'],
page=next_page)
編寫管道文件
導(dǎo)入模塊
import scrapy
import os
from scrapy.pipelines.files import FilesPipeline
from FmFiles.settings import FILES_STORE
from scrapy.exceptions import DropItem
定義管道類屬性
動(dòng)態(tài)獲取資源文件默認(rèn)父目錄“full”,并保存為屬性:
__file_dir = None
該屬性在爬蟲關(guān)閉后會(huì)執(zhí)行刪除文件夾操作。
編寫管道執(zhí)行函數(shù)
通過(guò)上述爬蟲文件中獲取到資源文件所在url后,交給scrapy的文件管道類下載,重寫scrapy媒體文件下載函數(shù):
def get_media_requests(self, item, info):
file_url = item['file_url']
yield scrapy.Request(file_url)
重寫該函數(shù)后scrapy會(huì)自動(dòng)處理url并下載。
資源下載完成后修改文件名:
def item_completed(self, results, item, info):
file_paths = [x['path'] for ok, x in results if ok]
if not self.__file_dir:
self.__file_dir = file_paths[0].split('/')[0]
if not file_paths:
raise DropItem("Item contains no files")
os.rename(FILES_STORE + file_paths[0],
FILES_STORE + item['file_album'] + '/' + item['file_name'])
return item
爬蟲結(jié)束后刪除默認(rèn)父文件夾:
def close_spider(self, spider):
if self.__file_dir is None:
return None
ori_filepath = FILES_STORE + self.__file_dir
if os.path.exists(ori_filepath):
os.rmdir(ori_filepath)
執(zhí)行爬蟲
代碼書寫完畢后,執(zhí)行main文件開始爬取。
