? ? 前端時間自學了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阻塞等待時的空閑時間執行其他線程,提升效率。