本文使用Python 2.7.3實現了一個自動訪問部落格的指令碼,涉及以下技術點:
- 語言基礎
- 容器(線性表、字典)
- 邏輯分支、迴圈
- 控制台格式化輸出
- HTTP用戶端網路編程
- PythonRegex
總覽
自動訪問部落格頁面這個操作實際上和網路爬蟲做的事情差不多,基本流程如下:
圖1 部落格自動訪問器工作原理
- 給訪問器一個開始位置(例如部落格首頁URL)
- 訪問器將URL指向的網頁爬回(爬回網頁這個操作本身相當於在瀏覽器中開啟頁面)
- 2中爬回的網頁交給分析器分析。分析器分析後提取出其中的URL加入待訪問URL列表,即URL庫。然後從URL庫中取出下一個要訪問的頁面URL
- 迴圈2、3步,直到達到某一終止條件程式退出
剛開始編程時,我們什麼都沒有,只有一個部落格首頁的URL。緊跟著這個URL需要解決的問題就是如何編程爬取URL指向的頁面。爬取到了頁面,才能算是訪問了部落格,也才能獲得頁面的內容,從而提取更多的URL,進行更進一步的爬取。
這樣一來就帶來了如何根據URL擷取頁面資訊的問題,要解決這個問題,需要用到HTTP用戶端編程,這也是接下來一節解決的問題。
urllib2:HTTP用戶端編程
Python中可以實現HTTP用戶端編程的庫有好幾個,例如httplib, urllib, urllib2等。使用urllib2是因為它功能強大,使用簡單,並且可以很容易地使用HTTP代理。
使用urllib2建立一個HTTP串連並擷取頁面非常簡單,只需要3步:
import urllib2
opener = urllib2.build_opener()file = opener.open(url)content = file.read()
content即為HTTP請求響應的報文體,即網頁的HTML代碼。如果需要設定代理,在調用build_opener()時傳入一個參數即可:
?
opener =
urllib2.build_opener(urllib2.ProxyHandler({'http':
"localhost:8580"})) |
ProxyHandler函數接受一個字典類型的參數,其中key為協議名稱,value為host與連接埠號碼。也支援帶驗證的代理,更多細節見官方文檔。
接下來要解決的問題就是從頁面中分離出URL. 這就需要Regex。
Regex
Regex相關的函數位於Python的re模組中,使用前需import re
findall函數返回字串中所有滿足給定正則式的子串:
aelems = re.findall('<a href=".*<\/a>', content)
findall的第一個參數是正則式,第二個參數是字串,傳回值是字串數組,包含content中所有滿足給定正則式的子串。上述代碼返回所有以<a href="開頭,</a>結尾的子串,即所有的<a>標籤。對網頁HTML代碼應用此過濾可擷取所有超連結。如果需要進一步提高過濾的精確度,例如只需要連結指向本部落格(假設當前部落格是http://myblog.wordpress.com),且URL為絕對位址,則可以使用更精確的正則式,例如'<a href="http\:\/\/myblog\.wordpress\.com.*<\/a>'.
擷取到了<a>標籤,就需要進一步提取其中的URL,這裡推薦match函數。match函數的作用是將滿足指定正則式的子串的其中一部分返回。例如對於以下字串(假設存於aelem元素中):
<a href="http://myblog.wordpress.com/rss">RSS Feed</a>
如果需要提取出其中的URL(即http://myblog.wordpress.com/rss),則可以如下的match調用:
matches = re.match('<a href="(.*)"', aelem)
匹配成功時,match返回MatchObject對象,否則返回None. 對於MatchObject,可以使用groups()方法擷取其中包含的所有元素,也可以通過group(下標)擷取,注意group()方法的下標是從1開始的。
以上樣本僅對只含有href一個屬性的<a>元素有效,如果<a>元素中href屬性後還有別的屬性,則按照最長相符原則,上面的match調用會返回不正確的值:
<a href="http://myblog.wordpress.com/rss" alt="RSS Feed - Yet Another WordPress Blog">RSS Feed</a>
將會匹配為:http://myblog.wordpress.com/rss" alt="RSS Feed - Yet Another WordPress Blog
目前對於這種情況還沒有特別好的解決方案,我的做法是先按照空格split一下再匹配。由於href通常都是<a>中第一個出現的屬性,所以可以簡單地如下處理:
splits = aelem.split(' ')#0號元素為'<a',1號元素為'href="http://myblog.wordpress.com/"'aelem = splits[1]#這裡的正則式對應改變matches = re.match('href="(.*)"', aelem)
當然,這種方法不能保證100%正確。最好的做法應該還是用HTML Parser. 這裡懶得搞了。
提取出URL之後,就要將URL加入URL庫。為了避免重複訪問,需要對URL去重複,這就引出了下一節中字典的使用。
字典
字典,一種儲存key-value對的關聯容器,對應C++裡的stl::hash_map,Java裡的java.util.HashMap以及C#中的Dictionary. 由於key具有唯一性,因此字典可以用來去重。當然,也可以用set,很多set就是將map簡單封裝一下,例如java.util.HashSet和stl::hash_set.
要使用字典構建一個URL庫,首先我們需要考慮一下URL庫需要做什麼:
- URL去重:URL從HTML代碼中抽取出來後,如果是已經加入URL庫的URL,則不加入URL庫
- 取新URL:從URL庫中取一個還沒訪問過的URL進行下一次爬取
為了同時做到1、2,有以下兩種直觀的做法:
- 用一個url字典,其中URL作為key,是否已訪問(True/False)作為value;
- 用兩個字典,其中一個用來去重,另一個用來存放還沒訪問過的URL.
這裡簡單起見,用的是第2種方法:
#起始URLstarturl = 'http://myblog.wordpress.com/';#全部URL,用於URL去重totalurl[starturl] = True#未訪問URL,用於維護未訪問URL列表unusedurl[starturl] = True#中間省略若干代碼#取下一個未用的URLnexturl = unusedurl.keys()[0];#將該URL從unusedurl中刪除del unusedurl[nexturl]#擷取頁面內容content = get_file_content(nexturl)#抽取頁面中的URLurllist = extract_url(content)#對於抽取出的每個URLfor url in urllist: #如果該URL不存在於totalurl中 if not totalurl.has_key(url): #那麼它一定是不重複的,將其加入totalurl中 totalurl[url] = True #並且加入為訪問列表中 unusedurl[url] = True
結束
最後貼上完整的代碼:
import urllib2import timeimport retotalurl = {}unusedurl = {}#產生ProxyHandler對象def get_proxy(): return urllib2.ProxyHandler({'http': "localhost:8580"})#產生指向代理的url_openerdef get_proxy_http_opener(proxy): return urllib2.build_opener(proxy)#擷取指定URL指向的網頁,調用了前兩個函數def get_file_content(url): opener = get_proxy_http_opener(get_proxy()) content = opener.open(url).read() opener.close() #為方便正則匹配,將其中的分行符號消掉 return content.replace('\r', '').replace('\n', '')#根據網頁HTML代碼抽取網頁標題def extract_title(content): titleelem = re.findall('<title>.*<\/title>', content)[0] return re.match('<title>(.*)<\/title>', titleelem).group(1).strip()#根據網頁HTML代碼抽取所有<a>標籤中的URLdef extract_url(content): urllist = [] aelems = re.findall('<a href=".*?<\/a>', content) for aelem in aelems: splits = aelem.split(' ') if len(splits) != 1: aelem = splits[1] ##print aelem matches = re.match('href="(.*)"', aelem) if matches is not None: url = matches.group(1) if re.match('http:\/\/myblog\.wordpress\.com.*', url) is not None: urllist.append(url) return urllist#擷取字串格式的時間def get_localtime(): return time.strftime("%H:%M:%S", time.localtime())#主函數def begin_access(): starturl = 'http://myblog.wordpress.com/'; totalurl[starturl] = True unusedurl[starturl] = True print 'seq\ttime\ttitle\turl' i = 0 while i < 150: nexturl = unusedurl.keys()[0]; del unusedurl[nexturl] content = get_file_content(nexturl) title = extract_title(content) urllist = extract_url(content) for url in urllist: if not totalurl.has_key(url): totalurl[url] = True unusedurl[url] = True print '%d\t%s\t%s\t%s' %(i, get_localtime(), title, nexturl) i = i + 1 time.sleep(2)#調用主函數begin_access()
轉載自:http://www.cnblogs.com/mdyang/archive/2012/04/03/first_python_script.html