第一個Python程序——博客自動訪問腳本


動機

今天有朋友寫信說他認為自己的wordpress博客內顯示的訪問統計信息不正常,希望我能為他制造一些訪問信息,供他對比。朋友提出的請求是在短時間內快速打開100個不同的博客頁面,以便他從產生的訪問量變化中理解博客訪問數據。

本人作為一個搞計算機的人,有把任何重復性勞動自動化的沖動,所以雖然點開100個網頁的任務手工做並不復雜,但還是從一開始就徹底否定了。剛好想學Python很久了,於是就拿這次的小機會來學習一把,順便記錄下第一次的Python學習成果。

本文使用Python 2.7.3實現了一個自動訪問博客的腳本,涉及以下技術點:

  • 語言基礎
    • 容器(線性表、字典)
    • 邏輯分支、循環
    • 控制台格式化輸出
  • HTTP客戶端網絡編程
    • 處理HTTP請求
    • 使用HTTP代理服務器
  • Python正則表達式

總覽

自動訪問博客頁面這個操作實際上和網絡爬蟲做的事情差不多,基本流程如下:

圖1 博客自動訪問器工作原理

  1. 給訪問器一個開始位置(例如博客首頁URL)
  2. 訪問器將URL指向的網頁爬回(爬回網頁這個操作本身相當於在瀏覽器中打開頁面)
  3. 2中爬回的網頁交給分析器分析。分析器分析后提取出其中的URL加入待訪問URL列表,即URL庫。然后從URL庫中取出下一個要訪問的頁面URL
  4. 循環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. 這就需要正則表達式。

正則表達式

正則表達式相關的函數位於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庫需要做什么:

  1. URL去重:URL從HTML代碼中抽取出來后,如果是已經加入URL庫的URL,則不加入URL庫
  2. 取新URL:從URL庫中取一個還沒訪問過的URL進行下一次爬取

為了同時做到1、2,有以下兩種直觀的做法:

  1. 用一個url字典,其中URL作為key,是否已訪問(True/False)作為value;
  2. 用兩個字典,其中一個用來去重,另一個用來存放還沒訪問過的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()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM