いまさら select、といっても SQL ではないのだ
逃避行動してますか?
curl とか wget とかあるのに作りたくなってしまうのが http クライアントですよね。http 経由で色々落ちていますから、拾いたいです。簡単なの (エラー処理? なにそれ美味しいの?) ならボクにだって書けるよ!!
import urlparse import socket import sys url = urlparse.urlparse(sys.argv[1]) sock = socket.socket() sock.connect((url.hostname, 80)) print "retrieving %s" % sys.argv[1] file = open(url.path.split('/')[-1], 'wb') request = 'GET %s HTTP/1.0\nHost: %s\n\n' % (url.path, url.hostname) sock.send(request) buf = b'' while True: buf += sock.recv(8192) pos = buf.find(b'\r\n\r\n') if pos > -1: buf = buf[pos+4:] break file.write(buf) while True: tmp = sock.recv(8192) if tmp == '': break else: file.write(tmp) file.close()
こんなの作れば
$ time python wget.py http://pypi.python.jp/ipython/ipython-0.10.1.tar.gz retrieving http://pypi.python.jp/ipython/ipython-0.10.1.tar.gz real 0m5.167s user 0m0.116s sys 0m0.188s $ tar tfz ipython-0.10.1.tar.gz ipython-0.10.1/ ipython-0.10.1/IPython/ ipython-0.10.1/IPython/OutputTrap.py ipython-0.10.1/IPython/Prompts.py ipython-0.10.1/IPython/kernel/ ipython-0.10.1/IPython/kernel/parallelfunction.py ipython-0.10.1/IPython/kernel/__init__.py (あと省略)
わーい、取れたよー。でも 5 秒ちょっと掛かってますね。沢山一片に取りたいときには
$ time (python wget.py http://pypi.python.jp/ipython/ipython-0.10.1.tar.gz; python wget.py http://pypi.python.org/packages/source/D/Django/Django-1.3.tar.gz) retrieving http://pypi.python.jp/ipython/ipython-0.10.1.tar.gz retrieving http://pypi.python.org/packages/source/D/Django/Django-1.3.tar.gz real 0m15.543s user 0m0.280s sys 0m0.392s
まぁこうすればいいわけですが、でもこれ、何か無駄な気がしますよね。1つめの file を取得してから 2 つめのファイルを取得していますが、違うサイトから取ってるなら同時取得すればいいのに。
shell で 2 プロセス走らせるってのは 1 つの解決策だとは思いますが、何だか切ないし。そんなときに使うのが select です。select はファイルデスクリプタがいま読み書きなどできるかどうか kernel に問い合わせる仕組みです。
>>> import select >>> import socket >>> sock = socket.socket() >>> sock.connect(('www.google.com', 80)) >>> sock.fileno() 3 >>> select.select((3,), (3,), (), 1) ([], [3], []) >>> sock.send('GET / HTTP/1.0\n\n') 16 >>> select.select((3,), (3,), (), 1) ([3], [3], []) >>> sock.recv(64) 'HTTP/1.0 302 Found\r\nLocation: http://www.google.co.jp/\r\nCache-Co'
select.select() は list の 3-tuple を返してきて、それぞれ
- 読めるファイルデスクリプタ
- 書けるファイルデスクリプタ
- 奇しくなってるファイルディスクリプタ
の番号が詰まっています。上の例だと、1 つめの select.select() では何もしてないのでデータが送れるファイルデスクリプタのみ報告されてますが、GET を送った後の 2 つめの select.select() では読める 3 番が 1 つめの list の中に増えています。つーかこれ読めばいいと http://www.python.jp/doc/2.6/library/select.html 。
select を使うと多分こんな感じになります。
# coding=utf8 import urlparse import select import socket import sys # socket.socket.fileno() 毎に色々と準備する socks = {} files = {} bufs = {} states = {} # 引数は全て URL のつもりで取得するものとして登録してく for arg in sys.argv[1:]: print "retrieving %s" % arg url = urlparse.urlparse(arg) # socket 開いて fileno を取って登録 sock = socket.socket() fd = sock.fileno() socks[fd] = sock sock.connect((url.hostname, 80)) # 保存先ファイルを開いて登録 file = open(url.path.split('/')[-1], 'wb') files[fd] = file # 他のものも登録 bufs[fd] = b'' states[fd] = 'header' # GET 投げとく request = 'GET %s HTTP/1.0\nHost: %s\n\n' % (url.path, url.hostname) sock.send(request) while True: # 読めそうな socket を調べる rlist = select.select(socks.keys(), (), (), 1)[0] # 全ての socket に対して for fd in rlist: # HEADER が返ってきてるところなら if states[fd] == 'header': # 溜めて bufs[fd] += socks[fd].recv(8192) # HEADER が終わってたらコンテンツを保存開始 pos = bufs[fd].find(b'\r\n\r\n') if pos > -1: bufs[fd] = bufs[fd][pos+4:] states[fd] = 'contents' files[fd].write(bufs[fd]) # 保存し始めてたら else: tmp = socks[fd].recv(8192) # 読めるけど読んで空なら終わりかな if tmp == '': files[fd].close() socks[fd].close() del socks[fd] del files[fd] del bufs[fd] del states[fd] # 返ってきたものは保存 else: files[fd].write(tmp) # 監視する socket が無くなったら終わり if socks.keys() == []: break
$ time python wget2.py http://pypi.python.jp/ipython/ipython-0.10.1.tar.gz http://pypi.python.jp/Django/Django-1.3.tar.gz retrieving http://pypi.python.jp/ipython/ipython-0.10.1.tar.gz retrieving http://pypi.python.jp/Django/Django-1.3.tar.gz real 0m10.767s user 0m0.360s sys 0m0.296s
速くなったよ!!
でも、ファイル毎のダウンロード進捗でも出さないと面白くないですね。取ってきてるファイルも面白くないですね。