[programming][python] サーバに負担をかけずにしかも早くダウンロードする

yahooの検索APIなどを使って大量のURLのリストを手に入れた後は当然ダウンロードするわけですが、こういう検索エンジンを使って検索すると、同じホストにある違う名前のファイルが10個20個連なる事があります。
こんな感じ。これはyahoo画像検索で"python"をキーワードにしたときに得られたリストの一部。www.gotpetsonline.comが連続しています。

http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/python-pictures-breeders-babies/pictures-photos/python-0021.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/python-pictures-breeders-babies/pictures-photos/python-0033.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/python-pictures-breeders-babies/pictures-photos/python-0019.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/burmese-python-pictures-breeders-babies/pictures-photos/burmese-python-0012.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/burmese-python-pictures-breeders-babies/pictures-photos/burmese-python-0009.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/python-pictures-breeders-babies/pictures-photos/python-0019.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/burmese-python-pictures-breeders-babies/pictures-photos/burmese-python-0012.jpg
http://www.gotpetsonline.com/pictures-gallery/reptile-pictures-breeders-babies/burmese-python-pictures-breeders-babies/pictures-photos/burmese-python-0009.jpg

こういうときはサーバに負担をかけない為、ダウンロード毎に何秒か待ち時間をはさむのが一般的なようです。単純には、全てのダウンロードの間に待ち時間を入れれば同一のホストに対するリクエストが集中する事を防げます。ただし、全てのURLのホストが同じホストである訳ではないので、全てのダウンロード毎に待つのは無駄です。あるホストにリクエストした直後でも、異なるホストにリクエストは可能な(というか許容される)ので。あるいは直前のホストを記憶しておき、ダウンロード毎にそれと比較して同じならば待ちが完了するまで停止するというのも考えられます。この時も、その待ち時間の間、別のホストにリクエストを出すことが可能です。

どうにかして、サーバに負担を書けずに、しかも早くダウンロードしたい。と考えてスクリプトを書いてみました。汚いソース。。。一度メインループに入ったらプログラム全体が終了するまで走りつづけます。しかも複数のスレッドが同時にポーリングループを使うので、CPU使用率が跳ね上がります。
ただ、一応動きます。

#! /usr/bin/python
# -*- coding: sjis -*-
"""ダウンロード要求を受け取って、t秒以内にダウンロードした
ホストへのリクエストは待機が完了するまで待ち、それ以外の
ホストに対する要求は即座に実行する、そんなモジュール

キューとリストからなり、キューには要求URLが格納され、リストにはt秒
以内にリクエストを出したホストをあらわす文字列が格納される。
これは新しく生成されるスレッドによってt秒後にクリアされる。

リストは排他制御されていないので、競争状態が生じる可能性がある。
・・でももし起きたとしてもさしたる実害はないので放っておく。

"""
import thread
import re
import urllib
import socket
import time
from Queue import Queue

socket.setdefaulttimeout(10)

class ThreadDownloader:
    """startメソッドでスレッドスタート、requestメソッドで
    要求の送信。URLはホスト名の後に/が必要。
    >>> s = ThreadDownloader()
    >>> s.start()
    >>> s.request("http://www.example.com/") # OK
    >>> s.request("http://www.example.com") # NG!
    """
    splitter = re.compile(r"""(?:ftp|s?https?)://(.*?)/.*""")
    def __init__(self, maxhost=20, waittime=3, savedir="./"):
        self._maxhost = maxhost
        self._q = Queue()
        self._hosts = [""]*maxhost
        self.waittime = waittime
        self.savedir = savedir # ファイルの保存先のルートディレクトリ
    def maxhost(self):
        return self._maxhost
    def register(self, host):
        while self._hosts.count("")==0:
            pass
        idx = self._hosts.index("")
        #print "register",host
        self._hosts[idx] = host
        thread.start_new_thread(self.vanish, (idx,))
    def vanish(self, idx):
        time.sleep(self.waittime)
        #print "kill %d (%s)" % (idx, self._hosts[idx])
        self._hosts[idx] = ""
    def empty(self):
        return self._q.empty()
    def request(self, url):
        self._q.put(url)
    def start(self):
        thread.start_new_thread(self.mainloop, ())
    def download(self, url):
        "overload yourself"
        #print "download",url
        src = url
        dst = urllib.unquote( url.split("?")[0].split("/")[-1] )
        try:
            urllib.urlretrieve(src, self.savedir+dst)
        except:
            print "failure:",url
        else:
            print "success:",url
    def mainloop(self):
        while 1:
            url =""
            while url == "":
                try:
                    url = self._q.get(block=True, timeout=self.waittime)
                except:
                    pass
            host = self.splitter.findall(url)[0]
            if host in self._hosts:
                self._q.put(url)
            else:
                self.register(host)
                self.download(url)

if __name__ == '__main__':
    s = ThreadDownloader()
    s.start()
    for n in open("urls").read().split(): s.request(n)
    while not s.empty():
        continue
        print s._hosts
    time.sleep(s.waitime+1)

open("urls")では、改行で区切られたURLのファイルを開いています。

このクラスを使って、ヤフー画像検索で得られた(アレゲな)URLを全てダウンロードしてみて時間を計測したところ、全てのダウンロードの間に待ちを入れた場合51分かかっていたのが34分まで短縮できました。