标签:
在cnblogs也混了许久,不过碍于平日工作太忙,一篇随笔也没有写过。最近经常感觉到自己曾经积累过的经验逐步的丢失,于是开通了博客,主要是记录一下自己在业余时间里玩的一些东西。
言归正传。某次在在某高校网站闲逛,看到了一些有趣的东西想要保存起来,但是却分散在各个页面,难以下手。使用baidu,google却有无法避免的搜索到此站点之外的内容。于是就想如果有一个爬虫,可以抓取指定域名的某些感兴趣的内容,不是很好。在网上简单搜索了一下,简单的都不满意,功能强大的又太复杂,就想自己写一个。
一个爬虫最重要的部分可能就是如何抓取HTML页面了,python中使用urllib库可以轻松的实现html页面的抓取,再使用正则表达式或者HTMLParser库找出自己感兴趣的部分做进一步处理。下面是一个转来的小例子(出处为http://www.cnblogs.com/fnng/p/3576154.html,在此深表感谢)
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文件,原文中后面还有一段保存在本地的代码,这里就不转了。
不过,这仅仅是实现了指定页面抓取,简单搜索的爬虫例子基本都是到此为止,其实是没有真正“爬”起来。
所谓的爬虫,最重要的功能是在整个互联网上搜索任何的页面,只要给定了一个(或多个)线索。这里面涉及到的问题主要是:
1, 解析HTML页面找出里面的url和感兴趣的东西(见上文)
2, 记住目前已经访问过的页面(后面再遇到就直接跳过),同时逐个的访问(1)中新发现的url(类似递归)
3, 达到某种条件之后停止搜索,例如只搜索500个url。
问题1大体上已经解决,问题3相对容易。对于问题2,本质上其实就是一个图的遍历,整个互联网可以看作一张复杂的图,每个url是一个结点,所谓爬虫,就是按照某种规则对图进行遍历而已。我们知道图的遍历有深度优先和广度优先两种主要算法,这里我们选择广度优先,主要原因是,根据观察,一般来说,最重要信息(最关心的)往往和线索离的很近,而使用深度优先,则容易走上歧途。
对于一个爬虫,页面的解析可以分成两部分,一个是对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。主要也是为了测试,否则运行速度似蜗牛。
先给一个使用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
这里可以看出,使用强制继承的方式的坏处,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文件。
标签:
原文地址:http://www.cnblogs.com/cuiluo/p/4188195.html