A Django site.
8月 22, 2008
» Python版Yahooテキスト解析 APIライブラリを日本語係り受け解析に対応させました

ついでに指定形容詞係り先検索にも。

ダウンロード

yahooapi

使い方

python code
  1. from yahooapi import *
  2. client = DAServiceAPI("your_appid")
  3. result = client.parse(sentence=u"うちの庭には二羽鶏がいます。")
  4. for morph in result.Result.ChunkList.Chunk[0].MorphemList.Morphem:
  5.   print morph.Reading
  6. # => うち
  7. # の
  8.  
  9. client = DAServiceSearchAPI("your_appid")
  10. result = client.search(mode=MODE_URESHII)
  11. for word in result.Result.WordList.Word:
  12.   print word.Surface, word.Frequency
  13.  
  14. #クリック 35
  15. #応援 30
  16. #気持ち 26
  17. #金メダル 24
  18. #ホームラン 22
  19. #ニュース 17
  20. #デス 16
  21. # .
  22. # .
  23. # .
  24.  

うむ。

8月 8, 2008
» Python:re.ScannerでS式パーサ

RubyのStringScannerは個人的にかなり好きなモジュールで、Rubyでちょっとしたパーサなどを書くときに重宝しています。

一方、Pythonにはexperimentalながらre.Scannerというクラスがあります(>= 2.4)。experimentalなのでマニュアルにはのっていませんが。このre.Scannerはかなりシンプルなんですが典型的なStringScannerの使い方の範疇では、こちらのほうがキレイに書けるような気がします。

re.Scannerの使い方

使い方は非常に簡単で

  • (regex, action)のリストを渡してScannerオブジェクトを作成
    • actionは(scanner, string_matched) => stringな関数、Noneを返せば結果は無視される。
  • scanメソッドでスキャン。結果が配列で返ってくる

といった感じ。関数を渡すので、StringScannerのようなwhileループを作る必要がなく、キレイにまとまります。

例:S式パーサ

re.Scannerは簡単、ということでS式パーサでも。トークナイズ+αな処理をするので、actionをインスタンスメソッドにして状態を保存することにします。

目標は

  • 数値(っぽいもの)、文字列、シンボルが使える
  • シンボルのみ、新たにクラスを定義して(unicodeのサブクラス)それにマップ。それ以外は組み込み型に。
  • パースエラーも分かりやすく
  • 結果はPythonのリストorオブジェクトとして返る

python code
  1. import re, sys
  2. from unicodedata import east_asian_width
  3.  
  4. try:
  5.   from re import Scanner
  6. except ImportError:
  7.   from sre import Scanner
  8.  
  9. class ParseError(StandardError): pass
  10.  
  11. class Symbol(unicode):
  12.   def __repr__(self):
  13.     return "Symbol(%s)"%unicode.__repr__(self)
  14.  
  15. class TokenProcessor(object):
  16.   PAREN = {"]":"[", ")":"("}
  17.   def __init__(self, value):
  18.     self.result = []
  19.     self.append = self.result.append
  20.     self.string = value
  21.     self.paren_stack = []
  22.     self.pos = 0
  23.  
  24.   def __call__(self, name):
  25.     def _(*a):
  26.       self.before(*a)
  27.       return getattr(self, name)(*a)
  28.     return _
  29.  
  30.   def before(self, scanner, s):
  31.     self.pos += len(s)
  32.     self.skip(scanner, s)
  33.  
  34.   def error(self, scanner, s): self.raise_error("unknown token: %s"%s)
  35.  
  36.   def skip_whitespaces(self, scanner, s): self.append(",")
  37.  
  38.   def skip(self, scanner, s):
  39.     last = "".join(self.result[-2:])
  40.     if last in ["[,", ",,", ",]"]:
  41.       self.result[-2:] = sorted(last, key=ord)[1]
  42.  
  43.   def atom(self, scanner, s):
  44.     if s in ["(", "["]:
  45.       self.append("[")
  46.       self.paren_stack.append(s)
  47.     elif s in [")", "]"]:
  48.       if not self.paren_stack:
  49.         self.raise_error("missing opening parenthesis.")
  50.       if self.PAREN[s] != self.paren_stack.pop():
  51.         self.raise_error("missing closing parenthesis.")
  52.       self.append("]")
  53.     elif re.match(r"""^(".*)$""", s or ""):
  54.       self.append("u"+s)
  55.     elif re.match(r"""^((\-?\d[\de\.]+)|(\s*)|(.*"))$""", s or ""):
  56.       self.append(s)
  57.     else:
  58.       self.append("Symbol(u\"%s\")"%s)
  59.  
  60.   def raise_error(self, msg="parse error", range=3):
  61.     lines = self.string.split("\n")
  62.     curline = self.string[:self.pos].count("\n")
  63.     linepos = self.pos - len("\n".join(lines[:curline]))
  64.     buf = ["\n"]
  65.     for i in xrange(max(0, curline-range), curline+1):
  66.       buf.append("% 5d: %s"%(i+1, lines[i]))
  67.     width = 6 + sum(east_asian_width(c) == 'W' and 2 or 1 for c in lines[i])
  68.     buf.append("%s~"%(" "*width))
  69.     buf.append("line %d, %d: %s"%(curline+1,linepos, msg))
  70.     raise ParseError(("\n".join(buf)).encode(sys.stderr.encoding))
  71.  
  72. def read_sexp(sexp):
  73.   processor = TokenProcessor(sexp)
  74.   scanner = Scanner([
  75.     (r"\s+", processor("skip_whitespaces")),
  76.     (r";[^\n]*\n", processor("skip")),
  77.     (r""""(?:[^"])*"|(\]|\[|\)|\(|[^\(\)\s]+)""", processor("atom")),
  78.     (r".*", processor("error"))
  79.   ], re.M)
  80.   scanner.scan(processor.string)
  81.   if processor.paren_stack:
  82.     processor.raise_error("missing closing parenthesis.")
  83.   result = eval("".join(processor.result).lstrip(","))
  84.   return (isinstance(result, tuple) and (result[0],0) or (result,0))[0]
  85.  

