小白爬虫之爬虫快跑

使用多线程时好像在目录切换的问题上存在问题,可以给线程加个锁试试

Hello 大家好!我又来了。

你是不是发现下载图片速度特别慢、难以忍受啊!对于这种问题一般解决办法就是多进程了!一个进程速度慢!我就用十个进程,相当于十个人一起干。速度就会快很多啦!(为什么不说多线程?懂点Python的小伙伴都知道、GIL的存在 导致Python的多线程有点坑啊!)今天就教大家来做一个多进程的爬虫(其实吧、可以用来做一个超简化版的分布式爬虫)

其实吧!还有一种加速的方法叫做“异步”!不过这玩意儿我没怎么整明白就不出来误人子弟了!(因为爬虫大部分时间都是在等待response中!‘异步’则能让程序在等待response的时间去做的其他事情。)

学过Python基础的同学都知道、在多进程中,进程之间是不能相互通信的,这就有一个很坑爹的问题的出现了!多个进程怎么知道那那些需要爬取、哪些已经被爬取了!

这就涉及到一个东西!这玩意儿叫做队列!!队列!!队列!!其实吧正常来说应该给大家用队列来完成这个教程的,比如Tornado 的queue模块。(如果需要更为稳定健壮的队列,则请考虑使用Celery这一类的专用消息传递工具)

不过为了简化技术种类啊!(才不会告诉你们是我懒,嫌麻烦呢!)这次我们继续使用MongoDB。

好了!先来理一下思路:

每个进程需要知道那些URL爬取过了、哪些URL需要爬取!我们来给每个URL设置两种状态:

outstanding:等待爬取的URL

complete:爬取完成的URL

诶!等等我们好像忘了啥?失败的URL的怎么办啊?我们在增加一种状态:

processing:正在进行的URL。

嗯!当一个所有初始的URL状态都为outstanding;当开始爬取的时候状态改为:processing;爬取完成状态改为:complete;失败的URL重置状态为:outstanding。为了能够处理URL进程被终止的情况、我们设置一个计时参数,当超过这个值时;我们则将状态重置为outstanding。

下面开整Go Go Go!

首先我们需要一个模块:datetime(这个模块比内置time模块要好使一点)不会装??不是吧! pip install datetime

还有上一篇博文我们已经使用过的pymongo

下面是队列的代码:

Python

fromdatetimeimportdatetime,timedelta

frompymongoimportMongoClient,errors

classMogoQueue():

OUTSTANDING=1 ##初始状态

PROCESSING=2 ##正在下载状态

COMPLETE=3 ##下载完成状态

def__init__(self,db,collection,timeout=300): ##初始mongodb连接

self.client=MongoClient()

self.Client=self.client[db]

self.db=self.Client[collection]

self.timeout=timeout

def__bool__(self):

"""

这个函数,我的理解是如果下面的表达为真,则整个类为真

至于有什么用,后面我会注明的(如果我的理解有误,请指点出来谢谢,我也是Python新手)

$ne的意思是不匹配

"""

{'status':{'$ne':self.COMPLETE}}

)

returnTrueifrecordelseFalse

defpush(self,url,title): ##这个函数用来添加新的URL进队列

try:

print(url,'插入队列成功')

excepterrors.DuplicateKeyErrorase: ##报错则代表已经存在于队列之中了

print(url,'已经存在于队列中了')

pass

defpush_imgurl(self,title,url):

try:

print('图片地址插入成功')

excepterrors.DuplicateKeyErrorase:

print('地址已经存在了')

pass

defpop(self):

"""

这个函数会查询队列中的所有状态为OUTSTANDING的值,

更改状态,(query后面是查询)(update后面是更新)

并返回_id(就是我们的URL),MongDB好使吧,^_^

如果没有OUTSTANDING的值则调用repair()函数重置所有超时的状态为OUTSTANDING,

$set是设置的意思,和MySQL的set语法一个意思

"""

query={'status':self.OUTSTANDING},

update={'$set':{'status':self.PROCESSING,'timestamp':datetime.now()}}

)

