pythonでプログレスバーを表示させながらダウンロードする

バイナリファイルをオンメモリで一度に処理するのではなくバッファ単位で少しずつ処理するコードのサンプル作ってみた。ついでにプログレスバーで表示。

使い方:
入力:ファイルのurlリストをプログラムの引数で渡すか、標準入力から1行ごとに入力するかどちらか。
出力:プログレスバーを表示しながらファイルをダウンロードする

処理を簡単にするために、サーバは適切にContent-Lengthをレスポンスヘッダにつけると仮定してる。

# coding: utf-8
# downloader.py
# sample code for displaying progress bar while downloading file on network
#

import sys
import os
import itertools
import urllib

class ProgressBar:
    """displaying a simple progress bar"""
    def __init__(self, whole, barsize=30, ch="#", nch=" ", message=""):
        self.whole = whole
        self.barsize = barsize
        self.ch = ch
        self.nch = nch
        self.message = message
    def update(self, current):
        done = float(current) / self.whole
        chs = int(done * self.barsize)
        bar = [i <= chs and self.ch or self.nch for i in xrange(self.barsize)]
        bar.insert(0, "[")
        bar.append("] %d%%" % int(done * 100.))
        bar.append("\r")
        bar.insert(0, self.message + " : ")
        sys.stdout.write("".join(bar))
        sys.stdout.flush()
    def terminate(self):
        sys.stdout.write("\n")
        sys.stdout.flush()

class ProgressBarDownloader(ProgressBar):
    """download file on network with displaying progress bar"""
    BUFSIZE = 1024*100
    def __init__(self, fileurl):
        self.conn = urllib.urlopen(fileurl)
        filename = fileurl[fileurl.rfind("/")+1:].split("?")[0]
        if os.path.exists(filename):
            for i in itertools.count(1):
                if not os.path.exists(filename + "." + str(i)):
                    filename = filename + "." + str(i)
                    break
        if filename == "":
            filename = "default"
        # fetch content-length header
        self.contentLength = int(self.conn.headers["content-length"])
        message = filename
        if len(message) > 18:
            message = "".join([message[:10], " .. " , os.path.splitext(filename)[1]])
        message = message.ljust(20)
        ProgressBar.__init__(self, self.contentLength, message=message)
        sys.stdout.flush()
        # create file to be written
        self.file = open(filename, "wb")
    def start(self):
        self.update(0)
        acc = 0
        while True:
            buffer = self.conn.read(ProgressBarDownloader.BUFSIZE)
            if not buffer:
                break
            acc += len(buffer)
            self.file.write(buffer)
            self.update(acc)
        self.terminate()
        self.conn.close()
        self.file.close()

def main():
    urls = map(lambda s: s.strip(), sys.stdin.readlines())
    if sys.argv.__len__() >= 2:
        urls = sys.argv[1:]
    # UA変更    
    urllib.FancyURLopener.version = "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.1) Gecko/20090624 Firefox/3.5"

    for url in urls:
        ProgressBarDownloader(url).start()

if __name__ == '__main__':
    main()

入力ファイル例:
下のテキストをファイル名「in」で保存

http://upload.wikimedia.org/wikipedia/commons/thumb/8/82/CharlesBabbage.jpg/200px-CharlesBabbage.jpg
http://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Alan_Turing_Memorial_Closer.jpg/180px-Alan_Turing_Memorial_Closer.jpg
http://v-t.jp/jp/topics/column/img/shima.jpg
http://upload.wikimedia.org/wikipedia/commons/thumb/b/b3/Larry_Wall_YAPC_2007.jpg/180px-Larry_Wall_YAPC_2007.jpg
http://upload.wikimedia.org/wikipedia/commons/f/f7/Richard_Matthew_Stallman.jpeg
http://upload.wikimedia.org/wikipedia/commons/thumb/6/69/Linus_Torvalds.jpeg/391px-Linus_Torvalds.jpeg
http://upload.wikimedia.org/wikipedia/commons/0/00/James_Gosling_2005.jpg
http://upload.wikimedia.org/wikipedia/commons/e/e3/Paulgraham_240x320.jpg
http://japanese.joelonsoftware.com/Images/TinyJoel.jpg
http://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Donald_Knuth_DSC00624.jpg/180px-Donald_Knuth_DSC00624.jpg
http://jibun.atmarkit.co.jp/ljibun01/rensai/genius/01/images/genius01_01.jpg

実行結果

$ python downloader.py < in
200px-Char .. .jpg   : [##############################] 100%
180px-Alan .. .jpg   : [##############################] 100%
shima.jpg            : [##############################] 100%
180px-Larr .. .jpg   : [##############################] 100%
Richard_Ma .. .jpeg  : [##############################] 100%
391px-Linu .. .jpeg  : [##############################] 100%
James_Gosl .. .jpg   : [##############################] 100%
Paulgraham .. .jpg   : [##############################] 100%
TinyJoel.jpg         : [##############################] 100%
180px-Dona .. .jpg   : [##############################] 100%
genius01_01.jpg      : [##############################] 100%