無聊瀏覽某漫畫網站(你懂的。-_-),每次翻頁時都需要重新請求整個頁面,頁面雜七雜八的內容過多,導致頁面加載過程耗時略長。于是決定先把圖片先全部保存到本地。本文的主要內容,就是講解如何通過一個爬蟲程序,自動將所有圖片抓取到本地。
先來看網頁大概張什么樣。:)
漫畫封面列表界面:
每部漫畫點擊進去的界面
漫畫列表每一頁有幾十部漫畫,網站暫時有15頁列表,每部漫畫頁數有10+頁到近200頁不等,所以所有漫畫包含的圖片總數還是比較可觀的,通過手動將每張圖右鍵另存基本不可能完成。接下來將一步一步展示,如何用Python實現一個簡單爬蟲的程序,將網頁上所有漫畫全保存到本地。Python用的是2.7版本。
<h4>首先,何為網絡爬蟲?</h4>
網絡爬蟲(又被稱為網頁蜘蛛,網絡機器人),是一種按照一定的規則,自動地抓取萬維網信息的程序或者腳本。
一般而言,簡單的爬蟲實現主要分為兩步:
- 獲取指定網頁html源碼;
- 通過正則表達式提取出源碼中的目標內容。
<h3>本文程序實現大體步驟:</h3>
- 獲取每頁漫畫列表的url;
- 獲取每頁漫畫列表中每一部漫畫的url;
- 獲取每部漫畫每一頁圖片的url;
- 通過圖片的url將圖片保存到本地。
(總之就是通過for循環遍歷的過程。)
最后會附本文Demo完整源碼。
<h3>獲取每頁漫畫列表的url</h3>
程序最開始處理的頁面為:http://www.xeall.com/shenshi,即漫畫列表的首頁。紳士,你懂的。。
先創建一個文件Gentleman.py,引入需要用到的庫:
#coding:utf-8
import urllib2
import re
import os
import zlib
定義一個類,和初始化函數:
class Gentleman:
def __init__(self, url, path):
exists = os.path.exists(path)
if not exists:
print "文件路徑無效."
exit(0)
self.base_url = url
self.path = path
url為漫畫列表首頁url
path為圖片保存在本地的路徑,做個錯誤檢測,判斷本地是否存在對應的路徑
接下來定義 get_content 方法,獲取指定url的html源碼內容:
def get_content(self, url):
# 打開網頁
try:
request = urllib2.Request(url)
response = urllib2.urlopen(request, timeout=20)
# 將網頁內容解壓縮
decompressed_data = zlib.decompress(response.read(), 16 + zlib.MAX_WBITS)
# 網頁編碼格式為 gb2312
content = decompressed_data.decode('gb2312', 'ignore')
# print content
return content
except Exception, e:
print e
print "打開網頁: " + url + "失敗."
return None
urllib2.Request() 通過指定的url構建一個請求,urllib2.urlopen() 獲取請求返回的結果,timeout設為20秒無響應則做超時處理。urllib2.urlopen() 有可能出現打開網頁錯誤的情況,所以做了異常處理,保證在返回40X、50X之類的時候程序不會退出。后續相關的操作也都會做異常的處理。
原網頁為壓縮過的,請求到之后必須先進行解壓縮處理。
通過 charset=gb2312 可知解碼所需格式。
判斷網頁是否為壓縮的編碼類型,可以通過打印語句:
response.info().get('Content-Encoding')
打印結果為:
'gzip'
函數的返回結果為頁面html源碼文本。
跑起來試試:
url = "http://www.xeall.com/shenshi"
save_path = "/Users/moshuqi/Desktop/cartoon"
gentleman = Gentleman(url, save_path)
content = gentleman.get_content(url)
print content
可以看到打印結果:
接下來分析源碼文件,看如何從中獲取到每一頁漫畫列表的url。
頁面上有個選擇頁的控件:
通過分析對應控件的源碼,可知具體每一頁所對應的url:
控件的 name 為 sldd ,通過搜索全文發現只有這一處“sldd”字段,所以該字段可用來做標識。
option value 的值即為對應頁的url。
定義 get_page_url_arr 方法,獲取每一頁的url,返回一個數組:
def get_page_url_arr(self, content):
pattern = re.compile('name=\'sldd\'.*?>(.*?)</select>', re.S)
result = re.search(pattern, content)
option_list = result.groups(1)
pattern = re.compile('value=\'(.*?)\'.*?</option>', re.S)
items = re.findall(pattern, option_list[0])
arr = []
for item in items:
page_url = self.base_url + '/' + item
arr.append(page_url)
print "total pages: " + str(len(arr))
return arr
傳入的 content 為頁面源碼,首先通過 sldd 獲取到其中 option 的內容。采用正則表達式將內容提取出來。(關于正則表達式,不熟悉的同學推薦看這本 【正則表達式必知必會】,很薄的一本書,半天時間大概翻一遍基本就能用來處理大部分常見問題了)
正則表達式用了Python re模塊,具體方法的使用請自行百度, 這里只大概說一下思路。
首先用到的匹配模式:
'name=\'sldd\'.*?>(.*?)</select>'
' 為 ' 的轉義字符,匹配先找到以 name='sldd' 開頭的字符,.?* 是一個非貪婪匹配,用來匹配 name='sldd' 之后到最近的一個 > 之間的內容。(.?)* 意義和前一個類似,加上 () 表示為分組,可以在匹配結果中訪問,即我們需要識別出的內容。結尾的 select 標簽即為識別內容最后的標記。
運行后 option_list 識別到的內容應為:
<option value='p1.html' selected>1</option>
<option value='p2.html'>2</option>
<option value='p3.html'>3</option>
<option value='p4.html'>4</option>
<option value='p5.html'>5</option>
<option value='p6.html'>6</option>
<option value='p7.html'>7</option>
<option value='p8.html'>8</option>
<option value='p9.html'>9</option>
<option value='p10.html'>10</option>
<option value='p11.html'>11</option>
<option value='p12.html'>12</option>
<option value='p13.html'>13</option>
<option value='p14.html'>14</option>
<option value='p15.html'>15</option>
再正對 option_list 的內容進行識別,獲取到每個 option 的值,匹配模式為:
'value=\'(.*?)\'.*?</option>'
取到的值和一開始 base_url 連接拼接起來即為每一頁的url。
測試一下:
arr = gentleman.get_page_url_arr(content)
print arr
打印結果:
[u'http://www.xeall.com/shenshi/p1.html', u'http://www.xeall.com/shenshi/p2.html', u'http://www.xeall.com/shenshi/p3.html', u'http://www.xeall.com/shenshi/p4.html', u'http://www.xeall.com/shenshi/p5.html', u'http://www.xeall.com/shenshi/p6.html', u'http://www.xeall.com/shenshi/p7.html', u'http://www.xeall.com/shenshi/p8.html', u'http://www.xeall.com/shenshi/p9.html', u'http://www.xeall.com/shenshi/p10.html', u'http://www.xeall.com/shenshi/p11.html', u'http://www.xeall.com/shenshi/p12.html', u'http://www.xeall.com/shenshi/p13.html', u'http://www.xeall.com/shenshi/p14.html', u'http://www.xeall.com/shenshi/p15.html']
<h3>獲取每一頁包含的漫畫的url</h3>
例如,先對第一頁做處理http://www.xeall.com/shenshi/p1.html
打開網頁,找到這部分所對應的源碼:
瀏覽器上展示這部分的源碼很長,而且沒有換行,我們可以將文本拷貝到本地的編輯器上再進行分析。
可以看到內容比較長,以下兩張圖分別是開頭和結尾的截圖,紅色圈出的內容可作為識別這段文本的開頭和結尾。
定義 get_cartoon_arr 方法,獲取每一頁包含的漫畫的url,返回一個數組:
def get_cartoon_arr(self, url):
content = self.get_content(url)
if not content:
print "獲取網頁失敗."
return None
pattern = re.compile('class="piclist listcon".*?>(.*?)</ul>', re.S)
result = re.search(pattern, content)
cartoon_list = result.groups(1)
pattern = re.compile('href="/shenshi/(.*?)".*?class="pic show"', re.S)
items = re.findall(pattern, cartoon_list[0])
arr = []
for item in items:
# print item
page_url = self.base_url + '/' + item
arr.append(page_url)
return arr
匹配模式:
'class="piclist listcon".*?>(.*?)</ul>'
識別出漫畫列表的內容,內容太多就不打印了。
下圖為每一部漫畫所包含的信息,例如展示的第一部漫畫為圈出的部分。
我們只需要提取到 href 中的信息,可以分別用 /shenshi/ 和 class="pic show" 作為開頭結尾,提取出 “10444.html” 鏈接信息。
匹配模式為:
'href="/shenshi/(.*?)".*?class="pic show"'
將提取的結果和 base_url 連接拼接起來即為漫畫的url。
測試代碼:
arr = gentleman.get_cartoon_arr("http://www.xeall.com/shenshi/p1.html")
print arr
打印結果:
[u'http://www.xeall.com/shenshi/10444.html', u'http://www.xeall.com/shenshi/10440.html', u'http://www.xeall.com/shenshi/10423.html', u'http://www.xeall.com/shenshi/10414.html', u'http://www.xeall.com/shenshi/10406.html', ...]
<h3>創建Cartoon類</h3>
用來專門處理每一部漫畫的類,要處理的細節較多,所以專門封裝成一個類來實現。
類的初始化函數,參數 url 為漫畫地址
#coding:utf-8
import urllib2
import re
import zlib
import os
class Cartoon:
def __init__(self, url):
self.base_url = "http://www.xeall.com/shenshi"
self.url = url
Cartoon類中也會包含 get_content 函數,和之前的實現方式一樣,這里就不列出來了。當然,好的實現方式應該避免重復代碼,我懶得整理直接拷過來了。- -
定義獲取漫畫名的方法 get_title ,因為漫畫名后面要用來作為保存每部漫畫的文件夾名稱。
def get_title(self, content):
pattern = re.compile('name="keywords".*?content="(.*?)".*?/', re.S)
result = re.search(pattern, content)
if result:
title = result.groups(1)
return title[0]
else:
print "獲取標題失敗。"
return None
我們用這部漫畫做測試http://www.xeall.com/shenshi/10444.html
測試代碼:
title = cartoon.get_title(content)
print title
打印結果:
紳士漫畫:CURE UP↑↑秘密的寶島 (魔法少女同人志)
接下來需要獲取到翻頁對應的url,翻頁部分見下圖:
定義獲取漫畫每一頁的url方法 get_page_url_arr
def get_page_url_arr(self, content):
pattern = re.compile('class="pagelist">(.*?)</ul>', re.S)
result = re.search(pattern, content)
page_list = result.groups(1)
pattern = re.compile('<a href=\'(.*?)\'>.*?</a>', re.S)
items = re.findall(pattern, page_list[0])
arr = []
for item in items:
page_url = self.base_url + "/" + item
arr.append(page_url)
# pagelist中還包含了上一頁和下一頁,根據網頁格式可知分別在開始和結束,所以去掉首尾元素避免重復
arr.pop(0)
arr.pop(0)
arr.pop(len(arr) - 1)
return arr
打開網頁源碼:
可以看到圈出部分為每一頁按鈕對應的信息。
通過匹配模式獲取到這部分內容;
'class="pagelist">(.*?)</ul>'
對這部分內容進行匹配,獲取每一頁的url:
'<a href=\'(.*?)\'>.*?</a>'
看下圖會發現,前兩個鏈接為 href='#' ,最后一個為下一頁鏈接,重復了。
所以得到數組把這3個元素剔除掉。(至于為什么把 href='#' 都去掉,因為用這個鏈接后面會出錯,具體原因懶得追究,反正去掉之后也就少了第一頁的封面而已。)
arr.pop(0)
arr.pop(0)
arr.pop(len(arr) - 1)
測試代碼:
arr = cartoon.get_page_url_arr(content)
print arr
打印結果:
[u'http://www.xeall.com/shenshi/10444_2.html', u'http://www.xeall.com/shenshi/10444_3.html', u'http://www.xeall.com/shenshi/10444_4.html', ...]
<h3>獲取每頁漫畫圖片的url</h3>
例如針對之前獲取到數組的第一個元素:http://www.xeall.com/shenshi/10444_2.html
打開網頁,查看源碼:
![Uploading 14_014335.jpg . . .]
找到頁面圖片對應的代碼,通過搜索 img alt 可知文件中只有一處,所以可以用這個字段用來標識。
定義獲取圖片url的函數 get_pic_url :
def get_pic_url(self, page_url):
content = self.get_content(page_url)
if not content:
return None
pattern = re.compile('<img alt.*?src="(.*?)".*?/>', re.S)
result = re.search(pattern, content)
if result:
pic = result.groups(1)
return pic[0]
else:
print "獲取圖片地址失敗。"
print "url: " + page_url
return None
匹配模式為:
'<img alt.*?src="(.*?)".*?/>'
這里最初使用了后面的 p 標簽結束符作為結束標識,后來發現極少數部分頁面用的不是 p 而是 br,因而導致圖片url獲取失敗。
測試代碼:
url = cartoon.get_pic_url("http://www.xeall.com/shenshi/10444_2.html")
print url
打印結果:
http://tu.zzbzwy.com/xeall/uploadfile/gx02/160904/ww02.jpg
點擊打開看到直接就是一張圖片
<h3>將圖片保存到本地</h3>
定義保存函數的方法 save_pic
def save_pic(self, pic_url, path):
req = urllib2.Request(pic_url)
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36')
req.add_header('GET', pic_url)
try:
print "save pic url:" + pic_url
resp = urllib2.urlopen(req, timeout=20)
data = resp.read()
# print data
fp = open(path, "wb")
fp.write(data)
fp.close
print "save pic finished."
except Exception, e:
print e
print "save pic: " + pic_url + " failed."
pic_url 為圖片鏈接,path 為圖片保存到本地的路徑。
這遇到了個坑,Request 請求若不設置 User-Agent 信息會導致返回 403 Forbidden,服務器可能對訪問做了些限制。
最后Cartoon類定義一個對外的 save 方法,將漫畫所有圖片保存到本地
def save(self, path):
dir_path = path + "/" + self.title
self.create_dir_path(dir_path)
for i in range(0, len(self.page_url_arr)):
page_url = self.page_url_arr[i]
pic_url = self.get_pic_url(page_url)
if pic_url == None:
continue
pic_path = dir_path + "/" + str(i + 1) + ".jpg"
self.save_pic(pic_url, pic_path)
print self.title + " fetch finished."
給漫畫創建創建本地文件夾,文件夾名稱為漫畫名
def create_dir_path(self, path):
# 以漫畫名創建文件夾
exists = os.path.exists(path)
if not exists:
print "創建文件夾"
os.makedirs(path)
else:
print "文件夾已存在"
完成的初始化函數:
def __init__(self, url):
self.base_url = "http://www.xeall.com/shenshi"
self.url = url
content = self.get_content(self.url)
if not content:
print "Cartoon init failed."
return
self.title = self.get_title(content)
self.page_url_arr = self.get_page_url_arr(content)
下載一部完整漫畫的測試代碼:
url = "http://www.xeall.com/shenshi/10444.html"
save_path = "/Users/moshuqi/Desktop/cartoon"
cartoon = Cartoon(url)
cartoon.save(save_path)
運行起來,看看置頂文件夾結果:
<h3>將Cartoon類和Gentlman類結合,爬取所有漫畫圖片</h3>
Gentleman 完整的初始化函數:
def __init__(self, url, path):
exists = os.path.exists(path)
if not exists:
print "文件路徑無效."
exit(0)
self.base_url = url
self.path = path
content = self.get_content(url)
self.page_url_arr = self.get_page_url_arr(content)
外部調用的接口方法,遍歷所有頁和所有漫畫:
def hentai(self):
# 遍歷每一頁的內容
for i in range(0, len(self.page_url_arr)):
# 獲取每一頁漫畫的url
cartoon_arr = self.get_cartoon_arr(self.page_url_arr[i])
print "page " + str(i + 1) + ":"
print cartoon_arr
for j in range(0, len(cartoon_arr)):
cartoon = Cartoon(cartoon_arr[j])
cartoon.save(self.path)
print "======= page " + str(i + 1) + " fetch finished ======="
最終的結果跑起來:
url = "http://www.xeall.com/shenshi"
save_path = "/Users/moshuqi/Desktop/cartoon"
gentleman = Gentleman(url, save_path)
gentleman.hentai()
打印輸出:
可以看到置頂的文件夾里不斷生成新的漫畫文件夾和圖片。
<h3>圖片爬取結果</h3>
程序連續跑了幾個小時,一共抓取500+部漫畫,1W5+張圖片,文件總大小4G+。大概如上圖所示。- -
<h3>其他</h3>
程序里還做了些其他處理。偶爾會出現圖片請求失敗,導致一部漫畫缺少幾頁,對于這種情況的漫畫做處理時,通過判斷只對缺失的頁做請求。
重新跑程序時,每部漫畫都會先拿服務器上的總頁數與本地頁數做對比,若大于或等于(為什么不是等于,因為Mac系統會在文件夾里生成.DS_store文件。。)則說明該部漫畫已爬取完成,不再重新做處理。
總之就是避免重新運行程序時避免重復數據的爬取。處理在Demo代碼里面,已加上注釋說明。
<h3>最后。</h3>
本文只是展示了網絡爬蟲基本使用的大體思路而已,程序可以優化的地方還很多,如爬取時通過多線程,或用現成的爬蟲框架之類等等等。讀者請自行思考完善。當然,如果你想留郵箱的話。。不先去start下?:)
完。