ifrecord:

returnrecord['_id']

else:

self.repair()

raiseKeyError

defpop_title(self,url):

returnrecord['主题']

defpeek(self):

"""这个函数是取出状态为 OUTSTANDING的文档并返回_id(URL)"""

ifrecord:

returnrecord['_id']

defcomplete(self,url):

"""这个函数是更新已完成的URL完成"""

defrepair(self):

"""这个函数是重置状态$lt是比较"""

query={

'timestamp':{'$lt':datetime.now()-timedelta(seconds=self.timeout)},

'status':{'$ne':self.COMPLETE}

},

update={'$set':{'status':self.OUTSTANDING}}

)

ifrecord:

print('重置URL状态',record['_id'])

defclear(self):

"""这个函数只有第一次才调用、后续不要调用、因为这是删库啊!"""

好了,队列我们做好了,下面是获取所有页面的代码:

Python

fromDownloadimportrequest

frommongodb_queueimportMogoQueue

frombs4importBeautifulSoup

spider_queue=MogoQueue('meinvxiezhenji','crawl_queue')

defstart(url):

response=request.get(url,3)

Soup=BeautifulSoup(response.text,'lxml')

all_a=Soup.find('div',class_='all').find_all('a')

forainall_a:

title=a.get_text()

url=a['href']

spider_queue.push(url,title)

"""上面这个调用就是把URL写入MongoDB的队列了"""

if__name__=="__main__":

"""这一段儿就不解释了哦!超级简单的"""

下面就是多进程+多线程的下载代码了:

Python

importos

importtime

importthreading

importmultiprocessing

frommongodb_queueimportMogoQueue

fromDownloadimportrequest

frombs4importBeautifulSoup

SLEEP_TIME=1

defmzitu_crawler(max_threads=10):

crawl_queue=MogoQueue('meinvxiezhenji','crawl_queue') ##这个是我们获取URL的队列

##img_queue = MogoQueue('meinvxiezhenji', 'img_queue')

defpageurl_crawler():

whileTrue:

try:

url=crawl_queue.pop()

print(url)

exceptKeyError:

print('队列没有数据')

break

else:

img_urls=[]

req=request.get(url,3).text

title=crawl_queue.pop_title(url)

mkdir(title)

os.chdir('D:\mzitu\\'+title)

max_span=BeautifulSoup(req,'lxml').find('div',class_='pagenavi').find_all('span')[-2].get_text()

forpageinrange(1,int(max_span)+1):

page_url=url+'/'+str(page)

img_url=BeautifulSoup(request.get(page_url,3).text,'lxml').find('div',class_='main-image').find('img')['src']

img_urls.append(img_url)

save(img_url)

crawl_queue.complete(url) ##设置为完成状态

##img_queue.push_imgurl(title, img_urls)

##print('插入数据库成功')

defsave(img_url):

name=img_url[-9:-4]

print(u'开始保存:',img_url)

img=request.get(img_url,3)

f=open(name+'.jpg','ab')

f.write(img.content)

f.close()

defmkdir(path):

path=path.strip()

ifnotisExists:

print(u'建了一个名字叫做',path,u'的文件夹!')

returnTrue

else:

print(u'名字叫做',path,u'的文件夹已经存在了!')

returnFalse

threads=[]

whilethreadsorcrawl_queue:

"""

这儿crawl_queue用上了,就是我们__bool__函数的作用,为真则代表我们MongoDB队列里面还有数据

threads 或者 crawl_queue为真都代表我们还没下载完成,程序就会继续执行

"""

forthreadinthreads:

ifnotthread.is_alive(): ##is_alive是判断是否为空,不是空则在队列中删掉

threads.remove(thread)

whilelen(threads)orcrawl_queue.peek(): ##线程池中的线程少于max_threads 或者 crawl_qeue时

thread=threading.Thread(target=pageurl_crawler) ##创建线程

thread.setDaemon(True) ##设置守护线程

thread.start() ##启动线程

threads.append(thread) ##添加进线程队列

time.sleep(SLEEP_TIME)

