Pythonコード中のSQL文インデントを考える

2011年2月21日 18:03

 Pythonコード内にSQL文を書くときどうすれば良いのか、いまいち答えが探せなかったのでググって見た話。

 ちなみに、今までは下記のような感じのコードを書いていた訳だが、これ見るからに分かりにくい。

# コード例その1
        cur.execute("""
          create table count (
            sid text,
            count int);""")

# コード例その2
        try:
            cur.execute("""insert into count ( sid, count )
                           values ( :sid, :count );""", d)
        except sqlite3.IntegrityError:
            cur.execute("""update count set sid = :sid, count = : count
                           where sid = :sid;""", d)
# コード例その3
        cmd = """select sid, title, date from stories where date >= ? and date < ? and sid in (
                 select sid from topics where topic == ? and sid in (
                 select sid from topics where topic == ? )) order by date
        """
        cur.execute(cmd, (begin_t, end_t, t1, t2))

 「SQL インデント」でググると、そういう話のネタが一杯出てくるでてくる。その中から拾ってみたのが下記。

 異端だけど、俺的コーディングルール SQL編 – suVeneのアレというのも参考になった。

 で、この辺をまとめたところ、だいたい以下のようなルールに落ち着いた。

  • SQLキーワードは大文字で
  • 括弧挟まれた部分はインデントレベルを+1する
  • カンマ、ANDの直後で改行
  • カンマやANDでつなげられている部分はなるべくキーワード部分でそろえる

 このルールで書いたコードは下記のような感じ。

# コード例その1
        cur.execute("""
            CREATE TABLE count (
                sid text,
                count int
            );
        """)

# コード例その2
        try:
            cur.execute("""
                INSERT INTO count (
                     sid,
                     count
                )
                VALUES (
                    :sid,
                    :count
                )
            """, d)
        except sqlite3.IntegrityError:
            cur.execute("""
                UPDATE count
                SET sid = :sid,
                    count = : count
                WHERE sid = :sid
            """, d)
# コード例その3
        cmd = """
            SELECT sid,
                   title,
                   date
            FROM stories
            WHERE date >= ? AND
                  date < ? AND
                  sid IN (
                      SELECT sid
                      FROM topics
                      WHERE topic == ? AND
                      sid IN (
                          SELECT sid
                          FROM topics
                          WHERE topic == ?
                      )
                  )
            ORDER BY DATE
        """
        cur.execute(cmd, (begin_t, end_t, t1, t2))

 
 本当にこれで良いのかはまだ自信がないが、おおむね間違ってはいないと思う。ていうかコード内にSQL文を直書きせずO/Rマッパー使え、という話もあるが……。

要ログインのサイトにPythonのurllib2でアクセスする

2010年10月25日 00:55

 ログインが必要なWebサイトに対してスクリプトでページを取得/送信したい場合、まずログイン用のURLに対しログイン情報をPOSTしてCookieを取得する、という作業を行うのが一般的だ。しかし、Pythonのurllib2を利用すると簡単にPOSTは行えるのだが、なぜかCookieを取得できない、という問題が発生することがある。

 これはurllib2のurlopen()でURLを開き、帰ってきたオブジェクトのinfo()メソッド経由でSet-Cookieヘッダを取得しようとする場合に発生する。たとえばCMSを使用しているWebサイトで、管理用ページ以外に(正当な)Cookieを持たずにアクセスするとCookieなしでアクセスできるトップページ等にリダイレクトされる、というケース。多くのCMSではログインに成功すると管理ページトップにリダイレクトされるのだが、urllib2のデフォルト設定ではCookie処理を行ってくれない&リダイレクトを自動的に処理してくれるため、「ログイン成功→(Cookieを返すがurllib2はCookieを保存せず)→管理ページトップにリダイレクト→Cookieがないので公開ページのトップにリダイレクト→Cookieは受け取れず」という事態になってしまうことがある。

 この場合、urllib2.HTTPCookieProcessorを使ってCookie処理を行えばよいのだが、なぜかurllib2のドキュメントにはこのクラスの解説が無い。例にも上がっていない。ということで途方にくれるわけだが、実はCookieを扱うcookielibのほうに使い方の例が載っていたりする。

import cookielib
# :
# (このへんでurlやencoded_data、headersを準備)
# :
req = urllib2.Request(url, encoded_data, headers)
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
resp = opener.open(req)

 取得したCookieはCookieJarオブジェクトに保存されるので、必要に応じて適宜取り出せばOK。かわりにFileCookieJarオブジェクトを使えばファイルへのsave/loadも可能。

Pythonで被はてなブックマーク数を取得する

2010年5月31日 19:43

 Pythonのxmlrpclibモジュールで、指定したURLの被はてなブックマーク数を取得する例。簡単ですね。

 server.bookmark.getCount()はURLをキー、被はてブ数の値とするdictionaryの形で返してくれるので、返ってきたデータの参照も楽勝です。URLの一覧を引数の形で与えなければならないのがなんか引っかかりますが……。

#!/usr/bin/env python

import xmlrpclib

def main():
    uri = "http://b.hatena.ne.jp/xmlrpc"
    server = xmlrpclib.ServerProxy(uri)
    urls = ("http://sourceforge.jp/magazine/10/04/26/0244255",
             "http://sourceforge.jp/magazine/10/04/27/0326224",
             "http://sourceforge.jp/magazine/10/04/30/0243232")

    t = server.bookmark.getCount(*urls)
    for item in t:
        print item, t[item]

if __name__ == "__main__":
    main()

Pythonのwith構文と__enter__、__exit__

2010年5月18日 20:27

 Pythonのwith構文がいまいち掴めなかったので、ざっとまとめてみた(いまさらながら)。ドキュメントはPython リファレンスマニュアルの7.5 with 文にある。

 withを使ったコード例は、下記のような感じ。

c = ClassHogeHoge()
with c:
    c.foobar()

 上記のコードは、下記と等価となる。

c = ClassHogeHoge()
c.__enter__()
c.foobar()
c.__exit__()

 つまり、withに続くインデントブロックを実行する前に指定したオブジェクトの「__enter__()」メソッドを呼び出し、実行後に「__exit__()」メソッドが暗に呼び出される、という仕組み。

 __enter__()と__exit__()の定義は、Python リファレンスマニュアルの3.4.9 with文とコンテキストマネージャにある。__enter__()の引数はselfのみだが、__exit__()はself、exc_type、exc_value、tracebackの4つの引数をとる。withに続くインデントブロックが正常に実行された(つまり、例外が送出されなかった)場合、(self以外の)引数にはNoneが与えられる。なにか例外が発生した場合、その例外に関する情報が与えられるらしい。

 また、「with hogehoge as foo:」のような形でwith文を利用する場合、__enter__()の戻り値がfooに代入される。__exit__()の戻り値は例外処理の伝搬制御に使われ、Falseの場合例外が発生た場合でも例外を伝搬させず、Trueを返すと例外が伝搬されるとのこと。

 下記、使用例。

class CacheDB(object):
    DB_FILE = "database/db_dat"

    def __init__(self):
        self.con = None
        self.cur = None

    def __enter__(self):
        self.con = sqlite3.connect(self.DB_FILE)
        self.cur = self.con.cursor()

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            self.con = None
            self.cur = None
            return False
        self.con.commit()
        self.cur.close()
        self.con.close()
        self.con = None
        self.cur = None
        return True

    def add(self, foo, bar, hoge):
        try:
            self.cur.execute("""insert into data ( foo, bar, hoge )
                           values (?, ?, ?);""", (foo, bar, hoge))

def main():
    usage = "%s logfile" % sys.argv[0]
    db = CacheDB()
    try:
        fname = args[0]
    except IndexError:
        sys.exit(usage)

    f = open(fname, "r")
    with db:
        for l in f:
            term = l.strip().decode("utf-8").rsplit("\t", 3)
            db.add(foo=term[0],
                   bar=term[1],
                   hoge=term[2])

if __name__ == '__main__':
    main()

 タブ区切りのデータファイルを1行ずつ読んでデータベースに突込む、という処理。withを使うことで、データベースアクセスの準備→データ挿入→コミットという流れをきれいに実装できました。

日本語リファレンスには書いてない話:urllibとurllib2の違いってなんだ

2010年2月10日 00:51

 Pythonでは、HTTPやFTPなどでファイルの送受信をするモジュールとして「urllib」と「urllib2」が用意されている。使い方も似ていて、どちらも引数としてURLを与えてurlopen()関数を呼び出すと自動的に対応したプロトコルでURLにアクセスしてデータを読み出せる、というものである。しかし、似たような名前で似たような機能を持つこの2つ、何が違うのかが明確にはドキュメントに記述されていない。そのためどちらを使うべきか迷って、そのたびにGoogleのお世話になるという状況だったのでざっとまとめてみよう。