在之前一篇抓取漫畫圖片的文章裡,通過實現一個簡單的Python程式,遍歷所有漫畫的url,對請求所返回的html原始碼進行正規表示式分析,來提取到需要的資料。
本篇文章,通過 scrapy 框架來實現相同的功能。scrapy 是一個為了爬取網站資料,提取結構性資料而編寫的應用框架。關於框架使用的更多詳情可瀏覽官方文件,本篇文章展示的是爬取漫畫圖片的大體實現過程。
scrapy環境配置
安裝
首先是 scrapy 的安裝,博主用的是Mac系統,直接執行命令列:
1 |
pip install Scrapy |
對於html節點資訊的提取使用了 Beautiful Soup 庫,大概的用法可見之前的一篇文章,直接通過命令安裝:
1 |
pip install beautifulsoup4 |
對於目標網頁的 Beautiful Soup 物件初始化需要用到 html5lib 直譯器,安裝的命令:
1 |
pip install html5lib |
安裝完成後,直接在命令列執行命令:
1 |
scrapy |
可以看到如下輸出結果,這時候證明scrapy安裝完成了。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Scrapy 1.2.1 - no active project Usage: scrapy <command> [options] [args] Available commands: bench Run quick benchmark test commands fetch Fetch a URL using the Scrapy downloader genspider Generate new spider using pre-defined templates runspider Run a self-contained spider (without creating a project) settings Get settings values ... |
專案建立
通過命令列在當前路徑下建立一個名為 Comics 的專案
1 |
scrapy startproject Comics |
建立完成後,當前目錄下出現對應的專案資料夾,可以看到生成的Comics
檔案結構為:
1 2 3 4 5 6 7 8 9 10 |
|____Comics | |______init__.py | |______pycache__ | |____items.py | |____pipelines.py | |____settings.py | |____spiders | | |______init__.py | | |______pycache__ |____scrapy.cfg |
Ps. 列印當前檔案結構命令為:
1 |
find . -print | sed -e 's;{FNXX==XXFN}*/;|____;g;s;____|; |;g' |
每個檔案對應的具體功能可查閱官方文件,本篇實現對這些檔案涉及不多,所以按下不表。
建立Spider類
建立一個用來實現具體爬取功能的類,我們所有的處理實現都會在這個類中進行,它必須為 scrapy.Spider
的子類。
在 Comics/spiders
檔案路徑下建立 comics.py
檔案。
comics.py
的具體實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#coding:utf-8 import scrapy class Comics(scrapy.Spider): name = "comics" def start_requests(self): urls = ['http://www.xeall.com/shenshi'] for url in urls: yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): self.log(response.body); |
自定義的類為scrapy.Spider
的子類,其中的name
屬性為該爬蟲的唯一標識,作為scrapy爬取命令的引數。其他方法的屬性後續再解釋。
執行
建立好自定義的類後,切換到Comics
路徑下,執行命令,啟動爬蟲任務開始爬取網頁。
1 |
scrapy crawl comics |
列印的結果為爬蟲執行過程中的資訊,和目標爬取網頁的html原始碼。
1 2 3 4 5 6 7 |
2016-11-26 22:04:35 [scrapy] INFO: Scrapy 1.2.1 started (bot: Comics) 2016-11-26 22:04:35 [scrapy] INFO: Overridden settings: {'ROBOTSTXT_OBEY': True, 'BOT_NAME': 'Comics', 'NEWSPIDER_MODULE': 'Comics.spiders', 'SPIDER_MODULES': ['Comics.spiders']} 2016-11-26 22:04:35 [scrapy] INFO: Enabled extensions: ['scrapy.extensions.corestats.CoreStats', 'scrapy.extensions.telnet.TelnetConsole', 'scrapy.extensions.logstats.LogStats'] ... |
此時,一個基本的爬蟲建立完成了,下面是具體過程的實現。
爬取漫畫圖片
起始地址
爬蟲的起始地址為:
1 |
http://www.xeall.com/shenshi |
我們主要的關注點在於頁面中間的漫畫列表,列表下方有顯示頁數的控制元件。如下圖所示
爬蟲的主要任務是爬取列表中每一部漫畫的圖片,爬取完當前頁後,進入下一頁漫畫列表繼續爬取漫畫,依次不斷迴圈直至所有漫畫爬取完畢。
起始地址的url
我們放在了start_requests
函式的urls
陣列中。其中start_requests
是過載了父類的方法,爬蟲任務開始時會執行到這個方法。
start_requests
方法中主要的執行在這一行程式碼:請求指定的url
,請求完成後呼叫對應的回撥函式self.parse
1 |
scrapy.Request(url=url, callback=self.parse) |
對於之前的程式碼其實還有另一種實現方式:
1 2 3 4 5 6 7 8 9 10 11 |
#coding:utf-8 import scrapy class Comics(scrapy.Spider): name = "comics" start_urls = ['http://www.xeall.com/shenshi'] def parse(self, response): self.log(response.body); |
start_urls
是框架中提供的屬性,為一個包含目標網頁url的陣列,設定了start_urls
的值後,不需要過載start_requests
方法,爬蟲也會依次爬取start_urls
中的地址,並在請求完成後自動呼叫parse
作為回撥方法。
不過為了在過程中方便調式其它的回撥函式,demo中還是使用了前一種實現方式。
爬取漫畫url
從起始網頁開始,首先我們要爬取到每一部漫畫的url。
當前頁漫畫列表
起始頁為漫畫列表的第一頁,我們要從當前頁中提取出所需資訊,動過實現回撥parse
方法。
在開頭匯入BeautifulSoup
庫
1 |
from bs4 import BeautifulSoup |
請求返回的html原始碼用來給BeautifulSoup
初始化。
1 2 3 |
def parse(self, response): content = response.body; soup = BeautifulSoup(content, "html5lib") |
初始化指定了html5lib
直譯器,若沒安裝這裡會報錯。BeautifulSoup初始化時若不提供指定直譯器,則會自動使用自認為匹配的最佳直譯器,這裡有個坑,對於目標網頁的原始碼使用預設最佳直譯器為lxml
,此時解析出的結果會有問題,而導致無法進行接下來的資料提取。所以當發現有時候提取結果又問題時,列印soup
看看是否正確。
檢視html原始碼可知,頁面中顯示漫畫列表的部分為類名為listcon
的ul
標籤,通過listcon
類能唯一確認對應的標籤
提取包含漫畫列表的標籤
1 |
listcon_tag = soup.find('ul', class_='listcon') |
上面的find
方法意為尋找class
為listcon
的ul
標籤,返回的是對應標籤的所有內容。
在列表標籤中查詢所有擁有href
屬性的a
標籤,這些a
標籤即為每部漫畫對應的資訊。
1 |
com_a_list = listcon_tag.find_all('a', attrs={'href': True}) |
然後將每部漫畫的href
屬性合成完整能訪問的url地址,儲存在一個陣列中。
1 2 3 4 5 |
comics_url_list = [] base = 'http://www.xeall.com' for tag_a in com_a_list: url = base + tag_a['href'] comics_url_list.append(url) |
此時comics_url_list
陣列即包含當前頁每部漫畫的url。
下一頁列表
看到列表下方的選擇頁控制元件,我們可以通過這個地方來獲取到下一頁的url。
獲取選擇頁標籤中,所有包含href
屬性的a
標籤
1 2 |
page_tag = soup.find('ul', class_='pagelist') page_a_list = page_tag.find_all('a', attrs={'href': True}) |
這部分原始碼如下圖,可看到,所有的a
標籤中,倒數第一個代表末頁的url,倒數第二個代表下一頁的url,因此,我們可以通過取page_a_list
陣列中倒數第二個元素來獲取到下一頁的url。
但這裡需要注意的是,若當前為最後一頁時,不需要再取下一頁。那麼如何判斷當前頁是否是最後一頁呢?
可以通過select
控制元件來判斷。通過原始碼可以判斷,當前頁對應的option
標籤會具有selected
屬性,下圖為當前頁為第一頁
下圖為當前頁為最後一頁
通過當前頁數與最後一頁頁數做對比,若相同則說明當前頁為最後一頁。
1 2 3 4 5 6 7 |
select_tag = soup.find('select', attrs={'name': 'sldd'}) option_list = select_tag.find_all('option') last_option = option_list[-1] current_option = select_tag.find('option' ,attrs={'selected': True}) is_last = (last_option.string == current_option.string) |
當前不為最後一頁,則繼續對下一頁做相同的處理,請求依然通過回撥parse
方法做處理
1 2 3 4 5 6 |
if not is_last: next_page = 'http://www.xeall.com/shenshi/' + page_a_list[-2]['href'] if next_page is not None: print('\n------ parse next page --------') print(next_page) yield scrapy.Request(next_page, callback=self.parse) |
通過同樣的方式依次處理每一頁,直到所有頁處理完成。
爬取漫畫圖片
在parse
方法中提取到當前頁的所有漫畫url時,就可以開始對每部漫畫進行處理。
在獲取到comics_url_list
陣列的下方加上下面程式碼:
1 2 |
for url in comics_url_list: yield scrapy.Request(url=url, callback=self.comics_parse) |
對每部漫畫的url進行請求,回撥處理方法為self.comics_parse
,comics_parse
方法用來處理每部漫畫,下面為具體實現。
當前頁圖片
首相將請求返回的原始碼構造一個BeautifulSoup
,和前面基本一致
1 2 3 |
def comics_parse(self, response): content = response.body; soup = BeautifulSoup(content, "html5lib") |
提取選擇頁控制元件標籤,頁面顯示和原始碼如下所示
提取class
為pagelist
的ul
標籤
1 |
page_list_tag = soup.find('ul', class_='pagelist') |
檢視原始碼可以看到當前頁的li
標籤的class
屬性thisclass
,以此獲取到當前頁頁數
1 2 |
current_li = page_list_tag.find('li', class_='thisclass') page_num = current_li.a.string |
當前頁圖片的標籤和對應原始碼
獲取當前頁圖片的url,以及漫畫的標題。漫畫標題之後用來作為儲存對應漫畫的資料夾名稱。
1 2 3 4 5 |
li_tag = soup.find('li', id='imgshow') img_tag = li_tag.find('img') img_url = img_tag['src'] title = img_tag['alt'] |
儲存到本地
當提取到圖片url時,便可通過url請求圖片並儲存到本地
1 |
self.save_img(page_num, title, img_url) |
定義了一個專門用來儲存圖片的方法save_img
,具體完整實現如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# 先匯入庫 import os import urllib import zlib def save_img(self, img_mun, title, img_url): # 將圖片儲存到本地 self.log('saving pic: ' + img_url) # 儲存漫畫的資料夾 document = '/Users/moshuqi/Desktop/cartoon' # 每部漫畫的檔名以標題命名 comics_path = document + '/' + title exists = os.path.exists(comics_path) if not exists: self.log('create document: ' + title) os.makedirs(comics_path) # 每張圖片以頁數命名 pic_name = comics_path + '/' + img_mun + '.jpg' # 檢查圖片是否已經下載到本地,若存在則不再重新下載 exists = os.path.exists(pic_name) if exists: self.log('pic exists: ' + pic_name) return try: user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers = { 'User-Agent' : user_agent } req = urllib.request.Request(img_url, headers=headers) response = urllib.request.urlopen(req, timeout=30) # 請求返回到的資料 data = response.read() # 若返回資料為壓縮資料需要先進行解壓 if response.info().get('Content-Encoding') == 'gzip': data = zlib.decompress(data, 16 + zlib.MAX_WBITS) # 圖片儲存到本地 fp = open(pic_name, "wb") fp.write(data) fp.close self.log('save image finished:' + pic_name) except Exception as e: self.log('save image error.') self.log(e) |
函式主要用到3個引數,當前圖片的頁數,漫畫的名稱,圖片的url。
圖片會儲存在以漫畫名稱命名的資料夾中,若不存在對應資料夾,則建立一個,一般在獲取第一張圖時需要自主建立一個資料夾。
document
為本地指定的資料夾,可自定義。
每張圖片以頁數.jpg
的格式命名,若本地已存在同名圖片則不再進行重新下載,一般用在反覆開始任務的情況下進行判斷以避免對已存在圖片進行重複請求。
請求返回的圖片資料是被壓縮過的,可以通過response.info().get('Content-Encoding')
的型別來進行判斷。壓縮過的圖片要先經過zlib.decompress
解壓再儲存到本地,否則圖片打不開。
大體實現思路如上,程式碼中也附上註釋了。
下一頁圖片
和在漫畫列表介面中的處理方式類似,在漫畫頁面中我們也需要不斷獲取下一頁的圖片,不斷的遍歷直至最後一頁。
當下一頁標籤的href
屬性為#
時為漫畫的最後一頁
1 2 3 4 5 6 7 |
a_tag_list = page_list_tag.find_all('a') next_page = a_tag_list[-1]['href'] if next_page == '#': self.log('parse comics:' + title + 'finished.') else: next_page = 'http://www.xeall.com/shenshi/' + next_page yield scrapy.Request(next_page, callback=self.comics_parse) |
若當前為最後一頁,則該部漫畫遍歷完成,否則繼續通過相同方式處理下一頁
1 |
yield scrapy.Request(next_page, callback=self.comics_parse) |
執行結果
大體的實現基本完成,執行起來,可以看到控制檯列印情況
本地資料夾儲存到的圖片
scrapy框架執行的時候使用了多執行緒,能夠看到多部漫畫是同時進行爬取的。
目標網站資源伺服器感覺比較慢,會經常出現請求超時的情況。跑的時候請耐心等待。:)
最後
本文介紹的只是scrapy框架非常基本的用法,還有各種很細節的特性配置,如使用FilesPipeline
、ImagesPipeline
來儲存下載的檔案或者圖片;框架本身自帶了個XPath
類用來對網頁資訊進行提取,這個的效率要比BeautifulSoup
高;也可以通過專門的item
類將爬取的資料結果儲存作為一個類返回。具體請查閱官網。
最後附上完整Demo原始碼。