defprocess_crawler():

process=[]

num_cpus=multiprocessing.cpu_count()

print('将会启动进程数为:',num_cpus)

foriinrange(num_cpus):

p=multiprocessing.Process(target=mzitu_crawler) ##创建进程

p.start() ##启动进程

process.append(p) ##添加进进程队列

forpinprocess:

p.join() ##等待进程队列里面的进程结束

if__name__=="__main__":

process_crawler()

好啦!一个多进程多线的爬虫就完成了,(其实你可以设置一下MongoDB,然后调整一下连接配置,在多台机器上跑哦!!嗯,就是超级简化版的分布式爬虫了,虽然很是简陋。)

本来还想下载图片那一块儿加上异步(毕竟下载图片是I\O等待最久的时间了,),可惜异步我也没怎么整明白,就不拿出来贻笑大方了。

另外,各位小哥儿可以参考上面代码,单独处理图片地址试试(就是多个进程直接下载图片)?

我测试了一下八分钟下载100套图

PS:请务必使用 第二篇博文中的下载模块,或者自己写一个自动更换代理的下载模块!!!不然寸步难行,分分钟被服务器BAN掉!

这个所有代码我放在这个位置了:https://github.com/thsheep/mzitu/

自然,芝麻HTTP会一直提供网络资源支持。

结束语

转载请注明:静觅»小白爬虫第四弹之爬虫快跑(多进程+多线程)

本文来自企鹅号 - Python热爱着媒体

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据库

第七节 关联映射之多对多

本次用 MyBatis 实现多对多关联映射:一个学生可以选多门课,一个课也可以由多个学生选。 一、 数据库准备 为了测试方便,我们在这里新建一个数据库并取名my...

29650
来自专栏网络

摸金Redis漏洞

摘要:漏洞可以参考乌云案例 ? 1.Redis漏洞基本信息漏洞名称:Redis服务器远程执行漏洞漏洞详情:Redis因配置不当可以无密码登录,导致未授权访问。 ...

33550
来自专栏编程

RxJava系列一

前言 提升开发效率,降低维护成本一直是开发团队永恒不变的宗旨。近一年来国内的技术圈子中越来越多的开始提及Rx,经过一段时间的学习和探索之后我也深深的感受到了Rx...

266100
来自专栏挖掘大数据

超详细的大数据学习资源推荐(下)

服务编程 Akka Toolkit:JVM中分布性、容错事件驱动应用程序的运行时间; Apache Avro:数据序列化系统; Apache Cura...

29950
来自专栏云计算

云计算,迷你版线程同步

昨天发了那个吹牛的文章,一不注意把今天推送文章的机会用掉了,所以我现在(PM 8:50)虽然已准备好,但也发不出来,抱歉,说好的今天发线程同步的内容只等到明天凌...

25060
来自专栏智能大石头

XCode读取Excel数据(适用于任何数据库)

虽然是充血模型,虽然是强类型,XCode同样支持遍历任何数据库结构,并以强类型(相对于DataSet等字典访问)方式读取数据。 要遍历数据库结构是很容易的事情,...

23580
来自专栏企鹅号快讯

浅析负载均衡的6种算法,Ngnix的5种算法

常见的几种负载均衡算法 ? 1、轮询法 将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。 2、...

39190
来自专栏智能大石头

你知道吗?多个类多线程环境下静态构造函数的执行顺序

调用A a=new A() 请问输出是什么?为什么? class A { static A() { Stopwatch sw =...

21470
来自专栏数据库

余军:分布式数据库在金融行业的创新实践

分布式数据库系统通常使用较小的计算机系统,每台计算机可单独放在一个地方,每台计算机中都可能有DBMS的一份完整拷贝副本,或者部分拷贝副本,并具有自己局部的数据库...

343100
来自专栏数据库

“多态”的数据库连接池实现

需求 数据库连接的池化管理 支持不同数据源链接池化处理 问题 如何维护链接对象 如何区分不同数据源池化 如何实现资源同步问题 分析 如何维护对象 使用阻塞队列实...

21390

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励