HTMLを正規表現で指定してRSSに変換するスクリプト

今でもブログを使わずにHTML直書きで日記を書いている方が居る。RSSリーダーで読みたい。そういうときに使えるサービスとしてPage2Feed APIがあるけど、自動処理よりは自分で指定した方が綺麗に抜き出せるので、そんなスクリプトを書いた。
著作権とかセキュリティとかが不安なので設置した物を公開するのは止めておきます。適当なサーバーに置いて使ってください。スクリプトは続きに。

こんな感じのページに対して、

こんな感じで設定して、リンクをRSSリーダーに登録すると、

こんな感じになる。
ETagをRSSの項目にすることができるので正規表現から漏れても更新されたことはわかる。
RSSのリンクの末尾にconf=onを付けると再設定可能。

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

# 使用例
#   Title: Google のサービス
#   URL: http://www.google.co.jp/intl/ja/options/
#   RegExp: <li id=".*?"><a href=".*?">(.*?)<span></span></a><p>(.*?)\n
#   Title group: 1
#   Description group: 2

import urllib2
import cgi
import re
import htmllib
import sys
import sqlite3
import time
import hashlib

MAX_PAGE_SIZE   = 100*1024
CACHE_FILE      = r"page2rss/cache"
GOOGLE_API_KEY  = ""

class MyError(Exception): pass

# main
def main():
    stor = cgi.FieldStorage()
    
    if len(stor)==0 or "conf" in stor and stor["conf"].value.decode("utf-8")=="on":
        printhtml(stor)
        return

    try:
        # 読み込み
        if "title" not in stor:
            raise MyError(u"ページタイトルが指定されていません")
        if "url" not in stor:
            raise MyError(u"URLが指定されていません")

        content,etag = loadpage(stor["url"].value.decode("utf-8"))

        # 解析
        try:
            if "reg" in stor:
                item = []
                r = re.compile(stor["reg"].value.decode("utf-8"),re.M|re.S)
                for m in re.finditer(r,content):
                    t = {}
                    if "gtitle" in stor:
                        t["title"] = m.group(int(stor["gtitle"].value))
                    if "gdesc" in stor:
                        t["desc"] = m.group(int(stor["gdesc" ].value))
                    else:
                        t["desc"] = m.group(0)
                    item += [t]

            if "etag" in stor and stor["etag"].value=="on" and etag!="":
                item += [{"title":"ETag","desc":etag}]
        except:
            raise MyError(u"ページの解析に失敗しました")

        # 出力
        printrss(title=stor["title"].value.decode("utf-8"),
                 link=stor["url"].value.decode("utf-8"),
                 item=item)

    except MyError, e:
        printrss(title=u"エラー",desc=unicode(e))
        return

# ページの読み込み
# ページ内容とETagを返す
def loadpage(url):
    # テーブルが無ければ作成
    # キャッシュを取得
    conn = sqlite3.connect(CACHE_FILE)
    c = conn.cursor()

    c.execute("select * from sqlite_master where type='table' and name='cache'")
    if c.fetchone()==None:
        c.execute("create table cache (url text primary key, time real, content text, etag text)")
        conn.commit()

    c.execute("select * from cache where url=?",(url,))
    cache = c.fetchone()

    conn.close()

    # 現在時刻
    tm = time.time()

    update = False
    
    # 1時間以内のキャッシュがあればそれを返す
    if cache and tm<=cache[1]+3600:
        return cache[2],cache[3]

    # 読み込み
    try:
        request = urllib2.Request(url)
        if cache and cache[3]!="":
            request.add_header("If-None-Match",cache[3])
        page = urllib2.urlopen(request)
        
        content = page.read(MAX_PAGE_SIZE)
        etag = page.info()["Etag"] if "ETag" in page.info() else ""
        modified = True
        
    except urllib2.HTTPError, e:
        if e.code==304:
            content,etag = cache[2],cache[3]
            modified = False
        else:
            raise MyError(u"ページの読み込みに失敗しました")
    except:
        raise MyError(u"ページの読み込みに失敗しました")

    # 文字コード変換
    if modified:
        # 例外が発生しなければ正解でいいだろ……
        charcode = ["ascii","shift_jis","euc_jp","utf_8","cp932"]
        content,tmp = None,content
        for i in range(8):
            for code in charcode:
                try:
                    content = tmp.decode(code)
                except:
                    pass
                if content!=None: break
            if content!=None: break
            # マルチバイト文字の途中で切れている可能性があるので
            tmp = tmp[:-1]
        if not content:
            raise MyError(u"文字コードの変換に失敗しました")

    # データベースに追加
    conn = sqlite3.connect(CACHE_FILE)
    c = conn.cursor()

    if cache!=None:
        c.execute("delete from cache where url=?",(url,))
    c.execute("insert into cache values (?,?,?,?)",(url,tm,content,etag))

    conn.commit()
    conn.close()

    return content,etag

