從VOA慢速英語的網站上下載Mp3與PDF文字的Python指令碼

yinchuan發表於2013-07-24

想通過聽寫練習來提高自己的英語聽力。VOA慢速英語語速很慢(約100-120字/分鐘),發音標準清晰,內容豐富(包括新聞、詞彙諺語、人物故事等),提供錄音及文字下載,是非常不錯的入門聽寫材料。

為什麼要寫指令碼來下載呢?首先,雖然VOA有對應的Podcast節目,更新方便,但下載連結在大陸是無法訪問的,只能看到節目有更新,就是無法下載。其次,每天手動到VOA網站上去下載,似乎太傻瓜,這種重複性地工作應該要自動化處理的。

從網頁中抓取內容是非常常見的應用,處理流程一般就是下載網頁,用正則或其他解析html的工具提取需要的連結,再下載連結內容。在寫這個指令碼的過程中,遇到並解決了以下幾個問題,記錄在此。

  • 提取節目的下載連結
  • 下載內容時顯示進度條
  • 保證檔案能夠完全下載,避免重複下載
  • 使用Socks代理來解決GFW的問題

提取節目的下載連結

需要獲取3個資料:節目標題、節目PDF連結、節目Mp3連結。

VOS慢速英語除了Podcast外,還提供了RSS訂閱,可以直接解析RSS內容來獲取Mp3的下載連結;但是RSS輸出中沒有PDF文字的連結,所以我選擇了直接拉取web頁面,用正則來提取內容。

VOA慢速英語目前有7個主題,在每個主題自己的頁面可以看到當月更新的節目(我沒有找到可以列出所有節目的頁面)。在每期節目自己的頁面中,可以找到PDF的下載連結;但是Mp3的連結還需要進入線上收聽頁面才能找到。

這是主題列表頁面部分截圖:enter image description here 這是某個主題下面最新節目列表頁面部分截圖:enter image description here 這是PDF下載連結與Mp3線上收聽頁面部分截圖:enter image description here

下載內容時顯示進度條

知道當前的進度總是一件減少焦慮感的事情。找到需要的下載連結後,使用Python的urllib.urlretrieve()這個方法來下載檔案。urllib.urlretrieve()的語法如下:

urlretrieve(url, filename=None, reporthook=None, data=None)

其中,reporthook是接收一定資料後要呼叫的函式,可以利用此回撥函式來實現進度條的功能,reporthook函式的定義如下:

reporthook(blocknum, blocksize, totalsize)

其中,blocknum是接收到的資料包個數,blocksize是資料包的位元組數,totalsize是總的需要下載的位元組數。對傳入的引數進行簡單處理就可以得到當前的下載進度。如果直接用print語句輸出結果的話,每次呼叫此函式都會產生新的一行,這顯然不是進度條的行為,我們希望在同一行更新進度。首先使用sys.stdout.write()代替print語句可以去掉print自動輸出的換行符。然後,在每次輸出內容時,先輸出回車符('\b')讓游標回到行首,這樣就可以在當前行輸出內容,實現進度條的行為。reporthook函式的程式碼如下:

# show download progress
# 此函式感謝 http://ljdam.iteye.com/blog/1415336 的分享
def reporthook(blocks_read,block_size,total_size):  
    if not blocks_read:  
        print ("Connection opened")  
    if total_size <0:  
        sys.stdout.write("\rRead %d blocks   "  % blocks_read)
        sys.stdout.flush()
    else:  
        sys.stdout.write("\rdownloading: %d KB, totalsize: %d KB   " % (blocks_read*block_size/1024.0,total_size/1024.0))
        sys.stdout.flush()

保證檔案能夠完全下載,避免重複下載

首先避免重複下載,當本地已存在待下載檔案時,先從伺服器上獲取待下載的位元組數,與本地檔案對比,如果大小相等,那麼認為已經正常下載,跳過此檔案。這裡存在一個問題,有的伺服器在hearder中不包含Content-Length這個欄位,這時無法判斷是否已完全下載。考慮到這種情況很少,且重複下載代價較高,所以也不下載此檔案。

if os.path.isfile(file_path):
    # check length
    length_s = urllib.urlopen( url ).info().get('Content-Length', 0)
    length_l = os.path.getsize( file_path )
    if length_s == 0 or long(length_s) == length_l:
        return True

然後避免出現未下載完成的檔案。在下載時,當關閉程式視窗、手動停止(Ctrl+C)、出現錯誤時,會在檔案系統中殘留未下載完成的檔案,考慮到這種檔案會影響收聽,所以應當避免出現未下載完成的檔案。處理的思路是,如果在下載過程中遇到任何異常,那麼首先刪除未完成的郵件,然後將異常冒泡進行後續處理。

try:
    urllib.urlretrieve(url, file_path, reporthook)
except:
    os.remove( file_path)
    raise
else:
    return True

使用Socks代理來解決GFW的問題

首先感謝這個部落格的分享。Python可以通過SocksiPy模組來使用Socks代理。SocksiPy的下載與安裝見官網。這部分的程式碼如下:

import socks
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', 9050, rdns=False)
socket.socket = socks.socksocket

這裡首先設定了socks的代理資訊,socks.PROXY_TYPE_SOCKS5表示型別為socks5,127.0.0.19050分別是代理的IP與埠。我這裡使用的是本機作為代理。然後用具有代理功能的套接字覆蓋系統預設的套接字,後續連線就全部走代理了,非常方便。

完整程式碼請訪問這個gist。最後是程式執行截圖:enter image description here

相關文章