网络爬虫详解与python实现
网络爬虫详解与python实现
潘少online 发表于3年前
网络爬虫详解与python实现
  • 发表于 3年前
  • 阅读 101
  • 收藏 1
  • 点赞 0
  • 评论 0

网络爬虫是捜索引擎抓取系统的重要组成部分。爬虫的主要目的是将互联网上的网页下载到本地形成一个或联网内容的镜像备份。这篇博客主要对爬虫以及抓取系统进行一个简单的概述。

一、网络爬虫的基本结构及工作流程

    一个通用的网络爬虫的框架如图所示:

    网络爬虫的基本工作流程如下:

    1.首先选取一部分精心挑选的种子URL;

    2.将这些URL放入待抓取URL队列;

    3.从待抓取URL队列中取出待抓取在URL,解析DNS,并且得到主机的ip,并将URL对应的网页下载下来,存储进已下载网页库中。此外,将这些URL放进已抓取URL队列。

    4.分析已抓取URL队列中的URL,分析其中的其他URL,并且将URL放入待抓取URL队列,从而进入下一个循环。

二、从爬虫的角度对互联网进行划分

    对应的,可以将互联网的所有页面分为五个部分:

    1.已下载未过期网页

    2.已下载已过期网页:抓取到的网页实际上是互联网内容的一个镜像与备份,互联网是动态变化的,一部分互联网上的内容已经发生了变化,这时,这部分抓取到的网页就已经过期了。

    3.待下载网页:也就是待抓取URL队列中的那些页面

    4.可知网页:还没有抓取下来,也没有在待抓取URL队列中,但是可以通过对已抓取页面或者待抓取URL对应页面进行分析获取到的URL,认为是可知网页。

    5.还有一部分网页,爬虫是无法直接抓取下载的。称为不可知网页。

三、抓取策略

    在爬虫系统中,待抓取URL队列是很重要的一部分。待抓取URL队列中的URL以什么样的顺序排列也是一个很重要的问题,因为这涉及到先抓取那个页面,后抓取哪个页面。而决定这些URL排列顺序的方法,叫做抓取策略。下面重点介绍几种常见的抓取策略:

    1.深度优先遍历策略

深度优先遍历策略是指网络爬虫会从起始页开始,一个链接一个链接跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。我们以下面的图为例:

    遍历的路径:A-F-G  E-H-I B C D

    2.宽度优先遍历策略

    宽度优先遍历策略的基本思路是,将新下载网页中发现的链接直接插入待抓取URL队列的末尾。也就是指网络爬虫会先抓取起始网页中链接的所有网页,然后再选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。还是以上面的图为例:

    遍历路径:A-B-C-D-E-F G H I

    3.反向链接数策略

    反向链接数是指一个网页被其他网页链接指向的数量。反向链接数表示的是一个网页的内容受到其他人的推荐的程度。因此,很多时候搜索引擎的抓取系统会使用这个指标来评价网页的重要程度,从而决定不同网页的抓取先后顺序。

    在真实的网络环境中,由于广告链接、作弊链接的存在,反向链接数不能完全等他我那个也的重要程度。因此,搜索引擎往往考虑一些可靠的反向链接数。

    4.Partial PageRank策略

    Partial PageRank算法借鉴了PageRank算法的思想:对于已经下载的网页,连同待抓取URL队列中的URL,形成网页集合,计算每个页面的PageRank值,计算完之后,将待抓取URL队列中的URL按照PageRank值的大小排列,并按照该顺序抓取页面。

    如果每次抓取一个页面,就重新计算PageRank值,一种折中方案是:每抓取K个页面后,重新计算一次PageRank值。但是这种情况还会有一个问题:对于已经下载下来的页面中分析出的链接,也就是我们之前提到的未知网页那一部分,暂时是没有PageRank值的。为了解决这个问题,会给这些页面一个临时的PageRank值:将这个网页所有入链传递进来的PageRank值进行汇总,这样就形成了该未知页面的PageRank值,从而参与排序。下面举例说明:

    5.OPIC策略策略

    该算法实际上也是对页面进行一个重要性打分。在算法开始前,给所有页面一个相同的初始现金(cash)。当下载了某个页面P之后,将P的现金分摊给所有从P中分析出的链接,并且将P的现金清空。对于待抓取URL队列中的所有页面按照现金数进行排序。

    6.大站优先策略

    对于待抓取URL队列中的所有网页,根据所属的网站进行分类。对于待下载页面数多的网站,优先下载。这个策略也因此叫做大站优先策略。 