# HTMLを出力
def printhtml(stor):
    value = lambda s: cgi.escape(stor[s].value,True) if s in stor else ""
    
    print r"""Content-Type: text/html; charset=utf-8;

<html>
  <head><title>page2html</title></head>"""
    print '  <script type="text/javascript" src="https://www.google.com/jsapi?key=%s"></script>' % GOOGLE_API_KEY
    print r"""
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <script type="text/javascript">
  <!--
$(function(){
    function up()
    {
        s = document.location.href.split("?")[0]+"?";
        s += "title="+encodeURIComponent($("#title").val())+"&";
        s += "url="+encodeURIComponent($("#url").val())+"&";
        s += "reg="+encodeURIComponent($("#reg").val())+"&";
        s += "gtitle="+encodeURIComponent($("#gtitle").val())+"&";
        s += "gdesc="+encodeURIComponent($("#gdesc").val())+"&";
        if($("#etag").attr("checked"))
            s += "etag="+encodeURIComponent($("#etag").val())+"&";
        $("#rss").attr("href",s).text(s);
    }
    $("#title").change(up).keyup(up);
    $("#url").change(up).keyup(up);
    $("#reg").change(up).keyup(up);
    $("#gtitle").change(up).keyup(up);
    $("#gdesc").change(up).keyup(up);
    $("#etag").change(up).keyup(up);
    up();
});
  //-->
  </script>
  <body>
    <form method=get action="">
      <dl>
        <dt><label for=title>Title:</label></dt>"""
    print '        <dd><input type=text size=64 name=title id=title value="%s" /></dd>' % value("title")
    print "        <dt><label for=url>URL:</label></dt>"
    print '        <dd><input type=text size=64 name=url id=url value="%s" /></dd>' % value("url")
    print "        <dt><label for=reg>RegExp:</label></dt>"
    print '        <dd><input type=text size=64 name=reg id=reg value="%s" /></dd>' % value("reg")
    print "        <dt><label for=gtitle>Title group:</label></dt>"
    print '        <dd><input type=text size=4 name=gtitle id=gtitle value="%s" /></dd>' % value("gtitle")
    print "        <dt><label for=gdesc>Description group:</label></dt>"
    print '        <dd><input type=text size=4 name=gdesc id=gdesc value="%s" /></dd>' % value("gdesc")
    print "        <dt><label for=etag>ETag:</label></dt>"
    print '        <dd><input type=checkbox name=etag id=etag value=on %s /></dd>' % ("checked" if value("etag")=="on" else "")
    print r"""        <br />
        <dt><strong>RSS:</strong></dt>
        <dd><a id=rss href="">&nbsp;</a></dd>
      </dl>
      <input type=submit value="Preview" />
    </form>
  </body>
</html>"""

# RSSを出力
def printrss(title="",link="",desc="",item=None):
    if item==None:
        item = []

    esc = lambda x: cgi.escape(x,True).encode("utf-8")

    print "Content-Type: application/rss+xml; charset=utf-8;"
    print ""
    print '<?xml version="1.0"?>'
    print '<rss version="2.0">'
    print '  <channel xml:base="%s">' % esc(link)
    print "    <title>%s</title>" % esc(title)
    print "    <link>%s</link>" % esc(link)
    print "    <description>%s</description>" % esc(desc)

    for i in item:
        print "    <item>"
        t = ""
        if "title" in i:
            t += "      <title>%s</title>\n" % esc(i["title"])
        t += "      <link>%s</link>\n" % esc(link)
        if "desc" in i:
            t+= "      <description>%s</description>\n" % esc(i["desc"])
        print t[:-1]
        print '      <guid isPermaLink="false">%s</guid>' % hashlib.sha1(t).hexdigest()
        print "    </item>"
        
    print "  </channel>"
    print "</rss>"

if __name__=="__main__":
    main()