Golangでつくる検索エンジン(Webクローラ、MongoDB、Kagome、gin)
最近、Golangを書き始めたので勉強として、1年半ほど前にPythonで作っていたWebクローラと検索エンジンをGolangで実装してみた。WebフレームワークはFlaskの代わりにgin、Mecabの代わりにKagomeを使用、Datastoreは前回と同じくMognoDBを使ってます。
- GitHub - c-bata/gosearch: Web crawler and Search engine in Golang.
- 今回のコード
- Pythonでつくる検索エンジン(Webクローラ, Mecab, MongoDB, Flask) - c-bata web
今回はHTML書くの面倒だったので、フロントはginでJSON返すだけにしました。
以下使ったライブラリやGoに関するメモ
可変長引数の挙動
スライスを展開して渡すときは、他の引数もスライスにappendしてから展開して渡さないとToo many argumentsで落ちるみたいです。
gore> hoge := func(args ...string) { ..... for _, a := range args { ..... fmt.Println(a) ..... } ..... } (func(...string))0x2610 gore> hoge("foo", "bar") foo bar gore> hoge([]string{"foo", "bar"}...) foo bar foo bar gore> hoge("foo", []string{"bar"}...) # command-line-arguments /var/folders/nc/jkpxzpsd6459zf5qb2stk1cwnk6_vq/T/364524589/gore_session.go:31: too many arguments in call to hoge error: exit status 2 exit status 2
仕様だとは思いますが、少し面倒な気もします。
gore(GolangのREPL)の挙動
gore> :import fmt gore> fmt.Println("foo") foo (int)4 (interface {})<nil> gore> fmt.Println("bar") foo bar (int)4 (interface {})<nil> gore> fmt.Println("baz") foo bar baz (int)4 (interface {})<nil>
Goの勉強中はgoreというREPLをヨック使ってました。 標準出力に過去のprint内容が混ざっているので、最初バグかと思ったのですが、goreではそれまでに入力されてたものをすべて連結して毎回コンパイルするので実装的に仕方ないみたいです。
スライスがあるアイテムを持つかどうかの判定
pythonで言うところの 'baz' in ['foo', 'bar', 'baz']
のようなチェックをしたい場合、Golangではそういう演算子や構文が特に用意されていないみたい。少し泥臭いですが、以下のページのように対応するよりなさそうです。
analyzerについて
前回Pythonで実装したものもそうなのですが、N-gramは使わずにanalyzerは形態素解析エンジンのみです。 以前はMecabを使用していましたが、今回はPure Goな形態素解析エンジンであるKagomeを使用しました。 こちらも特にトラブルなく使えました。
GitHub - ikawaha/kagome: Self-contained Japanese Morphological Analyzer written in pure golang
Godep
GitHub - tools/godep: dependency tool for go
Goの依存関係を記述するツールとしてGodepを使いました。まだ godep save しただけなので特に問題は起きてないです。
envの扱い
MongoDBのDataBase URIとかは環境によって変更したいので、↓のようなファイルを用意して環境変数に応じて変えました。
デバッガ(delve)
delveをとりあえず入れて何度か動かしてみたんですが、まだ慣れなくてスムーズには扱えてないです。 結構assertionのレポートとかを頼りにデバッグしてます。 アサートが失敗したタイミングでカジュアルにdelveを起動できるといいのかも(ちゃんと調べてないだけです。ありそうな気もします)。
テスト
Goでは1テストメソッド複数アサーションが普通に行われるみたいです。 アサーションライブラリはtestifyを使用しました。GoConveyも良さそうだったのですが、個人的に書き方に慣れているので、今回はこれを使用しました。
GitHub - stretchr/testify: A sacred extension to the standard go testing package
// https://github.com/c-bata/gosearch/blob/master/crawler/crawl_test.go#L68 func TestRemoveTags(t *testing.T) { assert := assert.New(t) input := `<ul><li>item</li></ul>` actual := RemoveTags(input) expected := "item" assert.Equal(expected, actual) }
Webクローラについて
Webクローラ完成時のコードは↓です。これ実行すると結構スピード出るので使う際は気をつけてください。
GitHub - c-bata/gosearch at 57aa4107e247254e9de5e95590aa6b3ecd38d67d
mgo
MongoDBのドライバでは一番人気そうだったので使用。 Sessionの取得とかは この辺 参考にしてください。
GetCollection
c := Session.DB("database_name").C("collction_name")
Insert
type Index struct { ID string `bson:"_id"` Keyword string `bson:"keyword"` Url []string `bson:"url"` } c := Session.DB("test").C("index") index := &Index{"hoge", []string{"http://example.com"}} c.Insert(index)
Update
err = c.Update(bson.M{"keyword": "keyword1"}, bson.M{"$push": bson.M{"url": "http://1.example.com"}}) err = c.UpdateId(result.ID, bson.M{"$push": bson.M{"url": "http://2.example.com"}})
- Update operator(
$set
や$put
、$pop
)は基本的に使えるはず - https://docs.mongodb.org/manual/reference/operator/update-array/
UpdateAll
err = c.Update(bson.M{"keyword": "keyword1"}, bson.M{"$pushAll": bson.M{"url": []string{"http://hoge.example.com"}}}) err = c.Update(bson.M{"_id": result.ID}, bson.M{"$pushAll": bson.M{"url": []string{"http://6.example.com"}}})
Find
result := &Index{} c.Find(bson.M{"keyword": "hoge"}).One(&result)
FindAll
var results []Index c.Find(nil).All(&results) results
Goの所感など
- 並行処理周り以外ではそれなりにすぐに理解できた
- 並行処理まわりは気づいたらブロックして出力が何もないことが多かったです。
- こちら Go の並行処理 - Block Rockin’ Codes を読んで理解していきました。ありがとうございます。
- デバッガはまだ慣れなくて、ついついテストとPrintデバッグに頼っちゃってます
- 個人的に一番期待しているのはクロスコンパイルとシングルバイナリなのですが、その辺の良さが一番出るのはコマンドラインツールな気がするので、次はその辺で何か作ってみます。github.com
- 作者: Mat Ryer,鵜飼文敏,牧野聡
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/01/22
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Python製WebフレームワークのURL DispatcherとType Hintsの活用について
少し前から趣味で実装してるKobinというWebアプリケーションフレームワークのURL Dispatcherの実装をする時に、BottleやDjangoを参考にしながら考えてみました。 これらの比較とType Hintsを利用したKobinのURL Dispatcherの実装についてまとめます。
(追記) リバースルーティング
ちょっと長いですが、追記です。結論としては元々紹介していた正規表現ベースのルーティングとは違う方法をKobinで採用しました
この記事では正規表現によるルーティングについて解説していたのですが、このルーティング方法は逆引きが少し難しくなります(Djangoは正規表現ベースの逆引きですが、自由度の高い正規表現からURLを生成するために一部妥協しているところもあるようです)。 実際の問題では正規表現が必要となるほど複雑なURL構成になることは稀かもしれません。
逆引きのしやすさを考えると次のようなルーティングがいいかと思いました。
@app.route('/users/{user_id}') def user_detail(user_id: int): pass
この方法で優れているのは、 .format
メソッドにより逆引きが出来てとてもシンプルです。
正引きの実装は正規表現モジュールを使ったときほど、簡単ではないですがそこまで複雑になるわけでもありません。
追記終わり
Pythonの正規表現モジュールのおさらい
BottleやDjangoのURL Dispatcherについて見ていく前に、Pythonの正規表現モジュール について簡単におさらい。
>>> import re >>> url_scheme = '/users/(?P<user_id>\d+)/' >>> pattern = re.compile(url_scheme) >>> pattern.match('/users/1/').groupdict() {'user_id': '1'}
このように名前付きグループでパターンを定義し、マッチするか確認してからgroupdictを呼ぶことでuser_idの部分の数字が文字列で取得出来ます。
Django, Bottleのルーティング手法
Django
Djangoは正規表現でURLスキーマを定義するため、自由度が高く複雑な場合にも対応出来ます。
# https://docs.djangoproject.com/en/1.9/topics/http/urls/ from django.conf.urls import url from . import views urlpatterns = [ url(r'^blog/$', views.page), url(r'^blog/page(?P<num>[0-9]+)/$', views.page), ] # View (in blog/views.py) def page(request, num="1"): # Output the appropriate page of blog entries, according to num. ...
この方法は正規表現モジュールで簡単に実装が出来そうですが、型情報の推測が困難なため、Djangoではviewの関数に渡されるとき全て文字列で渡されます。 整数として扱いたい場合などには、いちいちview関数の中でキャストする必要があります。
Bottle
一方Bottleは下記のように <id: int>
のような指定方法です。型が分かるため、view関数には型変換したオブジェクトを渡すことができます。
ただ、これだけでは表現しきれない場合があるため、複雑な指定をする時のために正規表現による指定方法も用意されています。
# http://bottlepy.org/docs/dev/tutorial.html#request-routing @route('/object/<id:int>') def callback(id): assert isinstance(id, int) @route('/show/<name:re:[a-z]+>') def callback(name): assert name.isalpha()
内部の実装としては、こんな感じのdictを用意して、一度正規表現に変換しているようです。 Flaskも実装を読んだわけではないですが、同じように指定するため実装方法も大きく変わらないんじゃないかなと思います。
この方法は、↓のように感じます。
- 一度正規表現に変換する必要があるため、Djangoの方法に比べると実装が少し手間
- ただ正規表現に慣れてないユーザにとっては扱いが簡単ですね
- BottleやFlaskでType Hintsも使おうとすると、URL Dispatcherでの型を指定と重複してしまう
- ↓の例のように2箇所で型情報を定義することになってしまう
@route('/object/<id:int>') # ここで int を指定 def callback(id: int): # ここでも int を指定 pass
正規表現による指定とType Hintsの活用
Djangoのような正規表現によるURL Dispatchは比較的実装が簡単そうなのでKobinでも採用しました。 ただKobinではそれに加えてType Hintsの型定義情報からURLの変数情報(urlvars)その型に変換しています。
# https://github.com/c-bata/kobin/blob/master/example/hello_world.py from kobin import Kobin app = Kobin() @app.route('^/years/(?P<year>\d{4})$') def casted_year(year: int): return 'A "year" argument is integer? : {}'.format(isinstance(year, int))
上の例を実行するとFlaskやBottleのようにint型で値が渡ってきます。 感じているメリットとしては、
- 正規表現をそのままつかっているため、自由度が高く複雑なURLスキーマにも対応できる
- さらにDjangoと違い、Type Hintsの型情報によってキャストされたオブジェクトをview関数で受け取れる
- Type Hintsの型定義情報を利用しているため、ドキュメントの生成時やIDEの型チェックなどType Hintsによる恩恵をそのまま受けることが出来そう
- FlaskやBottleでは型の情報をURLのところに含めていたため、Type Hintsのエコシステムによる恩恵は受けれなかった
- 実装がとても簡単
re.compile('/user/(?P<id>\d+').match('/users/1/').groupdict()
みたいにidにあたる数字を文字列で取得してから、Type Hintsで指定している型情報にキャストするだけ
ご意見あればお願いします。
感想
Python3.5でとても面白い機能が追加されたと思うのですが、Webアプリを書いてる時にType Hintsをどう組み込もうか考えているとこのようになりました。 Type Hintsの導入によって型を頼りにコードが読みやすくなるとか、ドキュメントやIDEへの情報が増えるとかメリットが有りますが、それ以外にもこれによって実装が変わってくるような面白い使い方が出来ないか考えていこうと思います。
[おまけ] Type Hintsを使ってみて
ついでに最近Type Hintsを使っていて気づいたことをメモ。 Type Hints自体の基本的な使い方は↓の翻訳記事を見るのが良いと思います。
Flake8の時にF401: import but unused
で怒られる
from typing import Dict def hoge(foo: Dict[str]) -> None: pass
とかするとDictが使われてませんって怒られます。対処としては # NOQA
をつけてskipするか、F401
をignoreするかだけど、今のところ後者の方法をとってます。
# setup.cfg [flake8] ignore = F401
ただこのチェックを無視するのはあんまりやりたくないなぁという印象
型定義情報の取得
型定義の情報は以下のように get_type_hints
関数で簡単に習得出来ます。
>>> from typing import get_type_hints >>> def hoge(a: int) -> None: ... pass ... >>> get_type_hints(hoge) {'return': None, 'a': <class 'int'>} >>> hoge.__annotations__ {'return': None, 'a': <class 'int'>}
エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)
- 作者: Michal Jaworski,TarekZiade,稲田直哉,芝田将,渋川よしき,清水川貴之,森本哲也
- 出版社/メーカー: ドワンゴ
- 発売日: 2018/02/26
- メディア: Kindle版
- この商品を含むブログを見る
就活終わりました
最後どこに行くか結構悩んでしまいましたが、就職先を決め内定承諾書を返送しました。 相談に乗っていただいた方々、ありがとうございます。
感じたこと
就職先を人で選ぶのが、意外と難しい
就活中にいただいたアドバイスの中で一番多かったのは 「どんな人が働いているか・どういう人と一緒に働きたいかを大事にすると良いよ」 っていう意見でした。 この意見にはなるほどって思うところがあったんですが、実際にエンジニアの人たちと話してみると自分が考えていた企業はどこにも技術的な部分はもちろん人柄も含めて尊敬できる人たちばかりで、ここで決めるのはかなり難しいように感じました。
給与面
エンジニアの方と話すと、 「新卒の給与なんて気にしない方がいい」 という意見が目立ってたように感じます(もちろんそうでない人もいましたが)。 転職の多い業界なので、どんな経験を積めるか・どんなふうにスキルを伸ばせるかを考えたほうがいいとの話でした。 とはいえ会社を選ぶ上で結構気になる要素で、他の就活生と飲みにいったりすると自然と給与とか残業時間とかの話になることが多かったです。 自分の場合、最終的には給与以外で良いと思えるところを見つけて会社を選びました。
就活を終えて
人事の方に相談してエンジニアの方と話をする機会を何度も頂いたのですが、自分のことを知るいい機会でした。
最終的に3社のWeb系の会社の選考に進み、その中で1つを決めました(社名はまた直接お会いした時にでも聞いてください)。他の就活生に比べるとかなり少ない方だった気がしますが、いざ1社を決めるとなるとかなり悩みました。 選んだ会社が正しかったのかは、自分のこれからの活動次第ですよね。精進します。
内定祝いの欲しいものリスト作りました。何か届いたらめっちゃ喜びます。
追記
結構減っててびっくりしました。たくさんのお祝いありがとうございます!