四、更新策略

    互联网是实时变化的,具有很强的动态性。网页更新策略主要是决定何时更新之前已经下载过的页面。常见的更新策略又以下三种:

    1.历史参考策略

    顾名思义,根据页面以往的历史更新数据,预测该页面未来何时会发生变化。一般来说,是通过泊松过程进行建模进行预测。

    2.用户体验策略
    尽管搜索引擎针对于某个查询条件能够返回数量巨大的结果,但是用户往往只关注前几页结果。因此,抓取系统可以优先更新那些现实在查询结果前几页中的网页,而后再更新那些后面的网页。这种更新策略也是需要用到历史信息的。用户体验策略保留网页的多个历史版本,并且根据过去每次内容变化对搜索质量的影响,得出一个平均值,用这个值作为决定何时重新抓取的依据。
    3.聚类抽样策略

    前面提到的两种更新策略都有一个前提:需要网页的历史信息。这样就存在两个问题:第一,系统要是为每个系统保存多个版本的历史信息,无疑增加了很多的系统负担;第二,要是新的网页完全没有历史信息,就无法确定更新策略。

    这种策略认为,网页具有很多属性,类似属性的网页,可以认为其更新频率也是类似的。要计算某一个类别网页的更新频率,只需要对这一类网页抽样,以他们的更新周期作为整个类别的更新周期。基本思路如图:


        

五、分布式抓取系统结构


    一般来说,抓取系统需要面对的是整个互联网上数以亿计的网页。单个抓取程序不可能完成这样的任务。往往需要多个抓取程序一起来处理。一般来说抓取系统往往是一个分布式的三层结构。如图所示:

    最下一层是分布在不同地理位置的数据中心,在每个数据中心里有若干台抓取服务器,而每台抓取服务器上可能部署了若干套爬虫程序。这就构成了一个基本的分布式抓取系统。

    对于一个数据中心内的不同抓去服务器,协同工作的方式有几种:

    1.主从式(Master-Slave)

    主从式基本结构如图所示:

    对于主从式而言,有一台专门的Master服务器来维护待抓取URL队列,它负责每次将URL分发到不同的Slave服务器,而Slave服务器则负责实际的网页下载工作。Master服务器除了维护待抓取URL队列以及分发URL之外,还要负责调解各个Slave服务器的负载情况。以免某些Slave服务器过于清闲或者劳累。

    这种模式下,Master往往容易成为系统瓶颈。

    2.对等式(Peer to Peer)

    对等式的基本结构如图所示:

    在这种模式下,所有的抓取服务器在分工上没有不同。每一台抓取服务器都可以从待抓取在URL队列中获取URL,然后对该URL的主域名的hash值H,然后计算H mod m(其中m是服务器的数量,以上图为例,m为3),计算得到的数就是处理该URL的主机编号。

    举例:假设对于URL www.baidu.com,计算器hash值H=8,m=3,则H mod m=2,因此由编号为2的服务器进行该链接的抓取。假设这时候是0号服务器拿到这个URL,那么它将该URL转给服务器2,由服务器2进行抓取。

    这种模式有一个问题,当有一台服务器死机或者添加新的服务器,那么所有URL的哈希求余的结果就都要变化。也就是说,这种方式的扩展性不佳。针对这种情况,又有一种改进方案被提出来。这种改进的方案是一致性哈希法来确定服务器分工。其基本结构如图所示:

    一致性哈希将URL的主域名进行哈希运算,映射为一个范围在0-232之间的某个数。而将这个范围平均的分配给m台服务器,根据URL主域名哈希运算的值所处的范围判断是哪台服务器来进行抓取。

    如果某一台服务器出现问题,那么本该由该服务器负责的网页则按照顺时针顺延,由下一台服务器进行抓取。这样的话,及时某台服务器出现问题,也不会影响其他的工作。


