也写一个简单的网络爬虫

引子

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

缘起

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

抓取HTML页面

一个爬虫最重要的部分可能就是如何抓取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会很好。不过此刻突然没了兴致,留作以后重构吧。

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

时间: 2024-12-24 12:49:12

也写一个简单的网络爬虫的相关文章

一个简单的网络爬虫-从网上爬取美女图片

CrawlerPicture.java 文件 package com.lym.crawlerDemo; import java.io.DataInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import

使用requests + beautifulsoup 写一个简单的漫画爬虫

from bs4 import BeautifulSoupimport requestsimport osfrom time import sleep class get_img(object): def get_url(self): self.new_url = [] url = "http://www.gumua.com/Manhua/28307.html" #首页网址 headers = {'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linu

一个简单的分布式爬虫

下载scrapy-redis: https://github.com/rmax/scrapy-redis 下载zip文件之后解压 建立两个批处理文件,start.bat和clear.batstart.bat的内容为redis-server redis.windows.confclear.bat的内容为redis-cli flushdb双击start.bat启动 这样就说明下好了,运行正常. 我们需要构建一个分布式爬虫系统:由一个master爬虫和slave爬虫组成,master端部署了redis

Python:requests库、BeautifulSoup4库的基本使用(实现简单的网络爬虫)

Python:requests库.BeautifulSoup4库的基本使用(实现简单的网络爬虫) 一.requests库的基本使用 requests是python语言编写的简单易用的HTTP库,使用起来比urllib更加简洁方便. requests是第三方库,使用前需要通过pip安装. pip install requests 1.基本用法: import requests #以百度首页为例 response = requests.get('http://www.baidu.com') #res

linux设备驱动第三篇:写一个简单的字符设备驱动

在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动.本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存. 下面就开始学习如何写一个简单的字符设备驱动.首先我们来分解一下字符设备驱动都有那些结构或者方法组成,也就是说实现一个可以使用的字符设备驱动我们必须做些什么工作. 1.主设备号和次设备号 对于字符设备的访问是通过文件系统中的设备名称进行的.他们通常位于/dev目录下.如下: [plain] vie

(2)自己写一个简单的servle容器

自己写一个简单的servlet,能够跑一个简单的servlet,说明一下逻辑. 首先是写一个简单的servlet,这就关联到javax.servlet和javax.servlet.http这两个包的类,其中一个比较重要的接口就是:javax.servlet.Servlet,所有的servlet必须实现实现或者继承实现该接口的类. Servlet接口有五个方法: public void init(ServletConfig config) throws ServletException publi

分享:计算机图形学期末作业!!利用WebGL的第三方库three.js写一个简单的网页版“我的世界小游戏”

这几天一直在忙着期末考试,所以一直没有更新我的博客,今天刚把我的期末作业完成了,心情澎湃,所以晚上不管怎么样,我也要写一篇博客纪念一下我上课都没有听,还是通过强大的度娘完成了我的作业的经历.(当然作业不是百度来的,我只是百度了一些示例代码的意思,怎么用!算了,越解释万一越黑呢!哈哈O(∩_∩)O哈哈~) ----------------------------------------------------------------分界线------------------------------

Java写一个简单学生管理系统

其实作为一名Java的程序猿,无论你是初学也好,大神也罢,学生管理系统一直都是一个非常好的例子,初学者主要是用数组.List等等来写出一个简易的学生管理系统,二.牛逼一点的大神则用数据库+swing来做一个有界面的学生管理系统.其实都并不会太难. 今天我就先写一个简单的用List来实现学生管理系统: 首先,管理系统是针对学生对象的,所以我们先把学生对象就写出来: package bean; public class Student { String name; String studentId;

DuiVision开发教程(2)-如何写一个简单的界面程序

基于DuiVision界面库开发的界面程序主要包括如下几部分内容: 1.资源定义,包括图片资源.各个窗口界面的xml定义文件 2.事件处理类代码,用于处理界面响应消息 3.其他业务逻辑代码 下面举例说明如何写一个简单的界面程序. 第一步:使用VC向导创建一个有两个tab页面的DuiVision工程 向导生成的解决方案文件如下: 默认有两个工程,分别是DuiVision库和应用程序工程.自动生成的代码目录中bin目录下的内容那个如下,bkimg目录存放窗口背景图片,skins目录存放图片资源,xm