Web::Scraper in Python (もしくは scrAPI in Python)

lxml2.0からCSSSelectorが導入されたので、Web::Scraperのようなものを作ってみました。
とりあえず動作するところまでいったので載せておきます。機能はまだ全然足りてないので、簡単なことしかできません。
Python2.5とlxml2.0alpha が必要です。

Pythonはリストや辞書の中にある日本語をそのままprintで表示できないようなので、めんどくさいことに全部stringにして出力してます。

Web::Scraper - naoyaのはてなダイアリーよりキーワードページから必要なデータをもってくる例。

#!/usr/bin/env python2.5
from scraper import scraper, process
import codecs, sys
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)

s = scraper(
    process('span.title > a:first-child', title='TEXT', url='@href'),
    process('span.furigana', furigana='TEXT'),
    process('ul.list-circle > li:first-child > a', category='TEXT'),
)

result = s.scrape('http://d.hatena.ne.jp/keyword/%BA%B0%CC%EE%A4%A2%A4%B5%C8%FE')
print ''.join(result['category'])
print ''.join(result['furigana'])
print ''.join(result['title'])
print ''.join(result['url'])
$ ./keyword.py
アイドル
こんのあさみ
紺野あさ美
/keyword/%ba%b0%cc%ee%a4%a2%a4%b5%c8%fe

Web::ScraperよりFlickrからサムネイルURLをとってくる例。

#!/usr/bin/env python2.5
from scraper import scraper, process
import codecs, sys
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)

s = scraper(
    process('a.image_link img', thumbs="@src"),
)

result = s.scrape('http://www.flickr.com/photos/bulknews/sets/72157601700510359/')

print "\n".join(result['thumbs'])

scrape.py自体はこんな感じ

#!/usr/bin/env python2.5
# -*- coding: utf-8 -*-
from urllib import urlopen
from lxml import etree

def scraper(*funcs):
    class Scraper(object):
        def __init__(self, funcs):
            self.funcs = funcs
            
        def scrape(self, url):
            from StringIO import StringIO
            stash = {}
            res = urlopen(url)
            html = res.read().decode(res.headers.getparam('charset') or 'latin-1')
            tree = etree.parse(StringIO(html), etree.HTMLParser())
            for f in self.funcs:
                xpath, attr = f()
                for key, val in attr.iteritems():
                    if val.startswith('@'):
                        stash[key] = [e.attrib[val[1:]] for e in tree.xpath(xpath)]
                    elif val.upper() == "TEXT":
                        stash[key] = [e.text for e in tree.xpath(xpath)]
                    else:
                        print "Got an unknown thingy: ", what
            return stash
        
    return Scraper(funcs)


def create_process(func):
    def do(selector, **kwargs):
        def wrap():
            return func(selector, kwargs)
        return wrap
    return do


@create_process
def process(selector, kwargs):
    from lxml.cssselect import  CSSSelector
    xpath = selector if selector.startswith('/') else CSSSelector(selector).path
    return xpath, kwargs