こんな感じ。非常にシンプルな気がします。

python code
  1. print read_sexp(u"""("ほげほげ"
  2. ;comment
  3. ;comment
  4. (hogehoge 123) ;aaaaaaa
  5. "hoge\\"aaaa"
  6. ;comment
  7. ;comment
  8.  
  9. aaaa b)""")
  10.  

output:

 code
  1. [u'\u307b\u3052\u307b\u3052', [Symbol(u'hogehoge'), 123], u'hoge"aaaa', Symbol(u'aaaa'), Symbol(u'b')]
  2.  

エラーも一応。

python code
  1. print read_sexp(u"""(
  2. aaaa
  3. bbbb (ccc ddd) )
  4. (eee
  5. ああああああ""")
  6.  

output:

 code
  1. __main__.ParseError:
  2.  
  3.     2: aaaa
  4.     3: bbbb (ccc ddd) )
  5.     4: (eee
  6.     5: ああああああ
  7.                   ~
  8. line 5, 7: missing closing parenthesis.
  9.  

エラー表示もいい感じ。フォントにもよりますが(等幅なら大丈夫)、一応文字幅を考慮して~をエラー箇所に出すようにしています。HTML上だと日本語はずれちゃうかもだけど。

というわけで

Pythonでトークナイズするときにはかなり便利なんじゃないかと思いました。

7月 28, 2008
» XREAで好きなバージョンのPythonを使う方法

前のエントリーから自作のブログソフトに移行してみたわけだけど、やっぱり自分で作ったものはなんとなく気持ちいいですね。

さて、このブログはXREAでPython2.5で動いているわけですが、もちろん?XREAにPython2.5ははいっていません。ではどうやって動かすか・・・というと答えは簡単。バイナリとして動かしているわけです。

XREAはバイナリCGIが動くのでバイナリにしてしまえばどんなバージョンのPythonでも問題ありません。しかも、たとえばMySQLDBなどの拡張モジュールもきちんとバイナリに含まれるので、多少ファイルサイズは大きいですがアップロードするだけで動くので非常に楽です。

今回はXREAでローカルで作ったPython2.5アプリをバイナリ化して動かすまでを紹介しようかと思います。

バイナリ化ライブラリ:bbfreeze

バイナリ化に使うライブラリですが、俺はbbfreezeを使いました。bbfreezeの特徴は

  • すごく簡単
  • ライブラリの探索に優れる
  • Linux用バイナリが作れる

というところでしょう。

bbfreezeを使う上での注意点

ただ、使う上で色々注意点もあります。書きなぐりなのでちょっと汚いですが、このブログアプリ用のfreezeスクリプトです。

python code
  1. from bbfreeze import Freezer
  2. import sys, os, shutil
  3. from os.path import dirname, abspath, join
  4. root = abspath(dirname(__file__))
  5. sys.path = [join(root, d) for d in ["libs"]] + sys.path
  6. include_lst = []
  7. for r, dirs, files in os.walk(join(root, "libs")): #指定ディレクトリ以下を全部追加
  8.   if "__init__.py" in files:
  9.     include_lst.append(r)
  10.   for file in (f for f in files if f.endswith(".py") and f != "__init__.py"):
  11.     include_lst.append(join(r, file[:-3]))
  12. def replace(f):
  13.   return f[len(join(root, "libs"))+1:].replace("/", ".")
  14. include_lst = map(replace, include_lst)
  15. include_lst.append("_mysql") # MySQLDBの_mysqlが含まれないので手動で追加
  16. shutil.copy(join(root, "index.py"), join(root, "index.cgi.py")) #cgi用にリネーム
  17. f = Freezer("dist", includes=include_lst, excludes=(), )
  18. f.addScript("index.cgi.py", True)
  19.  
  20. f() # starts the freezing process
  21.  
  22. os.remove(join(root, "index.cgi.py"))
  23.  

と、こんな感じです。ほんと汚いな・・・。さてでは注意点を一つ一つ。

  • できる限り自動的に使用しているライブラリを検索してくれるのですが、__import__で動的にインポートする場合はさすがに無理です。なので、そういうモジュールがある場合、自分で追加する必要があります。このブログアプリではPygmentsMarkdownあたりが__import__による動的インポートを行っています。
  • MySQLDBの_mysqlモジュールのように探索されないのもありますので、注意しないといけません。
  • 出力されるバイナリファイル名はメインスクリプトから.pyを除いたものになります。XREAでcgiとして動かす場合はあらかじめindex.cgi.pyみたいにリネームしておくとindex.cgiが吐き出されるので便利です。

また、dirname(__file__)でディレクトリを取得し、テンプレートファイル用ディレクトリなどを設定している場合、注意が必要です。以下のようにします。

python code
  1. root_dir = abspath(dirname(__file__))
  2. if "library.zip" in root_dir:
  3.   root_dir = dirname(root_dir)
  4.  
  5. template_dir = os.path.join(root_dir, "templates")
  6.  

ファイルはlibrary.zipにまとめられますので、実行時は/library.zip/hogehoge.pyのような扱いになります。そのためlibrary.zipがパスに含まれる場合はさらに1個上のディレクトリが求めているディレクトリになります。

アップロード

生成されたファイル群をアップロードするだけです。さすがにサイズは大きいですが。もちろんアップした後index.cgiのパーミッションは設定しましょう。

アクセスしましょう

バッチリ動くはずです。


というわけで、bbfreezeを使えばサクッと任意のバージョンのPythonで作ったアプリがXREAで動かせます。あとは負荷だけが問題です。1日見た限りではweb.pyの場合負荷は大丈夫っぽいです。ぶっちゃけ、Wordpressより速い気がします。web.pyはライトウェイトWEBフレームワークとしてもう少し評価されるべき。Djangoとかだとどうなんですかね。チャレンジャーな方は、ゼヒ。

7月 27, 2008
» 試験的に自作ブログに移行しました

前のエントリーでGAEにおけるトランザクションの問題は一応解決をみた。

その後、GAEで開発を続けた結果、やはりGAEにブログを移行するのは断念した。理由としては

  • os.listdirなどで不審な挙動が見られた
  • スキーマを変更したときが非常に面倒
  • インポート、エクスポートの難しさ

が主なところ。

os.listdirについては、web.pyでテンプレートを検索するときにos.listdirを使っているのだけど、どうも動きが怪しい。正しくファイルリストが帰ってきたり帰ってこなかったりするのだ。何回も本番にアップして試したところ、os.listdir("hoge");os.listdir("hoge")というように同じ内容で2回連続で呼び出すとなぜか確実にファイルリストが帰ってくる、という・・・これはちょっと・・・

スキーマの変更に関しては、まんま。変更したとき、それを本番に反映させるのがめんどくさい。

インポートエクスポートも負荷を考えるとしんどい。現在もWEBはレンタルサーバで運用しているのでわざわざGAEにもっていく旨みもない。

ということでせっかくブログをつくったのに移行をやめたのである。


しかし、せっかく作ったのにもったいない。ということで今度はGAE用につくったブログを普通にMySQLを使うようにポーティングした。今回はそれに試験的に移行してみたのだ。

なぜWordpressからわざわざ独自ブログソフトウェアに移行したのか。理由は前のエントリーにも書いたとおりだけど

  • Wordpressのコードが気に食わない(OSSとしてバランスをとっている、というのはあるんですが)
  • 使用しているテンプレートや、プラグインも含めると出力されるHTMLが汚い。
  • ついている機能の半分以上は使っていない。ブログライトユーザの俺には機能が多すぎる。
    • 自分の使う機能が固まった。それさえあれば俺には十分とわかり始めた。
  • セキュリティ的にもWordpressはターゲットになっていて微妙。

以上のようなところをふまえ

  • 明快なコード。
  • キレイなXHTML。
  • 自分が使う機能だけ実装。
  • 堅牢なコード。

ということを心がけた。またレンタルサーバで動かすということで

  • なるべく静的HTMLとしてキャッシュしてmod_rewriteで飛ばす
  • 見た目や外部連携に関する動的な部分はなるべくクライアントサイドで

ということにも気を配った。

このブログは3年目に入るのだけど、やはりWEBの世界というのは流れが速いもので大分変化があった。その中で、外部連携はほぼJSONPで行えるようになったのでサーバサイドでやらなくても良くなった。今回実装したブログでは各SBMのブクマ数を表示しているけど、これも全てJSONPでクライアントサイドで実現している。それに対し、旧ブログ(Wordpress)ではサーバサイドで定期的に取得していた。

という感じで出来上がったのがこのブログだ。もうひとつ、実はこのブログ、XREAでしかもPython2.5で動かしている。あれ、XREAでPython2.5使えたっけ、というあなた。実は簡単に使えちゃうワザがあるんですよ。それについては、また。


あまりテストもせず試験的に移行してみたのでおかしい部分もあるかと思いますが、そこはおいおい。パーマリンクはそのままになっていると思います。基本的にURLは変更していません。また、CGIで動かしていますので、負荷が高ければ前のに戻す予定です。

7月 3, 2008
» GoogleAppEngineのトランザクションをねじふせる

前のエントリーで書いたように、GoogleAppEngineのトランザクションは使い勝手が悪い。しかし、GAEにブログを移行しようと思うとこれは乗り越えなければならない。

GAEでトランザクションを行う条件を簡単にまとめると

  • db.run_in_transactionにトランザクションとして実行したい関数を渡すがその関数内では
  • 同じエンティティグループに属しているモデルで
  • get, put, deleteのみ

しか実行できない。なのでトランザクションのページに紹介されているような

python code
  1. from google.appengine.ext import db
  2.  
  3. class Accumulator(db.Model):
  4.   counter = db.IntegerProperty()
  5.  
  6. def increment_counter(key, amount):
  7.   obj = db.get(key)
  8.   obj.counter += amount
  9.   obj.put()
  10.  
  11. q = db.GqlQuery("SELECT * FROM Accumulator")
  12. acc = q.get()
  13.  
  14. db.run_in_transaction(increment_counter, acc.key(), 5)
  15.  

先にGQLを発行しておいて、run_in_transactionに渡す関数ではGQLの結果として取得したkeyを引数にとる、という回りくどいコードになる。コレは書きづらい。

これも前のエントリーに書いたように、今回作ったブログアプリでは月別アーカイブやタグアーカイブの記事数をエントリのCRUD時に操作している。

エントリ作成時なら

python code
  1.   @classmethod
  2.   def create(cls, *a, **k):
  3.     obj = super(Entry, cls).create(*a, **k)
  4.     TagCount.inc(obj.status, *obj.tags)
  5.     MonthCount.inc(obj.status, obj.created_month)
  6.     return obj
  7.  

というような感じだ(statusは公開、とか非公開とかが入る)。当然、このcreateメソッドではTagCount,MonthCountobjについて一貫性が保たれなければならない。しかし、TagCount.incでは対象のタグを検索して、なければ作成しputする、という操作を行う。「対象のタグを検索して」というトランザクション内で許可されていない操作が入っているのだ。

さて、こういうときはとりあえず?力技で乗り切ろう。かなり強引だが、下のようなコードで乗り切ってみた。一応、サーバにアップロードして動作することは確認している。

python code
  1. class RootModel(db.Model):
  2.   id = db.IntegerProperty(default=1)
  3.   def __call__(self, k):
  4.     v = k or {}
  5.     v.update(parent=with_parent)
  6.     return v
  7. with_parent = RootModel.get_or_insert("id", id=1)
  8.  

まず、全てのエンティティを同一エンティティグループに所属させることにする。そのため、全てのエンティティの親となるエンティティを作成する。

次は強引さの根源のようなクラスだ。

python code
  1. class Transaction(object):
  2.   MAGIC_NAME = "__magic__lst__"
  3.   SEARCH_MAX = 12
  4.   def __init__(self, f):
  5.     self.f = f
  6.  
  7.   def execute(self):
  8.     __magic__lst__ = []
  9.     result = self.f()
  10.     def commit():
  11.       for dbop in __magic__lst__:
  12.         dbop()
  13.       return True
  14.     committed = db.run_in_transaction(commit)
  15.     if committed is None:
  16.       raise db.Rollback()
  17.     return result
  18.  

これだけでは分からないと思うが、execute内でrun_in_transactionが実行されている。その引数にはcommitcommitの中では__magic__lst__というリスト内の関数を実行しているようだが、__magic__lst__に要素が追加されている形跡がない。

そして、全てのモデルの親となる基本モデルを定義する。

python code
  1. class BaseModel(db.Model):
  2.   @classmethod
  3.   def create(cls, *a, **k):
  4.     obj = cls(*a, **with_parent(k))
  5.     obj.put()
  6.     return obj
  7.  
  8.   def _with_transaction(self, name):
  9.     i = 1
  10.     f = sys._getframe(i)
  11.     while f and i < Transaction.SEARCH_MAX:
  12.       if Transaction.MAGIC_NAME in f.f_locals:
  13.         f.f_locals[Transaction.MAGIC_NAME].append( \
  14.           lambda : getattr(super(BaseModel, self), name)())
  15.         return
  16.       i += 1
  17.       f = sys._getframe(i)
  18.     return getattr(super(BaseModel, self),name)()
  19.  
  20.   def put(self):
  21.     return self._with_transaction("put")
  22.  
  23.   def delete(self):
  24.     return self._with_transaction("delete")
  25.  
  26.   def update(self, **k):
  27.     for prop in self.properties().values():
  28.       if prop.name in k:
  29.         prop.__set__(self, k[prop.name])
  30.     self.put()
  31.  
  32.   @classmethod
  33.   def get_by_id(cls, id):
  34.     return super(BaseModel, cls).get_by_id(id, parent=with_parent)
  35.  

この基本クラスではまず、createというメソッドを定義し、エンティティはすべてこのメソッドを通して作成するようにしている。createでは必ず親エンティティとしてwith_parentを指定する。これにより全てのエンティティは同一エンティティグループに属することになる。

  • 同じエンティティグループに属しているモデルで

という条件はクリアしたことになる。次は

  • get, put, deleteのみ

という条件だ。これは_with_transactionというメソッドでクリアしている。putdeleteをオーバーライドして_with_transactionを呼び出すようにしている。さて、この_with_transactionではフレームをさかのぼって、__magic__lst__という名前のローカル変数が存在するフレームがないか探索する。そのフレームが存在した場合、一連のトランザクション内での実行とみなし、__magic__lst__put,deleteを行うthunkを登録し、終了する。存在しない場合は通常の実行ということで、その場でput, deleteを行う。つまり、トランザクション内で実行されたことを検出してput, deleteの評価を遅延させるのだ。

さきほどのcreate

python code
  1.   @classmethod
  2.   def create(cls, *a, **k):
  3.     def _():
  4.       obj = super(Entry, cls).create(*a, **k)
  5.       TagCount.inc(obj.status, *obj.tags)
  6.       MonthCount.inc(obj.status, obj.created_month)
  7.       return obj
  8.     return Transaction(_).execute()
  9.  

と若干のコード挿入のみできちんと一貫性が保証されるようになる。


というわけで、かなり強引な気がするけどトランザクションの問題は一応クリアされた。めんどくさければHTTPリクエストを受けてから、レスポンスを返すまでを関数でラップし、Transaction.executeすれば大丈夫だ。

ここまでやってしまったので、やっぱり今のブログはGAEに移行しようかなあ、と思い始めたり。デザインも今と似ているけどやっぱ一から自分でマークアップしたから好みだし、なによりXHTMLとしてValidだ。機能的にはあと、スパム対策さえあれば大丈夫。

実はOpenIDも自分でサーバ立ててたりするので、それもGAEで実装しなおしたいなあとか色々やりたいことは尽きない。GAEでの開発はこのモデルの問題さえつぶしてしまえばかなり楽だし、夢がひろがるなあ。

6月 30, 2008
» GoogleAppEngineのモデルをいじってみた

なんの幸運か、Softbank(しかもJ-phone時代からの)ユーザだったので、SMS認証もアッサリ通ってGAEのアカウントをゲットしたわけですが・・・(ただしiPhoneには興味ほぼなし)。

というわけでishikawaさんがブログをGAEに移行された、というのを知って俺も移行しようかなあとか思いつつブログアプリ作りました。すでに今ブログで使っている機能は全て実装済みだったりします。デザインも全頁XHTMLTr validでCSSもvalidなことを確認済みだったりします。しかもDjangoじゃなくてweb.pyだったりします。慣れてるしね。

それはおいといて、データストアまわりがそのままだと

  • filter("hoge =", value)が長ったらしい
  • CRUDが微妙。
  • フォーム作成用等に空のモデルを一時的に作成したいけど、作成した時点でvalidateされるので作成できない

らへんがいまいちだったで、ちょっといじってみました。

python code
  1. def _getattr(self, name):
  2.   if name.endswith("_eq"):
  3.     return lambda v: self.filter("%s ="%name[:-3], v)
  4.   raise AttributeError(name)
  5. setattr(db.Query, "__getattr__", _getattr)
  6.  
  7. class ModelMixin(object):
  8.  
  9.   default_order = None
  10.   _stab_class = None
  11.  
  12.   @classmethod
  13.   def stab(cls):
  14.     if not cls._stab_class:
  15.       stab = type('Stab%s'%cls.__name__, (cls,), dict())
  16.       for k,v in stab._properties.iteritems():
  17.         v.required = False
  18.         v.choice = None
  19.         v.validator= None
  20.       cls._stab_class = stab
  21.     stab = cls._stab_class()
  22.     for k,v in stab._properties.iteritems():
  23.       v.__set__(stab, v.default_value())
  24.     return stab
  25.  
  26.   @classmethod
  27.   def create(cls, *a, **k):
  28.     obj = cls(*a, **k)
  29.     obj.put()
  30.     return obj
  31.  
  32.   @classmethod
  33.   def find_all(cls):
  34.     result = cls.all()
  35.     if cls.default_order is not None:
  36.       return result.order(cls.default_order)
  37.     return result
  38.  
  39.   def update(self, **k):
  40.     for prop in self.properties().values():
  41.       if prop.name in k:
  42.         prop.__set__(self, k[prop.name])
  43.     self.put()
  44.  
  45.   def destroy(self):
  46.     self.delete()
  47.  

これで

python code
  1. class Test(ModelMixin, db.Model):
  2.   name = db.StringProperty(require=True)
  3.   created_at = db.DatetTmeProperty(auto_now_add=True)
  4.  
  5.   default_order="-created_at"
  6.  
  7. Test.find_all() # created_atの降順
  8. Test.all().name_eq("user").update(created_at = datetime.now())
  9. stab = Test.stab() #nameがなくても作成できる
  10.  

こんな感じにかけます。

さて、そんなこんなでブログアプリを作ったのはいいんだけど、なんとなく移行する気にならない。現在はWordpressで運用しているけど、正直セキュリティホールは多いし、ソースコードを見ると泣きたくなるようなコードだし、頻繁なバージョンアップはめんどくさい・・・。プラグインのセキュリティホールまで考えるとなおめんどくさい。しかもそこまで機能を使い倒しているわけじゃないし。

だからGAEにでも移行したいんだけど・・・。データがGoogleに握られるのはそこまで心配じゃない。ブログサービスでブログをやれば、そのブログサービス事業者にデータを握られるってのと同じだから気にならないんだな。移行作業自体も記事数がかなり少ないのでそんなにめんどくさそうではない。

ひとつ引っかかっているのはトランザクションの問題。まぁめったにトランザクションで問題はおきないと思うけど、GAEのトランザクションは一応ある、といった程度でほとんど使えない。使おうとしたらtransactionの中でこれはしちゃだめ、これもしちゃだめ、とおこられまくったので現在トランザクション管理ができていない。GAEには集約系のメソッドがないので、月別記事数とか、タグ別記事数とかを記事のCRUD時に別モデルで管理しているわけだけど、ここのトランザクションが管理できないのは結構不安。

もうひとつはなんとなーく使い切らないんだけど無料の上限である500Mという容量が微妙。画像のアップとかしなけりゃまず大丈夫なんだけど。

その他にもなんとなくGoogleの罠じゃないかとかいろいろ気になって移行に踏み切れないでいる今日この頃。実際に本格的にGAEに移行した人の感想がききたいなあ。