動機
今天有朋友寫信說他認為自己的wordpress部落格內顯示的訪問統計資訊不正常,希望我能為他製造一些訪問資訊,供他對比。朋友提出的請求是在短時間內快速開啟100個不同的部落格頁面,以便他從產生的訪問量變化中理解部落格訪問資料。
本人作為一個搞電腦的人,有把任何重複性勞動自動化的衝動,所以雖然點開100個網頁的任務手工做並不複雜,但還是從一開始就徹底否定了。剛好想學Python很久了,於是就拿這次的小機會來學習一把,順便記錄下第一次的Python學習成果。
本文使用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種方法:
#起始URL
starturl = 'http://myblog.wordpress.com/';
#全部URL,用於URL去重
totalurl[starturl] = True
#未訪問URL,用於維護未訪問URL列表
unusedurl[starturl] = True
#中間省略若干代碼
#取下一個未用的URL
nexturl = unusedurl.keys()[0];
#將該URL從unusedurl中刪除
del unusedurl[nexturl]
#擷取頁面內容
content = get_file_content(nexturl)
#抽取頁面中的URL
urllist = extract_url(content)
#對於抽取出的每個URL
for url in urllist:
#如果該URL不存在於totalurl中
if not totalurl.has_key(url):
#那麼它一定是不重複的,將其加入totalurl中
totalurl[url] = True
#並且加入為訪問列表中
unusedurl[url] = True
結束
最後貼上完整的代碼:
import urllib2
import time
import re
totalurl = {}
unusedurl = {}
#產生ProxyHandler對象
def get_proxy():
return urllib2.ProxyHandler({'http': "localhost:8580"})
#產生指向代理的url_opener
def 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>標籤中的URL
def 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()