最近我忽然對網絡爬蟲開竅了,真正作起來的時候發現並不算太難,都怪我之前有點懶,不過近兩年編寫了一些程序,手感積累了一些確定也是因素,總之,仍是慚愧了。好了,說正題,我把這兩天作爬蟲的過程當中遇到的問題總結一下:html
需求:作一個爬蟲,爬取一個網站上全部的圖片(只爬大圖,小圖標就略過)python
思路:一、獲取網站入口,這個入口網頁上有不少圖片集合入口,進入這些圖片集合就能看到圖片連接了,因此爬取的深度爲2,比較簡單;二、各個子圖片集合內所包含的圖片連接有兩種形式:一種是絕對圖片路徑(直接下載便可),另外一種的相對圖片路徑(須要自行拼接當前網頁路徑)。總之這些子圖集合的表現形式簡單,沒有考慮更復雜的狀況。三、在爬取的過程當中保存已成功爬取的路徑,防止每次爬取都執行重複的任務,固然,當每次啓動時要首先加載該歷史數據庫,剩下的就是細節了。正則表達式
快速連接:數據庫
2.1 日誌系統瀏覽器
2.3 異常處理app
2.4 網頁自動編碼判斷ide
1、所有代碼
直接先來代碼,再詳細說優化的過程和內容吧:
1 __author__ = 'KLH' 2 # -*- coding:utf-8 -*- 3 4 import urllib 5 import urllib2 6 import chardet 7 import re 8 import os 9 import time 10 from myLogger import * 11 12 # 網絡蜘蛛 13 class Spider: 14 15 # 類初始化 16 def __init__(self): 17 self.contentFolder = u"抓取內容" 18 self.dbName = "url.db" 19 self.createFolder(self.contentFolder) 20 self.urlDB = set() 21 22 # 獲取URL數據庫以獲取爬過的網頁地址 23 def loadDatabase(self): 24 isExists = os.path.exists(self.dbName) 25 if not isExists: 26 logging.info(u"建立URL數據庫文件:'" + self.dbName + u"'") 27 f = open(self.dbName, 'w+') 28 f.write("#Create time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + '\n') 29 f.close() 30 return 31 db = open(self.dbName, 'r') 32 for line in db.readlines(): 33 if not line.startswith('#'): 34 self.urlDB.add(line.strip('\n')) 35 db.close() 36 logging.info(u"URL數據庫加載完成!") 37 38 # 追加數據庫文件 39 def writeToDatabase(self, url): 40 db = open(self.dbName, 'a') 41 db.write(url + '\n') 42 db.close() 43 44 # 處理路徑名稱中的空格字符 45 def getPathName(self, pathName): 46 newName = "" 47 subName = pathName.split() 48 i = 0 49 while i < len(subName) - 1: 50 newName = newName + subName[i] 51 i = i + 1 52 return newName 53 54 # 獲取索引頁面的內容 55 def getPage(self, pageURL, second): 56 try: 57 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'} 58 request = urllib2.Request(pageURL, headers = headers) 59 response = urllib2.urlopen(request, timeout = second) 60 data = response.read() 61 response.close() 62 return data.decode('gbk'), True 63 except urllib2.HTTPError,e: #HTTPError必須排在URLError的前面 64 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read()) 65 return "", False 66 except urllib2.URLError, e: 67 logging.error("URLError reason:" + str(e.reason) + " - " + str(e)) 68 return "", False 69 except Exception, e: 70 logging.error(u"獲取網頁失敗:" + str(e)) 71 return "", False 72 73 # 獲取索引界面全部子頁面信息,list格式 74 def getContents(self, pageURL, second): 75 contents = [] 76 page, succeed = self.getPage(pageURL, second) 77 if succeed: 78 # 這裏的正則表達式很重要,決定了第一步的抓取內容: 79 pattern = re.compile('<tr>.*?<a href="(.*?)".*?<b>(.*?)</b>.*?</tr>',re.S) 80 items = re.findall(pattern,page) 81 for item in items: 82 contents.append([item[0],item[1]]) 83 contents.sort() 84 return contents 85 86 # 獲取頁面全部圖片 87 def getAllImgURL(self, infoURL): 88 images = [] 89 succeed = True 90 try: 91 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'} 92 request = urllib2.Request(infoURL, headers = headers) 93 data = urllib2.urlopen(request).read() 94 chardet1 = chardet.detect(data) # 自動判斷網頁編碼 95 page = data.decode(str(chardet1['encoding'])) 96 97 # 第一種解碼格式: 98 pattern = re.compile('<option value="(.*?)">(.*?)</option>') 99 items = re.findall(pattern, page) 100 # item[0]爲圖片URL尾部,item[1]爲圖片名稱 101 for item in items: 102 if item.startswith('http://'): 103 imageURL = item[0] 104 if imageURL in self.urlDB: 105 logging.info(u"得到圖片URL(曾被訪問,跳過):" + imageURL) 106 else: 107 logging.info(u"得到圖片URL:" + imageURL) 108 images.append(imageURL) 109 else: 110 imageURL = infoURL + item[0] 111 if imageURL in self.urlDB: 112 logging.info(u"得到圖片URL(曾被訪問,跳過):" + imageURL) 113 else: 114 logging.info(u"得到圖片URL:" + imageURL) 115 images.append(imageURL) 116 117 # 第二種解碼格式 118 pattern = re.compile('<IMG src="(.*?)".*?>') 119 items = re.findall(pattern, page) 120 # item爲圖片URL 121 for item in items: 122 if item.startswith('http://'): 123 if item in self.urlDB: 124 logging.info(u"得到圖片URL(曾被訪問,跳過):" + item) 125 else: 126 logging.info(u"得到圖片URL:" + item) 127 images.append(item) 128 129 except Exception, e: 130 logging.warning(u"在獲取子路徑圖片列表時出現異常:" + str(e)) 131 succeed = False 132 return images, succeed 133 134 # 保存全部圖片 135 def saveImgs(self, images, name): 136 logging.info(u'發現"' + name + u'"共有' + str(len(images)) + u"張照片") 137 allSucceed = True 138 for imageURL in images: 139 splitPath = imageURL.split('/') 140 fTail = splitPath.pop() 141 fileName = name + "/" + fTail 142 logging.info(u"開始準備保存圖片(超時設置:120秒):" + imageURL) 143 startTime = time.time() 144 succeed = self.saveImg(imageURL, fileName, 120) 145 spanTime = time.time() - startTime 146 if succeed: 147 logging.info(u"保存圖片完成(耗時:" + str(spanTime) + u"秒):" + fileName) 148 # 保存文件存儲記錄 149 self.urlDB.add(imageURL) 150 self.writeToDatabase(imageURL) 151 else: 152 logging.warning(u"保存圖片失敗(耗時:" + str(spanTime) + u"秒):" + imageURL) 153 allSucceed = False 154 # 爲了防止網站封殺,這裏暫停1秒 155 time.sleep(1) 156 return allSucceed 157 158 # 傳入圖片地址,文件名,超時時間,保存單張圖片 159 def saveImg(self, imageURL, fileName, second): 160 try: 161 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'} 162 request = urllib2.Request(imageURL, headers = headers) 163 u = urllib2.urlopen(request, timeout = second) 164 data = u.read() 165 f = open(fileName, 'wb') 166 f.write(data) 167 f.close() 168 u.close() 169 return True 170 except urllib2.HTTPError,e: #HTTPError必須排在URLError的前面 171 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read()) 172 return False 173 except urllib2.URLError, e: 174 logging.error("URLError reason:" + str(e.reason) + " - " + str(e)) 175 return False 176 except Exception, e: 177 logging.error(u"保存圖片失敗:" + str(e)) 178 return False 179 180 # 建立新目錄 181 def createFolder(self, path): 182 path = path.strip() 183 # 判斷路徑是否存在 184 isExists=os.path.exists(path) 185 # 判斷結果 186 if not isExists: 187 # 若是不存在則建立目錄 188 logging.info(u"建立文件夾:'" + path + u"'") 189 # 建立目錄操做函數 190 os.makedirs(path) 191 return True 192 else: 193 # 若是目錄存在則不建立,並提示目錄已存在 194 logging.info(u"名爲'" + path + u"'的文件夾已經存在,跳過") 195 return False 196 197 # 獲取的首頁地址 198 def savePageInfo(self, pageURL): 199 logging.info(u"準備獲取網頁內容(超時設置:60秒):" + pageURL) 200 contents = self.getContents(pageURL, 60) 201 logging.info(u"網頁內容獲取完成,子路徑個數:" + str(len(contents))) 202 index = 1 203 for item in contents: 204 #(1)item[0]子路徑URL, item[1]子路徑名稱 205 folderURL = item[0] 206 folderName = self.contentFolder + '\\' + str(index) + "-" + self.getPathName(item[1]) 207 self.createFolder(folderName) 208 index = index + 1 209 210 #(2)判斷連接頭部合法性和重複性 211 if not folderURL.startswith('http://'): 212 folderURL = pageURL + folderURL 213 if folderURL in self.urlDB: 214 logging.info(u'"' + folderName + u'"的連接地址(已訪問,跳過)爲:' + folderURL) 215 continue 216 else: 217 logging.info(u'"' + folderName + u'"的連接地址爲:' + folderURL) 218 219 #(3)獲取圖片URL列表,成功則保存圖片 220 images, succeed = self.getAllImgURL(folderURL) 221 if succeed: 222 succeed = self.saveImgs(images, folderName) 223 if succeed: 224 self.urlDB.add(folderURL) 225 self.writeToDatabase(folderURL) 226 227 # 初始化系統日誌存儲 228 InitLogger() 229 # 傳入初始網頁地址,自動啓動爬取圖片: 230 spider = Spider() 231 spider.loadDatabase() 232 spider.savePageInfo('http://365.tw6000.com/xtu/') 233 logging.info(u"所有網頁內容爬取完成!程序退出。")
2、問題歷史
在上面的代碼中有很多的細節是優化解決過的,相關的知識點以下:
Python的日誌系統是至關的不錯,很是的方便,詳細的資料能夠參考Python官方文檔,或者上一篇博文也是提到過的:《Python中的日誌管理Logging模塊》,應用到我這個爬蟲這裏的代碼就是myLogger.py模塊了,用起來很方便:
1 __author__ = 'KLH' 2 # -*- coding:utf-8 -*- 3 4 import logging 5 import time 6 7 def InitLogger(): 8 logFileName = 'log_' + time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) + '.txt' 9 logging.basicConfig(level=logging.DEBUG, 10 format='[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s] - %(message)s', 11 filename=logFileName, 12 filemode='w') 13 14 # 定義一個StreamHandler將INFO級別以上的信息打印到控制檯 15 console = logging.StreamHandler() 16 console.setLevel(logging.INFO) 17 formatter = logging.Formatter('[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s] - %(message)s') 18 console.setFormatter(formatter) 19 logging.getLogger('').addHandler(console)
注意在爬蟲的代碼執行前調用一下該函數:
from myLogger import *
# 初始化系統日誌存儲 InitLogger()
日誌調用和打印的結果以下:
logging.info(u"準備獲取網頁內容(超時設置:60秒):" + pageURL) #打印結果以下: [2015-11-09 22:35:02,976][spider2.py:182][INFO] - 準備獲取網頁內容(超時設置:60秒):http://365.XXXXX.com/xtu/
這裏記錄URL的訪問歷史是爲了防止執行重複任務,在內存中保持一個Set就能知足需求,在磁盤上能夠簡單的保存成一個TXT文件,每一個URL保存成一行便可。因此這裏是邏輯順序應該是在初始化時建立一個Set用來保存訪問歷史,任務執行以前從數據庫中加載訪問歷史,若是是首次運行還沒有建立數據庫還須要進行一次建立操做。而後就好辦了,每次完成一個URL的訪問就保存一下訪問記錄。相關代碼以下:
1 # 類初始化 2 def __init__(self): 3 self.contentFolder = u"抓取內容" 4 self.dbName = "url.db" # 一、定義數據庫文件名 5 self.createFolder(self.contentFolder) # 二、建立內容存儲目錄 6 self.urlDB = set() # 三、建立內存數據庫 7 8 # 獲取URL數據庫以獲取爬過的網頁地址 9 def loadDatabase(self): 10 isExists = os.path.exists(self.dbName) # 四、首先判斷是不是首次運行,若是數據庫文件不存在則建立一下 11 if not isExists: 12 logging.info(u"建立URL數據庫文件:'" + self.dbName + u"'") 13 f = open(self.dbName, 'w+') 14 f.write("#Create time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + '\n') 15 f.close() 16 return 17 db = open(self.dbName, 'r') # 五、從磁盤中加載數據庫 18 for line in db.readlines(): 19 if not line.startswith('#'): 20 self.urlDB.add(line.strip('\n')) 21 db.close() 22 logging.info(u"URL數據庫加載完成!") 23 24 # 追加數據庫文件 25 def writeToDatabase(self, url): # 六、在系統運行過程當中,如需記錄日誌,追加日誌內容便可 26 db = open(self.dbName, 'a') 27 db.write(url + '\n') 28 db.close()
有了上面的代碼,在記錄日誌過程當中就很方便了:
succeed = self.saveImgs(images, folderName) if succeed: self.urlDB.add(folderURL) self.writeToDatabase(folderURL)
訪問網絡資源不可避免的會有不少異常狀況,要處理這些異常狀況才能穩定運行,Python的異常處理很簡單,請參考以下獲取網頁的代碼段:
1 # 獲取索引頁面的內容 2 def getPage(self, pageURL, second): 3 try: 4 request = urllib2.Request(pageURL) 5 response = urllib2.urlopen(request, timeout = second) 6 data = response.read() 7 return data.decode('gbk'), True 8 except urllib2.HTTPError,e: # HTTPError必須排在URLError的前面 9 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read()) 10 return "", False 11 except urllib2.URLError, e: 12 logging.error("URLError reason:" + str(e.reason) + " - " + str(e)) 13 return "", False 14 except Exception, e: # 其餘全部類型的異常 15 logging.error(u"獲取網頁失敗:" + str(e)) 16 return "", False # 這裏返回兩個值方便判斷
昨天在爬取網頁的過程當中忽然發現,有的頁面竟然說編碼不能經過utf-8進行解析,我看了看有的網頁確實不是utf-8編碼的,那怎麼辦呢?怎麼才能自動進行解碼?從網上能夠搜索到一個Python的開源庫很好用,叫作chardet,默認Python2.7是不帶的須要下載,好比我下載的是:chardet-2.3.0.tar.gz
有了這個壓縮包解壓出來cahrdet子文件夾拷貝到:C:\Python27\Lib\site-packages目錄下便可。下面看看用法實例:
1 import chardet 2 3 # 獲取頁面全部圖片 4 def getAllImgURL(self, infoURL): 5 images = [] 6 succeed = True 7 try: 8 data = urllib2.urlopen(infoURL).read() # 一、先獲取網頁內容 9 chardet1 = chardet.detect(data) # 二、再調用該模塊的方法自動判斷網頁編碼 10 page = data.decode(str(chardet1['encoding'])) # 三、注意,獲得的chardet1是一個字典相似於:{'confidence': 0.98999999999999999, 'encoding': 'GB2312'} 11 12 # 第一種解碼格式: 13 pattern = re.compile('<option value="(.*?)">(.*?)</option>') 14 items = re.findall(pattern, page) 15 # item[0]爲圖片URL尾部,item[1]爲圖片名稱 16 for item in items: 17 imageURL = infoURL + item[0] 18 if imageURL in self.urlDB: 19 logging.info(u"得到圖片URL(曾被訪問,跳過):" + imageURL) 20 else: 21 logging.info(u"得到圖片URL:" + imageURL) 22 images.append(imageURL) 23 24 # 第二種解碼格式 25 pattern = re.compile('<IMG src="(.*?)".*?>') 26 items = re.findall(pattern, page) 27 # item爲圖片URL 28 for item in items: 29 if item.startswith('http://'): # 四、這裏也注意一下,在這種網頁中相對路徑的圖片都是插圖之類的小圖片不須要下載,因此過濾掉了。 30 if item in self.urlDB: 31 logging.info(u"得到圖片URL(曾被訪問,跳過):" + item) 32 else: 33 logging.info(u"得到圖片URL:" + item) 34 images.append(item) 35 36 except Exception, e: 37 logging.warning(u"在獲取子路徑圖片列表時出現異常:" + str(e)) 38 succeed = False 39 return images, succeed
在今天的爬取過程當中我發現了一個問題,爬到後面的內容都出錯了,錯誤信息參見以下:
[2015-11-09 23:48:51,082][spider2.py:130][INFO] - 開始準備保存圖片(超時設置:300秒):http://xz1.XXXX.com/st/st-06/images/009.jpg [2015-11-09 23:48:55,095][spider2.py:160][ERROR] - 保存圖片失敗:[Errno 10054] [2015-11-09 23:48:55,096][spider2.py:140][WARNING] - 保存圖片失敗(耗時:4.01399993896秒):http://xz1.XXXX.com/st/st-06/images/009.jpg [2015-11-09 23:48:55,098][spider2.py:130][INFO] - 開始準備保存圖片(超時設置:300秒):http://xz1.XXXX.com/st/st-06/images/010.jpg [2015-11-09 23:48:56,576][spider2.py:160][ERROR] - 保存圖片失敗:[Errno 10054] [2015-11-09 23:48:56,578][spider2.py:140][WARNING] - 保存圖片失敗(耗時:1.48000001907秒):http://xz1.XXXX.com/st/st-06/images/010.jpg
能夠看到都在報這個錯誤代碼,網上一查,發現有不少同窗已經遇到過了,請參考知乎上的討論:http://www.zhihu.com/question/27248551
從網上討論的結果來看,能夠確定的是直接緣由在於「遠程主機主動關閉了當前連接」,而根本緣由則在於:網站啓用了反爬蟲的策略,也就是說個人爬蟲爬的過快而且被發現不是真正的瀏覽器瀏覽了。怎麼辦了?針對這兩個緣由逐個處理,一是假裝成瀏覽器,二是不要訪問的過快,三是每次訪問完成都關閉連接。相關代碼以下:
1 # 傳入圖片地址,文件名,超時時間,保存單張圖片 2 def saveImg(self, imageURL, fileName, second): 3 try: 4 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'} # 一、這裏構造一個瀏覽器的頭部 5 request = urllib2.Request(imageURL, headers = headers) 6 u = urllib2.urlopen(request, timeout = second) # 二、這裏的超時設置timeout不要太長 7 data = u.read() 8 f = open(fileName, 'wb') 9 f.write(data) 10 f.close() 11 u.close() # 三、注意這裏的連接要主動調用一下關閉 12 return True 13 except urllib2.HTTPError,e: #HTTPError必須排在URLError的前面 14 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read()) 15 return False 16 except urllib2.URLError, e: 17 logging.error("URLError reason:" + str(e.reason) + " - " + str(e)) 18 return False 19 except Exception, e: 20 logging.error(u"保存圖片失敗:" + str(e)) 21 return False # 四、注意調用完這個函數以後再time.sleep(1)一下,防止過快訪問被發現了,呵呵
使用正則表達式來匹配網頁中的字段確實是太費勁了。用BeautifulSoup就省力多了,功能很強大:
soup = BeautifulSoup(page_data, 'lxml') entries = soup.find_all(src=re.compile('.jpg'), border='0') # 找到全部字段爲src正則匹配的標籤內容,同時增長一個約束條件是border=‘0’
Beautiful Soup支持Python標準庫中的HTML解析器,還支持一些第三方的解析器,若是咱們不安裝它,則 Python 會使用 Python默認的解析器,lxml 解析器更增強大,速度更快,推薦安裝。