python数据采集与多线程效率分析
以前一直使用PHP写爬虫,用Snoopy配合simple_html_dom用起来也挺好的,至少能够解决问题。
PHP一直没有一个好用的多线程机制,虽然可以使用一些trick的手段来实现并行的效果(例如借助apache或者nginx服务器等,或者fork一个子进程,或者直接动态生成多个PHP脚本多进程运行),但是无论从代码结构上,还是从使用的复杂程度上,用起来都不是那么顺手。还听说过一个pthreads的PHP的扩展,这是一个真正能够实现PHP多线程的扩展,看github上它的介绍:Absolutely, this is not a hack, we don't use forking or any other such nonsense, what you create are honest to goodness posix threads that are completely compatible with PHP and safe ... this is true multi-threading :)
扯远了,PHP的内容在本文中不再赘述,既然决定尝试一下Python的采集,同时一定要学习一下Python的多线程知识的。以前一直听各种大牛们将Python有多么多么好用,不真正用一次试试,自己也没法明确Python具体的优势在哪,处理哪些问题用Python合适。
废话就说这么多吧,进入正题
- 采集目标:淘宝
- 采集数据:某一关键词领域的淘宝店铺名称、URL地址、店铺等级
- 用到的第三方packages:
- requests(话说是看了前两天的一篇文章 Python modules you should know (传送门) 才知道的,以前只知道urllib2)
- BeautifulSoup(现在貌似有新版本bs4了,不过我用的是旧版本的)
- Redis
采集
单线程版本
代码:
search_config.py
#!/usr/bin/env python
# coding=utf-8
class config:
keyword = '青岛'
search_type = 'shop'
url = 'http://s.taobao.com/search?q=' + keyword + '&commend=all&search_type='+ search_type +'&sourceId=tb.index&initiative_id=tbindexz_20131207&app=shopsearch&s='
single_scrapy.py
#!/usr/bin/env python
# coding=utf-8
import requests
from search_config import config class Scrapy(): def __init__(self, threadname, start_num):
self.threadname = threadname
self.start_num = start_num
print threadname + 'start.....' def run(self):
url = config.url + self.start_num
response = requests.get(url)
print self.threadname + 'end......' def main():
for i in range(0,13,6):
scrapy = Scrapy('scrapy', str(i))
scrapy.run() if __name__ == '__main__':
main()
运行分析:
这是最简单最常规的一种采集方式,按照顺序循环进行网络连接,获取页面信息。看截图可知,这种方式的效率其实是极低的,一个连接进行网络I/O的时候,其他的必须等待前面的连接完成才能进行连接,换句话说,就是前面的连接阻塞的后面的连接。
多线程版本
代码:
#!/usr/bin/env python
# coding=utf-8
import requests
from search_config import config
import threading class Scrapy(threading.Thread): def __init__(self, threadname, start_num):
threading.Thread.__init__(self, name = threadname)
self.threadname = threadname
self.start_num = start_num
print threadname + 'start.....' #重写Thread类的run方法
def run(self):
url = config.url + self.start_num
response = requests.get(url)
print self.threadname + 'end......' def main():
for i in range(0,13,6):
scrapy = Scrapy('scrapy', str(i))
scrapy.start() if __name__ == '__main__':
main()
运行分析:
通过截图可以看到,采集同样数量的页面,通过开启多线程,时间缩短了很多,但是CPU利用率高了。
页面信息解析
html页面信息拿到以后,我们需要对其进行解析操作,从中提取出我们所需要的信息,包含:
- 店铺名称
- 店铺URL
- 店铺等级
使用BeautifulSoup这个库,可以直接按照class或者id等html的attr来进行提取,比直接写正则直观不少,难度也小了很多,当然,执行效率上,相应的也就大打折扣了。
代码:
这里使用
Queue实现一个生产者和消费者模式- 生产者消费者模式:
- 生产者将数据依次存入队列,消费者依次从队列中取出数据。
- 本例中,通过scrapy线程不断提供数据,parse线程从队列中取出数据进行相应解析
- 生产者消费者模式:
Queue模块
- Python中的Queue对象也提供了对线程同步的支持,使用Queue对象可以实现多个生产者和多个消费者形成的FIFO的队列。
- 当共享信息需要安全的在多线程之间交换时,Queue非常有用。
- Queue的默认长度是无限的,但是可以设置其构造函数的maxsize参数来设定其长度。
#!/usr/bin/env python
# coding=utf-8
import requests
from BeautifulSoup import BeautifulSoup
from search_config import config from Queue import Queue
import threading class Scrapy(threading.Thread): def __init__(self, threadname, queue, out_queue):
threading.Thread.__init__(self, name = threadname)
self.sharedata = queue
self.out_queue= out_queue
self.threadname = threadname
print threadname + 'start.....' def run(self):
url = config.url + self.sharedata.get()
response = requests.get(url)
self.out_queue.put(response)
print self.threadname + 'end......' class Parse(threading.Thread):
def __init__(self, threadname, queue, out_queue):
threading.Thread.__init__(self, name = threadname)
self.sharedata = queue
self.out_queue= out_queue
self.threadname = threadname
print threadname + 'start.....' def run(self):
response = self.sharedata.get()
body = response.content.decode('gbk').encode('utf-8')
soup = BeautifulSoup(body)
ul_html = soup.find('ul',{'id':'list-container'})
lists = ul_html.findAll('li',{'class':'list-item'})
stores = []
for list in lists:
store= {}
try:
infos = list.findAll('a',{'trace':'shop'})
for info in infos:
attrs = dict(info.attrs)
if attrs.has_key('class'):
if 'rank' in attrs['class']:
rank_string = attrs['class']
rank_num = rank_string[-2:]
if (rank_num[0] == '-'):
store['rank'] = rank_num[-1]
else:
store['rank'] = rank_num
if attrs.has_key('title'):
store['title'] = attrs['title']
store['href'] = attrs['href']
except AttributeError:
pass
if store:
stores.append(store) for store in stores:
print store['title'] + ' ' + store['rank']
print self.threadname + 'end......' def main():
queue = Queue()
targets = Queue()
stores = Queue()
scrapy = []
for i in range(0,13,6):
#queue 原始请求
#targets 等待解析的内容
#stores解析完成的内容,这里为了简单直观,直接在线程中输出了内容,并没有使用该队列
queue.put(str(i))
scrapy = Scrapy('scrapy', queue, targets)
scrapy.start()
parse = Parse('parse', targets, stores)
parse.start() if __name__ == '__main__':
main()
运行结果
看这个运行结果,可以看到,我们的scrapy过程很快就完成了,我们的parse也很早就开始了,可是在运行的时候,却卡在parse上好长时间才出的运行结果,每一个解析结果出现,都需要3~5秒的时间,虽然我用的是台老IBM破本,但按理说使用了多线程以后不应该会这么慢的啊。
同样的数据,我们再看一下单线程下,运行结果。这里为了方便,我在上一个multi_scrapy里加入了redis,使用redis存储爬行下来的原始页面,这样在single_parse.py里面可以单独使用,更方便一些。
单线程版本:
代码:
#!/usr/bin/env python
# coding=utf-8
from BeautifulSoup import BeautifulSoup
import redis class Parse():
def __init__(self, threadname, content):
self.threadname = threadname
self.content = content
print threadname + 'start.....' def run(self):
response = self.content
if response:
body = response.decode('gbk').encode('utf-8')
soup = BeautifulSoup(body)
ul_html = soup.find('ul',{'id':'list-container'})
lists = ul_html.findAll('li',{'class':'list-item'})
stores = []
for list in lists:
store= {}
try:
infos = list.findAll('a',{'trace':'shop'})
for info in infos:
attrs = dict(info.attrs)
if attrs.has_key('class'):
if 'rank' in attrs['class']:
rank_string = attrs['class']
rank_num = rank_string[-2:]
if (rank_num[0] == '-'):
store['rank'] = rank_num[-1]
else:
store['rank'] = rank_num
if attrs.has_key('title'):
store['title'] = attrs['title']
store['href'] = attrs['href']
except AttributeError:
pass
if store:
stores.append(store) for store in stores:
try:
print store['title'] + ' ' + store['rank']
except KeyError:
pass
print self.threadname + 'end......'
else:
pass def main():
r = redis.StrictRedis(host='localhost', port=6379)
while True:
content = r.lpop('targets')
if (content):
parse = Parse('parse', content)
parse.run()
else:
break if __name__ == '__main__':
main()
运行结果:

