はじめに
こんにちは、Python界の炭酸x2倍のジョルトコーラです。なんか適当に書いたコードが予想外にはてブ付いたので「みんな好きモノなんだなー」と思いました。同期なコードだとURL増えたときに詰まっちゃうので、非同期なやつもちょろっと紹介しますよ。
あ、くれぐれも闇雲なスクレイピングはしないでくださいよ。DoS攻撃と変わらないですから。捕まっても僕は責任とりませんよ。
リンク
- 5分でWebスクレイピングをする - YAMAGUCHI::weblog
- 前回のエントリ。これは直列なコードなのでURLが増えれば増えるほど線形に遅くなる。
やってみよう
おさらい
前回は指定した複数のURLのページにあるHTMLからaタグを全部抜き出す、というようなことをしたのでした。で、今回はそれを非同期化しましょうという話。
非同期にしたいところ
前のコードでforとかになってるところは基本的に同期じゃなくていいですよね。なので次の2つに関しては同時にばーっと処理させたいわけです。
用意するもの
Pythonでネットワークを並列処理しようと思ったらいくつかライブラリがありますが、geventがとてもらくちんなので今回はこれを使いましょう。
まあ前回のエントリと一緒ですが、今回はgeventを使うのでそのへんの設定が必要です。geventはlibeventとgreenletにめっちゃ依存してるので先にインストールしておきます。めんどくさいのでMacPortsとpipでいれときますよ。
ほんとはgeventもpipでインストールしたかったんだけど、なんかevent.hが見つからないみたいなエラーが出てるんでsetup.py叩きます。
$ sudo port install libevent $ pip install greenlet $ curl http://pypi.python.org/packages/source/g/gevent/gevent-0.13.1.tar.gz#md5=5c1b03d9ce39fee4cfe5ea8befb1d4c4 -o gevent-0.13.1.tgz $ tar xzf gevent-0.13.1.tgz $ cd gevent-0.13.1 $ python setup.py install -I /opt/local/include -L /opt/local/lib ... Copying gevent-0.13.1-py2.6-macosx-10.6-x86_64.egg to /Users/ymotongpoo/.virtualenvs/main/lib/python2.6/site-packages Adding gevent 0.13.1 to easy-install.pth file Installed /Users/ymotongpoo/.virtualenvs/main/lib/python2.6/site-packages/gevent-0.13.1-py2.6-macosx-10.6-x86_64.egg Processing dependencies for gevent==0.13.1 Searching for greenlet==0.3.1 Best match: greenlet 0.3.1 Adding greenlet 0.3.1 to easy-install.pth file Using /Users/ymotongpoo/.virtualenvs/main/lib/python2.6/site-packages Finished processing dependencies for gevent==0.13.1
今回はこんな感じ
urls = [ 'http://python-history-jp.blogspot.com/', # UTF-8 'http://iblinux.rios.co.jp/PyJdoc/lib-j/', # EUC-JP 'http://osksn2.hep.sci.osaka-u.ac.jp/~taku/osx/python/encoding.html', # ISO-2022-JP 'http://www.atmarkit.co.jp/news/200812/04/python.html', # Shift_JIS 'http://pyunit.sourceforge.net/pyunit_ja.html', # EUC-JP, w/o META 'http://www.f7.ems.okayama-u.ac.jp/~yan/python/', # ISO-2022-JP, w/o META 'http://weyk.com/weyk/etc/OpenRPG.html', # Shift_JIS, w/o META ] import gevent from gevent import monkey monkey.patch_all() # 諸々の標準ライブラリにパッチを当てる import urllib import chardet from pyquery import PyQuery as pq def find_hyperlinks(url): # spawnさせたい関数 data = ''.join(urllib.urlopen(url).readlines()) guess = chardet.detect(data) p = dict(url=url,data=data,**guess) print '***** %s -> %s (%s)' % (p['url'], p['encoding'], p['confidence']) p['data'] = p['data'].decode(p['encoding']) d = pq(p['data']) page_title = pq(d.find('title')).text() def extract_hyperlink(page_title, link): # 関数内でさらにspawnさせるために内部で宣言 link_title = pq(link).text() link_url = pq(link).attr.href print '(%s) : %s => %s' % (page_title, link_title, link_url) jobs = [gevent.spawn(extract_hyperlink, page_title, link) for link in pq(d.find('a'))] gevent.joinall(jobs) if __name__ == '__main__': jobs = [gevent.spawn(find_hyperlinks, url) for url in urls] decoded = gevent.joinall(jobs)
まあこのサンプルだとネットワークに繋ぎにいってるところが実質1箇所なのでgevent使ってる意味がほぼないわけですが、ちょっと変えてやると大変いい感じになります。
おわりに
この例ではあくまでネットワークへの接続はマルチスレッド化されていますが、マルチプロセス化はしてません。あくまで非同期に多数の接続をしたいですね、というだけですので。もし負荷が高い処理がしたいなら、multiprocessingモジュールを使ってもいいかもしれませんね。
おまけ
MacPorts上のlibeventを読みに行けなかった時のエラーはこんな感じで始まる。
... In file included from gevent/core.c:202: gevent/libevent.h:9:19: error: event.h: No such file or directory gevent/libevent.h:34:20: error: evhttp.h: No such file or directory ...
おまけ2
Cygwinでもgeventはインストールできたんですが、gevent.monkey.patch_socket()でgevent.socketがどうもおかしい。Windowsでやるんだったらlibeventとかはmemcachedとかに入ってるやつを使った方がいいと思います。
$ python test.py Traceback (most recent call last): File "build/bdist.cygwin-1.7.7-i686/egg/gevent/greenlet.py", line 405, in run result = self._run(*self.args, **self.kwargs) File "test.py", line 15, in url_get print opener.open(url).read() File "/usr/lib/python2.6/urllib2.py", line 391, in open response = self._open(req, data) File "/usr/lib/python2.6/urllib2.py", line 409, in _open '_open', req) File "/usr/lib/python2.6/urllib2.py", line 369, in _call_chain result = func(*args) File "/usr/lib/python2.6/urllib2.py", line 1161, in http_open return self.do_open(httplib.HTTPConnection, req) File "/usr/lib/python2.6/urllib2.py", line 1136, in do_open raise URLError(err) URLError: <urlopen error [Errno 67] request timed out> <Greenlet at 0x7fd1836c: url_get('http://www.google.com')> failed with URLError
一応インストールは下記でできます。
libeventのtarballを落としてきて、configure && make && make installでコンパイルできます。デフォルトだったら/usr/local以下に入るはずなので、setup.pyも次のようになりますね。
$ python setup.py install -I /usr/local/include -L /usr/local/lib