2011-02-05

AppEngine上で一括ダウンローダーを作ろうとしたものの……

前回の記事で書きましたが、私が英語学習のために使っていたサービス「smart.fm」が終了し、有料の新サービス「iKnow!」に移行します。有料化というのも痛いのですが、新サービスのiKnow!には、旧サービスのsmart.fmにあったSNS的な機能がまったく存在しないというのもかなり重大な話です。

今後新サイトでSNS要素がどうなるかは分かりませんが、少なくともsmart.fmが終了するにあたり、smart.fmのSNSにおける主要コンテンツである「日記」については今後消滅する公算が大なので、なんとかローカルに保存したいところです。

というわけで、smart.fmの日記コンテンツをzipで一括ダウンロードする「Smart.fm Memento Maker」なるツールを大急ぎで作っているところです。日記を取得するスマートな方法がないものか模索したのですが、いい手はないようなので、まずは全日記エントリのHTMLを力技で抜いてくるプログラムをでっち上げました。

かなり時間はかかるものの、ローカルではちゃんと動いているようなのでGoogle AppEngineにデプロイしたのですが、「DeadlineExceededError」が発生してまともに動きません。しまった、AppEngineでは一定期間内に応答を返さないとダメなのを忘れていました。これは再設計が必要かも……。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import urllib2
import urlparse
import StringIO
import zipfile
import logging
import wsgiref.handlers
from HTMLParser import HTMLParser
from google.appengine.ext import webapp

class MyParser(HTMLParser) :
    def __init__(self, base_url):
        HTMLParser.__init__(self)
        self.base_url = re.match(r'(http://.*/)', base_url).group(0)
        self.entries = set()

    def handle_starttag(self, tag, attrs):
        dic = dict(attrs)
        if tag == 'a':
            url, frag = urlparse.urldefrag(urlparse.urljoin(self.base_url, dic['href']))
            if re.match(self.base_url + 'journal/[0-9]+/[0-9]+/[0-9]+/[0-9]+', url):
                self.entries.add(url)

class MainPage(webapp.RequestHandler):
    def get(self):
        # Gets the parameters.
        user_id = self.request.get('user_id').encode('utf8', 'replace')
        if not user_id:
            self.error(405)
            return

        # Gets all the journal URLs.
        entries = set()
        for page in range(1, 1000):
            url = 'http://smart.fm/users/' + user_id + '/journal?page=' + str(page)
#           logging.info('URL: ' + url)
            r = urllib2.urlopen(url)

            parser = MyParser(url)
            parser.feed(r.read())
            if not parser.entries:
                break

            entries |= parser.entries

        if not entries:
            self.response.headers['Content-Type'] = 'text/plain'
            self.response.out.write('Not found.\n')
            return

#       logging.info('URLs:\n' + '\n'.join(entries))

        # Gets all the journal HTMLs and stores them into a zip file.
        zipdata = StringIO.StringIO()
        zipobj = zipfile.ZipFile(zipdata, 'w', zipfile.ZIP_DEFLATED)
        for url in sorted(entries):
            r = urllib2.urlopen(url)
            filepath = re.sub(r'^http://', '/', re.match(r'(http://.*/[0-9]+)', url).group(0)) + '.html'
#           logging.info('File path: ' + filepath)
            if not filepath in zipobj.namelist():
                zipobj.writestr(filepath, r.read())
        zipobj.close()

        # Returns a generated zip file as a response.
        self.response.headers['Content-Type'] = 'application/octet-stream'
        self.response.headers['Content-Disposition'] = 'attachment; filename="' + user_id + '.zip"'
        self.response.out.write(zipdata.getvalue())

def main():
    application = webapp.WSGIApplication([
        ('/memento', MainPage),
        ], debug = True)
    wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
    main()

0 件のコメント:

コメントを投稿