结果可以看到,单线程版本中,耗时其实和多线程是差不多的,上文中的多线程版本,虽然包含了获取页面的时间,但是地一个例子里我们已经分析了,使用多线程以后,三个页面的抓取,完全可以在1s内完成的,也就是说,使用多线程进行数据解析,并没有获得实质上的效率提高。
分析原因
- 看两个运行的CPU占用,第一个127%,第二个98%,都是非常高的,这说明,在处理字符串解析匹配提取等运算密集型的工作时,并行的概念并没有很好得得到发挥
- 由于共享数据不存在安全问题,所以上面的例子都是非线程安全的,并没有为共享数据加锁,只是实现了最简单的FIFO,所以也不会是因为锁的开销导致效率没有得到真正提高
- 网上搜索资料,发现python多线程似乎并不能利用多核,问题似乎就是出在这里了,在python上开启多个线程,由于GIL的存在,每个单独线程都会在竞争到GIL后才运行,这样就干预OS内部的进程(线程)调度,结果在多核CPU上,python的多线程实际是串行执行的,并不会同一时间多个线程分布在多个CPU上运行。Python由于有全锁局的存在(同一时间只能有一个线程执行),并不能利用多核优势。所以,如果你的多线程进程是CPU密集型的,那多线程并不能带来效率上的提升,相反还可能会因为线程的频繁切换,导致效率下降;如果是IO密集型,多线程进程可以利用IO阻塞等待时的空闲时间执行其他线程,提升效率。
- 问题答案:由于数据解析操作是CPU密集型的操作,而网络请求是I/O密集型操作,所以出现了上述结果。
解决方法
GIL既然是针对一个python解释器进程而言的,那么,如果解释器可以多进程解释执行,那就不存在GIL的问题了。同样,他也不会导致你多个解释器跑在同一个核上。 所以,最好的解决方案,是多线程+多进程结合。通过多线程来跑I/O密集型程序,通过控制合适数量的进程来跑CPU密集型的操作,这样就可以跑慢CPU了:)
python数据采集与多线程效率分析的更多相关文章
- Python 多进程、多线程效率比较
Python 界有条不成文的准则: 计算密集型任务适合多进程,IO 密集型任务适合多线程.本篇来作个比较. 通常来说多线程相对于多进程有优势,因为创建一个进程开销比较大,然而因为在 python 中有 ...
- Python数据采集分析告诉你为何上海二手房你都买不起
感谢关注Python爱好者社区公众号,在这里,我们会每天向您推送Python相关的文章实战干货. 来吧,一起Python. 对商业智能BI.大数据分析挖掘.机器学习,python,R等数据领域感兴趣的 ...
- python并发之多线程
一开启线程的两种方式 from threading import Thread import time def haha(name): time.sleep(2) print('%s 你大爷..... ...
- valgrind的callgrind工具进行多线程性能分析
1.http://valgrind.org/downloads/old.html 2.yum install valgrind Valgrind的主要作者Julian Seward刚获得了今年的Goo ...
- python并发编程&多线程(二)
前导理论知识见:python并发编程&多线程(一) 一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 官网链 ...
- Python多进程与多线程编程及GIL详解
介绍如何使用python的multiprocess和threading模块进行多线程和多进程编程. Python的多进程编程与multiprocess模块 python的多进程编程主要依靠multip ...
- python 并发编程 多线程 GIL与多线程
GIL与多线程 有了GIL的存在,同一时刻同一进程中只有一个线程被执行 多进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势 1.cpu到底是用来做计算的,还是用来做I/ ...
- python爬虫之多线程、多进程+代码示例
python爬虫之多线程.多进程 使用多进程.多线程编写爬虫的代码能有效的提高爬虫爬取目标网站的效率. 一.什么是进程和线程 引用廖雪峰的官方网站关于进程和线程的讲解: 进程:对于操作系统来说,一个任 ...
- python高级之多线程
python高级之多线程 本节内容 线程与进程定义及区别 python全局解释器锁 线程的定义及使用 互斥锁 线程死锁和递归锁 条件变量同步(Condition) 同步条件(Event) 信号量 队列 ...
随机推荐
- javascript中的innerHTML是什么意思,怎么个用法?
innerHTML在JS是双向功能:获取对象的内容 或 向对象插入内容:如:<div id="aa">这是内容</div> ,我们可以通过 document ...
- 动态ViewPager导航页面
今天新学知识总计,个人信息,仅供参考: item设置: viewpager页面设置: <?xml version="1.0" encoding="utf-8&quo ...
- expecting SSH2_MSG_KEX_ECDH_REPLY ssh_dispatch_run_fatal问题解决
设置client的mtu ifconfig eth0 mtu 576 Ultimately, I added the following to /etc/ssh/ssh_config: Host * ...
- dojo 加载Json数据
1.今天研究了dojo datagrid加载WebService后台传上来的数据.研究来研究去发现他不是很难.用谷歌多调试一下就好了. 2.看很多例子,这个例子能够更好的帮我解决问题:https:// ...
- Python全栈--7模块--random os sys time datetime hashlib pickle json requests xml
模块分为三种: 自定义模块 内置模块 开源模块 一.安装第三方模块 # python 安装第三方模块 # 加入环境变量 : 右键计算机---属性---高级设置---环境变量---path--分号+py ...
- linux 下部署 kafka
参考文章 http://www.cnblogs.com/sunxucool/p/4459020.html http://www.cnblogs.com/oftenlin/p/4047504.html ...
- 转 SVN 在vs中的使用
给大家介绍一些SVN的入门知识!希望对大家的学习起到作用! 关于SVN与CVS的相关知识,大家可以自己去google一下. 一.准备 SVN是一个开源的版本控制系统 ...
- hdu 5818 (优先队列) Joint Stacks
题目:这里 题意: 两个类似于栈的列表,栈a和栈b,n个操作,push a x表示把数x放进a栈的栈底,pop b 表示将栈b的栈顶元素取出输出,并释放这个栈顶元素,merge a b表示把后面的那个 ...
- Android编译环境搭建(0818-0819)
1 在虚拟机VMware上安装64位Ubuntu14.04LTS 首先需要安装虚拟机并激活.然后新建虚拟机,选择使用下载好的Ubuntu镜像.注意需要将光驱改为自己下载的,而不是autoinst.is ...
- Android Preference使用
Android Preference经常使用在例如设置的功能,Android提供preference这个键值对的方式来处理这种情况,自动保存这些数据,并立时生效,这种就是使用android share ...