實驗原因:
目前有一個醫療百科檢索項目,該項目中對關鍵詞進行檢索后,返回的結果很多,可惜結果的排序很不好,影響用戶體驗。簡單來說,搜索出來的所有符合疾病中,有可能是最不常見的疾病是排在第一個的,而最有可能的疾病可能需要翻很多頁才能找到。
實驗目的:
為了優化對搜索結果的排序,想到了利用百度搜索后有顯示搜索到多少詞條,利用這個詞條數,可以有效的對疾病排名進行一個優化。從一方面看,某一個疾病在百度的搜索詞條數目越多,表示這個詞條的信息特別豐富,側面反映了搜索這個詞條的人特別多,從而可以推出這個疾病在人群中可能是發生概率較高的。反過來看,如果一個疾病很罕見,人們只有很低的概率會患這種疾病,那么相應的搜索這個詞條的人也就會少,相應的網頁也就少,因此搜索引擎搜索出來的詞條數目也就會少。
實驗過程:
第一階段:從數據庫中獲取疾病名稱
這一階段涉及到如何利用python從數據庫中提取數據,我利用的是MySQLdb庫,通過利用以下代碼建立連接提取數據:
db = MySQLdb.connect('localhost', 'root', '', 'medical_app', charset = 'utf8') cu = db.cursor() cu.execute('select * from table order by id')
其中在connect函數中我設置了charset屬性,這是因為如不這樣做python讀取出來的數據庫中文信息會變成亂碼。
在這一階段,我編寫了一個dbmanager類,主要負責數據庫的讀取和插入工作,剛才介紹的已經可以完成一個讀取任務了,那么怎么利用python對數據進行添加呢?
cu.execute('insert into table(id,name) value(?, ?)',[a,b])
第二階段:完成數據的爬取
最初我嘗試了利用python的urllib來對百度網頁進行爬取,發現這條路是走不通的,百度發現是機器爬取后會返回錯誤的網頁代碼。
於是就想到了模擬瀏覽器的方式來對百度網頁進行互動,爬取網頁的內容。找到了mechanize庫,表示這個庫非常好用,使用非常簡單。除了能夠模仿瀏覽器讀取網頁以外,還可以和網頁進行交互操作,然后還可以設置機器人選項,通過這個選項可以讀取屏蔽機器人的網頁,比如說百度。
br = mechanize.Browser() br.open("http://www.example.com/") # follow second link with element text matching regular expression response1 = br.follow_link(text_regex=r"cheese\s*shop", nr=1) assert br.viewing_html() print br.title() print response1.geturl() print response1.info() # headers print response1.read() # body br.select_form(name="order") # Browser passes through unknown attributes (including methods) # to the selected HTMLForm. br["cheeses"] = ["mozzarella", "caerphilly"] # (the method here is __setitem__) # Submit current form. Browser calls .close() on the current response on # navigation, so this closes response1 response2 = br.submit()
以上是簡單的mechanize的使用說明。以下是官網對mechanize的簡單說明,不得不說,這個配合HTML解析器用起來太方便了。
mechanize.Browser
andmechanize.UserAgentBase
implement the interface ofurllib2.OpenerDirector
, so:
any URL can be opened, not just
http:
mechanize.UserAgentBase
offers easy dynamic configuration of user-agent features like protocol, cookie, redirection androbots.txt
handling, without having to make a newOpenerDirector
each time, e.g. by callingbuild_opener()
.Easy HTML form filling.
Convenient link parsing and following.
Browser history (
.back()
and.reload()
methods).The
Referer
HTTP header is added properly (optional).Automatic observance of
robots.txt
.Automatic handling of HTTP-Equiv and Refresh.
關於BeautifulSoup,這是一個在數據挖掘領域非常好用的HTML解析器,他將網頁解析成有標簽所構成的樹形字典結構,想要找到網頁中的某一元素用find函數非常輕松的就可以找到。
html = urllib.open('http://mypage.com') soup = BeautifulSoup(html.read()) soup.find('div', {'class':'nums'})
上面這句話的意思是,找到網頁中class屬性為nums的div標簽內容。
第三階段:異常的捕獲和超時設置
爬取網頁內容的爬蟲已經寫好,拿出來跑一跑,發現百度經常性的會返回一些錯誤的網頁導致網頁無法正確填充表格或者分析,這個時候我們需要抓取這里面的異常讓程序能夠持續運行,讓抓取出現異常的疾病名稱放回隊列稍后再進行爬取。關於異常捕獲,代碼如下:
1 try: 2 br = mechanize.Browser() 3 br.set_handle_robots(False) 4 br.open(URL) 5 br.select_form('f') 6 br['wd'] = name[1].encode('utf8') 7 response = br.submit() 8 #print 'form submitted, waiting result...' 9 #分析網頁,有可能百度返回錯誤頁面 10 soup = BeautifulSoup(response.read()) 11 # text = soup.find('div', {'class':'nums'}).getText() 12 if soup.find('div', {'class':'nums'}): 13 text = soup.find('div', {'class':'nums'}).getText() 14 else: 15 print '$Return page error,collect again...' 16 self.manual.push_record(name) 17 continue 18 except socket.timeout: 19 print '$there is an error occur.it will check later...' 20 self.manual.push_record(name) 21 print name[1],' pushed into the list.' 22 continue
這里可以看到為了提高檢索效率,設置了一個超時異常,利用socket組件中的timeout。在這段代碼之前我們需要設置下超時時間
1 import socket 2 3 socket.setdefaulttimeout(5) 4 5 try: 6 ... 7 except socket.timeout : 8 print 'timeout'
這段示例代碼設置的是5秒鍾的超時時間。
到目前為止,利用這些知識,我已經完成了一個在爬取過程中不會出錯的一個單線程爬蟲模塊,顯然這個爬蟲爬取內容的效率是非常慢的。我決定用多線程來讓它快起來。
第四階段:多線程的爬蟲控制
這一階段,我們需要設計一個可以進行多線程網頁爬取的爬蟲設計。這里面我們主要考慮兩點:1,如何實現多線程;2,關於公共變量的同步讀取問題如何實現。
對於第一個問題,在python中多線程的實現有兩種方式:
第一種是函數式:
函數式:調用thread模塊中的start_new_thread()函數來產生新線程。語法如下:
thread.start_new_thread ( function, args[, kwargs] )參數說明:
- function - 線程函數。
- args - 傳遞給線程函數的參數,他必須是個tuple類型。
- kwargs - 可選參數。
第二種是線程模塊:
Python通過兩個標准庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖。
thread 模塊提供的其他方法:
- threading.currentThread(): 返回當前的線程變量。
- threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啟動后、結束前,不包括啟動前和終止后的線程。
- threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
- run(): 用以表示線程活動的方法。
- start():啟動線程活動。
- join([time]): 等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
- isAlive(): 返回線程是否活動的。
- getName(): 返回線程名。
- setName(): 設置線程名。
之前嘗試了利用第一種方式來實現函數,發現這樣實現出來的代碼結構很不清晰,在變量同步的時候回會顯得非常混亂。於是采用了第二種方法來實現這個函數:
class myThread (threading.Thread): #繼承父類threading.Thread def __init__(self, threadID, name, Manual): threading.Thread.__init__(self) self.lock = thread.allocate_lock() self.threadID = threadID self.name = name self.manual = Manual def run(self): #把要執行的代碼寫到run函數里面 線程在創建后會直接運行run函數 print "Starting " + self.name self.get_rank() print "Exiting " + self.name def get_rank(self): #爬蟲代碼,持續獲取疾病得分 ... ...
如何實現線程的運作呢?代碼如下:
for i in xrange(thread_count): #建立線程 mythread = myThread(i,'thread-'+str(i),m) thread_queue.append(mythread) for i in xrange(thread_count): #啟動線程 thread_queue[i].start() for i in xrange(thread_count): #結束線程 thread_queue[i].join()
現在我們已經可以通過利用線程來對疾病進行爬取了,可是在對爬取結果怎么進行同步存儲,怎么對疾病名稱進行同步讀取呢?
第四階段:變量的同步操作
這一階段我們需要設計好如何才能夠可行的對同步變量進行操作,這一方面是比較燒腦的。。。設計如下:
其中B類是線程類,A類是同步變量控制類,A類的主要功能是提供變量V1,V2的同步操作,包括讀取寫入之類的。B類是線程類,負責爬取網頁的數據。
B類在上面已經說過了,A類的實現如下:
class Manual: #同步變量控制 def __init__(self, names): self.names = names self.results = [] self.lock = threading.RLock() def get_name(self): #獲得疾病名稱 self.lock.acquire() if len(self.names): name = self.names.pop() #print 'name get' self.lock.release() return name else: self.lock.release() return None def put_result(self, result): #存放得分 self.lock.acquire() self.results.append(result) print '(%d/6811)' %len(self.results) self.lock.release() def push_record(self, name): #放回獲取失敗的疾病名 self.lock.acquire() self.names.append(name) self.lock.release()
最后,所有部分都已實現,就差組裝起來跑動啦。目前在實驗室苦逼的跑啊跑中。。網不給力,拿了4個線程跑,保守目測得4個小時。
【原創-Blaxon】