技術情報棚卸し(平日限定)

todoa2cの技術情報棚卸しです。平日限定ってことはアレだ。言わせんな恥ずかしい。

App EngineのDatastoreについて調査中

Google App EngineのDatastore周りを学習すべく、いくつか参考にしたサイトをまとめておきます。

まぁ雰囲気だけ掴んだ感じで、まだよく分かってないことが多いです。 土日に実際にDatastoreを使った実装に入ってみようかと思います。

余談

余談ですが、何故かF-SecureにGoソースコードのコンパイルをことごとく邪魔されるので、 私物Macで同様の環境を構築しました。 当初の話の通り、いちいちサーバーを再起動せずに変更が追従されるのがいいですね。

思えば私のWindows環境だと、64bit版Goコンパイラがコンパイル直後に落ちるので、 色々おかしいのかもしれないです。 Windows Updateも失敗しまくるし…環境を作り直して1年経ってないのに、また環境の作り直しか…

GoでGoogle App Engineを触るテスト

はじめに

Google App Engineを初めて触ることにしました。 しかもみんな大好きプログラミング言語Goで。 ここ最近Gopher Tシャツを着てPythonコードを書くことが多かったので、 リハビリも兼ねつつGoogle App Engineでのスピンアップ最速と言われるGoを 採用することにしました。 以下は作業メモです。

準備

開発環境はThe Development Environmentに書かれている。 Goの環境はSDKに含まれているけれど、別途Python 2.7が必要。 ローカルでサーバーを起動するなどで必要らしい。 既にPython 2.7は入れていたので、そのまま次に進む。

GAE/g のチュートリアル(英語)を読む。噂に違わず、Goの標準APIで大体の事が書けそうな雰囲気を掴む。

Using the Datastoreの部分は正直よく分かっていない。

あとはざっとGo Service APIsにも目を通す。

書いてみる

今回作ろうと思ったものは、GitHubに多少絡むものなので、 GitHub APIも使ってみることにする。

幸いGoogleがgo-githubを公開しているので、 下記のコマンドで使えるようにする。 go-githubには書かれていなかったが、 go-githubがgo-querystringに依存しているようなので、 合わせて入れる。

1
2
go get github.com/google/go-github
go get github.com/google/go-querystring

上記で取得できたライブラリを、プロジェクトフォルダと同じ場所に置く。 環境変数GOPATHの参照先に置く、といういつものルールではないことに注意が必要。

実際に、GitHub APIのSearch APIを叩いてみる。 とりあえずgo-githubのサンプル通りに…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package front

import (
  "github.com/google/go-github/github"
  "html/template"
  "net/http"
)

func init() {
  http.HandleFunc("/", index)
  http.HandleFunc("/find", find)
}

