Python 爬蟲:用 Scrapy 框架實現漫畫的爬取
14.jpg
在之前一篇 抓取漫畫圖片的文章 里,通過實現一個簡單的Python程序,遍歷所有漫畫的url,對請求所返回的html源碼進行正則表達式分析,來提取到需要的數據。
本篇文章,通過 scrapy 框架來實現相同的功能。 scrapy 是一個為了爬取網站數據,提取結構性數據而編寫的應用框架。
scrapy環境配置
安裝
首先是 scrapy 的安裝,博主用的是Mac系統,直接運行命令行:
pip install Scrapy
對于html節點信息的提取使用了 Beautiful Soup 庫,大概的用法可見 之前的一篇文章 ,直接通過命令安裝:
pip install beautifulsoup4
對于目標網頁的 Beautiful Soup 對象初始化需要用到 html5lib 解釋器,安裝的命令:
pip install html5lib
安裝完成后,直接在命令行運行命令:
scrapy
可以看到如下輸出結果,這時候證明scrapy安裝完成了。
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 的項目
scrapy startproject Comics
創建完成后,當前目錄下出現對應的項目文件夾,可以看到生成的 Comics 文件結構為:
|____Comics
| |______init__.py
| |______pycache__
| |____items.py
| |____pipelines.py
| |____settings.py
| |____spiders
| | |______init__.py
| | |______pycache__
|____scrapy.cfg
Ps. 打印當前文件結構命令為:
find . -print | sed -e 's;{FNXX==XXFN}*/;|____;g;s;____|; |;g'
每個文件對應的具體功能可查閱官方文檔,本篇實現對這些文件涉及不多,所以按下不表。
創建Spider類
創建一個用來實現具體爬取功能的類,我們所有的處理實現都會在這個類中進行,它必須為 scrapy.Spider 的子類。
在 Comics/spiders 文件路徑下創建 comics.py 文件。
comics.py 的具體實現:
#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 路徑下,運行命令,啟動爬蟲任務開始爬取網頁。
scrapy crawl comics
打印的結果為爬蟲運行過程中的信息,和目標爬取網頁的html源碼。
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']
...
此時,一個基本的爬蟲創建完成了,下面是具體過程的實現。
爬取漫畫圖片
起始地址
爬蟲的起始地址為:
http://www.xeall.com/shenshi
我們主要的關注點在于頁面中間的漫畫列表,列表下方有顯示頁數的控件。如下圖所示
1.jpg
爬蟲的主要任務是爬取列表中每一部漫畫的圖片,爬取完當前頁后,進入下一頁漫畫列表繼續爬取漫畫,依次不斷循環直至所有漫畫爬取完畢。
起始地址的 url 我們放在了 start_requests 函數的 urls 數組中。其中 start_requests 是重載了父類的方法,爬蟲任務開始時會執行到這個方法。
start_requests 方法中主要的執行在這一行代碼:請求指定的 url ,請求完成后調用對應的回調函數 self.parse
scrapy.Request(url=url, callback=self.parse)
對于之前的代碼其實還有另一種實現方式:
#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 庫
from bs4 import BeautifulSoup
請求返回的 html 源碼用來給 BeautifulSoup 初始化。
def parse(self, response):
content = response.body;
soup = BeautifulSoup(content, "html5lib")
初始化指定了 html5lib 解釋器,若沒安裝這里會報錯。BeautifulSoup初始化時若不提供指定解釋器,則會自動使用自認為匹配的最佳解釋器,這里有個坑,對于目標網頁的源碼使用默認最佳解釋器為 lxml ,此時解析出的結果會有問題,而導致無法進行接下來的數據提取。所以當發現有時候提取結果又問題時,打印 soup 看看是否正確。
查看html源碼可知,頁面中顯示漫畫列表的部分為類名為 listcon 的 ul 標簽,通過 listcon 類能唯一確認對應的標簽
2.jpg
提取包含漫畫列表的標簽
listcon_tag = soup.find('ul', class_='listcon')
上面的 find 方法意為尋找 class 為 listcon 的 ul 標簽,返回的是對應標簽的所有內容。
在列表標簽中查找所有擁有 href 屬性的 a 標簽,這些 a 標簽即為每部漫畫對應的信息。
com_a_list = listcon_tag.find_all('a', attrs={'href': True})
然后將每部漫畫的 href 屬性合成完整能訪問的url地址,保存在一個數組中。
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。
3.jpg
獲取選擇頁標簽中,所有包含 href 屬性的 a 標簽
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。
5.jpg
但這里需要注意的是,若當前為最后一頁時,不需要再取下一頁。那么如何判斷當前頁是否是最后一頁呢?
可以通過 select 控件來判斷。通過源碼可以判斷,當前頁對應的 option 標簽會具有 selected 屬性,下圖為當前頁為第一頁
4.jpg
下圖為當前頁為最后一頁
6.jpg
通過當前頁數與最后一頁頁數做對比,若相同則說明當前頁為最后一頁。
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 方法做處理
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 數組的下方加上下面代碼:
for url in comics_url_list:
yield scrapy.Request(url=url, callback=self.comics_parse)
對每部漫畫的url進行請求,回調處理方法為 self.comics_parse , comics_parse 方法用來處理每部漫畫,下面為具體實現。
當前頁圖片
首相將請求返回的源碼構造一個 BeautifulSoup ,和前面基本一致
def comics_parse(self, response):
content = response.body;
soup = BeautifulSoup(content, "html5lib")
提取選擇頁控件標簽,頁面顯示和源碼如下所示
7.jpg
8.jpg
提取 class 為 pagelist 的 ul 標簽
page_list_tag = soup.find('ul', class_='pagelist')
查看源碼可以看到當前頁的 li 標簽的 class 屬性 thisclass ,以此獲取到當前頁頁數
current_li = page_list_tag.find('li', class_='thisclass')
page_num = current_li.a.string
當前頁圖片的標簽和對應源碼
9.jpg
10.jpg
獲取當前頁圖片的url,以及漫畫的標題。漫畫標題之后用來作為存儲對應漫畫的文件夾名稱。
li_tag = soup.find('li', id='imgshow')
img_tag = li_tag.find('img')
img_url = img_tag['src']
title = img_tag['alt']
保存到本地
當提取到圖片url時,便可通過url請求圖片并保存到本地
self.save_img(page_num, title, img_url)
定義了一個專門用來保存圖片的方法 save_img ,具體完整實現如下
# 先導入庫
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 解壓再保存到本地,否則圖片打不開。
大體實現思路如上,代碼中也附上注釋了。
下一頁圖片
和在漫畫列表界面中的處理方式類似,在漫畫頁面中我們也需要不斷獲取下一頁的圖片,不斷的遍歷直至最后一頁。
11.jpg
當下一頁標簽的 href 屬性為 # 時為漫畫的最后一頁
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)
若當前為最后一頁,則該部漫畫遍歷完成,否則繼續通過相同方式處理下一頁
yield scrapy.Request(next_page, callback=self.comics_parse)
運行結果
大體的實現基本完成,運行起來,可以看到控制臺打印情況
12.jpg
本地文件夾保存到的圖片
13.jpg
scrapy框架運行的時候使用了多線程,能夠看到多部漫畫是同時進行爬取的。
目標網站資源服務器感覺比較慢,會經常出現請求超時的情況。跑的時候請耐心等待。:)
最后
本文介紹的只是scrapy框架非常基本的用法,還有各種很細節的特性配置,如使用 FilesPipeline 、 ImagesPipeline 來保存下載的文件或者圖片;框架本身自帶了個 XPath 類用來對網頁信息進行提取,這個的效率要比 BeautifulSoup 高;也可以通過專門的 item 類將爬取的數據結果保存作為一個類返回。
來自:http://www.jianshu.com/p/c1704b4dc04d