[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分まで短縮できました。