UserAgent判定器 Project Woothee はじめました
UserAgent判定ライブラリはCPANに数多くあるし他の言語でも似たようなものだと思うが、ライブラリや言語をまたがって一致した結果を返してくれるようなものは存在しない(と思う)。が、特にHadoopを使うようになってJavaの事情をある程度無視できなくなってくると、これがたいへん問題に思えてきた。Javaで書かれたUserAgent判定ロジックが欲しいが、普段書くコードはJavaではない*1ので、他の言語でも全く同じように判定してくれるライブラリが欲しい。結果が食い違っていたり、新しいUserAgentを判定したいときに片方だけ対応されて片方は置き去りになったりすると大変困る。
ということで、作った。v0.1.0。現状ではJavaとPerlの実装がある*2。
https://github.com/tagomoris/woothee https://github.com/tagomoris/woothee
(移動しました: 最近の多言語対応User-Agentパーサライブラリ woothee について - tagomorisのメモ置き場)
(v0.2.0 でruby/pythonが追加された。see: http://d.hatena.ne.jp/tagomoris/20120613/1339583174)
もちろんこのテのライブラリは作れば終わりではなく、むしろその後に延々と続くもぐら叩き*3が本番とも言えるので、作ったといって喜んでいるわけにはいかない。
にしてもいちおうひととおりAPIは整備して livedoor blog や livedoor news のログをぶちこんでも目立った誤検知はなさそうに見えたので、まあそこそこ使えるんではないかなと思う。
機能
UserAgent文字列をぶちこむと、判定結果を保持したハッシュを返す。基本的にはそれだけ。
// import is.tagomor.woothee.Classifier; // import is.tagomor.woothee.DataSet; Map<String,String> r = Classifier.parse("user agent string"); r.get("name"); // => name of browser (or string like name of user-agent r.get("category"); // => "pc", "smartphone", "mobilephone", "appliance", "crawler", "misc", "unknown" r.get("os"); // => os from user-agent, or carrier name of mobile phones r.get("version"); // => version of browser, or terminal type name of mobile phones
use Woothee; Woothee::parse("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)"); # => {'name'=>"Internet Explorer", 'category'=>"pc", 'os'=>"Windows 7", 'version'=>"8.0"} Woothee::parse("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"); # => {'name'=>"Googlebot", 'category'=>"crawler", 'os'=>"UNKNOWN", 'version'=>"UNKNOWN"} Woothee::parse("Opera/9.80 (Android; Opera Mini/6.5.27452/26.1305; U; ja) Presto/2.8.119 Version/10.54"); # => {'name'=>"Opera", 'category'=>"smartphone", 'os'=>"Android", 'version'=>"9.80"}
たいへんかんたんですね!
既知のメジャーなクローラのみ判定するAPIもいちおう作ってある。これだけ高速にやりたいケースが手元であったので。
use Woothee; Woothee::is_crawler("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)"); # => 0 Woothee::is_crawler("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"); # => 1
もう2012年でもあることなので、古いブラウザやケータイなどの判定はだいぶいいかげん。あとLinuxやBSDなどもかなりいいかげん(Androidを除いて)だし、ThunderbirdやIceweaselでのアクセスも見付けたけど、ブラウザ名称としてはUNKNOWN扱いで放置したりしてある。マイナーなものに対応するよりは速度をそこそこ高目に保っておきたい傾向。
hive udf
これは自分で使うので簡単に書いてみた。
add jar woothee.jar; create temporary function parse_agent as 'is.tagomor.woothee.hive.ParseAgent'; create temporary function is_pc as 'is.tagomor.woothee.hive.IsPC'; create temporary function is_smartphone as 'is.tagomor.woothee.hive.IsSmartPhone'; create temporary function is_mobilephone as 'is.tagomor.woothee.hive.IsMobilePhone'; create temporary function is_appliance as 'is.tagomor.woothee.hive.IsAppliance'; create temporary function is_crawler as 'is.tagomor.woothee.hive.IsCrawler'; create temporary function is_misc as 'is.tagomor.woothee.hive.IsMisc'; create temporary function is_unknown as 'is.tagomor.woothee.hive.IsUnknown'; create temporary function is_in as 'is.tagomor.woothee.hive.IsIn'; SELECT count(is_pc(parsed_agent)) as pc_pageviews, count(is_in(parsed_agent, array('pc', 'mobilephone', 'smartphone', 'appliance'))) as total_pageviews FROM ( SELECT parse_agent(useragent) as parsed_agent FROM access_log WHERE date='today' ) x
こんなふうに一発でクエリできる*4。超便利。
woothee.Classifier.parse() Iを2〜3行のwrapperでくるんであるだけなので、pigとかでも必要ならすぐ書けるんじゃないかな。
なんで作ったの
先に書いた通り、JavaとPerlで完全に一致する挙動のUserAgent判定機が欲しかった、ということ。どうやって作るのか、作ったあとのメンテナンスが本当に継続できるのか、といった問題は少し前に知恵熱が出るほど考えたんだけど、結局押し切った。
で、このへんの一番の問題である「同じように見えるんだけど微妙に違う」答えを返すとかそういうのに悩まされるのは絶対に避けたかったので、まず返り値に使うデータを言語をまたいだひとつのデータセットとして(yamlで)作った。同じようにテストケースについても、UserAgent文字列とその判定結果、というセットを言語をまたいで(yaml)で作った。
Perl/Javaのテストケースはこれらのyamlを読み込んで走るだけなので、余程おかしい書き方をしない限りは言語間での挙動のミスマッチは避けられるはず、程度のことは考えている。
実装方法についても、言語をまたいで実装の共有ができないかと、正規表現の集合で構成するとか何らかのDSLを発明して各言語のコードを生成するとかあれこれ考えたけど、結局はそれぞれに実装している。やってみてわかったが部分文字列探索と簡単な正規表現の集合にしかならないので、別言語で実装し直すことのコスト自体はそんなに高くなかった*5。
で、このライブラリでは複数の言語の実装をまとめて収容しているけど、結局大事なのはテストケースの共有だけなのかな、と思う。もちろん、たとえば南アフリカで一大勢力を誇っている携帯キャリアの判定を日本でしたいかと言われるとそんなことはないので、docomo/au/SoftBankの判定を行ってしまうこのライブラリが世界各国で必要かというとそんなことはないだろう。なので部分的にテストケースの実行や判定そのものをon/offできた方がいいんだろうなとは思う。万が一広く使われるようになったら、configure的な処理をビルド前に入れて、各国向けのコードを生成する、とかやる日が来るかもしれない。
が、それでも Windows/OSX/MSIE/Chrome/Safari/Firefox/iOS/Andorid/Googlebot といった、アクセス元の6割以上を占めるようなものの判定が世界中で共有されうるのは確かだと思う。そういった試みをそろそろ始めてもいいんじゃないかなということを頭に置きながらこのコードを書いていたし、いま改めてそう思ってこのエントリを書いている。