进入正题,描述如何实现:

拿到一个已经有了描述的办法,实现它可以按自顶向下的思路,先将大的步骤描述出来,然后分割成小的问题,一部分一部分地解决。
对于一个网络爬虫,如果要按广度遍历的方式下载,它就是这样干活的:
1.从给定的入口网址把第一个网页下载下来
2.从第一个网页中提取出所有新的网页地址,放入下载列表中
3.按下载列表中的地址,下载所有新的网页
4.从所有新的网页中找出没有下载过的网页地址,更新下载列表
5.重复3、4两步,直到更新后的下载列表为空表时停止
其实就是简化成下面的步骤:
1.按下载列表进行下载
2.更新下载列表
3.循环操作1,2,直到列表为空结束

所以最初的设想就是写一个函数里面干这个:
def craw():
while len(urlList) != 0
Init_url_list()
Download_list()
Update_list()
当然,上面这个函数是工作不起来的,它只是最顶层的一个想法,底层的实现还没做。不过这一步很重要,至少让自己知道该干什么了。
下面的事情就是将函数每一部分实现,这个可以放在一个类里去实现,我把它命名为WebCrawler。
在python里,要按一个地址下载一个网页那并不是什么难事,你可以用urllib里的urlopen去连接上某一个网页,然后调用获取到的对象的read方法,可以得到网页的内容的字符串,像这样:
IDLE 2.6.6 ==== No Subprocess ====
>>> import urllib
>>> f = urllib.urlopen('http://www.hfut.edu.cn')
>>> s = f.read()
>>>
这样上面变量 s 里面存的就是从http://www.hfut.edu.cn这个地址里获取到的网页的内容了,是str数据类型。下面你要怎么用都可以了,把写入文件或从中提取新的地址就随你意了。当然,只要写入文件,就算下载完了这个页面。

一个爬虫程序下载的速度肯定是很重要的问题,谁也不想用一个单线程的爬虫用一次只下一个网页速度去下载,我在学校校园网,测试了单线程的爬虫,平均每秒才下1k。所以解决的办法只有用多线程,多开几个连接同时下载就快了。本人是Python新手,东西都是临时拿来用的。
下载线程我是用了另外一个类,命名为CrawlerThread,它继承了threading.Thread这个类。

因为涉及到更新下载列表的问题,线程对某个表的读写还要考虑同步,我在代码里使用了线程锁,这个用threading.Lock()构造对象。调用对象的acquire()和release()保证每次只有一个线程对表进行操作。当然,为了保证表的更新能够实现,我使用了多个表,一个表肯定办不成。因为你即要知道当前要下载的网络地址,还要知道你已经下载过的网络地址。你要把已经下载过的地址从新的网页中获取到的网址列表中除去,这当中又涉及了一些临时的表。

爬虫在下载网页的时候,最好还要把哪个网页存到了哪个文件记录好,并且记录好网页是搜索到广度搜索到的第几层的深度记录好,因为如果要做搜索引擎,这个都是对制作索引和对网页排序有参考价值的信息。至少你自己会想知道爬虫给你下载到了什么,都放在哪了吧。对应的写记录的语句我在代码里的行末用##标注出来了。
写的文字已经很多了,不想再写了,直接贴上代码:


文件Test.py内容如下:(它调用了WebCrawler,运行时是运行它)

import WebCrawler

url = raw_input('设置入口url(例-->http://www.baidu.com): n')
thNumber = int(raw_input('设置线程数:')) #之前类型未转换出bug

wc = WebCrawler.WebCrawler(thNumber)
wc.Craw(url)

