不久前寫過一篇使用Scrapy框架寫的Crawlspider爬蟲筆記(五)- 關(guān)于Scrapy 全站遍歷Crawlspider翠语,本次我再次沿用上次的網(wǎng)站實現(xiàn)全站爬蟲韵卤,希望目標(biāo)網(wǎng)址的小伙伴原諒我~~~
目標(biāo)站點:www.cuiqingcai.com
代碼已經(jīng)上存到github下載
代碼已經(jīng)有詳細(xì)的注釋,這里附上流程圖和部分代碼解析~~
主要用到的庫和技術(shù)
import urllib2
from collections import deque # deque是為了高效實現(xiàn)插入和刪除操作的雙向列表
import httplib
from lxml import etree
from pybloom import BloomFilter
- urllib2:偽造請求
- deque:雙向列表着撩,存儲待下載的URL
- httplib:生成md5值
- BloomFilter:用于URL過濾
- etree:獲取頁面诅福,結(jié)合xpath過濾頁面中的URL
偽流程圖
首先是紫色部分,就是主要流程:通過主流程來控制整個爬取的過程拖叙。
然后氓润,主要流程里面有三個偽小流程【初始化流程__ini__】、【獲取URL流程getqueneURL】和【爬取流程getPageContent】薯鳍。
重點代碼說明
【初始化流程__ini__】
- 斷點續(xù)傳邏輯:通過將下載過的md5和url記錄到文件中的方式咖气,在每次執(zhí)行腳本前分析已記錄的md5值的方式來實現(xiàn)斷點續(xù)傳
self.md5_file = open(self.md5_file_name, 'r') # 只讀方式打開md5的文件
self.md5_lists = self.md5_file.readlines() # 將文件的內(nèi)容以列表的方式讀取出來
self.md5_file.close() # 關(guān)閉文件
for md5_item in self.md5_lists: # md5_item 的格式是"7e9229e7650b1f5b58c90773433ae2bc\r\n"
self.download_bf.add(md5_item[:-2]) # 將去掉回車換行符的md5寫入BloomFilter對象當(dāng)中
【獲取URL流程getqueneURL】
- deque雙向列表:通過popleft()高效讀取URL爬取。(這里有個GIL鎖挖滤,想了解可以自己深入了解下~~~)
# 用于記錄爬取URL的隊列(先進(jìn)先出)
now_queue = deque() # 爬取隊列
bak_queue = deque() # 備用隊列(爬取隊列為空的時候置換)
···
url = self.now_queue.popleft() # 從左邊進(jìn)行獲取隊列內(nèi)容
- try except:隊列為空的時候崩溪,增加深度&置換隊列
try:
url = self.now_queue.popleft() # 從左邊進(jìn)行獲取隊列內(nèi)容
return url
except IndexError:
self.now_level += 1 # 深度加一
if self.now_level == self.max_level: # 如果深度與設(shè)定的最大深度相等,停止爬蟲返回None
return None
if len(self.bak_queue) == 0: # 如果備用隊列長度為0斩松,停止爬蟲返回None
return None
self.now_queue = self.bak_queue # 將備用隊列傳遞給爬取隊列
self.bak_queue = deque() # 重置備用隊列
return self.getQueneURL() # 繼續(xù)執(zhí)行dequeuUrl方法伶唯,直到獲取到URL或者None
【爬取流程getPageContent】
- md5:__init__流程中的斷點續(xù)傳和過濾URL
# 計算md5的值并將md5和寫入到文件中
dumd5 = hashlib.md5(now_url).hexdigest() # 生成md5值
self.md5_lists.append(dumd5) # 將md5加入到md5的列表中
self.md5_file.write(dumd5 + '\r\n') # 將md5寫入文件
self.url_file.write(now_url + '\r\n') # 將url寫入文件
self.download_bf.add(dumd5) # 將md5加入到BloomFilter對象當(dāng)中
num_downloaded_pages += 1 # 用于統(tǒng)計當(dāng)前下載頁面的總數(shù)
- xpath:獲取當(dāng)前文件中所有的URL
html = etree.HTML(html_page.lower().decode('utf-8'))
hrefs = html.xpath(u"http://a")
for href in hrefs:
# 用于處理xpath抓取到的href,獲取有用的
try:
if 'href' in href.attrib:
val = href.attrib['href']
if val.find('javascript:') != -1: # 過濾掉類似"javascript:void(0)"
continue
if val.startswith('http://') is False: # 給"/mdd/calendar/"做拼接
if val.startswith('/'):
val = 'http://cuiqingcai.com/{0}'.format(val)
else:
continue
if val[-1] == '/': # 過濾掉末尾的/
val = val[0:-1]
- BloomFilter:過濾URL后入列
if hashlib.md5(val).hexdigest() not in self.download_bf:
self.bak_queue.append(val)
結(jié)果展示
(占位---待繼續(xù)更新)
代碼不夠200行惧盹,這里附上所有代碼
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
# @Time : 2017/6/9 19:23
# @Author : Spareribs
# @File : cuiqingcai_crawl.py
# @Notice : 這是使用寬度優(yōu)先算法BSF實現(xiàn)的全站爬取的爬蟲
詳解1:我們已經(jīng)將md5和URL記錄到md5.txt和url.txt中乳幸,但是我們暫時不用url.txt,我們只需要將md5的值讀取到用于做判斷邏輯的BloomFilter對象當(dāng)中即可
"""
import os
import time
import urllib2
from collections import deque # deque是為了高效實現(xiàn)插入和刪除操作的雙向列表
import httplib
import hashlib
from lxml import etree
from pybloom import BloomFilter
num_downloaded_pages = 0
class CuiQingCaiBSF():
"""
這是使用寬度優(yōu)先算法BSF實現(xiàn)的全站爬取的爬蟲類钧椰,通過max_level來自定義抓取的深度
"""
# 定義請求的頭部(目標(biāo)網(wǎng)站沒有做太多的安全措施粹断,所以原諒我)
request_headers = {
'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36",
}
# BSF寬度優(yōu)先算法的深度標(biāo)記
now_level = 0 # 初始深度
max_level = 2 # 爬取深度
# 記錄文件(URL和計算得到的md5)
dir_name = 'cuiqingcai/'
if not os.path.exists(dir_name): # 檢查用于存儲網(wǎng)頁文件夾是否存在,不存在則創(chuàng)建
os.makedirs(dir_name)
md5_file_name = dir_name + "md5.txt" # 記錄已經(jīng)下載的md5的值
url_file_name = dir_name + "url.txt" # 記錄已經(jīng)下載的URL
# 用于記錄爬取URL的隊列(先進(jìn)先出)
now_queue = deque() # 爬取隊列
bak_queue = deque() # 備用隊列(爬取隊列為空的時候置換)
# 定義一個BloomFilter對象嫡霞,用于做URL去重使用
download_bf = BloomFilter(1024 * 1024 * 16, 0.01)
# 定義一個存放md5值的列表
md5_lists = []
def __init__(self, begin_url):
"""
初始化處理瓶埋,主要是斷點續(xù)傳的邏輯
"""
self.root_url = begin_url # 將初始的URL傳入
self.now_queue.append(begin_url) # 將首個URL加入爬取隊列now_queue
self.url_file = open(self.url_file_name, 'a+') # 將首個url寫入url記錄文件
# 用于處理斷點續(xù)傳邏輯(詳細(xì)請看-->詳解一)
try:
self.md5_file = open(self.md5_file_name, 'r') # 只讀方式打開md5的文件
self.md5_lists = self.md5_file.readlines() # 將文件的內(nèi)容以列表的方式讀取出來
self.md5_file.close() # 關(guān)閉文件
for md5_item in self.md5_lists: # md5_item 的格式是"7e9229e7650b1f5b58c90773433ae2bc\r\n"
self.download_bf.add(md5_item[:-2]) # 將去掉回車換行符的md5寫入BloomFilter對象當(dāng)中
except IOError:
print "【Error】{0} - File not found".format(self.md5_file_name)
finally:
self.md5_file = open(self.md5_file_name, 'a+') # 增加編輯方式打開md5的文件
# def enqueueUrl(self, url):
# self.bak_queue.append(url) # 將獲取到的url加入到備用隊列當(dāng)中
def getQueneURL(self):
"""
爬取隊列為空的時候,將備用隊列置換到爬取隊列
"""
try:
url = self.now_queue.popleft() # 從左邊進(jìn)行獲取隊列內(nèi)容
return url
except IndexError:
self.now_level += 1 # 深度加一
if self.now_level == self.max_level: # 如果深度與設(shè)定的最大深度相等秒际,停止爬蟲返回None
return None
if len(self.bak_queue) == 0: # 如果備用隊列長度為0悬赏,停止爬蟲返回None
return None
self.now_queue = self.bak_queue # 將備用隊列傳遞給爬取隊列
self.bak_queue = deque() # 重置備用隊列
return self.getQueneURL() # 繼續(xù)執(zhí)行dequeuUrl方法,直到獲取到URL或者None
def getPageContent(self, now_url):
"""
下載當(dāng)前爬取頁面娄徊,
"""
global filename, num_downloaded_pages
print "【Download】正在下載網(wǎng)址 {0} 當(dāng)前深度為{1}".format(now_url, self.now_level)
try:
# 使用urllib庫請求now_url地址闽颇,將頁面通過read方法讀取下來
req = urllib2.Request(now_url, headers=self.request_headers)
response = urllib2.urlopen(req)
html_page = response.read()
filename = now_url[7:].replace('/', '_') # 處理URL信息,去掉"http://"寄锐,將/替換成_
# 將獲取到的頁面寫入到文件中
fo = open("{0}{1}.html".format(self.dir_name, filename), 'wb+')
fo.write(html_page)
fo.close()
# 處理各種異常情況
except urllib2.HTTPError, Arguments:
print "【Error】:{0}\n".format(Arguments)
return
except httplib.BadStatusLine:
print "【Error】:{0}\n".format('BadStatusLine')
return
except IOError:
print "【Error】:IOError {0}\n".format(filename)
return
except Exception, Arguments:
print "【Error】:{0}\n".format(Arguments)
return
# 計算md5的值并將md5和寫入到文件中
dumd5 = hashlib.md5(now_url).hexdigest() # 生成md5值
self.md5_lists.append(dumd5) # 將md5加入到md5的列表中
self.md5_file.write(dumd5 + '\r\n') # 將md5寫入文件
self.url_file.write(now_url + '\r\n') # 將url寫入文件
self.download_bf.add(dumd5) # 將md5加入到BloomFilter對象當(dāng)中
num_downloaded_pages += 1 # 用于統(tǒng)計當(dāng)前下載頁面的總數(shù)
# 解析頁面兵多,獲取當(dāng)前頁面中所有的URL
try:
html = etree.HTML(html_page.lower().decode('utf-8'))
hrefs = html.xpath(u"http://a")
for href in hrefs:
# 用于處理xpath抓取到的href尖啡,獲取有用的
try:
if 'href' in href.attrib:
val = href.attrib['href']
if val.find('javascript:') != -1: # 過濾掉類似"javascript:void(0)"
continue
if val.startswith('http://') is False: # 給"/mdd/calendar/"做拼接
if val.startswith('/'):
val = 'http://cuiqingcai.com/{0}'.format(val)
else:
continue
if val[-1] == '/': # 過濾掉末尾的/
val = val[0:-1]
# 判斷如果這個URL沒有在BloomFilter中就加入BloomFilter的隊列
if hashlib.md5(val).hexdigest() not in self.download_bf:
self.bak_queue.append(val)
else:
print '【Skip】已經(jīng)爬取 {0} 跳過'.format(val)
except ValueError:
continue
except UnicodeDecodeError: # 處理utf-8編碼無法解析的異常情況
pass
def start_crawl(self):
"""
啟動腳本的主程序
"""
while True:
# time.sleep(10)
url = self.getQueneURL()
if url is None:
break
self.getPageContent(url)
print "爬取隊列剩余URL數(shù)量為:{0},備用隊列剩余URL數(shù)量為:{1}".format(len(self.now_queue), len(self.bak_queue))
# 最后關(guān)閉打開的md5和rul文件
self.md5_file.close()
self.url_file.close()
if __name__ == "__main__":
print '【Begin】---------------------------------------------------------------'
start_time = time.time()
CuiQingCaiBSF("http://cuiqingcai.com/").start_crawl()
print '【End】下載了 {0} 個頁面剩膘,花費時間 {1:.2f} 秒'.format(num_downloaded_pages, time.time() - start_time)
以上都是我的個人觀點衅斩,如果有不對,或者有更好的方法怠褐,歡迎留言指正~~~