在cnblogs也混了許久,不過礙於平日工做太忙,一篇隨筆也沒有寫過。最近常常感受到本身曾經積累過的經驗逐步的丟失,因而開通了博客,主要是記錄一下本身在業餘時間裏玩的一些東西。php
言歸正傳。某次在在某高校網站閒逛,看到了一些有趣的東西想要保存起來,可是卻分散在各個頁面,難如下手。使用baidu,google卻有沒法避免的搜索到此站點以外的內容。因而就想若是有一個爬蟲,能夠抓取指定域名的某些感興趣的內容。在網上簡單搜索了一下,簡單的都不滿意,功能強大的又太複雜,就想本身寫一個。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會很好。不過此刻忽然沒了興致,留做之後重構吧。
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文件。