前端时间自学了python的基本语法,为深入了解python,就打算真正的鼓捣些东西,加深记忆。据说,python经常用来网页抓取(爬虫),故而新手小试,并记之。
网页数据采集多线程设计:
1>输入:多个线程共享一个任务队列,如果该任务方案支持网络,可设计为分布式集群采集;2>输出:最简单的做法是直接将采集数据放入数据库,但频繁操作数据库会增加耗时,故而多个输出可共享一个消息队列,再用线程处理消息队列;事实上该消息队列可模拟为OS经典生产者和消费者模式中的共享缓冲池。
1.网例一之:python多线程数据采集
要点1:python多线程
python的多线程编程,这里题外话的说一下线程和进程的区别:1>进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其他记录其运行轨迹的辅助数据(os层面)。进程可通过fork和spawn操作来完成其他任务,但由于各自内存独立,故只能使用进程间通讯(IPC),而不能直接共享信息;2>线程(有时被称为轻量级进程)与进程相似,不同是所有的线程运行在同一个进程中,共享相同的运行环境(包括数据空间,故可方便通讯)。线程有开始、顺序执行和结束三部分,持有自己的指令指针,记录自己运行到什么地方。线程的运行可能被中断(抢占)或睡眠(暂时被挂起),以让步于其他线程。线程一般都是并发执行的,事实上在单CPU系统中真正的并发是不可能的,每个线程会被安排成每次只占用CPU运行一段时间。多个线程共同访问同一片数据有可能导致数据结果的不一致的问题,这叫做竞态条件。
至于Python的多线程,由于其原始解释器CPython中存在着GIL(Global Interpreter Lock,全局解释器锁),虽然CPython线程库直接封装了系统的原生线程,但(整体进程)同一时间只会获得一个GIL线程执行,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。故即使多线程也只是做分时切换而已。
所以如果你的Python代码是CPU密集型,此时多线程的代码很有可能是线性执行的,这种情况下多线程是鸡肋,效率可能还不如单线程因;但如果你的代码是IO密集型,Python多线程可以明显提高效率。
但如果确实需要在CPU密集型的代码里用并行,可以用multiprocessing库。这个库是基于multi process实现了类multi thread的API接口,并且用pickle部分地实现了变量共享。
如果确实理不清Python代码是CPU密集型还是IO密集型,那么分别尝试下面的两种Python多线程方法:
1>from multiprocessing import Pool(这就是基于multithread实现了multiprocessing的API,多进程同步);
2>from multiprocessing.dummy import Pool(这是多线程实现的同步)两种方式都跑一下,哪个速度快用哪个就行了。
一般使用:
threading模块中的Thread类可用来创建线程,创建方法有如下两种:1.通过继承Thread类,重写它的run方法;2.创建一个threading.Thread对象,在初始化函数(__init__)中将可调用对象作为参数传入:threading.Thread.__init__(self,name=threadname)。
要点2. 目标数据及相关第三方packages
采集目标:淘宝; 采集数据:某一关键词领域的淘宝店铺名称、URL地址、店铺等级;
相关第三方packages:requests(http://devcharm.com/pages/11-python-modules-you-should-know);beautifulsoup4(PS:保证lxml model已安装过);Redis
代码部分:
1. search_config.py
#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='
simple_scrapy.py
#coding=utf-8
import request
from bs4 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,threadname)
self.sharedata=queue
self.out_queue=out_queue
self.threadname=threadname
print threadname+'start......'
def run(self):
url=config.url+self.sharesata.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.__int__(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
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:
info=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 AttibuteError:
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()
注解:1》使用BeautifulSoup库,可以直接按照class或者id等html的attr来进行提取,相较于直接写正则表达式难度系数降低很多,当然执行效率上,相应的也就大打折扣了。
2》通过scrapy线程不断提供数据,parse线程从队列中取出数据进行相应解析;作者将这二者写到了同一个循环体中:即get一个response后随即刻parse掉,但我认为这二者完全可以参照生产者消费者模式,将二者模块分开,仅共享一消息队列即可。
3》由于共享数据不存在安全问题,所以上面的例子都是非线程安全的,并没有为共享数据加锁,只是实现了最简单的FIFO,所以也不会是因为锁的开销导致效率没有得到真正提高
4》由于数据解析是CPU密集型操作,而网络请求是I/O密集型操作,考虑上文说到的GIL,数据解析操作操作的多线程并不能带来效率上的提升,相反可能因为线程的频繁切换,导致效率下降;而网络抓取的多线程则可以利用IO阻塞等待时的空闲时间执行其他线程,提升效率。