首页 > 代码库 > 也写一个简单的网络爬虫

也写一个简单的网络爬虫

引子

在cnblogs也混了许久,不过碍于平日工作太忙,一篇随笔也没有写过。最近经常感觉到自己曾经积累过的经验逐步的丢失,于是开通了博客,主要是记录一下自己在业余时间里玩的一些东西。

缘起

言归正传。某次在在某高校网站闲逛,看到了一些有趣的东西想要保存起来,但是却分散在各个页面,难以下手。使用baidu,google却有无法避免的搜索到此站点之外的内容。于是就想如果有一个爬虫,可以抓取指定域名的某些感兴趣的内容,不是很好。在网上简单搜索了一下,简单的都不满意,功能强大的又太复杂,就想自己写一个。

抓取HTML页面

一个爬虫最重要的部分可能就是如何抓取HTML页面了,python中使用urllib库可以轻松的实现html页面的抓取,再使用正则表达式或者HTMLParser库找出自己感兴趣的部分做进一步处理。下面是一个转来的小例子(出处为http://www.cnblogs.com/fnng/p/3576154.html,在此深表感谢)

import reimport urllibdef getHtml(url):    page = urllib.urlopen(url)    html = page.read()    return htmldef getImg(html):    reg = rsrc="http://www.mamicode.com/(.+?\.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-8from html.parser import HTMLParserclass 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会很好。不过此刻突然没了兴致,留作以后重构吧。

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文件。

 

 

也写一个简单的网络爬虫