也寫一個簡單的網絡爬蟲

引子

在cnblogs也混了許久,不過礙於平日工做太忙,一篇隨筆也沒有寫過。最近常常感受到本身曾經積累過的經驗逐步的丟失,因而開通了博客,主要是記錄一下本身在業餘時間裏玩的一些東西。php

緣起

言歸正傳。某次在在某高校網站閒逛,看到了一些有趣的東西想要保存起來,可是卻分散在各個頁面,難如下手。使用baidu,google卻有沒法避免的搜索到此站點以外的內容。因而就想若是有一個爬蟲,能夠抓取指定域名的某些感興趣的內容。在網上簡單搜索了一下,簡單的都不滿意,功能強大的又太複雜,就想本身寫一個。html

抓取HTML頁面

一個爬蟲最重要的部分可能就是如何抓取HTML頁面了,python中使用urllib庫能夠輕鬆的實現html頁面的抓取,再使用正則表達式或者HTMLParser庫找出本身感興趣的部分作進一步處理。下面是一個轉來的小例子(出處爲http://www.cnblogs.com/fnng/p/3576154.html,在此深表感謝)python

import re
import urllib

def getHtml(url):
    page = urllib.urlopen(url)
    html = page.read()
    return html

def getImg(html):
    reg = r'src="(.+?\.jpg)" pic_ext'
    imgre = re.compile(reg)
    imglist = re.findall(imgre,html)
    return imglist      
   
html = getHtml("http://tieba.baidu.com/p/2460150866")
print getImg(html)

此代碼抓取了頁面中的jpg文件,原文中後面還有一段保存在本地的代碼,這裏就不轉了。git

不過,這僅僅是實現了指定頁面抓取,簡單搜索的爬蟲例子基本都是到此爲止,實際上是沒有真正「爬」起來。正則表達式

待解決的問題

所謂的爬蟲,最重要的功能是在整個互聯網上搜索任何的頁面,只要給定了一個(或多個)線索。這裏面涉及到的問題主要是:算法

1, 解析HTML頁面找出裏面的url和感興趣的東西(見上文)app

2, 記住目前已經訪問過的頁面(後面再遇到就直接跳過),同時逐個的訪問(1)中新發現的url(相似遞歸)jsp

3, 達到某種條件以後中止搜索,例如只搜索500個url。ide

問題1大致上已經解決,問題3相對容易。對於問題2,本質上其實就是一個圖的遍歷,整個互聯網能夠看做一張複雜的圖,每一個url是一個結點,所謂爬蟲,就是按照某種規則對圖進行遍歷而已。咱們知道圖的遍歷有深度優先和廣度優先兩種主要算法,這裏咱們選擇廣度優先,主要緣由是,根據觀察,通常來講,最重要信息(最關心的)每每和線索離的很近,而使用深度優先,則容易走上歧途。post

頁面解析

對於一個爬蟲,頁面的解析能夠分紅兩部分,一個是對url的解析,決定了後面往哪裏「爬」,一個就是對用戶自己關心的內容的解析。使用正則表達式是很好的選擇,惋惜我實在不精於此道(須要進一步增強,hee),試驗了幾回都不滿意,而網上也沒有搜索到正好能夠解決問題的。因而決定使用HTMLParser庫。這個庫自己已經對解析作了封裝,提供了一組虛方法,只要繼承並實現了這些方法,就能夠很好的解析。

#coding=utf-8

from html.parser import HTMLParser

class UrlParser(HTMLParser):
    def __init__(self, 
                 filtrules = {'postfix' : ['.', 'html', 'shtml', 'asp', 'php', 'jsp', 'com', 'cn', 'net', 'org', 'edu', 'gov']}):
        HTMLParser.__init__(self)
        self.__urls = list()
        self.__filtrules = filtrules
        
    def setfilterrules(self, rules):
        self.__filtrules = rules
 
    def handle_starttag(self, tag, attrs):
        if(tag == 'a' or tag == 'frame'):
            self.__parse_href_attr(attrs)              
    
    def geturls(self):
        list(set(self.__urls))
        return list(set(self.__urls))
    
    def __parse_href_attr(self, attrs):
        for attr in attrs:
                if(attr[0] == 'href' and self.__match_url(attr[1])):
                    self.__urls.append(attr[1])
    
    def __match_url(self, text):
        return FilterManager(self.__filtrules).matchpostfix('postfix', text)

其中 def handle_starttag(self, tag, attrs): 即爲從基類繼承來的方法,用戶處理開始標籤,因爲這個類是爲了解析出url的,因此這裏咱們只關心'a'標籤和‘frame’標籤,而在屬性中,只關心‘href’。可是按照這樣的規則,許多本不是真正網址的url也會被記錄下來。因此須要有一個過濾規則。

過濾規則

 因爲玩不轉正則表達式,就本身寫了一個過濾器和一套過濾規則,主要是過濾前綴/後綴/數據的,先看代碼:

class FilterManager():
    def __init__(self, rules):
        self.__rules = rules
    
    def __str__(self):
        return self.__rules.__str__()
    
    def getrules(self):
        return self.__rules
    
    def updaterules(self, newrules):
        self.__rules.update(newrules)
            
    def removerules(self, delkeys):
        for key in delkeys:
            del(self.__rules[key])
            
    def clearrules(self):
        self.__rules.clear()
    
    def matchprefix(self, key, source):
        return self.__match(key, source, self.__handle_match_prefix)
    
    def matchpostfix(self, key, source):
        return self.__match(key, source, self.__handle_match_postfix)
    
    def matchdata(self, key, source):
        return self.__match(key, source, self.__handle_match_data)
    
    def __match(self, key, source, handle_match):
        try:
            if self.__rules.get(key):
                rule = self.__rules[key]
                return handle_match(rule, source)
        except:
            print('rules format error.')
        return True 
    
    def __handle_match_prefix(self, rule, source):
        return source.split(rule[0])[0] in rule[1:]
    
    def __handle_match_postfix(self, rule, source):
        return source.split(rule[0])[-1] in rule[1:]
    
    def __handle_match_data(self, rule, source):
        if rule[0] == '&':
            for word in rule[1:]:
                if not word in source:
                    return False
            return True
        else:
            for word in rule[1:]:
                if word in source:
                    return True
            return False

這裏面rules是一個字典,裏面是既定的過濾規則,而從中分析中傳入的數據是否符合篩選條件。我開始想作一個統一的規則格式,能夠不去區分前綴仍是後綴等,可是發現這樣規則就是很複雜,而對咱們這個簡單的爬蟲來講,這三個方法也基本夠用了,待後面發現須要擴充,再修改吧。

過濾方法的大致規則爲:

1,關鍵字,目前支持三個'prefix' , 'postfix', 'data' 分別代報要過濾的是前綴,後綴仍是數據

2, 分隔符/提示符, 表示如何分隔傳入的數據,或者對數據進行如何搜索

3, 匹配符,即傳入的數據中是否包含這些預約義的字段。

例如:rule = {'prefix' : ['://', 'http', 'https'], 'postfix' : ['.', 'jpg', 'png'], 'data' : ['&','Python', 'new']}

表示,此規則能夠過濾出前綴爲http, https的url, 後綴能夠是jpg,png的url,或者包含Python 且包含 new的文字內容。

這段代碼後面過濾data的部分寫的很不滿意,感受重複不少,一時還沒想到好方法消除,留做後面看吧。

FilterManager的測試用例,有助於理解這個我人爲規定的複雜東西。詳見末尾。

爬起來

終於到這一步了,咱們使用一個dic保存已經訪問過的url(選擇字典是由於感受其是使用哈希表實現的,訪問速度快,不過沒有考證),以後進行url解析。

class Spider(object):
    def __init__(self):
        self.__todocollection = list()
        self.__visitedtable = dict()
        self.__urlparser = UrlParser()
        self.__maxvisitedurls = 15
    
    def setfiltrules(self, rules):
        self.__urlparser.setfilterrules(rules)    
        
    def feed(self, root):
        self.__todocollection.append(root)
        self.__run()
        
    # Overridable -- handle do your own business
    def handle_do(self, htmlcode):
        pass
     
    def setmaxvisitedurls(self, maxvisitedurls):
        self.__maxvisitedurls = maxvisitedurls
                   
    def getvisitedurls(self):
        return self.__visitedtable.keys()
    
    def __run(self):
        maxcouter = 0
        while len(self.__todocollection) > 0 and maxcouter < self.__maxvisitedurls:
            if self.__try_deal_with_one_url(self.__todocollection.pop(0)):
                maxcouter += 1
    
    def __try_deal_with_one_url(self, url):
        if not self.__visitedtable.get(url):
            self.__parse_page(url)
            self.__visitedtable[url] = True
            self.__todocollection += self.__urlparser.geturls()
            return True
        return False
        
    def __parse_page(self, url):
        text = self.__get_html_text(url)
        self.handle_do(text)
        self.__urlparser.feed(text)
        
    def __get_html_text(self, url):
        filtermanager = FilterManager({'prefix' : ['://', 'http', 'https']})
        if filtermanager.matchprefix('prefix', url):
            return self.__get_html_text_from_net(url)
        else:
            return self.__get_html_text_from_local(url)  
     
    def __get_html_text_from_net(self, url):
        try:
            page = urllib.request.urlopen(url)
        except:
            print("url request error, please check your network.")
            return str()
        
        text = page.read()
        encoding = chardet.detect(text)['encoding']      
        return text.decode(encoding, 'ignore') 
    
    def __get_html_text_from_local(self, filepath):
        try:
            page = open(filepath)
        except:
            print("no such file, please check your file system.")
            return str()
        
        text = page.read()
        page.close()
        return text     

這裏面有幾個問題:

1, def handle_do(self, htmlcode): 方法是爲後面擴展使用,能夠override它解析本身關心的內容。這裏面其實有點小體大做,彷佛不須要這樣複雜,在Parser上作作文章應該能夠解決大部分問題,不過仍是留下了。

2,一個很嚴重的問題就是編解碼。不一樣的html頁面的編碼方式可能不一樣,主流不過是utf-8,gb2312等,可是咱們沒法預先知道。這裏使用了python庫chardet,自動識別編碼格式。這個庫須要本身下載安裝,這裏不細說了。

3, 這裏作了一個處理,若是被解析的url不符合過濾規則,則認爲是本地文件,在本地搜索,這個主要是爲了測試。

4, 搜索的中止條件默認爲訪問15個url。主要也是爲了測試,不然運行速度似蝸牛。

5,一個很嚴重的缺陷是沒有處理http的錯誤碼,目前是僅僅忽略掉。這樣有些返回301錯誤的頁面,本應該在報頭中找到新的地址繼續訪問的,如今這樣的處理,就被錯過了。

一個例子

先給一個使用Spider的簡單例子,獲取到全部被訪問的html頁面的title。

class TitleSpider(Spider):
    def __init__(self):
        Spider.__init__(self);
        self.__titleparser = TitleParser()
    
    def setfiltrules(self, rules):
        self.__titleparser.setfilterrules(rules)     
        
    def handle_do(self, htmlcode):
        self.__titleparser.feed(htmlcode)
    
    def gettitles(self):
        return self.__titleparser.gettitles()
                                              
class TitleParser(HTMLParser):
    def __init__(self, filtrules = {}):
        HTMLParser.__init__(self)
        self.__istitle = False
        self.__titles = list()
        self.__filtrules = filtrules;
    
    def setfilterrules(self, rules):
        self.__filtrules = rules
        
    def handle_starttag(self, tag, attrs):
        if(tag == 'title'):
            self.__istitle = True
            
    def handle_data(self, data):
        if self.__istitle and self.__match_data(data):
            self.__titles.append(data)
        self.__istitle = False
            
    def gettitles(self):
        return self.__titles      
    
    def __match_data(self, data):
        return FilterManager(self.__filtrules).matchdata('data', data)

這裏TitleSpider 繼承了Spider,並override handle_do方法,TitleParser則負責解析‘title’ 標籤。

另外一個略有點用處的例子

這個例子是下載訪問到的html頁面中的jpg文件。

class ImgSpider(Spider):
    def __init__(self):
        Spider.__init__(self);
        self.__imgparser = ImgParser()
        
    def handle_do(self, htmlcode):
        self.__imgparser.feed(htmlcode)
  
class ImgParser(HTMLParser):
    def __init__(self):
        HTMLParser.__init__(self)
        self.imgnameindex = 0
        
    def handle_starttag(self, tag, attrs):
        if(tag == 'img'):
            self.__parse_attrs(attrs)   
                    
    def __parse_attrs(self, attrs):
        for attr in attrs:
            self.__parse_one_attr(attr)
    
    def __parse_one_attr(self, attr):
        filtermanager = FilterManager({'postfix' : ['.', 'jpg']})
        if(attr[0] == 'src' and filtermanager.matchpostfix('postfix', attr[1])):
            self.__download_jpg(attr[1])
                           
    def __download_jpg(self, url):
        try:
            urllib.request.urlretrieve(url,'%s.jpg' % self.imgnameindex)
            self.imgnameindex += 1
        except:   
            pass

這個例子寫的很簡單,也有些問題,例如沒有過濾掉相同的jpg文件,這個能夠參考解析url的機制不難解決。並且這裏能夠看出,使用強制繼承的方式的壞處,ImgSpider類基本都是廢話,基類Spider若是支持直接傳入ImgParser會很好。不過此刻忽然沒了興致,留做之後重構吧。

main

if __name__ == '__main__': 
    #spider = TitleSpider()
    #spider.feed("http://mil.sohu.com/s2014/jjjs/index.shtml")
    #print(spider.gettitles())
    
    spider = ImgSpider()
    spider.feed("http://gaoqing.la")
    print(spider.getvisitedurls())

代碼和測試用例

代碼和測試用例託管在 https://git.oschina.net/augustus/MiniSpider.git

可使用git clone下來

用例寫的簡單且不正交,只是須要的時候寫了些,同時我刪除了.project文件。

相關文章
相關標籤/搜索