文件WebCrawler.py内容如下:

import threading
import GetUrl
import urllib

g_mutex = threading.Lock()
g_pages = [] #线程下载页面后,将页面内容添加到这个list中
g_dledUrl = [] #所有下载过的url
g_toDlUrl = [] #当前要下载的url
g_failedUrl = [] #下载失败的url
g_totalcount = 0 #下载过的页面数

class WebCrawler:
def __init__(self,threadNumber):
self.threadNumber = threadNumber
self.threadPool = []
self.logfile = file('#log.txt','w') ##

def download(self, url, fileName):
Cth = CrawlerThread(url, fileName)
self.threadPool.append(Cth)
Cth.start()

def downloadAll(self):
global g_toDlUrl
global g_totalcount
i = 0
while i < len(g_toDlUrl):
j = 0
while j < self.threadNumber and i + j < len(g_toDlUrl):
g_totalcount += 1 #进入循环则下载页面数加1
self.download(g_toDlUrl[i+j],str(g_totalcount)+'.htm')
print 'Thread started:',i+j,'--File number = ',g_totalcount
j += 1
i += j
for th in self.threadPool:
th.join(30) #等待线程结束,30秒超时
self.threadPool = [] #清空线程池
g_toDlUrl = [] #清空列表

def updateToDl(self):
global g_toDlUrl
global g_dledUrl
newUrlList = []
for s in g_pages:
newUrlList += GetUrl.GetUrl(s) #######GetUrl要具体实现
g_toDlUrl = list(set(newUrlList) - set(g_dledUrl)) #提示unhashable

def Craw(self,entryUrl): #这是一个深度搜索,到g_toDlUrl为空时结束
g_toDlUrl.append(entryUrl)
depth = 0
while len(g_toDlUrl) != 0:
depth += 1
print 'Searching depth ',depth,'...nn'
self.downloadAll()
self.updateToDl()
content = 'n>>>Depth ' + str(depth)+':n' ##(该标记表示此语句用于写文件记录)
self.logfile.write(content) ##
i = 0 ##
while i < len(g_toDlUrl): ##
content = str(g_totalcount + i) + '->' + g_toDlUrl[i] + 'n' ##
self.logfile.write(content) ##
i += 1 ##

class CrawlerThread(threading.Thread):
def __init__(self, url, fileName):
threading.Thread.__init__(self)
self.url = url #本线程下载的url
self.fileName = fileName

def run(self): #线程工作-->下载html页面
global g_mutex
global g_failedUrl
global g_dledUrl
try:
f = urllib.urlopen(self.url)
s = f.read()
fout = file(self.fileName, 'w')
fout.write(s)
fout.close()
except:
g_mutex.acquire() #线程锁-->锁上
g_dledUrl.append(self.url)
g_failedUrl.append(self.url)
g_mutex.release() #线程锁-->释放
print 'Failed downloading and saving',self.url
return None #记着返回!

g_mutex.acquire() #线程锁-->锁上
g_pages.append(s)
g_dledUrl.append(self.url)
g_mutex.release() #线程锁-->释放

文件GetUrl.py内容如下:(它里面的GetUrl从一个存有网页内容的字符串中获取所有url并以一个list返回,这部分实现方法很多,大家可以自己写个更好的)

urlSep = ['<','>','','(',')', r'"', ' ', 't', 'n']
urlTag = ['http://']

def is_sep(ch):
for c in urlSep:
if c == ch:
return True
return False

def find_first_sep(i,s):
while i < len(s):
if is_sep(s[i]):
return i
i+=1
return len(s)

def GetUrl(strPage):
rtList = []
for tag in urlTag:
i = 0
i = strPage.find(tag, i, len(strPage))
while i != -1:
begin = i
end = find_first_sep(begin+len(tag),strPage)
rtList.append(strPage[begin:end])
i = strPage.find(tag, end, len(strPage))


共有 人打赏支持
粉丝 9
博文 54
码字总数 94075
×
潘少online
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: