Pythonコード中のSQL文インデントを考える
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プログラミング作法
- SQL のコーディングスタイル(インデント) - 集中力なら売り切れたよ
- SQL文をきれいにフォーマットしてくれる『SQL in Form』 - POP*POP ~ 世界のニュースをクオリティ重視で
- VB.NETで作る! | SQL文の字下げ目安
- SQLの整形ツール~整形結果の例 - プログラマー'sペイジ
異端だけど、俺的コーディングルール 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でアクセスする
ログインが必要な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で被はてなブックマーク数を取得する
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__
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を使うことで、データベースアクセスの準備→データ挿入→コミットという流れをきれいに実装できました。