func index(w http.ResponseWriter, r *http.Request) {
  if err := indexTemplate.Execute(w, nil); err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

var indexTemplate = template.Must(template.New("index").Parse(indexTemplateHTML))

const indexTemplateHTML = `
<html>
  <body>
    <form action="/find" method="get">
      <div><input type="text" name="q"></div>
      <div><input type="submit" value="Search"></div>
    </form>
  </body>
</html>
`

func find(w http.ResponseWriter, r *http.Request) {
  client := github.NewClient(nil)
  query := r.FormValue("q")

  opts := &github.SearchOptions{Sort: "forks", Order: "desc", ListOptions: github.ListOptions{Page: 1, PerPage: 20}}
  res, _, err := client.Search.Repositories(query, opts)

  if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
  }

  if err := findTemplate.Execute(w, res); err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

var findTemplate = template.Must(template.New("find").Parse(findTemplateHTML))

const findTemplateHTML = `
<html>
  <body>
      <ul>
    
      <li><a href=""></a></li>
    
  </ul>
  </body>
</html>
`

で、goapp serveによりアプリを起動して、http://localhost:8080/にアクセスして、 いざ検索実行!

1
2
3
Get https://api.github.com/search/repositories?order=desc&page=1&per_page=20&q=abc&sort=forks:
http.DefaultTransport and http.DefaultClient are not available in App Engine.
See https://developers.google.com/appengine/docs/go/urlfetch/overview

ちーん。エラーが返ってきた。 そう言えばURL Fetch Go API Overviewに、 GAE環境用のHTTP clientを取得する必要があると書かれていたのを思い出す。

で、import文とfind関数を以下のように変更する。 appengine, appengine/urlfetchの2つをimportすることと、 appengine.NewContext(r)でContextなるものを取得し、それを使ってurlfetch.Client(c)で GAE環境用HTTP clientを取得するように変更した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import (
  "appengine"
  "appengine/urlfetch"
  "github.com/google/go-github/github"
  "html/template"
  "net/http"
)

func find(w http.ResponseWriter, r *http.Request) {
  // 下記の2行でApp Engine上でURL Fetchができる
  c := appengine.NewContext(r)  
  client := github.NewClient(urlfetch.Client(c))

  query := r.FormValue("q")

  opts := &github.SearchOptions{Sort: "forks", Order: "desc", ListOptions: github.ListOptions{Page: 1, PerPage: 20}}
  res, _, err := client.Search.Repositories(query, opts)

  if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
  }

  if err := findTemplate.Execute(w, res); err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

これで何とか動いた。 このくらいの内容であれば、あまり困らずにGoで書けることに驚き。

デプロイ

デプロイは事前にApplications Overviewで アプリケーションを登録しておき、app.yamlのアプリケーション名と名前を合わせて(よく分かってない)、 goapp deployコマンドでデプロイできる。

私は2段階認証を使っているので、事前にデプロイ用の固有パスワードを取得しておいた。

デプロイは割とあっさり完了し、普通にGAE/Go環境が本番環境でも動いている。凄い… 次はDatastoreまわりを学んでいこうっと。

分からない点

  • appengine.NewContextとは?
  • go-githubのSearchOptionsの詳細
  • Datastore
  • スケジュール実行
  • GAE/gに適したフレームワークってあるのかしら?
  • 関係ないけどそろそろシンタックスハイライトをこのブログでも使いたい
  • これまた関係ないけどF-SecureがひたすらWebアプリの実行を邪魔してツラい。

Emailがコラボレーションツールに適していないもう1点

ござ先輩による Eメールで作業内容を管理するのはやめましょう の記事が非常に共感できたのですが、一点だけハッキリ書きたかったことがあったので、書いておきます。

新規に参加したメンバーが過去の情報にアクセス出来ない

私が一番Eメール・メーリングリストによるコラボレーションに不満を持っているのはこの点です。 Eメールは各人のローカルPCに保存され、各人が管理していくことになります。 メーリングリストも、何らかのサービスを使わなければ、基本的に同様です。

そして一度送られたEメールは、どこか共通の場所に格納される、ということはありません。 情報が適切に蓄積されないわけですね。 その結果、新規に参加したメンバーが、過去の情報にアクセスできなくなります。 これは新規参加メンバーからすると、非常にツラいのです。

  • どのような議論の場所があるのか
  • 過去にどのような議論がなされていたか
  • 社内ではどのような用語が使われているか

などの情報に、新規参加メンバーはアクセスすることが出来ないからツラい。

しかもたちが悪いことに、昔からいた人たちは、過去の情報を持っているため、 過去の情報にアクセス出来ないという問題に気付かないのです。 そのため、昔から人たちからは、新しい情報共有の仕組みについて 議論されることが滅多にありません。

かなり観測範囲の狭い個人的な経験ですが、前職・現職ともにおんなじ状況でしたね。 ござ先輩の観測範囲も多分同様なことが起きている気がしますが、 他の環境はどうでしょう。。

対応したログが残らず、どんな意思決定をしたのか見えなくなる

に含まれているとは思いますが、新規メンバーが過去の情報にアクセス出来ない件については ちゃんと書いておいたほうがいいと思ったので、蛇足的に書かせていただきました。

合わせて読みたい: 情報共有の必要性について

JavaScriptのparseIntを数値入力の妥当性検証に使うと失敗する話

Webアプリケーションにおけるクライアント側のバリデーション処理で、 手抜きで下記のような書き方をしたら、(自分にとって)意外な答えが返ってきました。

1
2
3
var notNan = parseInt('10x', 10);
console.log(notNan);
// NaNが表示されるかと思ったけど、10が表示された

parseInt – JavaScript | MDNを見ると、確かにその通りに動いている事がわかります。下記は第1引数に関する記載の引用です。

parseInt が、指定された基数においては、数ではない文字に出会った場合、その文字とそれに続く文字の全てを無視し、その地点までパースされた値の整数を返します。 parseInt は、整数の値まで数を切り捨てます。 文字列の前後に空白があっても問題ありません。

JavaのInteger.parseInt()やPythonのint()のように、例外を返してくれるだろう、 または変換不可ということでNaNを返すだろう、と勝手な予想をしながら実装したのですが、 それがよくありませんでした。

他の言語で使い慣れているAPIのルールが、そのまま他の言語でも通じるだろう、 という思い込みは捨てて、ちゃんとAPIの挙動を調べて使うべきだ、と改めて痛感しました。

そもそもバリデーションですが、 バリデーションライブラリを使うか、 HTML5前提でIE 9を無視してよいのであれば、HTML5のForm Validationを使うのが便利かと思います。 今回のケースのように独自実装で頑張ると不具合混入しやすいですしね。 残念ながら私はまだこれらを使ったことがないので、ポインタだけ示しておきます。

jQuery Validation Plugin

HTML5 Form Validation のカスタマイズ

ちっちゃいWeb APIをPython 3.3に移行する

Python 2.6時代

2012年夏頃、ちょっとした自然言語処理用Web APIを作ることになったとき、 実現方法を色々探した結果、Natural Language Toolkit (NLTK)に辿り着き、 それからPythonを触るようになりました。

周囲には誰もPythonを使ったことのある人がいなかったので、 独学でPythonやNLTKを学んだ結果、以下の様な構成になりました。

  • Python 2.6 (EC2環境にデフォルトで入っていたものをそのまま利用…)
  • web.py (軽量Webアプリケーションフレームワーク)
  • NLTK (自然言語処理モジュール)
  • SQLAlchemy (ORM)
  • MySQL-Python (MySQLドライバ)

この構成はぼちぼちうまく動いていたのですが、文字列マルチバイト対応の面倒臭さや、 Python 2.6.9が2.6系最後のリリース とうことを知ってから、モジュールの構成を変えることを検討し始めたわけです。

Python 3.3時代

まず、無難に2.7系に行くか3.x系に行くかを検討しましたが、 文字列マルチバイト対応の面倒臭さから逃げたかったため、 Python 3.xを使うこと自体はあっさり決まりました。

3.xを使うことを決めた次に、ライブラリがそのまま使えるかを調査しました。 2系と3系の違い – Python入門から応用までの学習サイトにも書かれているように、 Python 2.x系から3.x系では互換性が失われる変更が多数入ったので、 2.x系で使えていたライブラリが3.x系で使えない可能性があるのです。

調査したところ、調査時点(2014年1月時点)ではこのような対応状況でした。 主要なライブラリが1コしか対応していない…なかなかの難局です。

  • web.py: 3.x系未対応(対応するためのPull Requestは出ているがここ数ヶ月動きなし)
  • NLTK: 3.x系未対応(ただしNLTK 3.0 Alpha Releasesはある)
  • SQLAlchemy: 対応済み
  • MySQL-Python: 3.x系未対応

結局それなりの時間を費やして調査比較検討した結果、下記のような環境に落ち着きました。

  • Python 2.6 → Python 3.3
  • web.py → Flask (web.py同様の軽量さに好感)
  • NLTK → scikit-learn (実際やっていたのはテキスト分類なので、機械学習ライブラリに移行)
  • SQLAlchemy → そのままSQLAlchemy
  • MySQL-Python → PyMySQL (Python 3のMySQLドライバ事情が参考になります)

これにより、Web層とロジックの書き換えこそ必要になってしまいましたが、 そもそも規模が小さいWeb APIなので、割と簡単に置き換えができました。

MySQL-PythonからPyMySQLへの移行は、接続先のURLのschema部分をmysql://からmysql+pymysql://に 変更するだけでOK。

移行してみて

Python 2.x系から3.x系に移行するには、やはり今まで使えたライブラリが使えない、 という問題が出てきます。 ですが、何かしら代替となりそうなライブラリはあるみたいですので、 調査や移行に時間こそかかりますが、Python 3.xに移行できるのかなぁと思った次第です。

また、本件では触れていませんが、Python 2.x系だけでしか動かなかったライブラリを 2.x系、3.x系両方で動かす対応を経験したのも大きいかもしれません。

代替ライブラリがない場合、最悪、フォークして自分でPython 3.x対応する、という手段もある、 と思うようになったことも、Python 3.xに移行するきっかけになったと思っています。

ちょっと話はそれましたが、まとめ。 規模が大きいプロジェクトの場合は移行が難しいかもしれませんが、 小規模なプロジェクトの場合であれば、移行は意外と何とかなるんじゃないかな、 というのが今回の感想でした。

カスタム条件で二分探索したい

Pythonのbisectの返す結果に戸惑った、 と同僚に話したところ、「いや他の言語のライブラリも大体同じですよ」 と言われたわけです。

戸惑った部分ですが、新人プログラマー野田さんの課題を 解いている過程で、「指定した金額以下で一番大きい金額のインデックス」が欲しい、と思ったのですよね。 結局それはbisect.bisect_leftを使って解決したんだけど、 bisect_leftの場合「返り値のインデックスの更に1コ前」が欲しい値だったわけです。

改めて二分探索の挙動について調べたところ、少なくともPython, Java共に、 あるデータが「挿入されてもソート状態が保たれる箇所」を探索してくれるものらしい。

例えば以下はPythonの例。

1
2
3
4
5
6
7
8
import bisect

a = [1, 2, 4, 8, 11]
data = 6
i = bisect.bisect(a, data)
# i の値は 3
a.insert(i, data)
# a の値は [1, 2, 4, 6, 8, 11]

なるほど。納得。

とは言え、カスタムの条件で二分探索したいケースもあると思うんですよね。例えば接尾辞配列のように。

超簡単な例だと、こういうイメージ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
txt = 'banana'
suf = [i for i in range(len(txt))]
# suf の値は [0, 1, 2, 3, 4, 5]

suf.sort(key=lambda i: txt[i:])
# suf の値は [5, 3, 1, 0, 4, 2]

print([txt[i:] for i in suf])
# ['a', 'ana', 'anana', 'banana', 'na', 'nana'] が出力される。
# この結果は、全部分文字列がソートされた状態
## a
## ana
## anana
## banana
## na
## nana
# sufにはソートされた状態の各部分文字列へのインデックスが格納されている。
# この中から、例えば'an'が含まれる全ての箇所を探す、というのはbisectでは探索できない…

こういう用途用の二分探索は、接尾辞配列ライブラリに含まれてると思うんですけどね。

少なくともPythonで言うと、リストの外の情報を使ってソートすることはできるけれど、 それに対する二分探索による探索がないのはちょっと不便な気がしたのです。 それを可能とするライブラリがあるのであれば、それを使うにして、 なければそれを実現するライブラリを作ろうかしら。

「それ~~でできるよ」情報があったらお教え下さいませ。