YAMAGUCHI::weblog

海水パンツとゴーグルで、巨万の富を築きました。カリブの怪物、フリーアルバイター瞳です。

gevent+pyqueryで並列にスクレイピングする

はじめに

こんにちは、Python界の炭酸x2倍のジョルトコーラです。なんか適当に書いたコードが予想外にはてブ付いたので「みんな好きモノなんだなー」と思いました。同期なコードだとURL増えたときに詰まっちゃうので、非同期なやつもちょろっと紹介しますよ。
あ、くれぐれも闇雲なスクレイピングはしないでくださいよ。DoS攻撃と変わらないですから。捕まっても僕は責任とりませんよ。

リンク

やってみよう

おさらい

前回は指定した複数のURLのページにあるHTMLからaタグを全部抜き出す、というようなことをしたのでした。で、今回はそれを非同期化しましょうという話。

非同期にしたいところ

前のコードでforとかになってるところは基本的に同期じゃなくていいですよね。なので次の2つに関しては同時にばーっと処理させたいわけです。

  1. ハイパーリンクを取得したいURLそれぞれ
  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