読者です 読者をやめる 読者になる 読者になる

たごもりすメモ

コードとかその他の話とか。

UserAgent判定器 Project Woothee はじめました

UserAgent判定ライブラリはCPANに数多くあるし他の言語でも似たようなものだと思うが、ライブラリや言語をまたがって一致した結果を返してくれるようなものは存在しない(と思う)。が、特にHadoopを使うようになってJavaの事情をある程度無視できなくなってくると、これがたいへん問題に思えてきた。Javaで書かれたUserAgent判定ロジックが欲しいが、普段書くコードはJavaではない*1ので、他の言語でも全く同じように判定してくれるライブラリが欲しい。結果が食い違っていたり、新しいUserAgentを判定したいときに片方だけ対応されて片方は置き去りになったりすると大変困る。

ということで、作った。v0.1.0。現状ではJavaPerlの実装がある*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 bloglivedoor 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

Javaならこうだし、Perlだとこっち。

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年でもあることなので、古いブラウザやケータイなどの判定はだいぶいいかげん。あとLinuxBSDなどもかなりいいかげん(Androidを除いて)だし、ThunderbirdIceweaselでのアクセスも見付けたけど、ブラウザ名称としては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とかでも必要ならすぐ書けるんじゃないかな。

なんで作ったの

先に書いた通り、JavaPerlで完全に一致する挙動の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割以上を占めるようなものの判定が世界中で共有されうるのは確かだと思う。そういった試みをそろそろ始めてもいいんじゃないかなということを頭に置きながらこのコードを書いていたし、いま改めてそう思ってこのエントリを書いている。

patches welcome!

パッチを大募集中です! 特にテストケースについて、「こんな端末がこんなUAだよ! これから増えるからサポートしてよ!」的なものを募集しております。UAの例の文字列だけでもよいのでお寄せいただけますとたいへん助かります。
またPerl/Java以外の言語の実装についてですが、Javaを見ながらPerlを書いたら7時間くらいで書けたので、他の言語でもそれなりに素早く書けそうな気がします。書いてしまったから入れろ! という心強いご連絡をお待ちしております。

*1:し、Javaで書きたいとも思わない

*2:あとhive udfの実装も

*3:新しいブラウザへの対応、既存ブラウザのバージョンアップによるUserAgentパターンの変化への追従、新OSや新クローラへの対応、などなどなどなど

*4:countで使えるように is_xxx 系のは全部 TRUE or NULL を返す

*5:Perlだと強力な拡張正規表現を使って云々とかもちょっと考えたが、簡単に速度計ってみたら遅くて使えなさそうだった。