图灵社区 : 阅读 : 从VOA慢速英语的网站上下载Mp3与PDF文本的Python脚本

想通过听写练习来提高自己的英语听力。VOA慢速英语语速很慢(约100-120字/分钟),发音标准清晰,内容丰富(包括新闻、词汇谚语、人物故事等),提供录音及文本下载,是非常不错的入门听写材料。

为什么要写脚本来下载呢?首先,虽然VOA有对应的Podcast节目,更新方便,但下载链接在大陆是无法访问的,只能看到节目有更新,就是无法下载。其次,每天手动到VOA网站上去下载,似乎太傻瓜,这种重复性地工作应该要自动化处理的。

从网页中抓取内容是非常常见的应用,处理流程一般就是下载网页,用正则或其他解析html的工具提取需要的链接,再下载链接内容。在写这个脚本的过程中,遇到并解决了以下几个问题,记录在此。

提取节目的下载链接

需要获取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